From dcb2a515722711a1a74532d85dfd7a2037694d1e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 9 Nov 2025 14:20:31 +0100 Subject: [PATCH 001/523] Start to implement yui interface with pure python code --- manatools/aui/__init__.py | 0 manatools/aui/yui.py | 149 + manatools/aui/yui_common.py | 466 +++ manatools/aui/yui_curses.py | 819 ++++ manatools/aui/yui_gtk.py | 377 ++ manatools/aui/yui_qt.py | 389 ++ setup.py | 2 +- sow/TODO.md | 27 + sow/yui.py | 7656 +++++++++++++++++++++++++++++++++++ test/test_combobox.py | 92 + test/test_multi_backend.py | 129 + 11 files changed, 10105 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/__init__.py create mode 100644 manatools/aui/yui.py create mode 100644 manatools/aui/yui_common.py create mode 100644 manatools/aui/yui_curses.py create mode 100644 manatools/aui/yui_gtk.py create mode 100644 manatools/aui/yui_qt.py create mode 100644 sow/TODO.md create mode 100644 sow/yui.py create mode 100644 test/test_combobox.py create mode 100644 test/test_multi_backend.py diff --git a/manatools/aui/__init__.py b/manatools/aui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manatools/aui/yui.py b/manatools/aui/yui.py new file mode 100644 index 0000000..9825163 --- /dev/null +++ b/manatools/aui/yui.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Unified YUI implementation that automatically selects the best available backend. +Priority: Qt > GTK > NCurses +""" + +import os +import sys +from enum import Enum + +class Backend(Enum): + QT = "qt" + GTK = "gtk" + NCURSES = "ncurses" + +class YUI: + _instance = None + _backend = None + + @classmethod + def _detect_backend(cls): + """Detect the best available backend""" + # Check environment variable first + backend_env = os.environ.get('YUI_BACKEND', '').lower() + if backend_env == 'qt': + return Backend.QT + elif backend_env == 'gtk': + return Backend.GTK + elif backend_env == 'ncurses': + return Backend.NCURSES + + # Auto-detect based on available imports + try: + import PyQt5.QtWidgets + return Backend.QT + except ImportError: + pass + + try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk + return Backend.GTK + except (ImportError, ValueError): + pass + + try: + import curses + return Backend.NCURSES + except ImportError: + pass + + raise RuntimeError("No UI backend available. Install PyQt5, PyGObject, or curses.") + + @classmethod + def ui(cls): + if cls._instance is None: + cls._backend = cls._detect_backend() + print(f"Detected backend: {cls._backend}") + + if cls._backend == Backend.QT: + from .yui_qt import YUIQt as YUIImpl + elif cls._backend == Backend.GTK: + from .yui_gtk import YUIGtk as YUIImpl + elif cls._backend == Backend.NCURSES: + from .yui_curses import YUICurses as YUIImpl + else: + raise RuntimeError(f"Unknown backend: {cls._backend}") + + cls._instance = YUIImpl() + + return cls._instance + + @classmethod + def backend(cls): + if cls._instance is None: + cls.ui() # This will detect the backend + return cls._backend + + @classmethod + def widgetFactory(cls): + return cls.ui().widgetFactory() + + @classmethod + def optionalWidgetFactory(cls): + return cls.ui().optionalWidgetFactory() + + @classmethod + def app(cls): + return cls.ui().app() + + @classmethod + def application(cls): + return cls.ui().application() + + @classmethod + def yApp(cls): + return cls.ui().yApp() + +# Global functions for compatibility with libyui API +def YUI_ui(): + return YUI.ui() + +def YUI_widgetFactory(): + return YUI.widgetFactory() + +def YUI_optionalWidgetFactory(): + return YUI.optionalWidgetFactory() + +def YUI_app(): + return YUI.app() + +def YUI_application(): + return YUI.application() + +def YUI_yApp(): + return YUI.yApp() + +def YUI_ensureUICreated(): + return YUI.ui() + +# Import common classes that are backend-agnostic +from .yui_common import ( + # Enums + YUIDimension, YAlignmentType, YDialogType, YDialogColorMode, + YEventType, YEventReason, YCheckBoxState, YButtonRole, + # Base classes + YWidget, YSingleChildContainerWidget, YSelectionWidget, + YSimpleInputField, YItem, YTreeItem, + # Events + YEvent, YWidgetEvent, YKeyEvent, YMenuEvent, YCancelEvent, + # Exceptions + YUIException, YUIWidgetNotFoundException, YUINoDialogException, + # Other common classes + YProperty, YPropertyValue, YPropertySet, YShortcut +) + +# Re-export everything for easy importing +__all__ = [ + 'YUI', 'YUI_ui', 'YUI_widgetFactory', 'YUI_app', 'YUI_application', 'YUI_yApp', + 'YUIDimension', 'YAlignmentType', 'YDialogType', 'YDialogColorMode', + 'YEventType', 'YEventReason', 'YCheckBoxState', 'YButtonRole', + 'YWidget', 'YSingleChildContainerWidget', 'YSelectionWidget', + 'YSimpleInputField', 'YItem', 'YTreeItem', + 'YEvent', 'YWidgetEvent', 'YKeyEvent', 'YMenuEvent', 'YCancelEvent', + 'YUIException', 'YUIWidgetNotFoundException', 'YUINoDialogException', + 'YProperty', 'YPropertyValue', 'YPropertySet', 'YShortcut', + 'Backend' +] \ No newline at end of file diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py new file mode 100644 index 0000000..ffc2a65 --- /dev/null +++ b/manatools/aui/yui_common.py @@ -0,0 +1,466 @@ +""" +Common base classes and definitions shared across all backends +""" + +from enum import Enum +import uuid + +# Enums +class YUIDimension(Enum): + YD_HORIZ = 0 + YD_VERT = 1 + +class YAlignmentType(Enum): + YAlignUnchanged = 0 + YAlignBegin = 1 + YAlignEnd = 2 + YAlignCenter = 3 + +class YDialogType(Enum): + YMainDialog = 0 + YPopupDialog = 1 + YWizardDialog = 2 + +class YDialogColorMode(Enum): + YDialogNormalColor = 0 + YDialogInfoColor = 1 + YDialogWarnColor = 2 + +class YEventType(Enum): + NoEvent = 0 + WidgetEvent = 1 + MenuEvent = 2 + KeyEvent = 3 + CancelEvent = 4 + TimeoutEvent = 5 + +class YEventReason(Enum): + Activated = 0 + ValueChanged = 1 + SelectionChanged = 2 + +class YCheckBoxState(Enum): + YCheckBox_dont_care = -1 + YCheckBox_off = 0 + YCheckBox_on = 1 + +class YButtonRole(Enum): + YCustomButton = 0 + YOKButton = 1 + YCancelButton = 2 + YHelpButton = 3 + +# Exceptions +class YUIException(Exception): + pass + +class YUIWidgetNotFoundException(YUIException): + pass + +class YUINoDialogException(YUIException): + pass + +class YUIInvalidWidgetException(YUIException): + pass + +# Events +class YEvent: + def __init__(self, event_type=YEventType.NoEvent, widget=None, reason=None): + self._event_type = event_type + self._widget = widget + self._reason = reason + self._serial = 0 + + def eventType(self): + return self._event_type + + def widget(self): + return self._widget + + def reason(self): + return self._reason + + def serial(self): + return self._serial + +class YWidgetEvent(YEvent): + def __init__(self, widget=None, reason=YEventReason.Activated, event_type=YEventType.WidgetEvent): + super().__init__(event_type, widget, reason) + +class YKeyEvent(YEvent): + def __init__(self, key_symbol, focus_widget=None): + super().__init__(YEventType.KeyEvent, focus_widget) + self._key_symbol = key_symbol + + def keySymbol(self): + return self._key_symbol + + def focusWidget(self): + return self.widget() + +class YMenuEvent(YEvent): + def __init__(self, item=None, id=None): + super().__init__(YEventType.MenuEvent) + self._item = item + self._id = id + + def item(self): + return self._item + + def id(self): + return self._id + +class YCancelEvent(YEvent): + def __init__(self): + super().__init__(YEventType.CancelEvent) + +# Base Widget Class +class YWidget: + _widget_counter = 0 + + def __init__(self, parent=None): + YWidget._widget_counter += 1 + self._id = f"widget_{YWidget._widget_counter}" + self._parent = parent + self._children = [] + self._enabled = True + self._help_text = "" + self._backend_widget = None + self._stretchable_horiz = False + self._stretchable_vert = False + self._weight_horiz = 0 + self._weight_vert = 0 + self._notify = True + self._auto_shortcut = False + self._function_key = 0 + + if parent and hasattr(parent, 'addChild'): + parent.addChild(self) + + def widgetClass(self): + return self.__class__.__name__ + + def debugLabel(self): + return f"{self.widgetClass()}({self._id})" + + def helpText(self): + return self._help_text + + def setHelpText(self, help_text): + self._help_text = help_text + + def hasChildren(self): + return len(self._children) > 0 + + def firstChild(self): + return self._children[0] if self._children else None + + def lastChild(self): + return self._children[-1] if self._children else None + + def childrenBegin(self): + return iter(self._children) + + def childrenEnd(self): + return iter([]) + + def childrenCount(self): + return len(self._children) + + def addChild(self, child): + if child not in self._children: + self._children.append(child) + child._parent = self + + def removeChild(self, child): + if child in self._children: + self._children.remove(child) + child._parent = None + + def parent(self): + return self._parent + + def hasParent(self): + return self._parent is not None + + def setEnabled(self, enabled=True): + self._enabled = enabled + if self._backend_widget: + self._set_backend_enabled(enabled) + + def isEnabled(self): + return self._enabled + + def stretchable(self, dim): + if dim == YUIDimension.YD_HORIZ: + return self._stretchable_horiz + else: + return self._stretchable_vert + + def setStretchable(self, dim, new_stretch): + if dim == YUIDimension.YD_HORIZ: + self._stretchable_horiz = new_stretch + else: + self._stretchable_vert = new_stretch + + def weight(self, dim): + if dim == YUIDimension.YD_HORIZ: + return self._weight_horiz + else: + return self._weight_vert + + def setWeight(self, dim, weight): + if dim == YUIDimension.YD_HORIZ: + self._weight_horiz = weight + else: + self._weight_vert = weight + + def setNotify(self, notify=True): + self._notify = notify + + def notify(self): + return self._notify + + def autoShortcut(self): + return self._auto_shortcut + + def setAutoShortcut(self, auto_shortcut): + self._auto_shortcut = auto_shortcut + + def functionKey(self): + return self._function_key + + def setFunctionKey(self, fkey_no): + self._function_key = fkey_no + + # Backend-specific methods to be implemented by concrete classes + def _set_backend_enabled(self, enabled): + pass + + def _create_backend_widget(self): + pass + + def get_backend_widget(self): + if self._backend_widget is None: + self._create_backend_widget() + return self._backend_widget + +class YSingleChildContainerWidget(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._child = None + + def addChild(self, child): + if self._child is not None: + self.removeChild(self._child) + self._child = child + child._parent = self + +class YSelectionWidget(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._items = [] + self._selected_items = [] + self._label = "" + self._icon_base_path = "" + + def label(self): + return self._label + + def setLabel(self, new_label): + self._label = new_label + + def addItem(self, item): + if isinstance(item, str): + item = YItem(item) + self._items.append(item) + + def deleteAllItems(self): + self._items.clear() + self._selected_items.clear() + + def itemsBegin(self): + return iter(self._items) + + def itemsEnd(self): + return iter([]) + + def hasItems(self): + return len(self._items) > 0 + + def itemsCount(self): + return len(self._items) + + def selectedItem(self): + return self._selected_items[0] if self._selected_items else None + + def selectedItems(self): + return self._selected_items + + def hasSelectedItem(self): + return len(self._selected_items) > 0 + + def selectItem(self, item, selected=True): + if selected and item not in self._selected_items: + self._selected_items.append(item) + elif not selected and item in self._selected_items: + self._selected_items.remove(item) + +class YSimpleInputField(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._value = "" + self._label = "" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + +class YItem: + def __init__(self, label, selected=False, icon_name=""): + self._label = label + self._selected = selected + self._icon_name = icon_name + self._index = 0 + self._data = None + + def label(self): + return self._label + + def setLabel(self, new_label): + self._label = new_label + + def selected(self): + return self._selected + + def setSelected(self, selected=True): + self._selected = selected + + def iconName(self): + return self._icon_name + + def hasIconName(self): + return bool(self._icon_name) + + def setIconName(self, new_icon_name): + self._icon_name = new_icon_name + + def index(self): + return self._index + + def setIndex(self, index): + self._index = index + + def data(self): + return self._data + + def setData(self, new_data): + self._data = new_data + +class YTreeItem(YItem): + def __init__(self, label, is_open=False, icon_name=""): + super().__init__(label, False, icon_name) + self._children = [] + self._is_open = is_open + self._parent_item = None + + def hasChildren(self): + return len(self._children) > 0 + + def childrenBegin(self): + return iter(self._children) + + def childrenEnd(self): + return iter([]) + + def addChild(self, item): + self._children.append(item) + item._parent_item = self + + def isOpen(self): + return self._is_open + + def setOpen(self, is_open=True): + self._is_open = is_open + +# Property system +class YPropertyType(Enum): + YUnknownPropertyType = 0 + YOtherProperty = 1 + YStringProperty = 2 + YBoolProperty = 3 + YIntegerProperty = 4 + +class YProperty: + def __init__(self, name, prop_type, is_readonly=False): + self._name = name + self._type = prop_type + self._is_readonly = is_readonly + + def name(self): + return self._name + + def type(self): + return self._type + + def isReadOnly(self): + return self._is_readonly + +class YPropertyValue: + def __init__(self, value=None, prop_type=YPropertyType.YUnknownPropertyType): + self._value = value + self._type = prop_type + + def type(self): + return self._type + + def stringVal(self): + return str(self._value) if self._value else "" + + def boolVal(self): + return bool(self._value) + + def integerVal(self): + return int(self._value) if self._value else 0 + +class YPropertySet: + def __init__(self): + self._properties = {} + + def add(self, prop): + self._properties[prop.name()] = prop + + def contains(self, name): + return name in self._properties + + def isEmpty(self): + return len(self._properties) == 0 + +class YShortcut: + def __init__(self, widget): + self._widget = widget + self._shortcut = '' + self._conflict = False + + def widget(self): + return self._widget + + def shortcutString(self): + return self._shortcut + + def setShortcut(self, new_shortcut): + self._shortcut = new_shortcut + + def conflict(self): + return self._conflict + + def setConflict(self, conflict=True): + self._conflict = conflict \ No newline at end of file diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py new file mode 100644 index 0000000..39df57d --- /dev/null +++ b/manatools/aui/yui_curses.py @@ -0,0 +1,819 @@ +""" +NCurses backend implementation for YUI +""" + +import curses +import curses.ascii +import sys +import os +import time +from .yui_common import * + +class YUICurses: + def __init__(self): + self._widget_factory = YWidgetFactoryCurses() + self._optional_widget_factory = None + self._application = YApplicationCurses() + self._stdscr = None + self._colors_initialized = False + self._running = False + + # Initialize curses + self._init_curses() + + def _init_curses(self): + try: + self._stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + curses.curs_set(1) # Show cursor + self._stdscr.keypad(True) + + # Enable colors if available + if curses.has_colors(): + curses.start_color() + curses.use_default_colors() + self._colors_initialized = True + # Define some color pairs + curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) + curses.init_pair(2, curses.COLOR_YELLOW, -1) + curses.init_pair(3, curses.COLOR_GREEN, -1) + curses.init_pair(4, curses.COLOR_RED, -1) + + except Exception as e: + print(f"Error initializing curses: {e}") + self._cleanup_curses() + raise + + def _cleanup_curses(self): + try: + if self._stdscr: + curses.nocbreak() + self._stdscr.keypad(False) + curses.echo() + curses.curs_set(1) + curses.endwin() + except: + pass + + def __del__(self): + self._cleanup_curses() + + def widgetFactory(self): + return self._widget_factory + + def optionalWidgetFactory(self): + return self._optional_widget_factory + + def app(self): + return self._application + + def application(self): + return self._application + + def yApp(self): + return self._application + +class YApplicationCurses: + def __init__(self): + self._icon_base_path = "" + self._product_name = "YUI Curses" + + def iconBasePath(self): + return self._icon_base_path + + def setIconBasePath(self, new_icon_base_path): + self._icon_base_path = new_icon_base_path + + def setProductName(self, product_name): + self._product_name = product_name + + def productName(self): + return self._product_name + +class YWidgetFactoryCurses: + def __init__(self): + pass + + def createMainDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): + return YDialogCurses(YDialogType.YMainDialog, color_mode) + + def createVBox(self, parent): + return YVBoxCurses(parent) + + def createHBox(self, parent): + return YHBoxCurses(parent) + + def createLabel(self, parent, text, isHeading=False, isOutputField=False): + return YLabelCurses(parent, text, isHeading, isOutputField) + + def createHeading(self, parent, label): + return YLabelCurses(parent, label, isHeading=True) + + def createInputField(self, parent, label, password_mode=False): + return YInputFieldCurses(parent, label, password_mode) + + def createPushButton(self, parent, label): + return YPushButtonCurses(parent, label) + + def createCheckBox(self, parent, label, is_checked=False): + return YCheckBoxCurses(parent, label, is_checked) + + def createComboBox(self, parent, label, editable=False): + return YComboBoxCurses(parent, label, editable) + + +# Curses Widget Implementations +class YDialogCurses(YSingleChildContainerWidget): + _open_dialogs = [] + _current_dialog = None + + def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): + super().__init__() + self._dialog_type = dialog_type + self._color_mode = color_mode + self._is_open = False + self._window = None + self._focused_widget = None + self._last_draw_time = 0 + self._draw_interval = 0.1 # seconds + YDialogCurses._open_dialogs.append(self) + + def widgetClass(self): + return "YDialog" + + def open(self): + if not self._window: + self._create_backend_widget() + + self._is_open = True + YDialogCurses._current_dialog = self + + # Find first focusable widget + focusable = self._find_focusable_widgets() + if focusable: + self._focused_widget = focusable[0] + self._focused_widget._focused = True + + self._run_event_loop() + + def isOpen(self): + return self._is_open + + def destroy(self, doThrow=True): + self._is_open = False + if self in YDialogCurses._open_dialogs: + YDialogCurses._open_dialogs.remove(self) + if YDialogCurses._current_dialog == self: + YDialogCurses._current_dialog = None + return True + + @classmethod + def deleteTopmostDialog(cls, doThrow=True): + if cls._open_dialogs: + dialog = cls._open_dialogs[-1] + return dialog.destroy(doThrow) + return False + + @classmethod + def currentDialog(cls, doThrow=True): + if not cls._open_dialogs: + if doThrow: + raise YUINoDialogException("No dialog open") + return None + return cls._open_dialogs[-1] + + def _create_backend_widget(self): + # Use the main screen + self._backend_widget = curses.newwin(0, 0, 0, 0) + + def _run_event_loop(self): + from manatools.aui.yui import YUI + ui = YUI.ui() + + while self._is_open: + try: + # Draw only if needed (throttle redraws) + current_time = time.time() + if current_time - self._last_draw_time >= self._draw_interval: + self._draw_dialog() + self._last_draw_time = current_time + + # Non-blocking input + ui._stdscr.nodelay(True) + key = ui._stdscr.getch() + + if key == -1: + time.sleep(0.01) # Small sleep to prevent CPU spinning + continue + + # Handle global keys + if key == curses.KEY_F10 or key == ord('q') or key == ord('Q'): + print("Quit requested") + break + elif key == curses.KEY_RESIZE: + # Handle terminal resize - force redraw + self._last_draw_time = 0 + continue + + # Handle tab navigation + if key == ord('\t'): + self._cycle_focus(forward=True) + self._last_draw_time = 0 # Force redraw + continue + elif key == curses.KEY_BTAB: # Shift+Tab + self._cycle_focus(forward=False) + self._last_draw_time = 0 # Force redraw + continue + + # Send key event to focused widget + if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): + handled = self._focused_widget._handle_key(key) + if handled: + self._last_draw_time = 0 # Force redraw + + except KeyboardInterrupt: + print("Keyboard interrupt") + break + except Exception as e: + # Don't crash on curses errors + print(f"Curses error: {e}") + time.sleep(0.1) + + self.destroy() + print("NCurses dialog closed") + + def _draw_dialog(self): + """Draw the entire dialog (called by event loop)""" + if not hasattr(self, '_backend_widget') or not self._backend_widget: + return + + try: + height, width = self._backend_widget.getmaxyx() + + # Clear screen + self._backend_widget.clear() + + # Draw border + self._backend_widget.border() + + # Draw title + title = " YUI NCurses Dialog " + title_x = max(0, (width - len(title)) // 2) + self._backend_widget.addstr(0, title_x, title, curses.A_BOLD) + + # Draw content area - fixed coordinates for child + content_height = height - 4 + content_width = width - 4 + content_y = 2 + content_x = 2 + + # Draw child content + if self._child: + self._draw_child_content(content_y, content_x, content_width, content_height) + + # Draw footer with instructions + footer_text = " TAB=Navigate | SPACE=Expand | ENTER=Select | F10/Q=Quit " + footer_x = max(0, (width - len(footer_text)) // 2) + if footer_x + len(footer_text) < width: + self._backend_widget.addstr(height - 1, footer_x, footer_text, curses.A_DIM) + + # Draw focus indicator + if self._focused_widget: + focus_text = f" Focus: {getattr(self._focused_widget, '_label', 'Unknown')} " + if len(focus_text) < width: + self._backend_widget.addstr(height - 2, 2, focus_text, curses.A_REVERSE) + + self._backend_widget.refresh() + + except curses.error as e: + # Ignore curses errors (like writing beyond screen bounds) + pass + + def _draw_child_content(self, start_y, start_x, max_width, max_height): + """Draw the child widget content respecting container hierarchy""" + if not self._child: + return + + # Draw only the root child - it will handle drawing its own children + if hasattr(self._child, '_draw'): + self._child._draw(self._backend_widget, start_y, start_x, max_width, max_height) + + + def _cycle_focus(self, forward=True): + """Cycle focus between focusable widgets""" + focusable = self._find_focusable_widgets() + if not focusable: + return + + current_index = -1 + if self._focused_widget: + for i, widget in enumerate(focusable): + if widget == self._focused_widget: + current_index = i + break + + if current_index == -1: + new_index = 0 + else: + if forward: + new_index = (current_index + 1) % len(focusable) + else: + new_index = (current_index - 1) % len(focusable) + + # Update focus + if self._focused_widget: + self._focused_widget._focused = False + + self._focused_widget = focusable[new_index] + self._focused_widget._focused = True + + def _find_focusable_widgets(self): + """Find all widgets that can receive focus""" + focusable = [] + + def find_in_widget(widget): + if hasattr(widget, '_can_focus') and widget._can_focus: + focusable.append(widget) + for child in widget._children: + find_in_widget(child) + + if self._child: + find_in_widget(self._child) + + return focusable + +class YVBoxCurses(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YVBox" + + def _create_backend_widget(self): + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + current_y = y + for child in self._children: + if not hasattr(child, '_draw'): + continue + + # Get child height - only consider direct children + child_height = getattr(child, '_height', 1) + + # Check space + if current_y + child_height > y + height: + break + + # Draw ONLY the direct child + # The child will handle drawing its own children recursively + child._draw(window, current_y, x, width, child_height) + + # Move to next position + current_y += child_height + 1 # +1 for spacing + +class YHBoxCurses(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._height = 1 # HBox always takes one line + + def widgetClass(self): + return "YHBox" + + def _create_backend_widget(self): + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + # HBox draws its OWN children horizontally + num_children = len(self._children) + if num_children == 0: + return + + # Calculate equal widths for horizontal layout + child_width = width // num_children + current_x = x + + for i, child in enumerate(self._children): + if hasattr(child, '_draw'): + # Last child gets remaining space + actual_width = child_width if i < num_children - 1 else (x + width) - current_x + + # Draw the child - HBox handles its children's positioning + child._draw(window, y, current_x, actual_width, height) + + # Move to next horizontal position + current_x += child_width + +class YLabelCurses(YWidget): + def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): + super().__init__(parent) + self._text = text + self._is_heading = isHeading + self._is_output_field = isOutputField + self._height = 1 + self._focused = False + self._can_focus = False # Labels don't get focus + + def widgetClass(self): + return "YLabel" + + def text(self): + return self._text + + def setText(self, new_text): + self._text = new_text + + def _create_backend_widget(self): + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + try: + attr = 0 + if self._is_heading: + attr = curses.A_BOLD + + # Truncate text to fit available width + display_text = self._text[:width-1] + window.addstr(y, x, display_text, attr) + except curses.error: + pass + +class YInputFieldCurses(YWidget): + def __init__(self, parent=None, label="", password_mode=False): + super().__init__(parent) + self._label = label + self._value = "" + self._password_mode = password_mode + self._cursor_pos = 0 + self._focused = False + self._can_focus = True + self._height = 1 + + def widgetClass(self): + return "YInputField" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + self._cursor_pos = len(text) + + def label(self): + return self._label + + def _create_backend_widget(self): + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + try: + # Draw label + if self._label: + label_text = self._label + if len(label_text) > width // 3: + label_text = label_text[:width // 3] + window.addstr(y, x, label_text) + x += len(label_text) + 1 + width -= len(label_text) + 1 + + # Calculate available space for input + if width <= 0: + return + + # Prepare display value + if self._password_mode and self._value: + display_value = '*' * len(self._value) + else: + display_value = self._value + + # Handle scrolling for long values + if len(display_value) > width: + if self._cursor_pos >= width: + start_pos = self._cursor_pos - width + 1 + display_value = display_value[start_pos:start_pos + width] + else: + display_value = display_value[:width] + + # Draw input field background + field_bg = ' ' * width + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + window.addstr(y, x, field_bg, attr) + + # Draw text + if display_value: + window.addstr(y, x, display_value, attr) + + # Show cursor if focused + if self._focused: + cursor_display_pos = min(self._cursor_pos, width - 1) + if cursor_display_pos < len(display_value): + window.chgat(y, x + cursor_display_pos, 1, curses.A_REVERSE | curses.A_BOLD) + + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused: + return False + + handled = True + + if key == curses.KEY_BACKSPACE or key == 127 or key == 8: + if self._cursor_pos > 0: + self._value = self._value[:self._cursor_pos-1] + self._value[self._cursor_pos:] + self._cursor_pos -= 1 + elif key == curses.KEY_DC: # Delete key + if self._cursor_pos < len(self._value): + self._value = self._value[:self._cursor_pos] + self._value[self._cursor_pos+1:] + elif key == curses.KEY_LEFT: + if self._cursor_pos > 0: + self._cursor_pos -= 1 + elif key == curses.KEY_RIGHT: + if self._cursor_pos < len(self._value): + self._cursor_pos += 1 + elif key == curses.KEY_HOME: + self._cursor_pos = 0 + elif key == curses.KEY_END: + self._cursor_pos = len(self._value) + elif 32 <= key <= 126: # Printable characters + self._value = self._value[:self._cursor_pos] + chr(key) + self._value[self._cursor_pos:] + self._cursor_pos += 1 + else: + handled = False + + return handled + +class YPushButtonCurses(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._focused = False + self._can_focus = True + self._height = 1 # Fixed height - buttons are always one line + + def widgetClass(self): + return "YPushButton" + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + + def _create_backend_widget(self): + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + try: + # Center the button label within available width + button_text = f" {self._label} " + text_x = x + max(0, (width - len(button_text)) // 2) + + # Only draw if we have enough space + if text_x + len(button_text) <= x + width: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + if self._focused: + attr |= curses.A_BOLD + + window.addstr(y, text_x, button_text, attr) + except curses.error: + # Ignore drawing errors (out of bounds) + pass + + def _handle_key(self, key): + if not self._focused: + return False + + if key == ord('\n') or key == ord(' '): + # Button pressed + print(f"Button '{self._label}' pressed") + return True + + return False + +class YCheckBoxCurses(YWidget): + def __init__(self, parent=None, label="", is_checked=False): + super().__init__(parent) + self._label = label + self._is_checked = is_checked + self._focused = False + self._can_focus = True + self._height = 1 + + def widgetClass(self): + return "YCheckBox" + + def value(self): + return self._is_checked + + def setValue(self, checked): + self._is_checked = checked + + def label(self): + return self._label + + def _create_backend_widget(self): + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + try: + checkbox = "[X]" if self._is_checked else "[ ]" + text = f"{checkbox} {self._label}" + + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + window.addstr(y, x, text, attr) + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused: + return False + + if key == ord(' ') or key == ord('\n'): + self._is_checked = not self._is_checked + return True + + return False + +class YComboBoxCurses(YSelectionWidget): + def __init__(self, parent=None, label="", editable=False): + super().__init__(parent) + self._label = label + self._editable = editable + self._value = "" + self._focused = False + self._can_focus = True + self._height = 1 + self._expanded = False + self._hover_index = 0 + self._combo_x = 0 + self._combo_y = 0 + self._combo_width = 0 + + def widgetClass(self): + return "YComboBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + # Update selected items + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break + + def editable(self): + return self._editable + + def _create_backend_widget(self): + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + # Store position and dimensions for dropdown drawing + self._combo_y = y + self._combo_x = x + self._combo_width = width + + try: + # Calculate available space for combo box + label_space = len(self._label) + 1 if self._label else 0 + combo_space = width - label_space + + if combo_space <= 3: # Need at least space for " ▼ " + return + + # Draw label + if self._label: + label_text = self._label + if len(label_text) > label_space - 1: + label_text = label_text[:label_space - 1] + window.addstr(y, x, label_text) + x += len(label_text) + 1 + + # Prepare display value - always show current value + display_value = self._value if self._value else "Select..." + max_display_width = combo_space - 3 # Reserve space for " ▼ " + if len(display_value) > max_display_width: + display_value = display_value[:max_display_width] + "..." + + # Draw combo box background + combo_bg = " " * combo_space + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + window.addstr(y, x, combo_bg, attr) + + # Draw combo box content + combo_text = f" {display_value} ▼" + if len(combo_text) > combo_space: + combo_text = combo_text[:combo_space] + + window.addstr(y, x, combo_text, attr) + + # Draw expanded list if active + if self._expanded: + self._draw_expanded_list(window) + + except curses.error: + # Ignore drawing errors + pass + + def _draw_expanded_list(self, window): + """Draw the expanded dropdown list at correct position""" + if not self._expanded or not self._items: + return + + try: + list_height = min(len(self._items), 6) # Max 6 items visible + + # Calculate dropdown position - right below the combo box + dropdown_y = self._combo_y + 1 + dropdown_x = self._combo_x + (len(self._label) + 1 if self._label else 0) + dropdown_width = self._combo_width - (len(self._label) + 1 if self._label else 0) + + # Make sure we don't draw outside screen + screen_height, screen_width = window.getmaxyx() + + # If not enough space below, draw above + if dropdown_y + list_height >= screen_height: + dropdown_y = max(1, self._combo_y - list_height - 1) + + # Ensure dropdown doesn't go beyond right edge + if dropdown_x + dropdown_width >= screen_width: + dropdown_width = screen_width - dropdown_x - 1 + + if dropdown_width <= 5: # Need reasonable width + return + + # Draw dropdown background for each item + for i in range(list_height): + if i >= len(self._items): + break + + item = self._items[i] + item_text = item.label() + if len(item_text) > dropdown_width - 2: + item_text = item_text[:dropdown_width - 2] + "..." + + # Highlight hovered item + attr = curses.A_REVERSE if i == self._hover_index else curses.A_NORMAL + + # Create background for the item + bg_text = " " + item_text.ljust(dropdown_width - 2) + if len(bg_text) > dropdown_width: + bg_text = bg_text[:dropdown_width] + + # Ensure we don't write beyond screen bounds + if (dropdown_y + i < screen_height and + dropdown_x < screen_width and + dropdown_x + len(bg_text) <= screen_width): + try: + window.addstr(dropdown_y + i, dropdown_x, bg_text, attr) + except curses.error: + pass # Ignore out-of-bounds errors + + except curses.error: + # Ignore drawing errors + pass + + def _handle_key(self, key): + if not self._focused: + return False + + handled = True + + if key == ord('\n') or key == ord(' '): + # Toggle expanded state + self._expanded = not self._expanded + if self._expanded and self._items: + # Set hover index to current value if exists + self._hover_index = 0 + if self._value: + for i, item in enumerate(self._items): + if item.label() == self._value: + self._hover_index = i + break + elif self._expanded: + # Handle navigation in expanded list + if key == curses.KEY_UP: + if self._hover_index > 0: + self._hover_index -= 1 + elif key == curses.KEY_DOWN: + if self._hover_index < len(self._items) - 1: + self._hover_index += 1 + elif key == ord('\n') or key == ord(' '): + # Select hovered item + if self._items and 0 <= self._hover_index < len(self._items): + selected_item = self._items[self._hover_index] + self.setValue(selected_item.label()) # Use setValue to update display + self._expanded = False + elif key == 27: # ESC key + self._expanded = False + else: + handled = False + else: + handled = False + + return handled diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py new file mode 100644 index 0000000..018cc36 --- /dev/null +++ b/manatools/aui/yui_gtk.py @@ -0,0 +1,377 @@ +""" +GTK backend implementation for YUI +""" + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GObject +import threading +from .yui_common import * + +class YUIGtk: + def __init__(self): + self._widget_factory = YWidgetFactoryGtk() + self._optional_widget_factory = None + self._application = YApplicationGtk() + + def widgetFactory(self): + return self._widget_factory + + def optionalWidgetFactory(self): + return self._optional_widget_factory + + def app(self): + return self._application + + def application(self): + return self._application + + def yApp(self): + return self._application + +class YApplicationGtk: + def __init__(self): + self._icon_base_path = "/usr/share/icons" + self._product_name = "YUI GTK" + + def iconBasePath(self): + return self._icon_base_path + + def setIconBasePath(self, new_icon_base_path): + self._icon_base_path = new_icon_base_path + + def setProductName(self, product_name): + self._product_name = product_name + + def productName(self): + return self._product_name + +class YWidgetFactoryGtk: + def __init__(self): + pass + + def createMainDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): + return YDialogGtk(YDialogType.YMainDialog, color_mode) + + def createPopupDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): + return YDialogGtk(YDialogType.YPopupDialog, color_mode) + + def createVBox(self, parent): + return YVBoxGtk(parent) + + def createHBox(self, parent): + return YHBoxGtk(parent) + + def createPushButton(self, parent, label): + return YPushButtonGtk(parent, label) + + def createLabel(self, parent, text, isHeading=False, isOutputField=False): + return YLabelGtk(parent, text, isHeading, isOutputField) + + def createHeading(self, parent, label): + return YLabelGtk(parent, label, isHeading=True) + + def createInputField(self, parent, label, password_mode=False): + return YInputFieldGtk(parent, label, password_mode) + + def createCheckBox(self, parent, label, is_checked=False): + return YCheckBoxGtk(parent, label, is_checked) + + def createPasswordField(self, parent, label): + return YInputFieldGtk(parent, label, password_mode=True) + + def createComboBox(self, parent, label, editable=False): + return YComboBoxGtk(parent, label, editable) + +# GTK Widget Implementations +class YDialogGtk(YSingleChildContainerWidget): + _open_dialogs = [] + + def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): + super().__init__() + self._dialog_type = dialog_type + self._color_mode = color_mode + self._is_open = False + self._window = None + YDialogGtk._open_dialogs.append(self) + + def widgetClass(self): + return "YDialog" + + def open(self): + if not self._window: + self._create_backend_widget() + + self._window.show_all() + self._is_open = True + + # Start GTK main loop + Gtk.main() + + def isOpen(self): + return self._is_open + + def destroy(self, doThrow=True): + if self._window: + self._window.destroy() + self._window = None + self._is_open = False + if self in YDialogGtk._open_dialogs: + YDialogGtk._open_dialogs.remove(self) + + # Stop GTK main loop if no dialogs left + if not YDialogGtk._open_dialogs: + Gtk.main_quit() + return True + + @classmethod + def deleteTopmostDialog(cls, doThrow=True): + if cls._open_dialogs: + dialog = cls._open_dialogs[-1] + return dialog.destroy(doThrow) + return False + + @classmethod + def currentDialog(cls, doThrow=True): + if not cls._open_dialogs: + if doThrow: + raise YUINoDialogException("No dialog open") + return None + return cls._open_dialogs[-1] + + def _create_backend_widget(self): + self._window = Gtk.Window(title="YUI GTK Dialog") + self._window.set_default_size(600, 400) + self._window.set_border_width(10) + + if self._child: + self._window.add(self._child.get_backend_widget()) + + self._backend_widget = self._window + self._window.connect("destroy", self._on_destroy) + + def _on_destroy(self, widget): + self.destroy() + +class YVBoxGtk(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YVBox" + + def _create_backend_widget(self): + self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + + for child in self._children: + widget = child.get_backend_widget() + expand = child.stretchable(YUIDimension.YD_VERT) + fill = True + padding = 0 + self._backend_widget.pack_start(widget, expand, fill, padding) + +class YHBoxGtk(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YHBox" + + def _create_backend_widget(self): + self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + + for child in self._children: + widget = child.get_backend_widget() + expand = child.stretchable(YUIDimension.YD_HORIZ) + fill = True + padding = 0 + self._backend_widget.pack_start(widget, expand, fill, padding) + +class YLabelGtk(YWidget): + def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): + super().__init__(parent) + self._text = text + self._is_heading = isHeading + self._is_output_field = isOutputField + + def widgetClass(self): + return "YLabel" + + def text(self): + return self._text + + def setText(self, new_text): + self._text = new_text + if self._backend_widget: + self._backend_widget.set_text(new_text) + + def _create_backend_widget(self): + self._backend_widget = Gtk.Label(label=self._text) + self._backend_widget.set_xalign(0.0) # Left align + + if self._is_heading: + markup = f"{self._text}" + self._backend_widget.set_markup(markup) + +class YInputFieldGtk(YWidget): + def __init__(self, parent=None, label="", password_mode=False): + super().__init__(parent) + self._label = label + self._value = "" + self._password_mode = password_mode + + def widgetClass(self): + return "YInputField" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_entry_widget') and self._entry_widget: + self._entry_widget.set_text(text) + + def label(self): + return self._label + + def _create_backend_widget(self): + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + + if self._label: + label = Gtk.Label(label=self._label) + label.set_xalign(0.0) + hbox.pack_start(label, False, False, 0) + + if self._password_mode: + entry = Gtk.Entry() + entry.set_visibility(False) + else: + entry = Gtk.Entry() + + entry.set_text(self._value) + entry.connect("changed", self._on_changed) + + hbox.pack_start(entry, True, True, 0) + self._backend_widget = hbox + self._entry_widget = entry + + def _on_changed(self, entry): + self._value = entry.get_text() + +class YPushButtonGtk(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + + def widgetClass(self): + return "YPushButton" + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + if self._backend_widget: + self._backend_widget.set_label(label) + + def _create_backend_widget(self): + self._backend_widget = Gtk.Button(label=self._label) + self._backend_widget.connect("clicked", self._on_clicked) + + def _on_clicked(self, button): + print(f"Button clicked: {self._label}") + +class YCheckBoxGtk(YWidget): + def __init__(self, parent=None, label="", is_checked=False): + super().__init__(parent) + self._label = label + self._is_checked = is_checked + + def widgetClass(self): + return "YCheckBox" + + def value(self): + return self._is_checked + + def setValue(self, checked): + self._is_checked = checked + if self._backend_widget: + self._backend_widget.set_active(checked) + + def label(self): + return self._label + + def _create_backend_widget(self): + self._backend_widget = Gtk.CheckButton(label=self._label) + self._backend_widget.set_active(self._is_checked) + self._backend_widget.connect("toggled", self._on_toggled) + + def _on_toggled(self, button): + self._is_checked = button.get_active() + print(f"Checkbox toggled: {self._label} = {self._is_checked}") + +class YComboBoxGtk(YSelectionWidget): + def __init__(self, parent=None, label="", editable=False): + super().__init__(parent) + self._label = label + self._editable = editable + self._value = "" + + def widgetClass(self): + return "YComboBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_combo_widget') and self._combo_widget: + if self._editable: + self._combo_widget.get_child().set_text(text) + else: + # Find and select the item + for i, item in enumerate(self._items): + if item.label() == text: + self._combo_widget.set_active(i) + break + + def editable(self): + return self._editable + + def _create_backend_widget(self): + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + + if self._label: + label = Gtk.Label(label=self._label) + label.set_xalign(0.0) + hbox.pack_start(label, False, False, 0) + + if self._editable: + # Create a ComboBoxText that is editable + combo = Gtk.ComboBoxText.new_with_entry() + combo.get_child().connect("changed", self._on_text_changed) + else: + combo = Gtk.ComboBoxText() + combo.connect("changed", self._on_changed) + + # Add items to combo box + for item in self._items: + combo.append_text(item.label()) + + hbox.pack_start(combo, True, True, 0) + self._backend_widget = hbox + self._combo_widget = combo + + def _on_text_changed(self, entry): + self._value = entry.get_text() + + def _on_changed(self, combo): + active_id = combo.get_active() + if active_id >= 0: + self._value = combo.get_active_text() + # Update selected items + self._selected_items = [] + for item in self._items: + if item.label() == self._value: + self._selected_items.append(item) + break diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py new file mode 100644 index 0000000..3ccab46 --- /dev/null +++ b/manatools/aui/yui_qt.py @@ -0,0 +1,389 @@ +""" +Qt backend implementation for YUI +""" + +import sys +from PyQt5 import QtWidgets, QtCore, QtGui +from .yui_common import * + +class YUIQt: + def __init__(self): + self._widget_factory = YWidgetFactoryQt() + self._optional_widget_factory = None + self._application = YApplicationQt() + + # Ensure QApplication exists + self._qapp = QtWidgets.QApplication.instance() + if not self._qapp: + self._qapp = QtWidgets.QApplication(sys.argv) + + def widgetFactory(self): + return self._widget_factory + + def optionalWidgetFactory(self): + return self._optional_widget_factory + + def app(self): + return self._application + + def application(self): + return self._application + + def yApp(self): + return self._application + +class YApplicationQt: + def __init__(self): + self._icon_base_path = "/usr/share/icons" + self._product_name = "YUI Qt" + + def iconBasePath(self): + return self._icon_base_path + + def setIconBasePath(self, new_icon_base_path): + self._icon_base_path = new_icon_base_path + + def setProductName(self, product_name): + self._product_name = product_name + + def productName(self): + return self._product_name + +class YWidgetFactoryQt: + def __init__(self): + pass + + def createMainDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): + return YDialogQt(YDialogType.YMainDialog, color_mode) + + def createPopupDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): + return YDialogQt(YDialogType.YPopupDialog, color_mode) + + def createVBox(self, parent): + return YVBoxQt(parent) + + def createHBox(self, parent): + return YHBoxQt(parent) + + def createPushButton(self, parent, label): + return YPushButtonQt(parent, label) + + def createLabel(self, parent, text, isHeading=False, isOutputField=False): + return YLabelQt(parent, text, isHeading, isOutputField) + + def createHeading(self, parent, label): + return YLabelQt(parent, label, isHeading=True) + + def createInputField(self, parent, label, password_mode=False): + return YInputFieldQt(parent, label, password_mode) + + def createCheckBox(self, parent, label, is_checked=False): + return YCheckBoxQt(parent, label, is_checked) + + def createPasswordField(self, parent, label): + return YInputFieldQt(parent, label, password_mode=True) + + def createComboBox(self, parent, label, editable=False): + return YComboBoxQt(parent, label, editable) + + def createSelectionBox(self, parent, label): + return YSelectionBoxQt(parent, label) + + def createProgressBar(self, parent, label, max_value=100): + return YProgressBarQt(parent, label, max_value) + + def createComboBox(self, parent, label, editable=False): + return YComboBoxQt(parent, label, editable) + +# Qt Widget Implementations +class YDialogQt(YSingleChildContainerWidget): + _open_dialogs = [] + + def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): + super().__init__() + self._dialog_type = dialog_type + self._color_mode = color_mode + self._is_open = False + self._qwidget = None + YDialogQt._open_dialogs.append(self) + + def widgetClass(self): + return "YDialog" + + def open(self): + if not self._qwidget: + self._create_backend_widget() + + self._qwidget.show() + self._is_open = True + + # Start Qt event loop if not already running + app = QtWidgets.QApplication.instance() + if app: + app.exec_() + + def isOpen(self): + return self._is_open + + def destroy(self, doThrow=True): + if self._qwidget: + self._qwidget.close() + self._qwidget = None + self._is_open = False + if self in YDialogQt._open_dialogs: + YDialogQt._open_dialogs.remove(self) + return True + + @classmethod + def deleteTopmostDialog(cls, doThrow=True): + if cls._open_dialogs: + dialog = cls._open_dialogs[-1] + return dialog.destroy(doThrow) + return False + + @classmethod + def currentDialog(cls, doThrow=True): + if not cls._open_dialogs: + if doThrow: + raise YUINoDialogException("No dialog open") + return None + return cls._open_dialogs[-1] + + def _create_backend_widget(self): + self._qwidget = QtWidgets.QMainWindow() + self._qwidget.setWindowTitle("YUI Qt Dialog") + self._qwidget.resize(600, 400) + + central_widget = QtWidgets.QWidget() + self._qwidget.setCentralWidget(central_widget) + + if self._child: + layout = QtWidgets.QVBoxLayout(central_widget) + layout.addWidget(self._child.get_backend_widget()) + + self._backend_widget = self._qwidget + self._qwidget.closeEvent = self._on_close_event + + def _on_close_event(self, event): + self.destroy() + event.accept() + +class YVBoxQt(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YVBox" + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(self._backend_widget) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) + + for child in self._children: + widget = child.get_backend_widget() + expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 + layout.addWidget(widget, stretch=expand) + +class YHBoxQt(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YHBox" + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(self._backend_widget) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) + + for child in self._children: + widget = child.get_backend_widget() + expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 + layout.addWidget(widget, stretch=expand) + +class YLabelQt(YWidget): + def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): + super().__init__(parent) + self._text = text + self._is_heading = isHeading + self._is_output_field = isOutputField + + def widgetClass(self): + return "YLabel" + + def text(self): + return self._text + + def setText(self, new_text): + self._text = new_text + if self._backend_widget: + self._backend_widget.setText(new_text) + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QLabel(self._text) + if self._is_heading: + font = self._backend_widget.font() + font.setBold(True) + font.setPointSize(font.pointSize() + 2) + self._backend_widget.setFont(font) + +class YInputFieldQt(YWidget): + def __init__(self, parent=None, label="", password_mode=False): + super().__init__(parent) + self._label = label + self._value = "" + self._password_mode = password_mode + + def widgetClass(self): + return "YInputField" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_entry_widget') and self._entry_widget: + self._entry_widget.setText(text) + + def label(self): + return self._label + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + + if self._label: + label = QtWidgets.QLabel(self._label) + layout.addWidget(label) + + if self._password_mode: + entry = QtWidgets.QLineEdit() + entry.setEchoMode(QtWidgets.QLineEdit.Password) + else: + entry = QtWidgets.QLineEdit() + + entry.setText(self._value) + entry.textChanged.connect(self._on_text_changed) + layout.addWidget(entry) + + self._backend_widget = container + self._entry_widget = entry + + def _on_text_changed(self, text): + self._value = text + +class YPushButtonQt(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + + def widgetClass(self): + return "YPushButton" + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + if self._backend_widget: + self._backend_widget.setText(label) + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QPushButton(self._label) + self._backend_widget.clicked.connect(self._on_clicked) + + def _on_clicked(self): + print(f"Button clicked: {self._label}") + # In a real implementation, this would send a YEvent + +class YCheckBoxQt(YWidget): + def __init__(self, parent=None, label="", is_checked=False): + super().__init__(parent) + self._label = label + self._is_checked = is_checked + + def widgetClass(self): + return "YCheckBox" + + def value(self): + return self._is_checked + + def setValue(self, checked): + self._is_checked = checked + if self._backend_widget: + self._backend_widget.setChecked(checked) + + def label(self): + return self._label + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QCheckBox(self._label) + self._backend_widget.setChecked(self._is_checked) + self._backend_widget.stateChanged.connect(self._on_state_changed) + + def _on_state_changed(self, state): + self._is_checked = (state == QtCore.Qt.Checked) + print(f"Checkbox toggled: {self._label} = {self._is_checked}") + +class YComboBoxQt(YSelectionWidget): + def __init__(self, parent=None, label="", editable=False): + super().__init__(parent) + self._label = label + self._editable = editable + self._value = "" + + def widgetClass(self): + return "YComboBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_combo_widget') and self._combo_widget: + index = self._combo_widget.findText(text) + if index >= 0: + self._combo_widget.setCurrentIndex(index) + elif self._editable: + self._combo_widget.setEditText(text) + + def editable(self): + return self._editable + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + + if self._label: + label = QtWidgets.QLabel(self._label) + layout.addWidget(label) + + if self._editable: + combo = QtWidgets.QComboBox() + combo.setEditable(True) + else: + combo = QtWidgets.QComboBox() + + # Add items to combo box + for item in self._items: + combo.addItem(item.label()) + + combo.currentTextChanged.connect(self._on_text_changed) + layout.addWidget(combo) + + self._backend_widget = container + self._combo_widget = combo + + def _on_text_changed(self, text): + self._value = text + # Update selected items + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break diff --git a/setup.py b/setup.py index 032d205..e10a481 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ version=__project_version__, author='Angelo Naselli', author_email='anaselli@linux.it', - packages=['manatools', 'manatools.ui'], + packages=['manatools', 'manatools.aui', 'manatools.ui'], #scripts=['scripts/'], license='LGPLv2+', description='Python ManaTools framework.', diff --git a/sow/TODO.md b/sow/TODO.md new file mode 100644 index 0000000..246fe3f --- /dev/null +++ b/sow/TODO.md @@ -0,0 +1,27 @@ +The goal of this branch is to explore creating a pure-Python implementation of the libyui binding interface. + +While libyui has been a valuable dependency for this project for many years (and continues to be in other branches), this move aims to remove the hard dependency. We hope this will provide a more manageable backend and facilitate a smoother transition for related tools, **especially for dnfdragora**. + +As this is a non-profit project, we rely on developers who are contributing in their **very limited spare time**. To accelerate this effort, we are leveraging AI to assist with a significant portion of the development work. + +Next is the starting todo list. + +Missing Widgets comparing libyui: + + [ ] YComboBox (on going) + [ ] YSelectionBox + [ ] YMultiSelectionBox + [ ] YTree + [ ] YTable + [ ] YProgressBar + [ ] YRichText + [ ] YMultiLineEdit + [ ] YIntField + [ ] YMenuButton, YMenuBar + [ ] YWizard + [ ] YPackageSelector + [ ] YSpacing, YAlignment + [ ] YReplacePoint + [ ] YRadioButton, YRadioButtonGroup + +To check how to manage YEvents and YItems. diff --git a/sow/yui.py b/sow/yui.py new file mode 100644 index 0000000..cdc14e2 --- /dev/null +++ b/sow/yui.py @@ -0,0 +1,7656 @@ +# This file was automatically generated by SWIG (http://www.swig.org). +# Version 4.0.2 +# +# Do not make changes to this file unless you know what you are doing--modify +# the SWIG interface file instead. + +from sys import version_info as _swig_python_version_info +if _swig_python_version_info < (2, 7, 0): + raise RuntimeError("Python 2.7 or later required") + +# Import the low-level C/C++ module +if __package__ or "." in __name__: + from . import _yui +else: + import _yui + +try: + import builtins as __builtin__ +except ImportError: + import __builtin__ + +def _swig_repr(self): + try: + strthis = "proxy of " + self.this.__repr__() + except __builtin__.Exception: + strthis = "" + return "<%s.%s; %s >" % (self.__class__.__module__, self.__class__.__name__, strthis,) + + +def _swig_setattr_nondynamic_instance_variable(set): + def set_instance_attr(self, name, value): + if name == "thisown": + self.this.own(value) + elif name == "this": + set(self, name, value) + elif hasattr(self, name) and isinstance(getattr(type(self), name), property): + set(self, name, value) + else: + raise AttributeError("You cannot add instance attributes to %s" % self) + return set_instance_attr + + +def _swig_setattr_nondynamic_class_variable(set): + def set_class_attr(cls, name, value): + if hasattr(cls, name) and not isinstance(getattr(cls, name), property): + set(cls, name, value) + else: + raise AttributeError("You cannot add class attributes to %s" % cls) + return set_class_attr + + +def _swig_add_metaclass(metaclass): + """Class decorator for adding a metaclass to a SWIG wrapped class - a slimmed down version of six.add_metaclass""" + def wrapper(cls): + return metaclass(cls.__name__, cls.__bases__, cls.__dict__.copy()) + return wrapper + + +class _SwigNonDynamicMeta(type): + """Meta class to enforce nondynamic attributes (no new attributes) for a class""" + __setattr__ = _swig_setattr_nondynamic_class_variable(type.__setattr__) + + +class SwigPyIterator(object): + r"""Proxy of C++ swig::SwigPyIterator class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_SwigPyIterator + + def value(self): + r"""value(SwigPyIterator self) -> PyObject *""" + return _yui.SwigPyIterator_value(self) + + def incr(self, n=1): + r"""incr(SwigPyIterator self, size_t n=1) -> SwigPyIterator""" + return _yui.SwigPyIterator_incr(self, n) + + def decr(self, n=1): + r"""decr(SwigPyIterator self, size_t n=1) -> SwigPyIterator""" + return _yui.SwigPyIterator_decr(self, n) + + def distance(self, x): + r"""distance(SwigPyIterator self, SwigPyIterator x) -> ptrdiff_t""" + return _yui.SwigPyIterator_distance(self, x) + + def equal(self, x): + r"""equal(SwigPyIterator self, SwigPyIterator x) -> bool""" + return _yui.SwigPyIterator_equal(self, x) + + def copy(self): + r"""copy(SwigPyIterator self) -> SwigPyIterator""" + return _yui.SwigPyIterator_copy(self) + + def next(self): + r"""next(SwigPyIterator self) -> PyObject *""" + return _yui.SwigPyIterator_next(self) + + def __next__(self): + r"""__next__(SwigPyIterator self) -> PyObject *""" + return _yui.SwigPyIterator___next__(self) + + def previous(self): + r"""previous(SwigPyIterator self) -> PyObject *""" + return _yui.SwigPyIterator_previous(self) + + def advance(self, n): + r"""advance(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" + return _yui.SwigPyIterator_advance(self, n) + + def __eq__(self, x): + r"""__eq__(SwigPyIterator self, SwigPyIterator x) -> bool""" + return _yui.SwigPyIterator___eq__(self, x) + + def __ne__(self, x): + r"""__ne__(SwigPyIterator self, SwigPyIterator x) -> bool""" + return _yui.SwigPyIterator___ne__(self, x) + + def __iadd__(self, n): + r"""__iadd__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" + return _yui.SwigPyIterator___iadd__(self, n) + + def __isub__(self, n): + r"""__isub__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" + return _yui.SwigPyIterator___isub__(self, n) + + def __add__(self, n): + r"""__add__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" + return _yui.SwigPyIterator___add__(self, n) + + def __sub__(self, *args): + r""" + __sub__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator + __sub__(SwigPyIterator self, SwigPyIterator x) -> ptrdiff_t + """ + return _yui.SwigPyIterator___sub__(self, *args) + def __iter__(self): + return self + +# Register SwigPyIterator in _yui: +_yui.SwigPyIterator_swigregister(SwigPyIterator) + +class YUI(object): + r"""Proxy of C++ YUI class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YUI + + def shutdownThreads(self): + r"""shutdownThreads(YUI self)""" + return _yui.YUI_shutdownThreads(self) + + @staticmethod + def ui(): + r"""ui() -> YUI""" + return _yui.YUI_ui() + + @staticmethod + def widgetFactory(): + r"""widgetFactory() -> YWidgetFactory""" + return _yui.YUI_widgetFactory() + + @staticmethod + def optionalWidgetFactory(): + r"""optionalWidgetFactory() -> YOptionalWidgetFactory""" + return _yui.YUI_optionalWidgetFactory() + + @staticmethod + def app(): + r"""app() -> YApplication""" + return _yui.YUI_app() + + @staticmethod + def application(): + r"""application() -> YApplication""" + return _yui.YUI_application() + + @staticmethod + def yApp(): + r"""yApp() -> YApplication""" + return _yui.YUI_yApp() + + @staticmethod + def ensureUICreated(): + r"""ensureUICreated()""" + return _yui.YUI_ensureUICreated() + + def blockEvents(self, block=True): + r"""blockEvents(YUI self, bool block=True)""" + return _yui.YUI_blockEvents(self, block) + + def unblockEvents(self): + r"""unblockEvents(YUI self)""" + return _yui.YUI_unblockEvents(self) + + def eventsBlocked(self): + r"""eventsBlocked(YUI self) -> bool""" + return _yui.YUI_eventsBlocked(self) + + def deleteNotify(self, widget): + r"""deleteNotify(YUI self, YWidget widget)""" + return _yui.YUI_deleteNotify(self, widget) + + def topmostConstructorHasFinished(self): + r"""topmostConstructorHasFinished(YUI self)""" + return _yui.YUI_topmostConstructorHasFinished(self) + + def runningWithThreads(self): + r"""runningWithThreads(YUI self) -> bool""" + return _yui.YUI_runningWithThreads(self) + + def uiThreadMainLoop(self): + r"""uiThreadMainLoop(YUI self)""" + return _yui.YUI_uiThreadMainLoop(self) + + def builtinCaller(self): + r"""builtinCaller(YUI self) -> YBuiltinCaller""" + return _yui.YUI_builtinCaller(self) + + def setBuiltinCaller(self, caller): + r"""setBuiltinCaller(YUI self, YBuiltinCaller caller)""" + return _yui.YUI_setBuiltinCaller(self, caller) + + def runPkgSelection(self, packageSelector): + r"""runPkgSelection(YUI self, YWidget packageSelector) -> YEvent""" + return _yui.YUI_runPkgSelection(self, packageSelector) + + def sendWidgetID(self, id): + r"""sendWidgetID(YUI self, std::string const & id) -> YWidget""" + return _yui.YUI_sendWidgetID(self, id) + +# Register YUI in _yui: +_yui.YUI_swigregister(YUI) + +def YUI_ui(): + r"""YUI_ui() -> YUI""" + return _yui.YUI_ui() + +def YUI_widgetFactory(): + r"""YUI_widgetFactory() -> YWidgetFactory""" + return _yui.YUI_widgetFactory() + +def YUI_optionalWidgetFactory(): + r"""YUI_optionalWidgetFactory() -> YOptionalWidgetFactory""" + return _yui.YUI_optionalWidgetFactory() + +def YUI_app(): + r"""YUI_app() -> YApplication""" + return _yui.YUI_app() + +def YUI_application(): + r"""YUI_application() -> YApplication""" + return _yui.YUI_application() + +def YUI_yApp(): + r"""YUI_yApp() -> YApplication""" + return _yui.YUI_yApp() + +def YUI_ensureUICreated(): + r"""YUI_ensureUICreated()""" + return _yui.YUI_ensureUICreated() + +def start_ui_thread(ui_int): + r"""start_ui_thread(void * ui_int) -> void *""" + return _yui.start_ui_thread(ui_int) + +YUI_LOG_DEBUG = _yui.YUI_LOG_DEBUG + +YUI_LOG_MILESTONE = _yui.YUI_LOG_MILESTONE + +YUI_LOG_WARNING = _yui.YUI_LOG_WARNING + +YUI_LOG_ERROR = _yui.YUI_LOG_ERROR + +class YUILog(object): + r"""Proxy of C++ YUILog class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + + @staticmethod + def debug(logComponent, sourceFileName, lineNo, functionName): + r"""debug(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_debug(logComponent, sourceFileName, lineNo, functionName) + + @staticmethod + def milestone(logComponent, sourceFileName, lineNo, functionName): + r"""milestone(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_milestone(logComponent, sourceFileName, lineNo, functionName) + + @staticmethod + def warning(logComponent, sourceFileName, lineNo, functionName): + r"""warning(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_warning(logComponent, sourceFileName, lineNo, functionName) + + @staticmethod + def error(logComponent, sourceFileName, lineNo, functionName): + r"""error(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_error(logComponent, sourceFileName, lineNo, functionName) + + def log(self, logLevel, logComponent, sourceFileName, lineNo, functionName): + r"""log(YUILog self, YUILogLevel_t logLevel, char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_log(self, logLevel, logComponent, sourceFileName, lineNo, functionName) + + @staticmethod + def instance(): + r"""instance() -> YUILog""" + return _yui.YUILog_instance() + + @staticmethod + def enableDebugLogging(debugLogging=True): + r"""enableDebugLogging(bool debugLogging=True)""" + return _yui.YUILog_enableDebugLogging(debugLogging) + + @staticmethod + def debugLoggingEnabled(): + r"""debugLoggingEnabled() -> bool""" + return _yui.YUILog_debugLoggingEnabled() + + @staticmethod + def setLogFileName(logFileName): + r"""setLogFileName(std::string const & logFileName) -> bool""" + return _yui.YUILog_setLogFileName(logFileName) + + @staticmethod + def logFileName(): + r"""logFileName() -> std::string""" + return _yui.YUILog_logFileName() + + @staticmethod + def setLoggerFunction(loggerFunction): + r"""setLoggerFunction(YUILoggerFunction loggerFunction)""" + return _yui.YUILog_setLoggerFunction(loggerFunction) + + @staticmethod + def loggerFunction(returnStdLogger=False): + r"""loggerFunction(bool returnStdLogger=False) -> YUILoggerFunction""" + return _yui.YUILog_loggerFunction(returnStdLogger) + + @staticmethod + def setEnableDebugLoggingHooks(enableFunction, isEnabledFunction): + r"""setEnableDebugLoggingHooks(YUIEnableDebugLoggingFunction enableFunction, YUIDebugLoggingEnabledFunction isEnabledFunction)""" + return _yui.YUILog_setEnableDebugLoggingHooks(enableFunction, isEnabledFunction) + + @staticmethod + def enableDebugLoggingHook(): + r"""enableDebugLoggingHook() -> YUIEnableDebugLoggingFunction""" + return _yui.YUILog_enableDebugLoggingHook() + + @staticmethod + def debugLoggingEnabledHook(): + r"""debugLoggingEnabledHook() -> YUIDebugLoggingEnabledFunction""" + return _yui.YUILog_debugLoggingEnabledHook() + + @staticmethod + def basename(fileNameWithPath): + r"""basename(std::string const & fileNameWithPath) -> std::string""" + return _yui.YUILog_basename(fileNameWithPath) + +# Register YUILog in _yui: +_yui.YUILog_swigregister(YUILog) + +def YUILog_debug(logComponent, sourceFileName, lineNo, functionName): + r"""YUILog_debug(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_debug(logComponent, sourceFileName, lineNo, functionName) + +def YUILog_milestone(logComponent, sourceFileName, lineNo, functionName): + r"""YUILog_milestone(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_milestone(logComponent, sourceFileName, lineNo, functionName) + +def YUILog_warning(logComponent, sourceFileName, lineNo, functionName): + r"""YUILog_warning(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_warning(logComponent, sourceFileName, lineNo, functionName) + +def YUILog_error(logComponent, sourceFileName, lineNo, functionName): + r"""YUILog_error(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" + return _yui.YUILog_error(logComponent, sourceFileName, lineNo, functionName) + +def YUILog_instance(): + r"""YUILog_instance() -> YUILog""" + return _yui.YUILog_instance() + +def YUILog_enableDebugLogging(debugLogging=True): + r"""YUILog_enableDebugLogging(bool debugLogging=True)""" + return _yui.YUILog_enableDebugLogging(debugLogging) + +def YUILog_debugLoggingEnabled(): + r"""YUILog_debugLoggingEnabled() -> bool""" + return _yui.YUILog_debugLoggingEnabled() + +def YUILog_setLogFileName(logFileName): + r"""YUILog_setLogFileName(std::string const & logFileName) -> bool""" + return _yui.YUILog_setLogFileName(logFileName) + +def YUILog_logFileName(): + r"""YUILog_logFileName() -> std::string""" + return _yui.YUILog_logFileName() + +def YUILog_setLoggerFunction(loggerFunction): + r"""YUILog_setLoggerFunction(YUILoggerFunction loggerFunction)""" + return _yui.YUILog_setLoggerFunction(loggerFunction) + +def YUILog_loggerFunction(returnStdLogger=False): + r"""YUILog_loggerFunction(bool returnStdLogger=False) -> YUILoggerFunction""" + return _yui.YUILog_loggerFunction(returnStdLogger) + +def YUILog_setEnableDebugLoggingHooks(enableFunction, isEnabledFunction): + r"""YUILog_setEnableDebugLoggingHooks(YUIEnableDebugLoggingFunction enableFunction, YUIDebugLoggingEnabledFunction isEnabledFunction)""" + return _yui.YUILog_setEnableDebugLoggingHooks(enableFunction, isEnabledFunction) + +def YUILog_enableDebugLoggingHook(): + r"""YUILog_enableDebugLoggingHook() -> YUIEnableDebugLoggingFunction""" + return _yui.YUILog_enableDebugLoggingHook() + +def YUILog_debugLoggingEnabledHook(): + r"""YUILog_debugLoggingEnabledHook() -> YUIDebugLoggingEnabledFunction""" + return _yui.YUILog_debugLoggingEnabledHook() + +def YUILog_basename(fileNameWithPath): + r"""YUILog_basename(std::string const & fileNameWithPath) -> std::string""" + return _yui.YUILog_basename(fileNameWithPath) + +class YUIPlugin(object): + r"""Proxy of C++ YUIPlugin class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, pluginLibBaseName): + r"""__init__(YUIPlugin self, char const * pluginLibBaseName) -> YUIPlugin""" + _yui.YUIPlugin_swiginit(self, _yui.new_YUIPlugin(pluginLibBaseName)) + __swig_destroy__ = _yui.delete_YUIPlugin + + def unload(self): + r"""unload(YUIPlugin self)""" + return _yui.YUIPlugin_unload(self) + + def locateSymbol(self, symbol): + r"""locateSymbol(YUIPlugin self, char const * symbol) -> void *""" + return _yui.YUIPlugin_locateSymbol(self, symbol) + + def error(self): + r"""error(YUIPlugin self) -> bool""" + return _yui.YUIPlugin_error(self) + + def success(self): + r"""success(YUIPlugin self) -> bool""" + return _yui.YUIPlugin_success(self) + + def errorMsg(self): + r"""errorMsg(YUIPlugin self) -> std::string""" + return _yui.YUIPlugin_errorMsg(self) + +# Register YUIPlugin in _yui: +_yui.YUIPlugin_swigregister(YUIPlugin) + +YUIAllDimensions = _yui.YUIAllDimensions + +YD_HORIZ = _yui.YD_HORIZ + +YD_VERT = _yui.YD_VERT + +YAlignUnchanged = _yui.YAlignUnchanged + +YAlignBegin = _yui.YAlignBegin + +YAlignEnd = _yui.YAlignEnd + +YAlignCenter = _yui.YAlignCenter + +YMainDialog = _yui.YMainDialog + +YPopupDialog = _yui.YPopupDialog + +YWizardDialog = _yui.YWizardDialog + +YDialogNormalColor = _yui.YDialogNormalColor + +YDialogInfoColor = _yui.YDialogInfoColor + +YDialogWarnColor = _yui.YDialogWarnColor + +YCustomButton = _yui.YCustomButton + +YOKButton = _yui.YOKButton + +YApplyButton = _yui.YApplyButton + +YCancelButton = _yui.YCancelButton + +YHelpButton = _yui.YHelpButton + +YRelNotesButton = _yui.YRelNotesButton + +YMaxButtonRole = _yui.YMaxButtonRole + +YKDEButtonOrder = _yui.YKDEButtonOrder + +YGnomeButtonOrder = _yui.YGnomeButtonOrder + +class YWidget(object): + r"""Proxy of C++ YWidget class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YWidget + + def widgetClass(self): + r"""widgetClass(YWidget self) -> char const *""" + return _yui.YWidget_widgetClass(self) + + def debugLabel(self): + r"""debugLabel(YWidget self) -> std::string""" + return _yui.YWidget_debugLabel(self) + + def helpText(self): + r"""helpText(YWidget self) -> std::string""" + return _yui.YWidget_helpText(self) + + def setHelpText(self, helpText): + r"""setHelpText(YWidget self, std::string const & helpText)""" + return _yui.YWidget_setHelpText(self, helpText) + + def propertySet(self): + r"""propertySet(YWidget self) -> YPropertySet""" + return _yui.YWidget_propertySet(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YWidget self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YWidget_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YWidget self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YWidget_getProperty(self, propertyName) + + def hasChildren(self): + r"""hasChildren(YWidget self) -> bool""" + return _yui.YWidget_hasChildren(self) + + def firstChild(self): + r"""firstChild(YWidget self) -> YWidget""" + return _yui.YWidget_firstChild(self) + + def lastChild(self): + r"""lastChild(YWidget self) -> YWidget""" + return _yui.YWidget_lastChild(self) + + def childrenBegin(self): + r"""childrenBegin(YWidget self) -> YWidgetListIterator""" + return _yui.YWidget_childrenBegin(self) + + def childrenEnd(self): + r"""childrenEnd(YWidget self) -> YWidgetListIterator""" + return _yui.YWidget_childrenEnd(self) + + def childrenConstBegin(self): + r"""childrenConstBegin(YWidget self) -> YWidgetListConstIterator""" + return _yui.YWidget_childrenConstBegin(self) + + def childrenConstEnd(self): + r"""childrenConstEnd(YWidget self) -> YWidgetListConstIterator""" + return _yui.YWidget_childrenConstEnd(self) + + def begin(self): + r"""begin(YWidget self) -> YWidgetListIterator""" + return _yui.YWidget_begin(self) + + def end(self): + r"""end(YWidget self) -> YWidgetListIterator""" + return _yui.YWidget_end(self) + + def childrenCount(self): + r"""childrenCount(YWidget self) -> int""" + return _yui.YWidget_childrenCount(self) + + def contains(self, child): + r"""contains(YWidget self, YWidget child) -> bool""" + return _yui.YWidget_contains(self, child) + + def addChild(self, child): + r"""addChild(YWidget self, YWidget child)""" + return _yui.YWidget_addChild(self, child) + + def removeChild(self, child): + r"""removeChild(YWidget self, YWidget child)""" + return _yui.YWidget_removeChild(self, child) + + def deleteChildren(self): + r"""deleteChildren(YWidget self)""" + return _yui.YWidget_deleteChildren(self) + + def parent(self): + r"""parent(YWidget self) -> YWidget""" + return _yui.YWidget_parent(self) + + def hasParent(self): + r"""hasParent(YWidget self) -> bool""" + return _yui.YWidget_hasParent(self) + + def setParent(self, newParent): + r"""setParent(YWidget self, YWidget newParent)""" + return _yui.YWidget_setParent(self, newParent) + + def findDialog(self): + r"""findDialog(YWidget self) -> YDialog""" + return _yui.YWidget_findDialog(self) + + def findWidget(self, id, doThrow=True): + r"""findWidget(YWidget self, YWidgetID id, bool doThrow=True) -> YWidget""" + return _yui.YWidget_findWidget(self, id, doThrow) + + def preferredWidth(self): + r"""preferredWidth(YWidget self) -> int""" + return _yui.YWidget_preferredWidth(self) + + def preferredHeight(self): + r"""preferredHeight(YWidget self) -> int""" + return _yui.YWidget_preferredHeight(self) + + def preferredSize(self, dim): + r"""preferredSize(YWidget self, YUIDimension dim) -> int""" + return _yui.YWidget_preferredSize(self, dim) + + def setSize(self, newWidth, newHeight): + r"""setSize(YWidget self, int newWidth, int newHeight)""" + return _yui.YWidget_setSize(self, newWidth, newHeight) + + def isValid(self): + r"""isValid(YWidget self) -> bool""" + return _yui.YWidget_isValid(self) + + def beingDestroyed(self): + r"""beingDestroyed(YWidget self) -> bool""" + return _yui.YWidget_beingDestroyed(self) + + def widgetRep(self): + r"""widgetRep(YWidget self) -> void *""" + return _yui.YWidget_widgetRep(self) + + def setWidgetRep(self, toolkitWidgetRep): + r"""setWidgetRep(YWidget self, void * toolkitWidgetRep)""" + return _yui.YWidget_setWidgetRep(self, toolkitWidgetRep) + + def hasId(self): + r"""hasId(YWidget self) -> bool""" + return _yui.YWidget_hasId(self) + + def id(self): + r"""id(YWidget self) -> YWidgetID""" + return _yui.YWidget_id(self) + + def setId(self, newId_disown): + r"""setId(YWidget self, YWidgetID newId_disown)""" + return _yui.YWidget_setId(self, newId_disown) + + def setEnabled(self, enabled=True): + r"""setEnabled(YWidget self, bool enabled=True)""" + return _yui.YWidget_setEnabled(self, enabled) + + def setDisabled(self): + r"""setDisabled(YWidget self)""" + return _yui.YWidget_setDisabled(self) + + def isEnabled(self): + r"""isEnabled(YWidget self) -> bool""" + return _yui.YWidget_isEnabled(self) + + def stretchable(self, dim): + r"""stretchable(YWidget self, YUIDimension dim) -> bool""" + return _yui.YWidget_stretchable(self, dim) + + def setStretchable(self, dim, newStretch): + r"""setStretchable(YWidget self, YUIDimension dim, bool newStretch)""" + return _yui.YWidget_setStretchable(self, dim, newStretch) + + def setDefaultStretchable(self, dim, newStretch): + r"""setDefaultStretchable(YWidget self, YUIDimension dim, bool newStretch)""" + return _yui.YWidget_setDefaultStretchable(self, dim, newStretch) + + def weight(self, dim): + r"""weight(YWidget self, YUIDimension dim) -> int""" + return _yui.YWidget_weight(self, dim) + + def hasWeight(self, dim): + r"""hasWeight(YWidget self, YUIDimension dim) -> bool""" + return _yui.YWidget_hasWeight(self, dim) + + def setWeight(self, dim, weight): + r"""setWeight(YWidget self, YUIDimension dim, int weight)""" + return _yui.YWidget_setWeight(self, dim, weight) + + def setNotify(self, notify=True): + r"""setNotify(YWidget self, bool notify=True)""" + return _yui.YWidget_setNotify(self, notify) + + def notify(self): + r"""notify(YWidget self) -> bool""" + return _yui.YWidget_notify(self) + + def setNotifyContextMenu(self, notifyContextMenu=True): + r"""setNotifyContextMenu(YWidget self, bool notifyContextMenu=True)""" + return _yui.YWidget_setNotifyContextMenu(self, notifyContextMenu) + + def notifyContextMenu(self): + r"""notifyContextMenu(YWidget self) -> bool""" + return _yui.YWidget_notifyContextMenu(self) + + def sendKeyEvents(self): + r"""sendKeyEvents(YWidget self) -> bool""" + return _yui.YWidget_sendKeyEvents(self) + + def setSendKeyEvents(self, doSend): + r"""setSendKeyEvents(YWidget self, bool doSend)""" + return _yui.YWidget_setSendKeyEvents(self, doSend) + + def autoShortcut(self): + r"""autoShortcut(YWidget self) -> bool""" + return _yui.YWidget_autoShortcut(self) + + def setAutoShortcut(self, _newAutoShortcut): + r"""setAutoShortcut(YWidget self, bool _newAutoShortcut)""" + return _yui.YWidget_setAutoShortcut(self, _newAutoShortcut) + + def functionKey(self): + r"""functionKey(YWidget self) -> int""" + return _yui.YWidget_functionKey(self) + + def hasFunctionKey(self): + r"""hasFunctionKey(YWidget self) -> bool""" + return _yui.YWidget_hasFunctionKey(self) + + def setFunctionKey(self, fkey_no): + r"""setFunctionKey(YWidget self, int fkey_no)""" + return _yui.YWidget_setFunctionKey(self, fkey_no) + + def setKeyboardFocus(self): + r"""setKeyboardFocus(YWidget self) -> bool""" + return _yui.YWidget_setKeyboardFocus(self) + + def shortcutString(self): + r"""shortcutString(YWidget self) -> std::string""" + return _yui.YWidget_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YWidget self, std::string const & str)""" + return _yui.YWidget_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YWidget self) -> char const *""" + return _yui.YWidget_userInputProperty(self) + + def dumpWidgetTree(self, indentationLevel=0): + r"""dumpWidgetTree(YWidget self, int indentationLevel=0)""" + return _yui.YWidget_dumpWidgetTree(self, indentationLevel) + + def dumpDialogWidgetTree(self): + r"""dumpDialogWidgetTree(YWidget self)""" + return _yui.YWidget_dumpDialogWidgetTree(self) + + def setChildrenEnabled(self, enabled): + r"""setChildrenEnabled(YWidget self, bool enabled)""" + return _yui.YWidget_setChildrenEnabled(self, enabled) + + def saveUserInput(self, macroRecorder): + r"""saveUserInput(YWidget self, YMacroRecorder macroRecorder)""" + return _yui.YWidget_saveUserInput(self, macroRecorder) + + def startMultipleChanges(self): + r"""startMultipleChanges(YWidget self)""" + return _yui.YWidget_startMultipleChanges(self) + + def doneMultipleChanges(self): + r"""doneMultipleChanges(YWidget self)""" + return _yui.YWidget_doneMultipleChanges(self) + + def __eq__(self, w): + r"""__eq__(YWidget self, YWidget w) -> int""" + return _yui.YWidget___eq__(self, w) + + def __ne__(self, w): + r"""__ne__(YWidget self, YWidget w) -> int""" + return _yui.YWidget___ne__(self, w) + + def equals(self, w): + r"""equals(YWidget self, YWidget w) -> int""" + return _yui.YWidget_equals(self, w) + +# Register YWidget in _yui: +_yui.YWidget_swigregister(YWidget) + +class YSingleChildContainerWidget(YWidget): + r"""Proxy of C++ YSingleChildContainerWidget class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YSingleChildContainerWidget + + def preferredWidth(self): + r"""preferredWidth(YSingleChildContainerWidget self) -> int""" + return _yui.YSingleChildContainerWidget_preferredWidth(self) + + def preferredHeight(self): + r"""preferredHeight(YSingleChildContainerWidget self) -> int""" + return _yui.YSingleChildContainerWidget_preferredHeight(self) + + def setSize(self, newWidth, newHeight): + r"""setSize(YSingleChildContainerWidget self, int newWidth, int newHeight)""" + return _yui.YSingleChildContainerWidget_setSize(self, newWidth, newHeight) + + def stretchable(self, dim): + r"""stretchable(YSingleChildContainerWidget self, YUIDimension dim) -> bool""" + return _yui.YSingleChildContainerWidget_stretchable(self, dim) + +# Register YSingleChildContainerWidget in _yui: +_yui.YSingleChildContainerWidget_swigregister(YSingleChildContainerWidget) + +class YSelectionWidget(YWidget): + r"""Proxy of C++ YSelectionWidget class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YSelectionWidget + + def widgetClass(self): + r"""widgetClass(YSelectionWidget self) -> char const *""" + return _yui.YSelectionWidget_widgetClass(self) + + def label(self): + r"""label(YSelectionWidget self) -> std::string""" + return _yui.YSelectionWidget_label(self) + + def setLabel(self, newLabel): + r"""setLabel(YSelectionWidget self, std::string const & newLabel)""" + return _yui.YSelectionWidget_setLabel(self, newLabel) + + def addItem(self, *args): + r""" + addItem(YSelectionWidget self, YItem item_disown) + addItem(YSelectionWidget self, std::string const & itemLabel, bool selected=False) + addItem(YSelectionWidget self, std::string const & itemLabel, std::string const & iconName, bool selected=False) + """ + return _yui.YSelectionWidget_addItem(self, *args) + + def addItems(self, itemCollection): + r"""addItems(YSelectionWidget self, YItemCollection itemCollection)""" + return _yui.YSelectionWidget_addItems(self, itemCollection) + + def deleteAllItems(self): + r"""deleteAllItems(YSelectionWidget self)""" + return _yui.YSelectionWidget_deleteAllItems(self) + + def setItems(self, itemCollection): + r"""setItems(YSelectionWidget self, YItemCollection itemCollection)""" + return _yui.YSelectionWidget_setItems(self, itemCollection) + + def itemsBegin(self, *args): + r""" + itemsBegin(YSelectionWidget self) -> YItemIterator + itemsBegin(YSelectionWidget self) -> YItemConstIterator + """ + return _yui.YSelectionWidget_itemsBegin(self, *args) + + def itemsEnd(self, *args): + r""" + itemsEnd(YSelectionWidget self) -> YItemIterator + itemsEnd(YSelectionWidget self) -> YItemConstIterator + """ + return _yui.YSelectionWidget_itemsEnd(self, *args) + + def hasItems(self): + r"""hasItems(YSelectionWidget self) -> bool""" + return _yui.YSelectionWidget_hasItems(self) + + def itemsCount(self): + r"""itemsCount(YSelectionWidget self) -> int""" + return _yui.YSelectionWidget_itemsCount(self) + + def itemAt(self, index): + r"""itemAt(YSelectionWidget self, int index) -> YItem""" + return _yui.YSelectionWidget_itemAt(self, index) + + def firstItem(self): + r"""firstItem(YSelectionWidget self) -> YItem""" + return _yui.YSelectionWidget_firstItem(self) + + def selectedItem(self): + r"""selectedItem(YSelectionWidget self) -> YItem""" + return _yui.YSelectionWidget_selectedItem(self) + + def selectedItems(self): + r"""selectedItems(YSelectionWidget self) -> YItemCollection""" + return _yui.YSelectionWidget_selectedItems(self) + + def hasSelectedItem(self): + r"""hasSelectedItem(YSelectionWidget self) -> bool""" + return _yui.YSelectionWidget_hasSelectedItem(self) + + def selectItem(self, item, selected=True): + r"""selectItem(YSelectionWidget self, YItem item, bool selected=True)""" + return _yui.YSelectionWidget_selectItem(self, item, selected) + + def setItemStatus(self, item, status): + r"""setItemStatus(YSelectionWidget self, YItem item, int status)""" + return _yui.YSelectionWidget_setItemStatus(self, item, status) + + def deselectAllItems(self): + r"""deselectAllItems(YSelectionWidget self)""" + return _yui.YSelectionWidget_deselectAllItems(self) + + def setIconBasePath(self, basePath): + r"""setIconBasePath(YSelectionWidget self, std::string const & basePath)""" + return _yui.YSelectionWidget_setIconBasePath(self, basePath) + + def iconBasePath(self): + r"""iconBasePath(YSelectionWidget self) -> std::string""" + return _yui.YSelectionWidget_iconBasePath(self) + + def iconFullPath(self, *args): + r""" + iconFullPath(YSelectionWidget self, std::string const & iconName) -> std::string + iconFullPath(YSelectionWidget self, YItem item) -> std::string + """ + return _yui.YSelectionWidget_iconFullPath(self, *args) + + def itemsContain(self, item): + r"""itemsContain(YSelectionWidget self, YItem item) -> bool""" + return _yui.YSelectionWidget_itemsContain(self, item) + + def findItem(self, itemLabel): + r"""findItem(YSelectionWidget self, std::string const & itemLabel) -> YItem""" + return _yui.YSelectionWidget_findItem(self, itemLabel) + + def dumpItems(self): + r"""dumpItems(YSelectionWidget self)""" + return _yui.YSelectionWidget_dumpItems(self) + + def enforceSingleSelection(self): + r"""enforceSingleSelection(YSelectionWidget self) -> bool""" + return _yui.YSelectionWidget_enforceSingleSelection(self) + + def shortcutChanged(self): + r"""shortcutChanged(YSelectionWidget self)""" + return _yui.YSelectionWidget_shortcutChanged(self) + + def shortcutString(self): + r"""shortcutString(YSelectionWidget self) -> std::string""" + return _yui.YSelectionWidget_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YSelectionWidget self, std::string const & str)""" + return _yui.YSelectionWidget_setShortcutString(self, str) + +# Register YSelectionWidget in _yui: +_yui.YSelectionWidget_swigregister(YSelectionWidget) + +class YSimpleInputField(YWidget): + r"""Proxy of C++ YSimpleInputField class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YSimpleInputField + + def value(self): + r"""value(YSimpleInputField self) -> std::string""" + return _yui.YSimpleInputField_value(self) + + def setValue(self, text): + r"""setValue(YSimpleInputField self, std::string const & text)""" + return _yui.YSimpleInputField_setValue(self, text) + + def label(self): + r"""label(YSimpleInputField self) -> std::string""" + return _yui.YSimpleInputField_label(self) + + def setLabel(self, label): + r"""setLabel(YSimpleInputField self, std::string const & label)""" + return _yui.YSimpleInputField_setLabel(self, label) + + def setProperty(self, propertyName, val): + r"""setProperty(YSimpleInputField self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YSimpleInputField_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YSimpleInputField self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YSimpleInputField_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YSimpleInputField self) -> YPropertySet""" + return _yui.YSimpleInputField_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YSimpleInputField self) -> std::string""" + return _yui.YSimpleInputField_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YSimpleInputField self, std::string const & str)""" + return _yui.YSimpleInputField_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YSimpleInputField self) -> char const *""" + return _yui.YSimpleInputField_userInputProperty(self) + +# Register YSimpleInputField in _yui: +_yui.YSimpleInputField_swigregister(YSimpleInputField) + +class YItem(object): + r"""Proxy of C++ YItem class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YItem self, std::string const & label, bool selected=False) -> YItem + __init__(YItem self, std::string const & label, std::string const & iconName, bool selected=False) -> YItem + """ + _yui.YItem_swiginit(self, _yui.new_YItem(*args)) + __swig_destroy__ = _yui.delete_YItem + + def itemClass(self): + r"""itemClass(YItem self) -> char const *""" + return _yui.YItem_itemClass(self) + + def label(self): + r"""label(YItem self) -> std::string""" + return _yui.YItem_label(self) + + def setLabel(self, newLabel): + r"""setLabel(YItem self, std::string const & newLabel)""" + return _yui.YItem_setLabel(self, newLabel) + + def iconName(self): + r"""iconName(YItem self) -> std::string""" + return _yui.YItem_iconName(self) + + def hasIconName(self): + r"""hasIconName(YItem self) -> bool""" + return _yui.YItem_hasIconName(self) + + def setIconName(self, newIconName): + r"""setIconName(YItem self, std::string const & newIconName)""" + return _yui.YItem_setIconName(self, newIconName) + + def selected(self): + r"""selected(YItem self) -> bool""" + return _yui.YItem_selected(self) + + def setSelected(self, sel=True): + r"""setSelected(YItem self, bool sel=True)""" + return _yui.YItem_setSelected(self, sel) + + def status(self): + r"""status(YItem self) -> int""" + return _yui.YItem_status(self) + + def setStatus(self, newStatus): + r"""setStatus(YItem self, int newStatus)""" + return _yui.YItem_setStatus(self, newStatus) + + def setIndex(self, index): + r"""setIndex(YItem self, int index)""" + return _yui.YItem_setIndex(self, index) + + def index(self): + r"""index(YItem self) -> int""" + return _yui.YItem_index(self) + + def setData(self, newData): + r"""setData(YItem self, void * newData)""" + return _yui.YItem_setData(self, newData) + + def data(self): + r"""data(YItem self) -> void *""" + return _yui.YItem_data(self) + + def hasChildren(self): + r"""hasChildren(YItem self) -> bool""" + return _yui.YItem_hasChildren(self) + + def childrenBegin(self, *args): + r""" + childrenBegin(YItem self) -> YItemIterator + childrenBegin(YItem self) -> YItemConstIterator + """ + return _yui.YItem_childrenBegin(self, *args) + + def childrenEnd(self, *args): + r""" + childrenEnd(YItem self) -> YItemIterator + childrenEnd(YItem self) -> YItemConstIterator + """ + return _yui.YItem_childrenEnd(self, *args) + + def parent(self): + r"""parent(YItem self) -> YItem""" + return _yui.YItem_parent(self) + + def debugLabel(self): + r"""debugLabel(YItem self) -> std::string""" + return _yui.YItem_debugLabel(self) + + def limitLength(self, text, limit): + r"""limitLength(YItem self, std::string const & text, int limit) -> std::string""" + return _yui.YItem_limitLength(self, text, limit) + + def __eq__(self, i): + r"""__eq__(YItem self, YItem i) -> int""" + return _yui.YItem___eq__(self, i) + + def __ne__(self, i): + r"""__ne__(YItem self, YItem i) -> int""" + return _yui.YItem___ne__(self, i) + + def equals(self, i): + r"""equals(YItem self, YItem i) -> int""" + return _yui.YItem_equals(self, i) + +# Register YItem in _yui: +_yui.YItem_swigregister(YItem) + +class YTreeItem(YItem): + r"""Proxy of C++ YTreeItem class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YTreeItem self, std::string const & label, bool isOpen=False) -> YTreeItem + __init__(YTreeItem self, std::string const & label, std::string const & iconName, bool isOpen=False) -> YTreeItem + __init__(YTreeItem self, YTreeItem parent, std::string const & label, bool isOpen=False) -> YTreeItem + __init__(YTreeItem self, YTreeItem parent, std::string const & label, std::string const & iconName, bool isOpen=False) -> YTreeItem + """ + _yui.YTreeItem_swiginit(self, _yui.new_YTreeItem(*args)) + __swig_destroy__ = _yui.delete_YTreeItem + + def itemClass(self): + r"""itemClass(YTreeItem self) -> char const *""" + return _yui.YTreeItem_itemClass(self) + + def hasChildren(self): + r"""hasChildren(YTreeItem self) -> bool""" + return _yui.YTreeItem_hasChildren(self) + + def childrenBegin(self, *args): + r""" + childrenBegin(YTreeItem self) -> YItemIterator + childrenBegin(YTreeItem self) -> YItemConstIterator + """ + return _yui.YTreeItem_childrenBegin(self, *args) + + def childrenEnd(self, *args): + r""" + childrenEnd(YTreeItem self) -> YItemIterator + childrenEnd(YTreeItem self) -> YItemConstIterator + """ + return _yui.YTreeItem_childrenEnd(self, *args) + + def addChild(self, item_disown): + r"""addChild(YTreeItem self, YItem item_disown)""" + return _yui.YTreeItem_addChild(self, item_disown) + + def deleteChildren(self): + r"""deleteChildren(YTreeItem self)""" + return _yui.YTreeItem_deleteChildren(self) + + def isOpen(self): + r"""isOpen(YTreeItem self) -> bool""" + return _yui.YTreeItem_isOpen(self) + + def setOpen(self, open=True): + r"""setOpen(YTreeItem self, bool open=True)""" + return _yui.YTreeItem_setOpen(self, open) + + def setClosed(self): + r"""setClosed(YTreeItem self)""" + return _yui.YTreeItem_setClosed(self) + + def parent(self): + r"""parent(YTreeItem self) -> YTreeItem""" + return _yui.YTreeItem_parent(self) + +# Register YTreeItem in _yui: +_yui.YTreeItem_swigregister(YTreeItem) + +class YStringTree(object): + r"""Proxy of C++ YStringTree class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, textdomain): + r"""__init__(YStringTree self, char const * textdomain) -> YStringTree""" + _yui.YStringTree_swiginit(self, _yui.new_YStringTree(textdomain)) + __swig_destroy__ = _yui.delete_YStringTree + + def addBranch(self, content, delimiter=0, parent=None): + r"""addBranch(YStringTree self, std::string const & content, char delimiter=0, YStringTreeItem * parent=None) -> YStringTreeItem""" + return _yui.YStringTree_addBranch(self, content, delimiter, parent) + + def origPath(self, item, delimiter, startWithDelimiter=True): + r"""origPath(YStringTree self, YStringTreeItem const * item, char delimiter, bool startWithDelimiter=True) -> std::string""" + return _yui.YStringTree_origPath(self, item, delimiter, startWithDelimiter) + + def translatedPath(self, item, delimiter, startWithDelimiter=True): + r"""translatedPath(YStringTree self, YStringTreeItem const * item, char delimiter, bool startWithDelimiter=True) -> std::string""" + return _yui.YStringTree_translatedPath(self, item, delimiter, startWithDelimiter) + + def path(self, item, delimiter, startWithDelimiter=True): + r"""path(YStringTree self, YStringTreeItem const * item, char delimiter, bool startWithDelimiter=True) -> YTransText""" + return _yui.YStringTree_path(self, item, delimiter, startWithDelimiter) + + def logTree(self): + r"""logTree(YStringTree self)""" + return _yui.YStringTree_logTree(self) + + def root(self): + r"""root(YStringTree self) -> YStringTreeItem *""" + return _yui.YStringTree_root(self) + + def textdomain(self): + r"""textdomain(YStringTree self) -> char const *""" + return _yui.YStringTree_textdomain(self) + + def setTextdomain(self, domain): + r"""setTextdomain(YStringTree self, char const * domain)""" + return _yui.YStringTree_setTextdomain(self, domain) + + def translate(self, orig): + r"""translate(YStringTree self, std::string const & orig) -> std::string""" + return _yui.YStringTree_translate(self, orig) + +# Register YStringTree in _yui: +_yui.YStringTree_swigregister(YStringTree) + +class YWidgetFactory(object): + r"""Proxy of C++ YWidgetFactory class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + + def createMainDialog(self, colorMode=YDialogNormalColor): + r"""createMainDialog(YWidgetFactory self, YDialogColorMode colorMode=YDialogNormalColor) -> YDialog""" + return _yui.YWidgetFactory_createMainDialog(self, colorMode) + + def createPopupDialog(self, colorMode=YDialogNormalColor): + r"""createPopupDialog(YWidgetFactory self, YDialogColorMode colorMode=YDialogNormalColor) -> YDialog""" + return _yui.YWidgetFactory_createPopupDialog(self, colorMode) + + def createDialog(self, dialogType, colorMode=YDialogNormalColor): + r"""createDialog(YWidgetFactory self, YDialogType dialogType, YDialogColorMode colorMode=YDialogNormalColor) -> YDialog""" + return _yui.YWidgetFactory_createDialog(self, dialogType, colorMode) + + def createVBox(self, parent): + r"""createVBox(YWidgetFactory self, YWidget parent) -> YLayoutBox""" + return _yui.YWidgetFactory_createVBox(self, parent) + + def createHBox(self, parent): + r"""createHBox(YWidgetFactory self, YWidget parent) -> YLayoutBox""" + return _yui.YWidgetFactory_createHBox(self, parent) + + def createLayoutBox(self, parent, dimension): + r"""createLayoutBox(YWidgetFactory self, YWidget parent, YUIDimension dimension) -> YLayoutBox""" + return _yui.YWidgetFactory_createLayoutBox(self, parent, dimension) + + def createButtonBox(self, parent): + r"""createButtonBox(YWidgetFactory self, YWidget parent) -> YButtonBox *""" + return _yui.YWidgetFactory_createButtonBox(self, parent) + + def createPushButton(self, parent, label): + r"""createPushButton(YWidgetFactory self, YWidget parent, std::string const & label) -> YPushButton""" + return _yui.YWidgetFactory_createPushButton(self, parent, label) + + def createLabel(self, parent, text, isHeading=False, isOutputField=False): + r"""createLabel(YWidgetFactory self, YWidget parent, std::string const & text, bool isHeading=False, bool isOutputField=False) -> YLabel""" + return _yui.YWidgetFactory_createLabel(self, parent, text, isHeading, isOutputField) + + def createHeading(self, parent, label): + r"""createHeading(YWidgetFactory self, YWidget parent, std::string const & label) -> YLabel""" + return _yui.YWidgetFactory_createHeading(self, parent, label) + + def createInputField(self, parent, label, passwordMode=False): + r"""createInputField(YWidgetFactory self, YWidget parent, std::string const & label, bool passwordMode=False) -> YInputField""" + return _yui.YWidgetFactory_createInputField(self, parent, label, passwordMode) + + def createCheckBox(self, parent, label, isChecked=False): + r"""createCheckBox(YWidgetFactory self, YWidget parent, std::string const & label, bool isChecked=False) -> YCheckBox""" + return _yui.YWidgetFactory_createCheckBox(self, parent, label, isChecked) + + def createRadioButton(self, parent, label, isChecked=False): + r"""createRadioButton(YWidgetFactory self, YWidget parent, std::string const & label, bool isChecked=False) -> YRadioButton""" + return _yui.YWidgetFactory_createRadioButton(self, parent, label, isChecked) + + def createComboBox(self, parent, label, editable=False): + r"""createComboBox(YWidgetFactory self, YWidget parent, std::string const & label, bool editable=False) -> YComboBox""" + return _yui.YWidgetFactory_createComboBox(self, parent, label, editable) + + def createSelectionBox(self, parent, label): + r"""createSelectionBox(YWidgetFactory self, YWidget parent, std::string const & label) -> YSelectionBox""" + return _yui.YWidgetFactory_createSelectionBox(self, parent, label) + + def createTree(self, parent, label, multiselection=False, recursiveselection=False): + r"""createTree(YWidgetFactory self, YWidget parent, std::string const & label, bool multiselection=False, bool recursiveselection=False) -> YTree""" + return _yui.YWidgetFactory_createTree(self, parent, label, multiselection, recursiveselection) + + def createTable(self, parent, header_disown, multiSelection=False): + r"""createTable(YWidgetFactory self, YWidget parent, YTableHeader header_disown, bool multiSelection=False) -> YTable""" + return _yui.YWidgetFactory_createTable(self, parent, header_disown, multiSelection) + + def createProgressBar(self, parent, label, maxValue=100): + r"""createProgressBar(YWidgetFactory self, YWidget parent, std::string const & label, int maxValue=100) -> YProgressBar""" + return _yui.YWidgetFactory_createProgressBar(self, parent, label, maxValue) + + def createRichText(self, *args): + r"""createRichText(YWidgetFactory self, YWidget parent, std::string const & text=std::string(), bool plainTextMode=False) -> YRichText""" + return _yui.YWidgetFactory_createRichText(self, *args) + + def createBusyIndicator(self, parent, label, timeout=1000): + r"""createBusyIndicator(YWidgetFactory self, YWidget parent, std::string const & label, int timeout=1000) -> YBusyIndicator""" + return _yui.YWidgetFactory_createBusyIndicator(self, parent, label, timeout) + + def createIconButton(self, parent, iconName, fallbackTextLabel): + r"""createIconButton(YWidgetFactory self, YWidget parent, std::string const & iconName, std::string const & fallbackTextLabel) -> YPushButton""" + return _yui.YWidgetFactory_createIconButton(self, parent, iconName, fallbackTextLabel) + + def createOutputField(self, parent, label): + r"""createOutputField(YWidgetFactory self, YWidget parent, std::string const & label) -> YLabel""" + return _yui.YWidgetFactory_createOutputField(self, parent, label) + + def createIntField(self, parent, label, minVal, maxVal, initialVal): + r"""createIntField(YWidgetFactory self, YWidget parent, std::string const & label, int minVal, int maxVal, int initialVal) -> YIntField""" + return _yui.YWidgetFactory_createIntField(self, parent, label, minVal, maxVal, initialVal) + + def createPasswordField(self, parent, label): + r"""createPasswordField(YWidgetFactory self, YWidget parent, std::string const & label) -> YInputField""" + return _yui.YWidgetFactory_createPasswordField(self, parent, label) + + def createMenuButton(self, parent, label): + r"""createMenuButton(YWidgetFactory self, YWidget parent, std::string const & label) -> YMenuButton""" + return _yui.YWidgetFactory_createMenuButton(self, parent, label) + + def createMultiLineEdit(self, parent, label): + r"""createMultiLineEdit(YWidgetFactory self, YWidget parent, std::string const & label) -> YMultiLineEdit""" + return _yui.YWidgetFactory_createMultiLineEdit(self, parent, label) + + def createImage(self, parent, imageFileName, animated=False): + r"""createImage(YWidgetFactory self, YWidget parent, std::string const & imageFileName, bool animated=False) -> YImage""" + return _yui.YWidgetFactory_createImage(self, parent, imageFileName, animated) + + def createLogView(self, parent, label, visibleLines, storedLines=0): + r"""createLogView(YWidgetFactory self, YWidget parent, std::string const & label, int visibleLines, int storedLines=0) -> YLogView""" + return _yui.YWidgetFactory_createLogView(self, parent, label, visibleLines, storedLines) + + def createMultiSelectionBox(self, parent, label): + r"""createMultiSelectionBox(YWidgetFactory self, YWidget parent, std::string const & label) -> YMultiSelectionBox""" + return _yui.YWidgetFactory_createMultiSelectionBox(self, parent, label) + + def createPackageSelector(self, parent, ModeFlags=0): + r"""createPackageSelector(YWidgetFactory self, YWidget parent, long ModeFlags=0) -> YPackageSelector""" + return _yui.YWidgetFactory_createPackageSelector(self, parent, ModeFlags) + + def createPkgSpecial(self, parent, subwidgetName): + r"""createPkgSpecial(YWidgetFactory self, YWidget parent, std::string const & subwidgetName) -> YWidget""" + return _yui.YWidgetFactory_createPkgSpecial(self, parent, subwidgetName) + + def createHStretch(self, parent): + r"""createHStretch(YWidgetFactory self, YWidget parent) -> YSpacing""" + return _yui.YWidgetFactory_createHStretch(self, parent) + + def createVStretch(self, parent): + r"""createVStretch(YWidgetFactory self, YWidget parent) -> YSpacing""" + return _yui.YWidgetFactory_createVStretch(self, parent) + + def createHSpacing(self, parent, size=1.0): + r"""createHSpacing(YWidgetFactory self, YWidget parent, YLayoutSize_t size=1.0) -> YSpacing""" + return _yui.YWidgetFactory_createHSpacing(self, parent, size) + + def createVSpacing(self, parent, size=1.0): + r"""createVSpacing(YWidgetFactory self, YWidget parent, YLayoutSize_t size=1.0) -> YSpacing""" + return _yui.YWidgetFactory_createVSpacing(self, parent, size) + + def createSpacing(self, parent, dim, stretchable=False, size=0.0): + r"""createSpacing(YWidgetFactory self, YWidget parent, YUIDimension dim, bool stretchable=False, YLayoutSize_t size=0.0) -> YSpacing""" + return _yui.YWidgetFactory_createSpacing(self, parent, dim, stretchable, size) + + def createEmpty(self, parent): + r"""createEmpty(YWidgetFactory self, YWidget parent) -> YEmpty""" + return _yui.YWidgetFactory_createEmpty(self, parent) + + def createLeft(self, parent): + r"""createLeft(YWidgetFactory self, YWidget parent) -> YAlignment""" + return _yui.YWidgetFactory_createLeft(self, parent) + + def createRight(self, parent): + r"""createRight(YWidgetFactory self, YWidget parent) -> YAlignment""" + return _yui.YWidgetFactory_createRight(self, parent) + + def createTop(self, parent): + r"""createTop(YWidgetFactory self, YWidget parent) -> YAlignment""" + return _yui.YWidgetFactory_createTop(self, parent) + + def createBottom(self, parent): + r"""createBottom(YWidgetFactory self, YWidget parent) -> YAlignment""" + return _yui.YWidgetFactory_createBottom(self, parent) + + def createHCenter(self, parent): + r"""createHCenter(YWidgetFactory self, YWidget parent) -> YAlignment""" + return _yui.YWidgetFactory_createHCenter(self, parent) + + def createVCenter(self, parent): + r"""createVCenter(YWidgetFactory self, YWidget parent) -> YAlignment""" + return _yui.YWidgetFactory_createVCenter(self, parent) + + def createHVCenter(self, parent): + r"""createHVCenter(YWidgetFactory self, YWidget parent) -> YAlignment""" + return _yui.YWidgetFactory_createHVCenter(self, parent) + + def createMarginBox(self, *args): + r""" + createMarginBox(YWidgetFactory self, YWidget parent, YLayoutSize_t horMargin, YLayoutSize_t vertMargin) -> YAlignment + createMarginBox(YWidgetFactory self, YWidget parent, YLayoutSize_t leftMargin, YLayoutSize_t rightMargin, YLayoutSize_t topMargin, YLayoutSize_t bottomMargin) -> YAlignment + """ + return _yui.YWidgetFactory_createMarginBox(self, *args) + + def createMinWidth(self, parent, minWidth): + r"""createMinWidth(YWidgetFactory self, YWidget parent, YLayoutSize_t minWidth) -> YAlignment""" + return _yui.YWidgetFactory_createMinWidth(self, parent, minWidth) + + def createMinHeight(self, parent, minHeight): + r"""createMinHeight(YWidgetFactory self, YWidget parent, YLayoutSize_t minHeight) -> YAlignment""" + return _yui.YWidgetFactory_createMinHeight(self, parent, minHeight) + + def createMinSize(self, parent, minWidth, minHeight): + r"""createMinSize(YWidgetFactory self, YWidget parent, YLayoutSize_t minWidth, YLayoutSize_t minHeight) -> YAlignment""" + return _yui.YWidgetFactory_createMinSize(self, parent, minWidth, minHeight) + + def createAlignment(self, parent, horAlignment, vertAlignment): + r"""createAlignment(YWidgetFactory self, YWidget parent, YAlignmentType horAlignment, YAlignmentType vertAlignment) -> YAlignment""" + return _yui.YWidgetFactory_createAlignment(self, parent, horAlignment, vertAlignment) + + def createHSquash(self, parent): + r"""createHSquash(YWidgetFactory self, YWidget parent) -> YSquash""" + return _yui.YWidgetFactory_createHSquash(self, parent) + + def createVSquash(self, parent): + r"""createVSquash(YWidgetFactory self, YWidget parent) -> YSquash""" + return _yui.YWidgetFactory_createVSquash(self, parent) + + def createHVSquash(self, parent): + r"""createHVSquash(YWidgetFactory self, YWidget parent) -> YSquash""" + return _yui.YWidgetFactory_createHVSquash(self, parent) + + def createSquash(self, parent, horSquash, vertSquash): + r"""createSquash(YWidgetFactory self, YWidget parent, bool horSquash, bool vertSquash) -> YSquash""" + return _yui.YWidgetFactory_createSquash(self, parent, horSquash, vertSquash) + + def createFrame(self, parent, label): + r"""createFrame(YWidgetFactory self, YWidget parent, std::string const & label) -> YFrame""" + return _yui.YWidgetFactory_createFrame(self, parent, label) + + def createCheckBoxFrame(self, parent, label, checked): + r"""createCheckBoxFrame(YWidgetFactory self, YWidget parent, std::string const & label, bool checked) -> YCheckBoxFrame""" + return _yui.YWidgetFactory_createCheckBoxFrame(self, parent, label, checked) + + def createRadioButtonGroup(self, parent): + r"""createRadioButtonGroup(YWidgetFactory self, YWidget parent) -> YRadioButtonGroup""" + return _yui.YWidgetFactory_createRadioButtonGroup(self, parent) + + def createReplacePoint(self, parent): + r"""createReplacePoint(YWidgetFactory self, YWidget parent) -> YReplacePoint""" + return _yui.YWidgetFactory_createReplacePoint(self, parent) + + def createItemSelector(self, parent, enforceSingleSelection=True): + r"""createItemSelector(YWidgetFactory self, YWidget parent, bool enforceSingleSelection=True) -> YItemSelector""" + return _yui.YWidgetFactory_createItemSelector(self, parent, enforceSingleSelection) + + def createSingleItemSelector(self, parent): + r"""createSingleItemSelector(YWidgetFactory self, YWidget parent) -> YItemSelector""" + return _yui.YWidgetFactory_createSingleItemSelector(self, parent) + + def createMultiItemSelector(self, parent): + r"""createMultiItemSelector(YWidgetFactory self, YWidget parent) -> YItemSelector""" + return _yui.YWidgetFactory_createMultiItemSelector(self, parent) + + def createCustomStatusItemSelector(self, parent, customStates): + r"""createCustomStatusItemSelector(YWidgetFactory self, YWidget parent, YItemCustomStatusVector const & customStates) -> YItemSelector""" + return _yui.YWidgetFactory_createCustomStatusItemSelector(self, parent, customStates) + + def createMenuBar(self, parent): + r"""createMenuBar(YWidgetFactory self, YWidget parent) -> YMenuBar""" + return _yui.YWidgetFactory_createMenuBar(self, parent) + +# Register YWidgetFactory in _yui: +_yui.YWidgetFactory_swigregister(YWidgetFactory) + +class YDialog(YSingleChildContainerWidget): + r"""Proxy of C++ YDialog class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + + def widgetClass(self): + r"""widgetClass(YDialog self) -> char const *""" + return _yui.YDialog_widgetClass(self) + + def open(self): + r"""open(YDialog self)""" + return _yui.YDialog_open(self) + + def isOpen(self): + r"""isOpen(YDialog self) -> bool""" + return _yui.YDialog_isOpen(self) + + def waitForEvent(self, timeout_millisec=0): + r"""waitForEvent(YDialog self, int timeout_millisec=0) -> YEvent""" + return _yui.YDialog_waitForEvent(self, timeout_millisec) + + def pollEvent(self): + r"""pollEvent(YDialog self) -> YEvent""" + return _yui.YDialog_pollEvent(self) + + def isTopmostDialog(self): + r"""isTopmostDialog(YDialog self) -> bool""" + return _yui.YDialog_isTopmostDialog(self) + + def requestMultiPassLayout(self): + r"""requestMultiPassLayout(YDialog self)""" + return _yui.YDialog_requestMultiPassLayout(self) + + def layoutPass(self): + r"""layoutPass(YDialog self) -> int""" + return _yui.YDialog_layoutPass(self) + + def destroy(self, doThrow=True): + r"""destroy(YDialog self, bool doThrow=True) -> bool""" + return _yui.YDialog_destroy(self, doThrow) + + @staticmethod + def deleteTopmostDialog(doThrow=True): + r"""deleteTopmostDialog(bool doThrow=True) -> bool""" + return _yui.YDialog_deleteTopmostDialog(doThrow) + + @staticmethod + def deleteAllDialogs(): + r"""deleteAllDialogs()""" + return _yui.YDialog_deleteAllDialogs() + + @staticmethod + def deleteTo(dialog): + r"""deleteTo(YDialog dialog)""" + return _yui.YDialog_deleteTo(dialog) + + @staticmethod + def openDialogsCount(): + r"""openDialogsCount() -> int""" + return _yui.YDialog_openDialogsCount() + + @staticmethod + def currentDialog(doThrow=True): + r"""currentDialog(bool doThrow=True) -> YDialog""" + return _yui.YDialog_currentDialog(doThrow) + + @staticmethod + def topmostDialog(doThrow=True): + r"""topmostDialog(bool doThrow=True) -> YDialog""" + return _yui.YDialog_topmostDialog(doThrow) + + def setInitialSize(self): + r"""setInitialSize(YDialog self)""" + return _yui.YDialog_setInitialSize(self) + + def recalcLayout(self): + r"""recalcLayout(YDialog self)""" + return _yui.YDialog_recalcLayout(self) + + def dialogType(self): + r"""dialogType(YDialog self) -> YDialogType""" + return _yui.YDialog_dialogType(self) + + def isMainDialog(self): + r"""isMainDialog(YDialog self) -> bool""" + return _yui.YDialog_isMainDialog(self) + + def colorMode(self): + r"""colorMode(YDialog self) -> YDialogColorMode""" + return _yui.YDialog_colorMode(self) + + def checkShortcuts(self, force=False): + r"""checkShortcuts(YDialog self, bool force=False)""" + return _yui.YDialog_checkShortcuts(self, force) + + def postponeShortcutCheck(self): + r"""postponeShortcutCheck(YDialog self)""" + return _yui.YDialog_postponeShortcutCheck(self) + + def shortcutCheckPostponed(self): + r"""shortcutCheckPostponed(YDialog self) -> bool""" + return _yui.YDialog_shortcutCheckPostponed(self) + + def defaultButton(self): + r"""defaultButton(YDialog self) -> YPushButton""" + return _yui.YDialog_defaultButton(self) + + def deleteEvent(self, event): + r"""deleteEvent(YDialog self, YEvent event)""" + return _yui.YDialog_deleteEvent(self, event) + + def addEventFilter(self, eventFilter): + r"""addEventFilter(YDialog self, YEventFilter * eventFilter)""" + return _yui.YDialog_addEventFilter(self, eventFilter) + + def removeEventFilter(self, eventFilter): + r"""removeEventFilter(YDialog self, YEventFilter * eventFilter)""" + return _yui.YDialog_removeEventFilter(self, eventFilter) + + def highlight(self, child): + r"""highlight(YDialog self, YWidget child)""" + return _yui.YDialog_highlight(self, child) + + def setDefaultButton(self, defaultButton): + r"""setDefaultButton(YDialog self, YPushButton defaultButton)""" + return _yui.YDialog_setDefaultButton(self, defaultButton) + + def activate(self): + r"""activate(YDialog self)""" + return _yui.YDialog_activate(self) + + @staticmethod + def showText(text, richText=False): + r"""showText(std::string const & text, bool richText=False)""" + return _yui.YDialog_showText(text, richText) + + @staticmethod + def showHelpText(widget): + r"""showHelpText(YWidget widget) -> bool""" + return _yui.YDialog_showHelpText(widget) + + @staticmethod + def showRelNotesText(): + r"""showRelNotesText() -> bool""" + return _yui.YDialog_showRelNotesText() + +# Register YDialog in _yui: +_yui.YDialog_swigregister(YDialog) + +def YDialog_deleteTopmostDialog(doThrow=True): + r"""YDialog_deleteTopmostDialog(bool doThrow=True) -> bool""" + return _yui.YDialog_deleteTopmostDialog(doThrow) + +def YDialog_deleteAllDialogs(): + r"""YDialog_deleteAllDialogs()""" + return _yui.YDialog_deleteAllDialogs() + +def YDialog_deleteTo(dialog): + r"""YDialog_deleteTo(YDialog dialog)""" + return _yui.YDialog_deleteTo(dialog) + +def YDialog_openDialogsCount(): + r"""YDialog_openDialogsCount() -> int""" + return _yui.YDialog_openDialogsCount() + +def YDialog_currentDialog(doThrow=True): + r"""YDialog_currentDialog(bool doThrow=True) -> YDialog""" + return _yui.YDialog_currentDialog(doThrow) + +def YDialog_topmostDialog(doThrow=True): + r"""YDialog_topmostDialog(bool doThrow=True) -> YDialog""" + return _yui.YDialog_topmostDialog(doThrow) + +def YDialog_showText(text, richText=False): + r"""YDialog_showText(std::string const & text, bool richText=False)""" + return _yui.YDialog_showText(text, richText) + +def YDialog_showHelpText(widget): + r"""YDialog_showHelpText(YWidget widget) -> bool""" + return _yui.YDialog_showHelpText(widget) + +def YDialog_showRelNotesText(): + r"""YDialog_showRelNotesText() -> bool""" + return _yui.YDialog_showRelNotesText() + +class YAlignment(YSingleChildContainerWidget): + r"""Proxy of C++ YAlignment class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YAlignment + + def widgetClass(self): + r"""widgetClass(YAlignment self) -> char const *""" + return _yui.YAlignment_widgetClass(self) + + def alignment(self, dim): + r"""alignment(YAlignment self, YUIDimension dim) -> YAlignmentType""" + return _yui.YAlignment_alignment(self, dim) + + def leftMargin(self): + r"""leftMargin(YAlignment self) -> int""" + return _yui.YAlignment_leftMargin(self) + + def rightMargin(self): + r"""rightMargin(YAlignment self) -> int""" + return _yui.YAlignment_rightMargin(self) + + def topMargin(self): + r"""topMargin(YAlignment self) -> int""" + return _yui.YAlignment_topMargin(self) + + def bottomMargin(self): + r"""bottomMargin(YAlignment self) -> int""" + return _yui.YAlignment_bottomMargin(self) + + def totalMargins(self, dim): + r"""totalMargins(YAlignment self, YUIDimension dim) -> int""" + return _yui.YAlignment_totalMargins(self, dim) + + def setLeftMargin(self, margin): + r"""setLeftMargin(YAlignment self, int margin)""" + return _yui.YAlignment_setLeftMargin(self, margin) + + def setRightMargin(self, margin): + r"""setRightMargin(YAlignment self, int margin)""" + return _yui.YAlignment_setRightMargin(self, margin) + + def setTopMargin(self, margin): + r"""setTopMargin(YAlignment self, int margin)""" + return _yui.YAlignment_setTopMargin(self, margin) + + def setBottomMargin(self, margin): + r"""setBottomMargin(YAlignment self, int margin)""" + return _yui.YAlignment_setBottomMargin(self, margin) + + def minWidth(self): + r"""minWidth(YAlignment self) -> int""" + return _yui.YAlignment_minWidth(self) + + def minHeight(self): + r"""minHeight(YAlignment self) -> int""" + return _yui.YAlignment_minHeight(self) + + def setMinWidth(self, width): + r"""setMinWidth(YAlignment self, int width)""" + return _yui.YAlignment_setMinWidth(self, width) + + def setMinHeight(self, height): + r"""setMinHeight(YAlignment self, int height)""" + return _yui.YAlignment_setMinHeight(self, height) + + def setBackgroundPixmap(self, pixmapFileName): + r"""setBackgroundPixmap(YAlignment self, std::string const & pixmapFileName)""" + return _yui.YAlignment_setBackgroundPixmap(self, pixmapFileName) + + def backgroundPixmap(self): + r"""backgroundPixmap(YAlignment self) -> std::string""" + return _yui.YAlignment_backgroundPixmap(self) + + def addChild(self, child): + r"""addChild(YAlignment self, YWidget child)""" + return _yui.YAlignment_addChild(self, child) + + def moveChild(self, child, newx, newy): + r"""moveChild(YAlignment self, YWidget child, int newx, int newy)""" + return _yui.YAlignment_moveChild(self, child, newx, newy) + + def stretchable(self, dim): + r"""stretchable(YAlignment self, YUIDimension dim) -> bool""" + return _yui.YAlignment_stretchable(self, dim) + + def preferredWidth(self): + r"""preferredWidth(YAlignment self) -> int""" + return _yui.YAlignment_preferredWidth(self) + + def preferredHeight(self): + r"""preferredHeight(YAlignment self) -> int""" + return _yui.YAlignment_preferredHeight(self) + + def setSize(self, newWidth, newHeight): + r"""setSize(YAlignment self, int newWidth, int newHeight)""" + return _yui.YAlignment_setSize(self, newWidth, newHeight) + +# Register YAlignment in _yui: +_yui.YAlignment_swigregister(YAlignment) + +class YApplication(object): + r"""Proxy of C++ YApplication class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + + def findWidget(self, id, doThrow=True): + r"""findWidget(YApplication self, YWidgetID id, bool doThrow=True) -> YWidget""" + return _yui.YApplication_findWidget(self, id, doThrow) + + def iconBasePath(self): + r"""iconBasePath(YApplication self) -> std::string""" + return _yui.YApplication_iconBasePath(self) + + def setIconBasePath(self, newIconBasePath): + r"""setIconBasePath(YApplication self, std::string const & newIconBasePath)""" + return _yui.YApplication_setIconBasePath(self, newIconBasePath) + + def iconLoader(self): + r"""iconLoader(YApplication self) -> YIconLoader *""" + return _yui.YApplication_iconLoader(self) + + def defaultFunctionKey(self, label): + r"""defaultFunctionKey(YApplication self, std::string const & label) -> int""" + return _yui.YApplication_defaultFunctionKey(self, label) + + def setDefaultFunctionKey(self, label, fkey): + r"""setDefaultFunctionKey(YApplication self, std::string const & label, int fkey)""" + return _yui.YApplication_setDefaultFunctionKey(self, label, fkey) + + def clearDefaultFunctionKeys(self): + r"""clearDefaultFunctionKeys(YApplication self)""" + return _yui.YApplication_clearDefaultFunctionKeys(self) + + def setLanguage(self, *args): + r"""setLanguage(YApplication self, std::string const & language, std::string const & encoding=std::string())""" + return _yui.YApplication_setLanguage(self, *args) + + def language(self, stripEncoding=False): + r"""language(YApplication self, bool stripEncoding=False) -> std::string""" + return _yui.YApplication_language(self, stripEncoding) + + def glyph(self, glyphSymbolName): + r"""glyph(YApplication self, std::string const & glyphSymbolName) -> std::string""" + return _yui.YApplication_glyph(self, glyphSymbolName) + + def askForExistingDirectory(self, startDir, headline): + r"""askForExistingDirectory(YApplication self, std::string const & startDir, std::string const & headline) -> std::string""" + return _yui.YApplication_askForExistingDirectory(self, startDir, headline) + + def askForExistingFile(self, startWith, filter, headline): + r"""askForExistingFile(YApplication self, std::string const & startWith, std::string const & filter, std::string const & headline) -> std::string""" + return _yui.YApplication_askForExistingFile(self, startWith, filter, headline) + + def askForSaveFileName(self, startWith, filter, headline): + r"""askForSaveFileName(YApplication self, std::string const & startWith, std::string const & filter, std::string const & headline) -> std::string""" + return _yui.YApplication_askForSaveFileName(self, startWith, filter, headline) + + def askForWidgetStyle(self): + r"""askForWidgetStyle(YApplication self)""" + return _yui.YApplication_askForWidgetStyle(self) + + def openContextMenu(self, itemCollection): + r"""openContextMenu(YApplication self, YItemCollection itemCollection) -> bool""" + return _yui.YApplication_openContextMenu(self, itemCollection) + + def setProductName(self, productName): + r"""setProductName(YApplication self, std::string const & productName)""" + return _yui.YApplication_setProductName(self, productName) + + def productName(self): + r"""productName(YApplication self) -> std::string""" + return _yui.YApplication_productName(self) + + def setReleaseNotes(self, relNotes): + r"""setReleaseNotes(YApplication self, std::map< std::string,std::string,std::less< std::string >,std::allocator< std::pair< std::string const,std::string > > > const & relNotes)""" + return _yui.YApplication_setReleaseNotes(self, relNotes) + + def releaseNotes(self): + r"""releaseNotes(YApplication self) -> std::map< std::string,std::string,std::less< std::string >,std::allocator< std::pair< std::string const,std::string > > >""" + return _yui.YApplication_releaseNotes(self) + + def setShowProductLogo(self, show): + r"""setShowProductLogo(YApplication self, bool show)""" + return _yui.YApplication_setShowProductLogo(self, show) + + def showProductLogo(self): + r"""showProductLogo(YApplication self) -> bool""" + return _yui.YApplication_showProductLogo(self) + + def deviceUnits(self, dim, layoutUnits): + r"""deviceUnits(YApplication self, YUIDimension dim, float layoutUnits) -> int""" + return _yui.YApplication_deviceUnits(self, dim, layoutUnits) + + def layoutUnits(self, dim, deviceUnits): + r"""layoutUnits(YApplication self, YUIDimension dim, int deviceUnits) -> float""" + return _yui.YApplication_layoutUnits(self, dim, deviceUnits) + + def setReverseLayout(self, reverse): + r"""setReverseLayout(YApplication self, bool reverse)""" + return _yui.YApplication_setReverseLayout(self, reverse) + + def reverseLayout(self): + r"""reverseLayout(YApplication self) -> bool""" + return _yui.YApplication_reverseLayout(self) + + def busyCursor(self): + r"""busyCursor(YApplication self)""" + return _yui.YApplication_busyCursor(self) + + def normalCursor(self): + r"""normalCursor(YApplication self)""" + return _yui.YApplication_normalCursor(self) + + def makeScreenShot(self, fileName): + r"""makeScreenShot(YApplication self, std::string const & fileName)""" + return _yui.YApplication_makeScreenShot(self, fileName) + + def beep(self): + r"""beep(YApplication self)""" + return _yui.YApplication_beep(self) + + def redrawScreen(self): + r"""redrawScreen(YApplication self)""" + return _yui.YApplication_redrawScreen(self) + + def initConsoleKeyboard(self): + r"""initConsoleKeyboard(YApplication self)""" + return _yui.YApplication_initConsoleKeyboard(self) + + def setConsoleFont(self, console_magic, font, screen_map, unicode_map, language): + r"""setConsoleFont(YApplication self, std::string const & console_magic, std::string const & font, std::string const & screen_map, std::string const & unicode_map, std::string const & language)""" + return _yui.YApplication_setConsoleFont(self, console_magic, font, screen_map, unicode_map, language) + + def runInTerminal(self, command): + r"""runInTerminal(YApplication self, std::string const & command) -> int""" + return _yui.YApplication_runInTerminal(self, command) + + def openUI(self): + r"""openUI(YApplication self)""" + return _yui.YApplication_openUI(self) + + def closeUI(self): + r"""closeUI(YApplication self)""" + return _yui.YApplication_closeUI(self) + + def displayWidth(self): + r"""displayWidth(YApplication self) -> int""" + return _yui.YApplication_displayWidth(self) + + def displayHeight(self): + r"""displayHeight(YApplication self) -> int""" + return _yui.YApplication_displayHeight(self) + + def displayDepth(self): + r"""displayDepth(YApplication self) -> int""" + return _yui.YApplication_displayDepth(self) + + def displayColors(self): + r"""displayColors(YApplication self) -> long""" + return _yui.YApplication_displayColors(self) + + def defaultWidth(self): + r"""defaultWidth(YApplication self) -> int""" + return _yui.YApplication_defaultWidth(self) + + def defaultHeight(self): + r"""defaultHeight(YApplication self) -> int""" + return _yui.YApplication_defaultHeight(self) + + def isTextMode(self): + r"""isTextMode(YApplication self) -> bool""" + return _yui.YApplication_isTextMode(self) + + def hasImageSupport(self): + r"""hasImageSupport(YApplication self) -> bool""" + return _yui.YApplication_hasImageSupport(self) + + def hasIconSupport(self): + r"""hasIconSupport(YApplication self) -> bool""" + return _yui.YApplication_hasIconSupport(self) + + def hasAnimationSupport(self): + r"""hasAnimationSupport(YApplication self) -> bool""" + return _yui.YApplication_hasAnimationSupport(self) + + def hasFullUtf8Support(self): + r"""hasFullUtf8Support(YApplication self) -> bool""" + return _yui.YApplication_hasFullUtf8Support(self) + + def richTextSupportsTable(self): + r"""richTextSupportsTable(YApplication self) -> bool""" + return _yui.YApplication_richTextSupportsTable(self) + + def leftHandedMouse(self): + r"""leftHandedMouse(YApplication self) -> bool""" + return _yui.YApplication_leftHandedMouse(self) + + def hasWidgetStyleSupport(self): + r"""hasWidgetStyleSupport(YApplication self) -> bool""" + return _yui.YApplication_hasWidgetStyleSupport(self) + + def hasWizardDialogSupport(self): + r"""hasWizardDialogSupport(YApplication self) -> bool""" + return _yui.YApplication_hasWizardDialogSupport(self) + + def setApplicationTitle(self, title): + r"""setApplicationTitle(YApplication self, std::string const & title)""" + return _yui.YApplication_setApplicationTitle(self, title) + + def applicationTitle(self): + r"""applicationTitle(YApplication self) -> std::string const &""" + return _yui.YApplication_applicationTitle(self) + + def setApplicationIcon(self, icon): + r"""setApplicationIcon(YApplication self, std::string const & icon)""" + return _yui.YApplication_setApplicationIcon(self, icon) + + def applicationIcon(self): + r"""applicationIcon(YApplication self) -> std::string const &""" + return _yui.YApplication_applicationIcon(self) + +# Register YApplication in _yui: +_yui.YApplication_swigregister(YApplication) + +class YBarGraph(YWidget): + r"""Proxy of C++ YBarGraph class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YBarGraph + + def widgetClass(self): + r"""widgetClass(YBarGraph self) -> char const *""" + return _yui.YBarGraph_widgetClass(self) + + def addSegment(self, segment): + r"""addSegment(YBarGraph self, YBarGraphSegment segment)""" + return _yui.YBarGraph_addSegment(self, segment) + + def deleteAllSegments(self): + r"""deleteAllSegments(YBarGraph self)""" + return _yui.YBarGraph_deleteAllSegments(self) + + def segments(self): + r"""segments(YBarGraph self) -> int""" + return _yui.YBarGraph_segments(self) + + def segment(self, segmentIndex): + r"""segment(YBarGraph self, int segmentIndex) -> YBarGraphSegment""" + return _yui.YBarGraph_segment(self, segmentIndex) + + def setValue(self, segmentIndex, newValue): + r"""setValue(YBarGraph self, int segmentIndex, int newValue)""" + return _yui.YBarGraph_setValue(self, segmentIndex, newValue) + + def setLabel(self, segmentIndex, newLabel): + r"""setLabel(YBarGraph self, int segmentIndex, std::string const & newLabel)""" + return _yui.YBarGraph_setLabel(self, segmentIndex, newLabel) + + def setSegmentColor(self, segmentIndex, color): + r"""setSegmentColor(YBarGraph self, int segmentIndex, YColor color)""" + return _yui.YBarGraph_setSegmentColor(self, segmentIndex, color) + + def setTextColor(self, segmentIndex, color): + r"""setTextColor(YBarGraph self, int segmentIndex, YColor color)""" + return _yui.YBarGraph_setTextColor(self, segmentIndex, color) + + def setProperty(self, propertyName, val): + r"""setProperty(YBarGraph self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YBarGraph_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YBarGraph self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YBarGraph_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YBarGraph self) -> YPropertySet""" + return _yui.YBarGraph_propertySet(self) + +# Register YBarGraph in _yui: +_yui.YBarGraph_swigregister(YBarGraph) + +class YBarGraphSegment(object): + r"""Proxy of C++ YBarGraphSegment class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r"""__init__(YBarGraphSegment self, int value=0, std::string const & label=std::string(), YColor segmentColor=YColor(), YColor textColor=YColor()) -> YBarGraphSegment""" + _yui.YBarGraphSegment_swiginit(self, _yui.new_YBarGraphSegment(*args)) + + def value(self): + r"""value(YBarGraphSegment self) -> int""" + return _yui.YBarGraphSegment_value(self) + + def setValue(self, newValue): + r"""setValue(YBarGraphSegment self, int newValue)""" + return _yui.YBarGraphSegment_setValue(self, newValue) + + def label(self): + r"""label(YBarGraphSegment self) -> std::string""" + return _yui.YBarGraphSegment_label(self) + + def setLabel(self, newLabel): + r"""setLabel(YBarGraphSegment self, std::string const & newLabel)""" + return _yui.YBarGraphSegment_setLabel(self, newLabel) + + def segmentColor(self): + r"""segmentColor(YBarGraphSegment self) -> YColor""" + return _yui.YBarGraphSegment_segmentColor(self) + + def hasSegmentColor(self): + r"""hasSegmentColor(YBarGraphSegment self) -> bool""" + return _yui.YBarGraphSegment_hasSegmentColor(self) + + def setSegmentColor(self, color): + r"""setSegmentColor(YBarGraphSegment self, YColor color)""" + return _yui.YBarGraphSegment_setSegmentColor(self, color) + + def textColor(self): + r"""textColor(YBarGraphSegment self) -> YColor""" + return _yui.YBarGraphSegment_textColor(self) + + def hasTextColor(self): + r"""hasTextColor(YBarGraphSegment self) -> bool""" + return _yui.YBarGraphSegment_hasTextColor(self) + + def setTextColor(self, color): + r"""setTextColor(YBarGraphSegment self, YColor color)""" + return _yui.YBarGraphSegment_setTextColor(self, color) + __swig_destroy__ = _yui.delete_YBarGraphSegment + +# Register YBarGraphSegment in _yui: +_yui.YBarGraphSegment_swigregister(YBarGraphSegment) + +class YBarGraphMultiUpdate(object): + r"""Proxy of C++ YBarGraphMultiUpdate class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, barGraph): + r"""__init__(YBarGraphMultiUpdate self, YBarGraph barGraph) -> YBarGraphMultiUpdate""" + _yui.YBarGraphMultiUpdate_swiginit(self, _yui.new_YBarGraphMultiUpdate(barGraph)) + __swig_destroy__ = _yui.delete_YBarGraphMultiUpdate + +# Register YBarGraphMultiUpdate in _yui: +_yui.YBarGraphMultiUpdate_swigregister(YBarGraphMultiUpdate) + +class YBuiltinCaller(object): + r"""Proxy of C++ YBuiltinCaller class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YBuiltinCaller + + def call(self): + r"""call(YBuiltinCaller self)""" + return _yui.YBuiltinCaller_call(self) + +# Register YBuiltinCaller in _yui: +_yui.YBuiltinCaller_swigregister(YBuiltinCaller) + +class YBusyIndicator(YWidget): + r"""Proxy of C++ YBusyIndicator class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YBusyIndicator + + def widgetClass(self): + r"""widgetClass(YBusyIndicator self) -> char const *""" + return _yui.YBusyIndicator_widgetClass(self) + + def label(self): + r"""label(YBusyIndicator self) -> std::string""" + return _yui.YBusyIndicator_label(self) + + def setLabel(self, label): + r"""setLabel(YBusyIndicator self, std::string const & label)""" + return _yui.YBusyIndicator_setLabel(self, label) + + def timeout(self): + r"""timeout(YBusyIndicator self) -> int""" + return _yui.YBusyIndicator_timeout(self) + + def setTimeout(self, newTimeout): + r"""setTimeout(YBusyIndicator self, int newTimeout)""" + return _yui.YBusyIndicator_setTimeout(self, newTimeout) + + def alive(self): + r"""alive(YBusyIndicator self) -> bool""" + return _yui.YBusyIndicator_alive(self) + + def setAlive(self, newAlive): + r"""setAlive(YBusyIndicator self, bool newAlive)""" + return _yui.YBusyIndicator_setAlive(self, newAlive) + + def setProperty(self, propertyName, val): + r"""setProperty(YBusyIndicator self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YBusyIndicator_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YBusyIndicator self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YBusyIndicator_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YBusyIndicator self) -> YPropertySet""" + return _yui.YBusyIndicator_propertySet(self) + +# Register YBusyIndicator in _yui: +_yui.YBusyIndicator_swigregister(YBusyIndicator) + +class YCheckBoxFrame(YSingleChildContainerWidget): + r"""Proxy of C++ YCheckBoxFrame class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YCheckBoxFrame + + def widgetClass(self): + r"""widgetClass(YCheckBoxFrame self) -> char const *""" + return _yui.YCheckBoxFrame_widgetClass(self) + + def label(self): + r"""label(YCheckBoxFrame self) -> std::string""" + return _yui.YCheckBoxFrame_label(self) + + def setLabel(self, label): + r"""setLabel(YCheckBoxFrame self, std::string const & label)""" + return _yui.YCheckBoxFrame_setLabel(self, label) + + def setValue(self, isChecked): + r"""setValue(YCheckBoxFrame self, bool isChecked)""" + return _yui.YCheckBoxFrame_setValue(self, isChecked) + + def value(self): + r"""value(YCheckBoxFrame self) -> bool""" + return _yui.YCheckBoxFrame_value(self) + + def autoEnable(self): + r"""autoEnable(YCheckBoxFrame self) -> bool""" + return _yui.YCheckBoxFrame_autoEnable(self) + + def setAutoEnable(self, autoEnable): + r"""setAutoEnable(YCheckBoxFrame self, bool autoEnable)""" + return _yui.YCheckBoxFrame_setAutoEnable(self, autoEnable) + + def invertAutoEnable(self): + r"""invertAutoEnable(YCheckBoxFrame self) -> bool""" + return _yui.YCheckBoxFrame_invertAutoEnable(self) + + def setInvertAutoEnable(self, invertAutoEnable): + r"""setInvertAutoEnable(YCheckBoxFrame self, bool invertAutoEnable)""" + return _yui.YCheckBoxFrame_setInvertAutoEnable(self, invertAutoEnable) + + def handleChildrenEnablement(self, isChecked): + r"""handleChildrenEnablement(YCheckBoxFrame self, bool isChecked)""" + return _yui.YCheckBoxFrame_handleChildrenEnablement(self, isChecked) + + def shortcutString(self): + r"""shortcutString(YCheckBoxFrame self) -> std::string""" + return _yui.YCheckBoxFrame_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YCheckBoxFrame self, std::string const & str)""" + return _yui.YCheckBoxFrame_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YCheckBoxFrame self) -> char const *""" + return _yui.YCheckBoxFrame_userInputProperty(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YCheckBoxFrame self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YCheckBoxFrame_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YCheckBoxFrame self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YCheckBoxFrame_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YCheckBoxFrame self) -> YPropertySet""" + return _yui.YCheckBoxFrame_propertySet(self) + +# Register YCheckBoxFrame in _yui: +_yui.YCheckBoxFrame_swigregister(YCheckBoxFrame) + +YCheckBox_dont_care = _yui.YCheckBox_dont_care + +YCheckBox_off = _yui.YCheckBox_off + +YCheckBox_on = _yui.YCheckBox_on + +class YCheckBox(YWidget): + r"""Proxy of C++ YCheckBox class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YCheckBox + + def widgetClass(self): + r"""widgetClass(YCheckBox self) -> char const *""" + return _yui.YCheckBox_widgetClass(self) + + def value(self): + r"""value(YCheckBox self) -> YCheckBoxState""" + return _yui.YCheckBox_value(self) + + def setValue(self, state): + r"""setValue(YCheckBox self, YCheckBoxState state)""" + return _yui.YCheckBox_setValue(self, state) + + def isChecked(self): + r"""isChecked(YCheckBox self) -> bool""" + return _yui.YCheckBox_isChecked(self) + + def setChecked(self, checked=True): + r"""setChecked(YCheckBox self, bool checked=True)""" + return _yui.YCheckBox_setChecked(self, checked) + + def dontCare(self): + r"""dontCare(YCheckBox self) -> bool""" + return _yui.YCheckBox_dontCare(self) + + def setDontCare(self): + r"""setDontCare(YCheckBox self)""" + return _yui.YCheckBox_setDontCare(self) + + def label(self): + r"""label(YCheckBox self) -> std::string""" + return _yui.YCheckBox_label(self) + + def setLabel(self, label): + r"""setLabel(YCheckBox self, std::string const & label)""" + return _yui.YCheckBox_setLabel(self, label) + + def useBoldFont(self): + r"""useBoldFont(YCheckBox self) -> bool""" + return _yui.YCheckBox_useBoldFont(self) + + def setUseBoldFont(self, bold=True): + r"""setUseBoldFont(YCheckBox self, bool bold=True)""" + return _yui.YCheckBox_setUseBoldFont(self, bold) + + def setProperty(self, propertyName, val): + r"""setProperty(YCheckBox self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YCheckBox_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YCheckBox self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YCheckBox_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YCheckBox self) -> YPropertySet""" + return _yui.YCheckBox_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YCheckBox self) -> std::string""" + return _yui.YCheckBox_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YCheckBox self, std::string const & str)""" + return _yui.YCheckBox_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YCheckBox self) -> char const *""" + return _yui.YCheckBox_userInputProperty(self) + +# Register YCheckBox in _yui: +_yui.YCheckBox_swigregister(YCheckBox) + +class YColor(object): + r"""Proxy of C++ YColor class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YColor self, uchar red, uchar green, uchar blue) -> YColor + __init__(YColor self) -> YColor + """ + _yui.YColor_swiginit(self, _yui.new_YColor(*args)) + + def red(self): + r"""red(YColor self) -> uchar""" + return _yui.YColor_red(self) + + def green(self): + r"""green(YColor self) -> uchar""" + return _yui.YColor_green(self) + + def blue(self): + r"""blue(YColor self) -> uchar""" + return _yui.YColor_blue(self) + + def isUndefined(self): + r"""isUndefined(YColor self) -> bool""" + return _yui.YColor_isUndefined(self) + + def isDefined(self): + r"""isDefined(YColor self) -> bool""" + return _yui.YColor_isDefined(self) + __swig_destroy__ = _yui.delete_YColor + +# Register YColor in _yui: +_yui.YColor_swigregister(YColor) + +class YComboBox(YSelectionWidget): + r"""Proxy of C++ YComboBox class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YComboBox + + def widgetClass(self): + r"""widgetClass(YComboBox self) -> char const *""" + return _yui.YComboBox_widgetClass(self) + + def editable(self): + r"""editable(YComboBox self) -> bool""" + return _yui.YComboBox_editable(self) + + def value(self): + r"""value(YComboBox self) -> std::string""" + return _yui.YComboBox_value(self) + + def setValue(self, newText): + r"""setValue(YComboBox self, std::string const & newText)""" + return _yui.YComboBox_setValue(self, newText) + + def selectedItem(self): + r"""selectedItem(YComboBox self) -> YItem""" + return _yui.YComboBox_selectedItem(self) + + def selectedItems(self): + r"""selectedItems(YComboBox self) -> YItemCollection""" + return _yui.YComboBox_selectedItems(self) + + def selectItem(self, item, selected=True): + r"""selectItem(YComboBox self, YItem item, bool selected=True)""" + return _yui.YComboBox_selectItem(self, item, selected) + + def validChars(self): + r"""validChars(YComboBox self) -> std::string""" + return _yui.YComboBox_validChars(self) + + def setValidChars(self, validChars): + r"""setValidChars(YComboBox self, std::string const & validChars)""" + return _yui.YComboBox_setValidChars(self, validChars) + + def inputMaxLength(self): + r"""inputMaxLength(YComboBox self) -> int""" + return _yui.YComboBox_inputMaxLength(self) + + def setInputMaxLength(self, numberOfChars): + r"""setInputMaxLength(YComboBox self, int numberOfChars)""" + return _yui.YComboBox_setInputMaxLength(self, numberOfChars) + + def setProperty(self, propertyName, val): + r"""setProperty(YComboBox self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YComboBox_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YComboBox self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YComboBox_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YComboBox self) -> YPropertySet""" + return _yui.YComboBox_propertySet(self) + + def userInputProperty(self): + r"""userInputProperty(YComboBox self) -> char const *""" + return _yui.YComboBox_userInputProperty(self) + +# Register YComboBox in _yui: +_yui.YComboBox_swigregister(YComboBox) + +class YCommandLine(object): + r"""Proxy of C++ YCommandLine class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YCommandLine self) -> YCommandLine""" + _yui.YCommandLine_swiginit(self, _yui.new_YCommandLine()) + __swig_destroy__ = _yui.delete_YCommandLine + + def argc(self): + r"""argc(YCommandLine self) -> int""" + return _yui.YCommandLine_argc(self) + + def argv(self): + r"""argv(YCommandLine self) -> char **""" + return _yui.YCommandLine_argv(self) + + def size(self): + r"""size(YCommandLine self) -> int""" + return _yui.YCommandLine_size(self) + + def arg(self, index): + r"""arg(YCommandLine self, int index) -> std::string""" + return _yui.YCommandLine_arg(self, index) + + def add(self, arg): + r"""add(YCommandLine self, std::string const & arg)""" + return _yui.YCommandLine_add(self, arg) + + def remove(self, index): + r"""remove(YCommandLine self, int index)""" + return _yui.YCommandLine_remove(self, index) + + def replace(self, index, arg): + r"""replace(YCommandLine self, int index, std::string const & arg)""" + return _yui.YCommandLine_replace(self, index, arg) + + def find(self, argName): + r"""find(YCommandLine self, std::string const & argName) -> int""" + return _yui.YCommandLine_find(self, argName) + +# Register YCommandLine in _yui: +_yui.YCommandLine_swigregister(YCommandLine) + +class YDateField(YSimpleInputField): + r"""Proxy of C++ YDateField class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YDateField + + def widgetClass(self): + r"""widgetClass(YDateField self) -> char const *""" + return _yui.YDateField_widgetClass(self) + +# Register YDateField in _yui: +_yui.YDateField_swigregister(YDateField) + +class YDownloadProgress(YWidget): + r"""Proxy of C++ YDownloadProgress class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YDownloadProgress + + def widgetClass(self): + r"""widgetClass(YDownloadProgress self) -> char const *""" + return _yui.YDownloadProgress_widgetClass(self) + + def label(self): + r"""label(YDownloadProgress self) -> std::string""" + return _yui.YDownloadProgress_label(self) + + def setLabel(self, label): + r"""setLabel(YDownloadProgress self, std::string const & label)""" + return _yui.YDownloadProgress_setLabel(self, label) + + def filename(self): + r"""filename(YDownloadProgress self) -> std::string""" + return _yui.YDownloadProgress_filename(self) + + def setFilename(self, filename): + r"""setFilename(YDownloadProgress self, std::string const & filename)""" + return _yui.YDownloadProgress_setFilename(self, filename) + + def expectedSize(self): + r"""expectedSize(YDownloadProgress self) -> YFileSize_t""" + return _yui.YDownloadProgress_expectedSize(self) + + def setExpectedSize(self, newSize): + r"""setExpectedSize(YDownloadProgress self, YFileSize_t newSize)""" + return _yui.YDownloadProgress_setExpectedSize(self, newSize) + + def currentFileSize(self): + r"""currentFileSize(YDownloadProgress self) -> YFileSize_t""" + return _yui.YDownloadProgress_currentFileSize(self) + + def currentPercent(self): + r"""currentPercent(YDownloadProgress self) -> int""" + return _yui.YDownloadProgress_currentPercent(self) + + def value(self): + r"""value(YDownloadProgress self) -> int""" + return _yui.YDownloadProgress_value(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YDownloadProgress self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YDownloadProgress_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YDownloadProgress self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YDownloadProgress_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YDownloadProgress self) -> YPropertySet""" + return _yui.YDownloadProgress_propertySet(self) + +# Register YDownloadProgress in _yui: +_yui.YDownloadProgress_swigregister(YDownloadProgress) + +class YDumbTab(YSelectionWidget): + r"""Proxy of C++ YDumbTab class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YDumbTab + + def widgetClass(self): + r"""widgetClass(YDumbTab self) -> char const *""" + return _yui.YDumbTab_widgetClass(self) + + def addItem(self, item): + r"""addItem(YDumbTab self, YItem item)""" + return _yui.YDumbTab_addItem(self, item) + + def setProperty(self, propertyName, val): + r"""setProperty(YDumbTab self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YDumbTab_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YDumbTab self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YDumbTab_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YDumbTab self) -> YPropertySet""" + return _yui.YDumbTab_propertySet(self) + + def stretchable(self, dim): + r"""stretchable(YDumbTab self, YUIDimension dim) -> bool""" + return _yui.YDumbTab_stretchable(self, dim) + + def debugLabel(self): + r"""debugLabel(YDumbTab self) -> std::string""" + return _yui.YDumbTab_debugLabel(self) + + def activate(self): + r"""activate(YDumbTab self)""" + return _yui.YDumbTab_activate(self) + +# Register YDumbTab in _yui: +_yui.YDumbTab_swigregister(YDumbTab) + +class YEmpty(YWidget): + r"""Proxy of C++ YEmpty class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YEmpty + + def widgetClass(self): + r"""widgetClass(YEmpty self) -> char const *""" + return _yui.YEmpty_widgetClass(self) + + def preferredWidth(self): + r"""preferredWidth(YEmpty self) -> int""" + return _yui.YEmpty_preferredWidth(self) + + def preferredHeight(self): + r"""preferredHeight(YEmpty self) -> int""" + return _yui.YEmpty_preferredHeight(self) + +# Register YEmpty in _yui: +_yui.YEmpty_swigregister(YEmpty) + +class YEvent(object): + r"""Proxy of C++ YEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + NoEvent = _yui.YEvent_NoEvent + + UnknownEvent = _yui.YEvent_UnknownEvent + + WidgetEvent = _yui.YEvent_WidgetEvent + + MenuEvent = _yui.YEvent_MenuEvent + + KeyEvent = _yui.YEvent_KeyEvent + + CancelEvent = _yui.YEvent_CancelEvent + + TimeoutEvent = _yui.YEvent_TimeoutEvent + + DebugEvent = _yui.YEvent_DebugEvent + + SpecialKeyEvent = _yui.YEvent_SpecialKeyEvent + + InvalidEvent = _yui.YEvent_InvalidEvent + + UnknownReason = _yui.YEvent_UnknownReason + + Activated = _yui.YEvent_Activated + + SelectionChanged = _yui.YEvent_SelectionChanged + + ValueChanged = _yui.YEvent_ValueChanged + + ContextMenuActivated = _yui.YEvent_ContextMenuActivated + + + def __init__(self, *args): + r"""__init__(YEvent self, YEvent::EventType eventType=UnknownEvent) -> YEvent""" + _yui.YEvent_swiginit(self, _yui.new_YEvent(*args)) + + def eventType(self): + r"""eventType(YEvent self) -> YEvent::EventType""" + return _yui.YEvent_eventType(self) + + def serial(self): + r"""serial(YEvent self) -> unsigned long""" + return _yui.YEvent_serial(self) + + def widget(self): + r"""widget(YEvent self) -> YWidget""" + return _yui.YEvent_widget(self) + + def item(self): + r"""item(YEvent self) -> YItem""" + return _yui.YEvent_item(self) + + def dialog(self): + r"""dialog(YEvent self) -> YDialog""" + return _yui.YEvent_dialog(self) + + def isValid(self): + r"""isValid(YEvent self) -> bool""" + return _yui.YEvent_isValid(self) + + @staticmethod + def toString(*args): + r""" + toString(YEvent::EventType eventType) -> char const + toString(YEvent::EventReason reason) -> char const * + """ + return _yui.YEvent_toString(*args) + +# Register YEvent in _yui: +_yui.YEvent_swigregister(YEvent) + +def YEvent_toString(*args): + r""" + YEvent_toString(YEvent::EventType eventType) -> char const + YEvent_toString(YEvent::EventReason reason) -> char const * + """ + return _yui.YEvent_toString(*args) + +class YWidgetEvent(YEvent): + r"""Proxy of C++ YWidgetEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r"""__init__(YWidgetEvent self, YWidget widget=None, YEvent::EventReason reason=Activated, YEvent::EventType eventType=WidgetEvent) -> YWidgetEvent""" + _yui.YWidgetEvent_swiginit(self, _yui.new_YWidgetEvent(*args)) + + def widget(self): + r"""widget(YWidgetEvent self) -> YWidget""" + return _yui.YWidgetEvent_widget(self) + + def reason(self): + r"""reason(YWidgetEvent self) -> YEvent::EventReason""" + return _yui.YWidgetEvent_reason(self) + +# Register YWidgetEvent in _yui: +_yui.YWidgetEvent_swigregister(YWidgetEvent) + +class YKeyEvent(YEvent): + r"""Proxy of C++ YKeyEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, keySymbol, focusWidget=None): + r"""__init__(YKeyEvent self, std::string const & keySymbol, YWidget focusWidget=None) -> YKeyEvent""" + _yui.YKeyEvent_swiginit(self, _yui.new_YKeyEvent(keySymbol, focusWidget)) + + def keySymbol(self): + r"""keySymbol(YKeyEvent self) -> std::string""" + return _yui.YKeyEvent_keySymbol(self) + + def focusWidget(self): + r"""focusWidget(YKeyEvent self) -> YWidget""" + return _yui.YKeyEvent_focusWidget(self) + +# Register YKeyEvent in _yui: +_yui.YKeyEvent_swigregister(YKeyEvent) + +class YMenuEvent(YEvent): + r"""Proxy of C++ YMenuEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YMenuEvent self, YItem item) -> YMenuEvent + __init__(YMenuEvent self, char const * id) -> YMenuEvent + __init__(YMenuEvent self, std::string const & id) -> YMenuEvent + """ + _yui.YMenuEvent_swiginit(self, _yui.new_YMenuEvent(*args)) + + def item(self): + r"""item(YMenuEvent self) -> YItem""" + return _yui.YMenuEvent_item(self) + + def id(self): + r"""id(YMenuEvent self) -> std::string""" + return _yui.YMenuEvent_id(self) + +# Register YMenuEvent in _yui: +_yui.YMenuEvent_swigregister(YMenuEvent) + +class YCancelEvent(YEvent): + r"""Proxy of C++ YCancelEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YCancelEvent self) -> YCancelEvent""" + _yui.YCancelEvent_swiginit(self, _yui.new_YCancelEvent()) + +# Register YCancelEvent in _yui: +_yui.YCancelEvent_swigregister(YCancelEvent) + +class YDebugEvent(YEvent): + r"""Proxy of C++ YDebugEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YDebugEvent self) -> YDebugEvent""" + _yui.YDebugEvent_swiginit(self, _yui.new_YDebugEvent()) + +# Register YDebugEvent in _yui: +_yui.YDebugEvent_swigregister(YDebugEvent) + +class YSpecialKeyEvent(YEvent): + r"""Proxy of C++ YSpecialKeyEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YSpecialKeyEvent self, char const * id) -> YSpecialKeyEvent + __init__(YSpecialKeyEvent self, std::string const & id) -> YSpecialKeyEvent + """ + _yui.YSpecialKeyEvent_swiginit(self, _yui.new_YSpecialKeyEvent(*args)) + + def id(self): + r"""id(YSpecialKeyEvent self) -> std::string""" + return _yui.YSpecialKeyEvent_id(self) + +# Register YSpecialKeyEvent in _yui: +_yui.YSpecialKeyEvent_swigregister(YSpecialKeyEvent) + +class YTimeoutEvent(YEvent): + r"""Proxy of C++ YTimeoutEvent class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YTimeoutEvent self) -> YTimeoutEvent""" + _yui.YTimeoutEvent_swiginit(self, _yui.new_YTimeoutEvent()) + +# Register YTimeoutEvent in _yui: +_yui.YTimeoutEvent_swigregister(YTimeoutEvent) + +class YFrame(YSingleChildContainerWidget): + r"""Proxy of C++ YFrame class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YFrame + + def widgetClass(self): + r"""widgetClass(YFrame self) -> char const *""" + return _yui.YFrame_widgetClass(self) + + def setLabel(self, newLabel): + r"""setLabel(YFrame self, std::string const & newLabel)""" + return _yui.YFrame_setLabel(self, newLabel) + + def label(self): + r"""label(YFrame self) -> std::string""" + return _yui.YFrame_label(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YFrame self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YFrame_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YFrame self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YFrame_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YFrame self) -> YPropertySet""" + return _yui.YFrame_propertySet(self) + +# Register YFrame in _yui: +_yui.YFrame_swigregister(YFrame) + +class YImage(YWidget): + r"""Proxy of C++ YImage class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YImage + + def widgetClass(self): + r"""widgetClass(YImage self) -> char const *""" + return _yui.YImage_widgetClass(self) + + def imageFileName(self): + r"""imageFileName(YImage self) -> std::string""" + return _yui.YImage_imageFileName(self) + + def animated(self): + r"""animated(YImage self) -> bool""" + return _yui.YImage_animated(self) + + def setImage(self, imageFileName, animated=False): + r"""setImage(YImage self, std::string const & imageFileName, bool animated=False)""" + return _yui.YImage_setImage(self, imageFileName, animated) + + def setMovie(self, movieFileName): + r"""setMovie(YImage self, std::string const & movieFileName)""" + return _yui.YImage_setMovie(self, movieFileName) + + def hasZeroSize(self, dim): + r"""hasZeroSize(YImage self, YUIDimension dim) -> bool""" + return _yui.YImage_hasZeroSize(self, dim) + + def setZeroSize(self, dim, zeroSize=True): + r"""setZeroSize(YImage self, YUIDimension dim, bool zeroSize=True)""" + return _yui.YImage_setZeroSize(self, dim, zeroSize) + + def autoScale(self): + r"""autoScale(YImage self) -> bool""" + return _yui.YImage_autoScale(self) + + def setAutoScale(self, autoScale=True): + r"""setAutoScale(YImage self, bool autoScale=True)""" + return _yui.YImage_setAutoScale(self, autoScale) + +# Register YImage in _yui: +_yui.YImage_swigregister(YImage) + +class YInputField(YWidget): + r"""Proxy of C++ YInputField class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YInputField + + def widgetClass(self): + r"""widgetClass(YInputField self) -> char const *""" + return _yui.YInputField_widgetClass(self) + + def value(self): + r"""value(YInputField self) -> std::string""" + return _yui.YInputField_value(self) + + def setValue(self, text): + r"""setValue(YInputField self, std::string const & text)""" + return _yui.YInputField_setValue(self, text) + + def label(self): + r"""label(YInputField self) -> std::string""" + return _yui.YInputField_label(self) + + def setLabel(self, label): + r"""setLabel(YInputField self, std::string const & label)""" + return _yui.YInputField_setLabel(self, label) + + def passwordMode(self): + r"""passwordMode(YInputField self) -> bool""" + return _yui.YInputField_passwordMode(self) + + def validChars(self): + r"""validChars(YInputField self) -> std::string""" + return _yui.YInputField_validChars(self) + + def setValidChars(self, validChars): + r"""setValidChars(YInputField self, std::string const & validChars)""" + return _yui.YInputField_setValidChars(self, validChars) + + def inputMaxLength(self): + r"""inputMaxLength(YInputField self) -> int""" + return _yui.YInputField_inputMaxLength(self) + + def setInputMaxLength(self, numberOfChars): + r"""setInputMaxLength(YInputField self, int numberOfChars)""" + return _yui.YInputField_setInputMaxLength(self, numberOfChars) + + def shrinkable(self): + r"""shrinkable(YInputField self) -> bool""" + return _yui.YInputField_shrinkable(self) + + def setShrinkable(self, shrinkable=True): + r"""setShrinkable(YInputField self, bool shrinkable=True)""" + return _yui.YInputField_setShrinkable(self, shrinkable) + + def setProperty(self, propertyName, val): + r"""setProperty(YInputField self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YInputField_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YInputField self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YInputField_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YInputField self) -> YPropertySet""" + return _yui.YInputField_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YInputField self) -> std::string""" + return _yui.YInputField_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YInputField self, std::string const & str)""" + return _yui.YInputField_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YInputField self) -> char const *""" + return _yui.YInputField_userInputProperty(self) + + def saveUserInput(self, macroRecorder): + r"""saveUserInput(YInputField self, YMacroRecorder macroRecorder)""" + return _yui.YInputField_saveUserInput(self, macroRecorder) + +# Register YInputField in _yui: +_yui.YInputField_swigregister(YInputField) + +class YIntField(YWidget): + r"""Proxy of C++ YIntField class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YIntField + + def widgetClass(self): + r"""widgetClass(YIntField self) -> char const *""" + return _yui.YIntField_widgetClass(self) + + def value(self): + r"""value(YIntField self) -> int""" + return _yui.YIntField_value(self) + + def setValue(self, val): + r"""setValue(YIntField self, int val)""" + return _yui.YIntField_setValue(self, val) + + def minValue(self): + r"""minValue(YIntField self) -> int""" + return _yui.YIntField_minValue(self) + + def setMinValue(self, val): + r"""setMinValue(YIntField self, int val)""" + return _yui.YIntField_setMinValue(self, val) + + def maxValue(self): + r"""maxValue(YIntField self) -> int""" + return _yui.YIntField_maxValue(self) + + def setMaxValue(self, val): + r"""setMaxValue(YIntField self, int val)""" + return _yui.YIntField_setMaxValue(self, val) + + def label(self): + r"""label(YIntField self) -> std::string""" + return _yui.YIntField_label(self) + + def setLabel(self, label): + r"""setLabel(YIntField self, std::string const & label)""" + return _yui.YIntField_setLabel(self, label) + + def setProperty(self, propertyName, val): + r"""setProperty(YIntField self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YIntField_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YIntField self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YIntField_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YIntField self) -> YPropertySet""" + return _yui.YIntField_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YIntField self) -> std::string""" + return _yui.YIntField_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YIntField self, std::string const & str)""" + return _yui.YIntField_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YIntField self) -> char const *""" + return _yui.YIntField_userInputProperty(self) + +# Register YIntField in _yui: +_yui.YIntField_swigregister(YIntField) + +class YItemSelector(YSelectionWidget): + r"""Proxy of C++ YItemSelector class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YItemSelector + + def widgetClass(self): + r"""widgetClass(YItemSelector self) -> char const *""" + return _yui.YItemSelector_widgetClass(self) + + def visibleItems(self): + r"""visibleItems(YItemSelector self) -> int""" + return _yui.YItemSelector_visibleItems(self) + + def setVisibleItems(self, newVal): + r"""setVisibleItems(YItemSelector self, int newVal)""" + return _yui.YItemSelector_setVisibleItems(self, newVal) + + def setItemStatus(self, item, status): + r"""setItemStatus(YItemSelector self, YItem item, int status)""" + return _yui.YItemSelector_setItemStatus(self, item, status) + + def usingCustomStatus(self): + r"""usingCustomStatus(YItemSelector self) -> bool""" + return _yui.YItemSelector_usingCustomStatus(self) + + def customStatusCount(self): + r"""customStatusCount(YItemSelector self) -> int""" + return _yui.YItemSelector_customStatusCount(self) + + def customStatus(self, index): + r"""customStatus(YItemSelector self, int index) -> YItemCustomStatus const &""" + return _yui.YItemSelector_customStatus(self, index) + + def validCustomStatusIndex(self, index): + r"""validCustomStatusIndex(YItemSelector self, int index) -> bool""" + return _yui.YItemSelector_validCustomStatusIndex(self, index) + + def cycleCustomStatus(self, oldStatus): + r"""cycleCustomStatus(YItemSelector self, int oldStatus) -> int""" + return _yui.YItemSelector_cycleCustomStatus(self, oldStatus) + + def setProperty(self, propertyName, val): + r"""setProperty(YItemSelector self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YItemSelector_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YItemSelector self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YItemSelector_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YItemSelector self) -> YPropertySet""" + return _yui.YItemSelector_propertySet(self) + + def userInputProperty(self): + r"""userInputProperty(YItemSelector self) -> char const *""" + return _yui.YItemSelector_userInputProperty(self) + + def activateItem(self, item): + r"""activateItem(YItemSelector self, YItem item)""" + return _yui.YItemSelector_activateItem(self, item) + +# Register YItemSelector in _yui: +_yui.YItemSelector_swigregister(YItemSelector) + +class YLabel(YWidget): + r"""Proxy of C++ YLabel class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YLabel + + def widgetClass(self): + r"""widgetClass(YLabel self) -> char const *""" + return _yui.YLabel_widgetClass(self) + + def text(self): + r"""text(YLabel self) -> std::string""" + return _yui.YLabel_text(self) + + def value(self): + r"""value(YLabel self) -> std::string""" + return _yui.YLabel_value(self) + + def label(self): + r"""label(YLabel self) -> std::string""" + return _yui.YLabel_label(self) + + def setText(self, newText): + r"""setText(YLabel self, std::string const & newText)""" + return _yui.YLabel_setText(self, newText) + + def setValue(self, newValue): + r"""setValue(YLabel self, std::string const & newValue)""" + return _yui.YLabel_setValue(self, newValue) + + def setLabel(self, newLabel): + r"""setLabel(YLabel self, std::string const & newLabel)""" + return _yui.YLabel_setLabel(self, newLabel) + + def isHeading(self): + r"""isHeading(YLabel self) -> bool""" + return _yui.YLabel_isHeading(self) + + def isOutputField(self): + r"""isOutputField(YLabel self) -> bool""" + return _yui.YLabel_isOutputField(self) + + def useBoldFont(self): + r"""useBoldFont(YLabel self) -> bool""" + return _yui.YLabel_useBoldFont(self) + + def setUseBoldFont(self, bold=True): + r"""setUseBoldFont(YLabel self, bool bold=True)""" + return _yui.YLabel_setUseBoldFont(self, bold) + + def autoWrap(self): + r"""autoWrap(YLabel self) -> bool""" + return _yui.YLabel_autoWrap(self) + + def setAutoWrap(self, autoWrap=True): + r"""setAutoWrap(YLabel self, bool autoWrap=True)""" + return _yui.YLabel_setAutoWrap(self, autoWrap) + + def setProperty(self, propertyName, val): + r"""setProperty(YLabel self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YLabel_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YLabel self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YLabel_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YLabel self) -> YPropertySet""" + return _yui.YLabel_propertySet(self) + + def debugLabel(self): + r"""debugLabel(YLabel self) -> std::string""" + return _yui.YLabel_debugLabel(self) + +# Register YLabel in _yui: +_yui.YLabel_swigregister(YLabel) + +class YLayoutBox(YWidget): + r"""Proxy of C++ YLayoutBox class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YLayoutBox + + def widgetClass(self): + r"""widgetClass(YLayoutBox self) -> char const *""" + return _yui.YLayoutBox_widgetClass(self) + + def primary(self): + r"""primary(YLayoutBox self) -> YUIDimension""" + return _yui.YLayoutBox_primary(self) + + def secondary(self): + r"""secondary(YLayoutBox self) -> YUIDimension""" + return _yui.YLayoutBox_secondary(self) + + def debugLayout(self): + r"""debugLayout(YLayoutBox self) -> bool""" + return _yui.YLayoutBox_debugLayout(self) + + def setDebugLayout(self, deb=True): + r"""setDebugLayout(YLayoutBox self, bool deb=True)""" + return _yui.YLayoutBox_setDebugLayout(self, deb) + + def preferredSize(self, dim): + r"""preferredSize(YLayoutBox self, YUIDimension dim) -> int""" + return _yui.YLayoutBox_preferredSize(self, dim) + + def preferredWidth(self): + r"""preferredWidth(YLayoutBox self) -> int""" + return _yui.YLayoutBox_preferredWidth(self) + + def preferredHeight(self): + r"""preferredHeight(YLayoutBox self) -> int""" + return _yui.YLayoutBox_preferredHeight(self) + + def setSize(self, newWidth, newHeight): + r"""setSize(YLayoutBox self, int newWidth, int newHeight)""" + return _yui.YLayoutBox_setSize(self, newWidth, newHeight) + + def stretchable(self, dimension): + r"""stretchable(YLayoutBox self, YUIDimension dimension) -> bool""" + return _yui.YLayoutBox_stretchable(self, dimension) + + def moveChild(self, child, newX, newY): + r"""moveChild(YLayoutBox self, YWidget child, int newX, int newY)""" + return _yui.YLayoutBox_moveChild(self, child, newX, newY) + + @staticmethod + def isLayoutStretch(child, dimension): + r"""isLayoutStretch(YWidget child, YUIDimension dimension) -> bool""" + return _yui.YLayoutBox_isLayoutStretch(child, dimension) + +# Register YLayoutBox in _yui: +_yui.YLayoutBox_swigregister(YLayoutBox) + +def YLayoutBox_isLayoutStretch(child, dimension): + r"""YLayoutBox_isLayoutStretch(YWidget child, YUIDimension dimension) -> bool""" + return _yui.YLayoutBox_isLayoutStretch(child, dimension) + +class YLogView(YWidget): + r"""Proxy of C++ YLogView class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YLogView + + def widgetClass(self): + r"""widgetClass(YLogView self) -> char const *""" + return _yui.YLogView_widgetClass(self) + + def label(self): + r"""label(YLogView self) -> std::string""" + return _yui.YLogView_label(self) + + def setLabel(self, label): + r"""setLabel(YLogView self, std::string const & label)""" + return _yui.YLogView_setLabel(self, label) + + def visibleLines(self): + r"""visibleLines(YLogView self) -> int""" + return _yui.YLogView_visibleLines(self) + + def setVisibleLines(self, newVisibleLines): + r"""setVisibleLines(YLogView self, int newVisibleLines)""" + return _yui.YLogView_setVisibleLines(self, newVisibleLines) + + def maxLines(self): + r"""maxLines(YLogView self) -> int""" + return _yui.YLogView_maxLines(self) + + def setMaxLines(self, newMaxLines): + r"""setMaxLines(YLogView self, int newMaxLines)""" + return _yui.YLogView_setMaxLines(self, newMaxLines) + + def logText(self): + r"""logText(YLogView self) -> std::string""" + return _yui.YLogView_logText(self) + + def setLogText(self, text): + r"""setLogText(YLogView self, std::string const & text)""" + return _yui.YLogView_setLogText(self, text) + + def lastLine(self): + r"""lastLine(YLogView self) -> std::string""" + return _yui.YLogView_lastLine(self) + + def appendLines(self, text): + r"""appendLines(YLogView self, std::string const & text)""" + return _yui.YLogView_appendLines(self, text) + + def clearText(self): + r"""clearText(YLogView self)""" + return _yui.YLogView_clearText(self) + + def lines(self): + r"""lines(YLogView self) -> int""" + return _yui.YLogView_lines(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YLogView self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YLogView_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YLogView self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YLogView_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YLogView self) -> YPropertySet""" + return _yui.YLogView_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YLogView self) -> std::string""" + return _yui.YLogView_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YLogView self, std::string const & str)""" + return _yui.YLogView_setShortcutString(self, str) + +# Register YLogView in _yui: +_yui.YLogView_swigregister(YLogView) + +class YMacro(object): + r"""Proxy of C++ YMacro class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + + @staticmethod + def setRecorder(recorder): + r"""setRecorder(YMacroRecorder recorder)""" + return _yui.YMacro_setRecorder(recorder) + + @staticmethod + def setPlayer(player): + r"""setPlayer(YMacroPlayer player)""" + return _yui.YMacro_setPlayer(player) + + @staticmethod + def record(macroFile): + r"""record(std::string const & macroFile)""" + return _yui.YMacro_record(macroFile) + + @staticmethod + def endRecording(): + r"""endRecording()""" + return _yui.YMacro_endRecording() + + @staticmethod + def recording(): + r"""recording() -> bool""" + return _yui.YMacro_recording() + + @staticmethod + def play(macroFile): + r"""play(std::string const & macroFile)""" + return _yui.YMacro_play(macroFile) + + @staticmethod + def playNextBlock(): + r"""playNextBlock()""" + return _yui.YMacro_playNextBlock() + + @staticmethod + def playing(): + r"""playing() -> bool""" + return _yui.YMacro_playing() + + @staticmethod + def recorder(): + r"""recorder() -> YMacroRecorder""" + return _yui.YMacro_recorder() + + @staticmethod + def player(): + r"""player() -> YMacroPlayer""" + return _yui.YMacro_player() + + @staticmethod + def deleteRecorder(): + r"""deleteRecorder()""" + return _yui.YMacro_deleteRecorder() + + @staticmethod + def deletePlayer(): + r"""deletePlayer()""" + return _yui.YMacro_deletePlayer() + +# Register YMacro in _yui: +_yui.YMacro_swigregister(YMacro) + +def YMacro_setRecorder(recorder): + r"""YMacro_setRecorder(YMacroRecorder recorder)""" + return _yui.YMacro_setRecorder(recorder) + +def YMacro_setPlayer(player): + r"""YMacro_setPlayer(YMacroPlayer player)""" + return _yui.YMacro_setPlayer(player) + +def YMacro_record(macroFile): + r"""YMacro_record(std::string const & macroFile)""" + return _yui.YMacro_record(macroFile) + +def YMacro_endRecording(): + r"""YMacro_endRecording()""" + return _yui.YMacro_endRecording() + +def YMacro_recording(): + r"""YMacro_recording() -> bool""" + return _yui.YMacro_recording() + +def YMacro_play(macroFile): + r"""YMacro_play(std::string const & macroFile)""" + return _yui.YMacro_play(macroFile) + +def YMacro_playNextBlock(): + r"""YMacro_playNextBlock()""" + return _yui.YMacro_playNextBlock() + +def YMacro_playing(): + r"""YMacro_playing() -> bool""" + return _yui.YMacro_playing() + +def YMacro_recorder(): + r"""YMacro_recorder() -> YMacroRecorder""" + return _yui.YMacro_recorder() + +def YMacro_player(): + r"""YMacro_player() -> YMacroPlayer""" + return _yui.YMacro_player() + +def YMacro_deleteRecorder(): + r"""YMacro_deleteRecorder()""" + return _yui.YMacro_deleteRecorder() + +def YMacro_deletePlayer(): + r"""YMacro_deletePlayer()""" + return _yui.YMacro_deletePlayer() + +class YMacroPlayer(object): + r"""Proxy of C++ YMacroPlayer class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMacroPlayer + + def play(self, macroFile): + r"""play(YMacroPlayer self, std::string const & macroFile)""" + return _yui.YMacroPlayer_play(self, macroFile) + + def playNextBlock(self): + r"""playNextBlock(YMacroPlayer self)""" + return _yui.YMacroPlayer_playNextBlock(self) + + def playing(self): + r"""playing(YMacroPlayer self) -> bool""" + return _yui.YMacroPlayer_playing(self) + +# Register YMacroPlayer in _yui: +_yui.YMacroPlayer_swigregister(YMacroPlayer) + +class YMacroRecorder(object): + r"""Proxy of C++ YMacroRecorder class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMacroRecorder + + def record(self, macroFileName): + r"""record(YMacroRecorder self, std::string const & macroFileName)""" + return _yui.YMacroRecorder_record(self, macroFileName) + + def endRecording(self): + r"""endRecording(YMacroRecorder self)""" + return _yui.YMacroRecorder_endRecording(self) + + def recording(self): + r"""recording(YMacroRecorder self) -> bool""" + return _yui.YMacroRecorder_recording(self) + + def recordWidgetProperty(self, widget, propertyName): + r"""recordWidgetProperty(YMacroRecorder self, YWidget widget, char const * propertyName)""" + return _yui.YMacroRecorder_recordWidgetProperty(self, widget, propertyName) + + def recordMakeScreenShot(self, *args): + r"""recordMakeScreenShot(YMacroRecorder self, bool enabled=False, std::string const & filename=std::string())""" + return _yui.YMacroRecorder_recordMakeScreenShot(self, *args) + +# Register YMacroRecorder in _yui: +_yui.YMacroRecorder_swigregister(YMacroRecorder) + +class YMenuWidget(YSelectionWidget): + r"""Proxy of C++ YMenuWidget class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMenuWidget + + def widgetClass(self): + r"""widgetClass(YMenuWidget self) -> char const *""" + return _yui.YMenuWidget_widgetClass(self) + + def rebuildMenuTree(self): + r"""rebuildMenuTree(YMenuWidget self)""" + return _yui.YMenuWidget_rebuildMenuTree(self) + + def addItems(self, itemCollection): + r"""addItems(YMenuWidget self, YItemCollection itemCollection)""" + return _yui.YMenuWidget_addItems(self, itemCollection) + + def addItem(self, item_disown): + r"""addItem(YMenuWidget self, YItem item_disown)""" + return _yui.YMenuWidget_addItem(self, item_disown) + + def deleteAllItems(self): + r"""deleteAllItems(YMenuWidget self)""" + return _yui.YMenuWidget_deleteAllItems(self) + + def resolveShortcutConflicts(self): + r"""resolveShortcutConflicts(YMenuWidget self)""" + return _yui.YMenuWidget_resolveShortcutConflicts(self) + + def setItemEnabled(self, item, enabled): + r"""setItemEnabled(YMenuWidget self, YMenuItem item, bool enabled)""" + return _yui.YMenuWidget_setItemEnabled(self, item, enabled) + + def setItemVisible(self, item, visible): + r"""setItemVisible(YMenuWidget self, YMenuItem item, bool visible)""" + return _yui.YMenuWidget_setItemVisible(self, item, visible) + + def findItem(self, path): + r"""findItem(YMenuWidget self, std::vector< std::string,std::allocator< std::string > > & path) -> YMenuItem""" + return _yui.YMenuWidget_findItem(self, path) + + def activateItem(self, item): + r"""activateItem(YMenuWidget self, YMenuItem item)""" + return _yui.YMenuWidget_activateItem(self, item) + + def findMenuItem(self, index): + r"""findMenuItem(YMenuWidget self, int index) -> YMenuItem""" + return _yui.YMenuWidget_findMenuItem(self, index) + +# Register YMenuWidget in _yui: +_yui.YMenuWidget_swigregister(YMenuWidget) + +class YMenuBar(YMenuWidget): + r"""Proxy of C++ YMenuBar class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMenuBar + + def addMenu(self, *args): + r"""addMenu(YMenuBar self, std::string const & label, std::string const & iconName="") -> YMenuItem""" + return _yui.YMenuBar_addMenu(self, *args) + + def widgetClass(self): + r"""widgetClass(YMenuBar self) -> char const *""" + return _yui.YMenuBar_widgetClass(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YMenuBar self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YMenuBar_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YMenuBar self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YMenuBar_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YMenuBar self) -> YPropertySet""" + return _yui.YMenuBar_propertySet(self) + +# Register YMenuBar in _yui: +_yui.YMenuBar_swigregister(YMenuBar) + +class YMenuButton(YMenuWidget): + r"""Proxy of C++ YMenuButton class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMenuButton + + def addItem(self, *args): + r"""addItem(YMenuButton self, std::string const & label, std::string const & iconName="") -> YMenuItem""" + return _yui.YMenuButton_addItem(self, *args) + + def addMenu(self, *args): + r"""addMenu(YMenuButton self, std::string const & label, std::string const & iconName="") -> YMenuItem""" + return _yui.YMenuButton_addMenu(self, *args) + + def addSeparator(self): + r"""addSeparator(YMenuButton self) -> YMenuItem""" + return _yui.YMenuButton_addSeparator(self) + + def widgetClass(self): + r"""widgetClass(YMenuButton self) -> char const *""" + return _yui.YMenuButton_widgetClass(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YMenuButton self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YMenuButton_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YMenuButton self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YMenuButton_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YMenuButton self) -> YPropertySet""" + return _yui.YMenuButton_propertySet(self) + +# Register YMenuButton in _yui: +_yui.YMenuButton_swigregister(YMenuButton) + +class YMenuItem(YTreeItem): + r"""Proxy of C++ YMenuItem class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YMenuItem self, std::string const & label, std::string const & iconName="") -> YMenuItem + __init__(YMenuItem self, YMenuItem parent, std::string const & label, std::string const & iconName="") -> YMenuItem + """ + _yui.YMenuItem_swiginit(self, _yui.new_YMenuItem(*args)) + __swig_destroy__ = _yui.delete_YMenuItem + + def addItem(self, *args): + r"""addItem(YMenuItem self, std::string const & label, std::string const & iconName="") -> YMenuItem""" + return _yui.YMenuItem_addItem(self, *args) + + def addMenu(self, *args): + r"""addMenu(YMenuItem self, std::string const & label, std::string const & iconName="") -> YMenuItem""" + return _yui.YMenuItem_addMenu(self, *args) + + def addSeparator(self): + r"""addSeparator(YMenuItem self) -> YMenuItem""" + return _yui.YMenuItem_addSeparator(self) + + def parent(self): + r"""parent(YMenuItem self) -> YMenuItem""" + return _yui.YMenuItem_parent(self) + + def isMenu(self): + r"""isMenu(YMenuItem self) -> bool""" + return _yui.YMenuItem_isMenu(self) + + def isSeparator(self): + r"""isSeparator(YMenuItem self) -> bool""" + return _yui.YMenuItem_isSeparator(self) + + def isEnabled(self): + r"""isEnabled(YMenuItem self) -> bool""" + return _yui.YMenuItem_isEnabled(self) + + def setEnabled(self, enabled=True): + r"""setEnabled(YMenuItem self, bool enabled=True)""" + return _yui.YMenuItem_setEnabled(self, enabled) + + def isVisible(self): + r"""isVisible(YMenuItem self) -> bool""" + return _yui.YMenuItem_isVisible(self) + + def setVisible(self, visible=True): + r"""setVisible(YMenuItem self, bool visible=True)""" + return _yui.YMenuItem_setVisible(self, visible) + + def uiItem(self): + r"""uiItem(YMenuItem self) -> void *""" + return _yui.YMenuItem_uiItem(self) + + def setUiItem(self, uiItem): + r"""setUiItem(YMenuItem self, void * uiItem)""" + return _yui.YMenuItem_setUiItem(self, uiItem) + +# Register YMenuItem in _yui: +_yui.YMenuItem_swigregister(YMenuItem) + +class YMultiLineEdit(YWidget): + r"""Proxy of C++ YMultiLineEdit class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMultiLineEdit + + def widgetClass(self): + r"""widgetClass(YMultiLineEdit self) -> char const *""" + return _yui.YMultiLineEdit_widgetClass(self) + + def value(self): + r"""value(YMultiLineEdit self) -> std::string""" + return _yui.YMultiLineEdit_value(self) + + def setValue(self, text): + r"""setValue(YMultiLineEdit self, std::string const & text)""" + return _yui.YMultiLineEdit_setValue(self, text) + + def label(self): + r"""label(YMultiLineEdit self) -> std::string""" + return _yui.YMultiLineEdit_label(self) + + def setLabel(self, label): + r"""setLabel(YMultiLineEdit self, std::string const & label)""" + return _yui.YMultiLineEdit_setLabel(self, label) + + def inputMaxLength(self): + r"""inputMaxLength(YMultiLineEdit self) -> int""" + return _yui.YMultiLineEdit_inputMaxLength(self) + + def setInputMaxLength(self, numberOfChars): + r"""setInputMaxLength(YMultiLineEdit self, int numberOfChars)""" + return _yui.YMultiLineEdit_setInputMaxLength(self, numberOfChars) + + def defaultVisibleLines(self): + r"""defaultVisibleLines(YMultiLineEdit self) -> int""" + return _yui.YMultiLineEdit_defaultVisibleLines(self) + + def setDefaultVisibleLines(self, newVisibleLines): + r"""setDefaultVisibleLines(YMultiLineEdit self, int newVisibleLines)""" + return _yui.YMultiLineEdit_setDefaultVisibleLines(self, newVisibleLines) + + def setProperty(self, propertyName, val): + r"""setProperty(YMultiLineEdit self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YMultiLineEdit_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YMultiLineEdit self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YMultiLineEdit_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YMultiLineEdit self) -> YPropertySet""" + return _yui.YMultiLineEdit_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YMultiLineEdit self) -> std::string""" + return _yui.YMultiLineEdit_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YMultiLineEdit self, std::string const & str)""" + return _yui.YMultiLineEdit_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YMultiLineEdit self) -> char const *""" + return _yui.YMultiLineEdit_userInputProperty(self) + +# Register YMultiLineEdit in _yui: +_yui.YMultiLineEdit_swigregister(YMultiLineEdit) + +class YMultiProgressMeter(YWidget): + r"""Proxy of C++ YMultiProgressMeter class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMultiProgressMeter + + def widgetClass(self): + r"""widgetClass(YMultiProgressMeter self) -> char const *""" + return _yui.YMultiProgressMeter_widgetClass(self) + + def dimension(self): + r"""dimension(YMultiProgressMeter self) -> YUIDimension""" + return _yui.YMultiProgressMeter_dimension(self) + + def horizontal(self): + r"""horizontal(YMultiProgressMeter self) -> bool""" + return _yui.YMultiProgressMeter_horizontal(self) + + def vertical(self): + r"""vertical(YMultiProgressMeter self) -> bool""" + return _yui.YMultiProgressMeter_vertical(self) + + def segments(self): + r"""segments(YMultiProgressMeter self) -> int""" + return _yui.YMultiProgressMeter_segments(self) + + def maxValue(self, segment): + r"""maxValue(YMultiProgressMeter self, int segment) -> float""" + return _yui.YMultiProgressMeter_maxValue(self, segment) + + def currentValue(self, segment): + r"""currentValue(YMultiProgressMeter self, int segment) -> float""" + return _yui.YMultiProgressMeter_currentValue(self, segment) + + def setCurrentValue(self, segment, value): + r"""setCurrentValue(YMultiProgressMeter self, int segment, float value)""" + return _yui.YMultiProgressMeter_setCurrentValue(self, segment, value) + + def setCurrentValues(self, values): + r"""setCurrentValues(YMultiProgressMeter self, std::vector< float,std::allocator< float > > const & values)""" + return _yui.YMultiProgressMeter_setCurrentValues(self, values) + + def setProperty(self, propertyName, val): + r"""setProperty(YMultiProgressMeter self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YMultiProgressMeter_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YMultiProgressMeter self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YMultiProgressMeter_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YMultiProgressMeter self) -> YPropertySet""" + return _yui.YMultiProgressMeter_propertySet(self) + + def doUpdate(self): + r"""doUpdate(YMultiProgressMeter self)""" + return _yui.YMultiProgressMeter_doUpdate(self) + +# Register YMultiProgressMeter in _yui: +_yui.YMultiProgressMeter_swigregister(YMultiProgressMeter) + +class YMultiSelectionBox(YSelectionWidget): + r"""Proxy of C++ YMultiSelectionBox class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMultiSelectionBox + + def widgetClass(self): + r"""widgetClass(YMultiSelectionBox self) -> char const *""" + return _yui.YMultiSelectionBox_widgetClass(self) + + def shrinkable(self): + r"""shrinkable(YMultiSelectionBox self) -> bool""" + return _yui.YMultiSelectionBox_shrinkable(self) + + def setShrinkable(self, shrinkable=True): + r"""setShrinkable(YMultiSelectionBox self, bool shrinkable=True)""" + return _yui.YMultiSelectionBox_setShrinkable(self, shrinkable) + + def setProperty(self, propertyName, val): + r"""setProperty(YMultiSelectionBox self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YMultiSelectionBox_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YMultiSelectionBox self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YMultiSelectionBox_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YMultiSelectionBox self) -> YPropertySet""" + return _yui.YMultiSelectionBox_propertySet(self) + + def userInputProperty(self): + r"""userInputProperty(YMultiSelectionBox self) -> char const *""" + return _yui.YMultiSelectionBox_userInputProperty(self) + + def currentItem(self): + r"""currentItem(YMultiSelectionBox self) -> YItem""" + return _yui.YMultiSelectionBox_currentItem(self) + + def setCurrentItem(self, item): + r"""setCurrentItem(YMultiSelectionBox self, YItem item)""" + return _yui.YMultiSelectionBox_setCurrentItem(self, item) + + def saveUserInput(self, macroRecorder): + r"""saveUserInput(YMultiSelectionBox self, YMacroRecorder macroRecorder)""" + return _yui.YMultiSelectionBox_saveUserInput(self, macroRecorder) + +# Register YMultiSelectionBox in _yui: +_yui.YMultiSelectionBox_swigregister(YMultiSelectionBox) + +YWizardID = _yui.YWizardID + +YWizardContentsReplacePointID = _yui.YWizardContentsReplacePointID + +YWizardMode_Standard = _yui.YWizardMode_Standard + +YWizardMode_Steps = _yui.YWizardMode_Steps + +YWizardMode_Tree = _yui.YWizardMode_Tree + +YWizardMode_TitleOnLeft = _yui.YWizardMode_TitleOnLeft + +class YWizard(YWidget): + r"""Proxy of C++ YWizard class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YWizard + + def widgetClass(self): + r"""widgetClass(YWizard self) -> char const *""" + return _yui.YWizard_widgetClass(self) + + def wizardMode(self): + r"""wizardMode(YWizard self) -> YWizardMode""" + return _yui.YWizard_wizardMode(self) + + def backButton(self): + r"""backButton(YWizard self) -> YPushButton""" + return _yui.YWizard_backButton(self) + + def abortButton(self): + r"""abortButton(YWizard self) -> YPushButton""" + return _yui.YWizard_abortButton(self) + + def nextButton(self): + r"""nextButton(YWizard self) -> YPushButton""" + return _yui.YWizard_nextButton(self) + + def contentsReplacePoint(self): + r"""contentsReplacePoint(YWizard self) -> YReplacePoint""" + return _yui.YWizard_contentsReplacePoint(self) + + def protectNextButton(self, protect): + r"""protectNextButton(YWizard self, bool protect)""" + return _yui.YWizard_protectNextButton(self, protect) + + def nextButtonIsProtected(self): + r"""nextButtonIsProtected(YWizard self) -> bool""" + return _yui.YWizard_nextButtonIsProtected(self) + + def setButtonLabel(self, button, newLabel): + r"""setButtonLabel(YWizard self, YPushButton button, std::string const & newLabel)""" + return _yui.YWizard_setButtonLabel(self, button, newLabel) + + def setHelpText(self, helpText): + r"""setHelpText(YWizard self, std::string const & helpText)""" + return _yui.YWizard_setHelpText(self, helpText) + + def setDialogIcon(self, iconName): + r"""setDialogIcon(YWizard self, std::string const & iconName)""" + return _yui.YWizard_setDialogIcon(self, iconName) + + def setDialogTitle(self, titleText): + r"""setDialogTitle(YWizard self, std::string const & titleText)""" + return _yui.YWizard_setDialogTitle(self, titleText) + + def getDialogTitle(self): + r"""getDialogTitle(YWizard self) -> std::string""" + return _yui.YWizard_getDialogTitle(self) + + def setDialogHeading(self, headingText): + r"""setDialogHeading(YWizard self, std::string const & headingText)""" + return _yui.YWizard_setDialogHeading(self, headingText) + + def getDialogHeading(self): + r"""getDialogHeading(YWizard self) -> std::string""" + return _yui.YWizard_getDialogHeading(self) + + def addStep(self, text, id): + r"""addStep(YWizard self, std::string const & text, std::string const & id)""" + return _yui.YWizard_addStep(self, text, id) + + def addStepHeading(self, text): + r"""addStepHeading(YWizard self, std::string const & text)""" + return _yui.YWizard_addStepHeading(self, text) + + def deleteSteps(self): + r"""deleteSteps(YWizard self)""" + return _yui.YWizard_deleteSteps(self) + + def setCurrentStep(self, id): + r"""setCurrentStep(YWizard self, std::string const & id)""" + return _yui.YWizard_setCurrentStep(self, id) + + def updateSteps(self): + r"""updateSteps(YWizard self)""" + return _yui.YWizard_updateSteps(self) + + def addTreeItem(self, parentID, text, id): + r"""addTreeItem(YWizard self, std::string const & parentID, std::string const & text, std::string const & id)""" + return _yui.YWizard_addTreeItem(self, parentID, text, id) + + def selectTreeItem(self, id): + r"""selectTreeItem(YWizard self, std::string const & id)""" + return _yui.YWizard_selectTreeItem(self, id) + + def currentTreeSelection(self): + r"""currentTreeSelection(YWizard self) -> std::string""" + return _yui.YWizard_currentTreeSelection(self) + + def deleteTreeItems(self): + r"""deleteTreeItems(YWizard self)""" + return _yui.YWizard_deleteTreeItems(self) + + def addMenu(self, text, id): + r"""addMenu(YWizard self, std::string const & text, std::string const & id)""" + return _yui.YWizard_addMenu(self, text, id) + + def addSubMenu(self, parentMenuID, text, id): + r"""addSubMenu(YWizard self, std::string const & parentMenuID, std::string const & text, std::string const & id)""" + return _yui.YWizard_addSubMenu(self, parentMenuID, text, id) + + def addMenuEntry(self, parentMenuID, text, id): + r"""addMenuEntry(YWizard self, std::string const & parentMenuID, std::string const & text, std::string const & id)""" + return _yui.YWizard_addMenuEntry(self, parentMenuID, text, id) + + def addMenuSeparator(self, parentMenuID): + r"""addMenuSeparator(YWizard self, std::string const & parentMenuID)""" + return _yui.YWizard_addMenuSeparator(self, parentMenuID) + + def deleteMenus(self): + r"""deleteMenus(YWizard self)""" + return _yui.YWizard_deleteMenus(self) + + def showReleaseNotesButton(self, label, id): + r"""showReleaseNotesButton(YWizard self, std::string const & label, std::string const & id)""" + return _yui.YWizard_showReleaseNotesButton(self, label, id) + + def hideReleaseNotesButton(self): + r"""hideReleaseNotesButton(YWizard self)""" + return _yui.YWizard_hideReleaseNotesButton(self) + + def retranslateInternalButtons(self): + r"""retranslateInternalButtons(YWizard self)""" + return _yui.YWizard_retranslateInternalButtons(self) + + def ping(self): + r"""ping(YWizard self)""" + return _yui.YWizard_ping(self) + + def getProperty(self, propertyName): + r"""getProperty(YWizard self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YWizard_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YWizard self) -> YPropertySet""" + return _yui.YWizard_propertySet(self) + +# Register YWizard in _yui: +_yui.YWizard_swigregister(YWizard) + +class YOptionalWidgetFactory(object): + r"""Proxy of C++ YOptionalWidgetFactory class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + + def hasWizard(self): + r"""hasWizard(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasWizard(self) + + def createWizard(self, parent, backButtonLabel, abortButtonLabel, nextButtonLabel, wizardMode=YWizardMode_Standard): + r"""createWizard(YOptionalWidgetFactory self, YWidget parent, std::string const & backButtonLabel, std::string const & abortButtonLabel, std::string const & nextButtonLabel, YWizardMode wizardMode=YWizardMode_Standard) -> YWizard""" + return _yui.YOptionalWidgetFactory_createWizard(self, parent, backButtonLabel, abortButtonLabel, nextButtonLabel, wizardMode) + + def hasDumbTab(self): + r"""hasDumbTab(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasDumbTab(self) + + def createDumbTab(self, parent): + r"""createDumbTab(YOptionalWidgetFactory self, YWidget parent) -> YDumbTab""" + return _yui.YOptionalWidgetFactory_createDumbTab(self, parent) + + def hasSlider(self): + r"""hasSlider(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasSlider(self) + + def createSlider(self, parent, label, minVal, maxVal, initialVal): + r"""createSlider(YOptionalWidgetFactory self, YWidget parent, std::string const & label, int minVal, int maxVal, int initialVal) -> YSlider""" + return _yui.YOptionalWidgetFactory_createSlider(self, parent, label, minVal, maxVal, initialVal) + + def hasDateField(self): + r"""hasDateField(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasDateField(self) + + def createDateField(self, parent, label): + r"""createDateField(YOptionalWidgetFactory self, YWidget parent, std::string const & label) -> YDateField""" + return _yui.YOptionalWidgetFactory_createDateField(self, parent, label) + + def hasTimeField(self): + r"""hasTimeField(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasTimeField(self) + + def createTimeField(self, parent, label): + r"""createTimeField(YOptionalWidgetFactory self, YWidget parent, std::string const & label) -> YTimeField""" + return _yui.YOptionalWidgetFactory_createTimeField(self, parent, label) + + def hasBarGraph(self): + r"""hasBarGraph(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasBarGraph(self) + + def createBarGraph(self, parent): + r"""createBarGraph(YOptionalWidgetFactory self, YWidget parent) -> YBarGraph""" + return _yui.YOptionalWidgetFactory_createBarGraph(self, parent) + + def hasPatternSelector(self): + r"""hasPatternSelector(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasPatternSelector(self) + + def createPatternSelector(self, parent, modeFlags=0): + r"""createPatternSelector(YOptionalWidgetFactory self, YWidget parent, long modeFlags=0) -> YWidget""" + return _yui.YOptionalWidgetFactory_createPatternSelector(self, parent, modeFlags) + + def hasSimplePatchSelector(self): + r"""hasSimplePatchSelector(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasSimplePatchSelector(self) + + def createSimplePatchSelector(self, parent, modeFlags=0): + r"""createSimplePatchSelector(YOptionalWidgetFactory self, YWidget parent, long modeFlags=0) -> YWidget""" + return _yui.YOptionalWidgetFactory_createSimplePatchSelector(self, parent, modeFlags) + + def hasMultiProgressMeter(self): + r"""hasMultiProgressMeter(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasMultiProgressMeter(self) + + def createHMultiProgressMeter(self, parent, maxValues): + r"""createHMultiProgressMeter(YOptionalWidgetFactory self, YWidget parent, std::vector< float,std::allocator< float > > const & maxValues) -> YMultiProgressMeter""" + return _yui.YOptionalWidgetFactory_createHMultiProgressMeter(self, parent, maxValues) + + def createVMultiProgressMeter(self, parent, maxValues): + r"""createVMultiProgressMeter(YOptionalWidgetFactory self, YWidget parent, std::vector< float,std::allocator< float > > const & maxValues) -> YMultiProgressMeter""" + return _yui.YOptionalWidgetFactory_createVMultiProgressMeter(self, parent, maxValues) + + def createMultiProgressMeter(self, parent, dim, maxValues): + r"""createMultiProgressMeter(YOptionalWidgetFactory self, YWidget parent, YUIDimension dim, std::vector< float,std::allocator< float > > const & maxValues) -> YMultiProgressMeter""" + return _yui.YOptionalWidgetFactory_createMultiProgressMeter(self, parent, dim, maxValues) + + def hasPartitionSplitter(self): + r"""hasPartitionSplitter(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasPartitionSplitter(self) + + def createPartitionSplitter(self, parent, usedSize, totalFreeSize, newPartSize, minNewPartSize, minFreeSize, usedLabel, freeLabel, newPartLabel, freeFieldLabel, newPartFieldLabel): + r"""createPartitionSplitter(YOptionalWidgetFactory self, YWidget parent, int usedSize, int totalFreeSize, int newPartSize, int minNewPartSize, int minFreeSize, std::string const & usedLabel, std::string const & freeLabel, std::string const & newPartLabel, std::string const & freeFieldLabel, std::string const & newPartFieldLabel) -> YPartitionSplitter""" + return _yui.YOptionalWidgetFactory_createPartitionSplitter(self, parent, usedSize, totalFreeSize, newPartSize, minNewPartSize, minFreeSize, usedLabel, freeLabel, newPartLabel, freeFieldLabel, newPartFieldLabel) + + def hasDownloadProgress(self): + r"""hasDownloadProgress(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasDownloadProgress(self) + + def createDownloadProgress(self, parent, label, filename, expectedFileSize): + r"""createDownloadProgress(YOptionalWidgetFactory self, YWidget parent, std::string const & label, std::string const & filename, YFileSize_t expectedFileSize) -> YDownloadProgress""" + return _yui.YOptionalWidgetFactory_createDownloadProgress(self, parent, label, filename, expectedFileSize) + + def hasDummySpecialWidget(self): + r"""hasDummySpecialWidget(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasDummySpecialWidget(self) + + def createDummySpecialWidget(self, parent): + r"""createDummySpecialWidget(YOptionalWidgetFactory self, YWidget parent) -> YWidget""" + return _yui.YOptionalWidgetFactory_createDummySpecialWidget(self, parent) + + def hasTimezoneSelector(self): + r"""hasTimezoneSelector(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasTimezoneSelector(self) + + def createTimezoneSelector(self, parent, timezoneMap, timezones): + r"""createTimezoneSelector(YOptionalWidgetFactory self, YWidget parent, std::string const & timezoneMap, std::map< std::string,std::string,std::less< std::string >,std::allocator< std::pair< std::string const,std::string > > > const & timezones) -> YTimezoneSelector""" + return _yui.YOptionalWidgetFactory_createTimezoneSelector(self, parent, timezoneMap, timezones) + + def hasGraph(self): + r"""hasGraph(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasGraph(self) + + def createGraph(self, *args): + r""" + createGraph(YOptionalWidgetFactory self, YWidget parent, std::string const & filename, std::string const & layoutAlgorithm) -> YGraph + createGraph(YOptionalWidgetFactory self, YWidget parent, void * graph) -> YGraph * + """ + return _yui.YOptionalWidgetFactory_createGraph(self, *args) + + def hasContextMenu(self): + r"""hasContextMenu(YOptionalWidgetFactory self) -> bool""" + return _yui.YOptionalWidgetFactory_hasContextMenu(self) + +# Register YOptionalWidgetFactory in _yui: +_yui.YOptionalWidgetFactory_swigregister(YOptionalWidgetFactory) + +YPkg_TestMode = _yui.YPkg_TestMode + +YPkg_OnlineUpdateMode = _yui.YPkg_OnlineUpdateMode + +YPkg_UpdateMode = _yui.YPkg_UpdateMode + +YPkg_SearchMode = _yui.YPkg_SearchMode + +YPkg_SummaryMode = _yui.YPkg_SummaryMode + +YPkg_RepoMode = _yui.YPkg_RepoMode + +YPkg_RepoMgr = _yui.YPkg_RepoMgr + +YPkg_ConfirmUnsupported = _yui.YPkg_ConfirmUnsupported + +YPkg_OnlineSearch = _yui.YPkg_OnlineSearch + +class YPackageSelector(YWidget): + r"""Proxy of C++ YPackageSelector class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + + def widgetClass(self): + r"""widgetClass(YPackageSelector self) -> char const *""" + return _yui.YPackageSelector_widgetClass(self) + + def testMode(self): + r"""testMode(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_testMode(self) + + def onlineUpdateMode(self): + r"""onlineUpdateMode(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_onlineUpdateMode(self) + + def updateMode(self): + r"""updateMode(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_updateMode(self) + + def searchMode(self): + r"""searchMode(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_searchMode(self) + + def summaryMode(self): + r"""summaryMode(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_summaryMode(self) + + def repoMode(self): + r"""repoMode(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_repoMode(self) + + def repoMgrEnabled(self): + r"""repoMgrEnabled(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_repoMgrEnabled(self) + + def confirmUnsupported(self): + r"""confirmUnsupported(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_confirmUnsupported(self) + + def onlineSearchEnabled(self): + r"""onlineSearchEnabled(YPackageSelector self) -> bool""" + return _yui.YPackageSelector_onlineSearchEnabled(self) + __swig_destroy__ = _yui.delete_YPackageSelector + +# Register YPackageSelector in _yui: +_yui.YPackageSelector_swigregister(YPackageSelector) + +class YPackageSelectorPlugin(YUIPlugin): + r"""Proxy of C++ YPackageSelectorPlugin class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + + def createPackageSelector(self, parent, modeFlags=0): + r"""createPackageSelector(YPackageSelectorPlugin self, YWidget parent, long modeFlags=0) -> YPackageSelector""" + return _yui.YPackageSelectorPlugin_createPackageSelector(self, parent, modeFlags) + +# Register YPackageSelectorPlugin in _yui: +_yui.YPackageSelectorPlugin_swigregister(YPackageSelectorPlugin) + +class YPartitionSplitter(YWidget): + r"""Proxy of C++ YPartitionSplitter class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YPartitionSplitter + + def widgetClass(self): + r"""widgetClass(YPartitionSplitter self) -> char const *""" + return _yui.YPartitionSplitter_widgetClass(self) + + def value(self): + r"""value(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_value(self) + + def setValue(self, newValue): + r"""setValue(YPartitionSplitter self, int newValue)""" + return _yui.YPartitionSplitter_setValue(self, newValue) + + def usedSize(self): + r"""usedSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_usedSize(self) + + def totalFreeSize(self): + r"""totalFreeSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_totalFreeSize(self) + + def minFreeSize(self): + r"""minFreeSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_minFreeSize(self) + + def maxFreeSize(self): + r"""maxFreeSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_maxFreeSize(self) + + def freeSize(self): + r"""freeSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_freeSize(self) + + def newPartSize(self): + r"""newPartSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_newPartSize(self) + + def minNewPartSize(self): + r"""minNewPartSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_minNewPartSize(self) + + def maxNewPartSize(self): + r"""maxNewPartSize(YPartitionSplitter self) -> int""" + return _yui.YPartitionSplitter_maxNewPartSize(self) + + def usedLabel(self): + r"""usedLabel(YPartitionSplitter self) -> std::string""" + return _yui.YPartitionSplitter_usedLabel(self) + + def freeLabel(self): + r"""freeLabel(YPartitionSplitter self) -> std::string""" + return _yui.YPartitionSplitter_freeLabel(self) + + def newPartLabel(self): + r"""newPartLabel(YPartitionSplitter self) -> std::string""" + return _yui.YPartitionSplitter_newPartLabel(self) + + def freeFieldLabel(self): + r"""freeFieldLabel(YPartitionSplitter self) -> std::string""" + return _yui.YPartitionSplitter_freeFieldLabel(self) + + def newPartFieldLabel(self): + r"""newPartFieldLabel(YPartitionSplitter self) -> std::string""" + return _yui.YPartitionSplitter_newPartFieldLabel(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YPartitionSplitter self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YPartitionSplitter_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YPartitionSplitter self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YPartitionSplitter_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YPartitionSplitter self) -> YPropertySet""" + return _yui.YPartitionSplitter_propertySet(self) + + def userInputProperty(self): + r"""userInputProperty(YPartitionSplitter self) -> char const *""" + return _yui.YPartitionSplitter_userInputProperty(self) + +# Register YPartitionSplitter in _yui: +_yui.YPartitionSplitter_swigregister(YPartitionSplitter) + +class YProgressBar(YWidget): + r"""Proxy of C++ YProgressBar class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YProgressBar + + def widgetClass(self): + r"""widgetClass(YProgressBar self) -> char const *""" + return _yui.YProgressBar_widgetClass(self) + + def label(self): + r"""label(YProgressBar self) -> std::string""" + return _yui.YProgressBar_label(self) + + def setLabel(self, label): + r"""setLabel(YProgressBar self, std::string const & label)""" + return _yui.YProgressBar_setLabel(self, label) + + def maxValue(self): + r"""maxValue(YProgressBar self) -> int""" + return _yui.YProgressBar_maxValue(self) + + def value(self): + r"""value(YProgressBar self) -> int""" + return _yui.YProgressBar_value(self) + + def setValue(self, newValue): + r"""setValue(YProgressBar self, int newValue)""" + return _yui.YProgressBar_setValue(self, newValue) + + def setProperty(self, propertyName, val): + r"""setProperty(YProgressBar self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YProgressBar_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YProgressBar self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YProgressBar_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YProgressBar self) -> YPropertySet""" + return _yui.YProgressBar_propertySet(self) + +# Register YProgressBar in _yui: +_yui.YProgressBar_swigregister(YProgressBar) + +YUnknownPropertyType = _yui.YUnknownPropertyType + +YOtherProperty = _yui.YOtherProperty + +YStringProperty = _yui.YStringProperty + +YBoolProperty = _yui.YBoolProperty + +YIntegerProperty = _yui.YIntegerProperty + +class YProperty(object): + r"""Proxy of C++ YProperty class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, name, type, isReadOnly=False): + r"""__init__(YProperty self, std::string const & name, YPropertyType type, bool isReadOnly=False) -> YProperty""" + _yui.YProperty_swiginit(self, _yui.new_YProperty(name, type, isReadOnly)) + + def name(self): + r"""name(YProperty self) -> std::string""" + return _yui.YProperty_name(self) + + def type(self): + r"""type(YProperty self) -> YPropertyType""" + return _yui.YProperty_type(self) + + def isReadOnly(self): + r"""isReadOnly(YProperty self) -> bool""" + return _yui.YProperty_isReadOnly(self) + + @staticmethod + def typeAsStr(*args): + r""" + typeAsStr() -> std::string + typeAsStr(YPropertyType type) -> std::string + """ + return _yui.YProperty_typeAsStr(*args) + __swig_destroy__ = _yui.delete_YProperty + +# Register YProperty in _yui: +_yui.YProperty_swigregister(YProperty) + +def YProperty_typeAsStr(*args): + r""" + YProperty_typeAsStr() -> std::string + YProperty_typeAsStr(YPropertyType type) -> std::string + """ + return _yui.YProperty_typeAsStr(*args) + +class YPropertyValue(object): + r"""Proxy of C++ YPropertyValue class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YPropertyValue self, std::string const & str) -> YPropertyValue + __init__(YPropertyValue self, char const * str) -> YPropertyValue + __init__(YPropertyValue self, bool b) -> YPropertyValue + __init__(YPropertyValue self, YInteger num) -> YPropertyValue + __init__(YPropertyValue self, int num) -> YPropertyValue + __init__(YPropertyValue self, YPropertyType type) -> YPropertyValue + __init__(YPropertyValue self) -> YPropertyValue + """ + _yui.YPropertyValue_swiginit(self, _yui.new_YPropertyValue(*args)) + __swig_destroy__ = _yui.delete_YPropertyValue + + def __eq__(self, other): + r"""__eq__(YPropertyValue self, YPropertyValue other) -> bool""" + return _yui.YPropertyValue___eq__(self, other) + + def __ne__(self, other): + r"""__ne__(YPropertyValue self, YPropertyValue other) -> bool""" + return _yui.YPropertyValue___ne__(self, other) + + def type(self): + r"""type(YPropertyValue self) -> YPropertyType""" + return _yui.YPropertyValue_type(self) + + def typeAsStr(self): + r"""typeAsStr(YPropertyValue self) -> std::string""" + return _yui.YPropertyValue_typeAsStr(self) + + def stringVal(self): + r"""stringVal(YPropertyValue self) -> std::string""" + return _yui.YPropertyValue_stringVal(self) + + def boolVal(self): + r"""boolVal(YPropertyValue self) -> bool""" + return _yui.YPropertyValue_boolVal(self) + + def integerVal(self): + r"""integerVal(YPropertyValue self) -> YInteger""" + return _yui.YPropertyValue_integerVal(self) + +# Register YPropertyValue in _yui: +_yui.YPropertyValue_swigregister(YPropertyValue) + +class YPropertySet(object): + r"""Proxy of C++ YPropertySet class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YPropertySet self) -> YPropertySet""" + _yui.YPropertySet_swiginit(self, _yui.new_YPropertySet()) + + def check(self, *args): + r""" + check(YPropertySet self, std::string const & propertyName) + check(YPropertySet self, std::string const & propertyName, YPropertyType type) + check(YPropertySet self, YProperty prop) + """ + return _yui.YPropertySet_check(self, *args) + + def contains(self, *args): + r""" + contains(YPropertySet self, std::string const & propertyName) -> bool + contains(YPropertySet self, std::string const & propertyName, YPropertyType type) -> bool + contains(YPropertySet self, YProperty prop) -> bool + """ + return _yui.YPropertySet_contains(self, *args) + + def isEmpty(self): + r"""isEmpty(YPropertySet self) -> bool""" + return _yui.YPropertySet_isEmpty(self) + + def size(self): + r"""size(YPropertySet self) -> int""" + return _yui.YPropertySet_size(self) + + def add(self, *args): + r""" + add(YPropertySet self, YProperty prop) + add(YPropertySet self, YPropertySet otherSet) + """ + return _yui.YPropertySet_add(self, *args) + + def propertiesBegin(self): + r"""propertiesBegin(YPropertySet self) -> YPropertySet::const_iterator""" + return _yui.YPropertySet_propertiesBegin(self) + + def propertiesEnd(self): + r"""propertiesEnd(YPropertySet self) -> YPropertySet::const_iterator""" + return _yui.YPropertySet_propertiesEnd(self) + __swig_destroy__ = _yui.delete_YPropertySet + +# Register YPropertySet in _yui: +_yui.YPropertySet_swigregister(YPropertySet) + +class YPushButton(YWidget): + r"""Proxy of C++ YPushButton class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YPushButton + + def widgetClass(self): + r"""widgetClass(YPushButton self) -> char const *""" + return _yui.YPushButton_widgetClass(self) + + def label(self): + r"""label(YPushButton self) -> std::string""" + return _yui.YPushButton_label(self) + + def setLabel(self, label): + r"""setLabel(YPushButton self, std::string const & label)""" + return _yui.YPushButton_setLabel(self, label) + + def setIcon(self, iconName): + r"""setIcon(YPushButton self, std::string const & iconName)""" + return _yui.YPushButton_setIcon(self, iconName) + + def isDefaultButton(self): + r"""isDefaultButton(YPushButton self) -> bool""" + return _yui.YPushButton_isDefaultButton(self) + + def setDefaultButton(self, _def=True): + r"""setDefaultButton(YPushButton self, bool _def=True)""" + return _yui.YPushButton_setDefaultButton(self, _def) + + def setRole(self, role): + r"""setRole(YPushButton self, YButtonRole role)""" + return _yui.YPushButton_setRole(self, role) + + def role(self): + r"""role(YPushButton self) -> YButtonRole""" + return _yui.YPushButton_role(self) + + def setFunctionKey(self, fkey_no): + r"""setFunctionKey(YPushButton self, int fkey_no)""" + return _yui.YPushButton_setFunctionKey(self, fkey_no) + + def isHelpButton(self): + r"""isHelpButton(YPushButton self) -> bool""" + return _yui.YPushButton_isHelpButton(self) + + def setHelpButton(self, helpButton=True): + r"""setHelpButton(YPushButton self, bool helpButton=True)""" + return _yui.YPushButton_setHelpButton(self, helpButton) + + def isRelNotesButton(self): + r"""isRelNotesButton(YPushButton self) -> bool""" + return _yui.YPushButton_isRelNotesButton(self) + + def setRelNotesButton(self, relNotesButton=True): + r"""setRelNotesButton(YPushButton self, bool relNotesButton=True)""" + return _yui.YPushButton_setRelNotesButton(self, relNotesButton) + + def setProperty(self, propertyName, val): + r"""setProperty(YPushButton self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YPushButton_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YPushButton self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YPushButton_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YPushButton self) -> YPropertySet""" + return _yui.YPushButton_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YPushButton self) -> std::string""" + return _yui.YPushButton_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YPushButton self, std::string const & str)""" + return _yui.YPushButton_setShortcutString(self, str) + + def activate(self): + r"""activate(YPushButton self)""" + return _yui.YPushButton_activate(self) + +# Register YPushButton in _yui: +_yui.YPushButton_swigregister(YPushButton) + +class YRadioButtonGroup(YSingleChildContainerWidget): + r"""Proxy of C++ YRadioButtonGroup class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YRadioButtonGroup + + def widgetClass(self): + r"""widgetClass(YRadioButtonGroup self) -> char const *""" + return _yui.YRadioButtonGroup_widgetClass(self) + + def currentButton(self): + r"""currentButton(YRadioButtonGroup self) -> YRadioButton""" + return _yui.YRadioButtonGroup_currentButton(self) + + def value(self): + r"""value(YRadioButtonGroup self) -> YRadioButton""" + return _yui.YRadioButtonGroup_value(self) + + def addRadioButton(self, radioButton): + r"""addRadioButton(YRadioButtonGroup self, YRadioButton radioButton)""" + return _yui.YRadioButtonGroup_addRadioButton(self, radioButton) + + def removeRadioButton(self, radioButton): + r"""removeRadioButton(YRadioButtonGroup self, YRadioButton radioButton)""" + return _yui.YRadioButtonGroup_removeRadioButton(self, radioButton) + + def uncheckOtherButtons(self, radioButton): + r"""uncheckOtherButtons(YRadioButtonGroup self, YRadioButton radioButton)""" + return _yui.YRadioButtonGroup_uncheckOtherButtons(self, radioButton) + + def setProperty(self, propertyName, val): + r"""setProperty(YRadioButtonGroup self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YRadioButtonGroup_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YRadioButtonGroup self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YRadioButtonGroup_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YRadioButtonGroup self) -> YPropertySet""" + return _yui.YRadioButtonGroup_propertySet(self) + +# Register YRadioButtonGroup in _yui: +_yui.YRadioButtonGroup_swigregister(YRadioButtonGroup) + +class YRadioButton(YWidget): + r"""Proxy of C++ YRadioButton class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YRadioButton + + def widgetClass(self): + r"""widgetClass(YRadioButton self) -> char const *""" + return _yui.YRadioButton_widgetClass(self) + + def value(self): + r"""value(YRadioButton self) -> bool""" + return _yui.YRadioButton_value(self) + + def setValue(self, checked): + r"""setValue(YRadioButton self, bool checked)""" + return _yui.YRadioButton_setValue(self, checked) + + def label(self): + r"""label(YRadioButton self) -> std::string""" + return _yui.YRadioButton_label(self) + + def setLabel(self, label): + r"""setLabel(YRadioButton self, std::string const & label)""" + return _yui.YRadioButton_setLabel(self, label) + + def useBoldFont(self): + r"""useBoldFont(YRadioButton self) -> bool""" + return _yui.YRadioButton_useBoldFont(self) + + def setUseBoldFont(self, bold=True): + r"""setUseBoldFont(YRadioButton self, bool bold=True)""" + return _yui.YRadioButton_setUseBoldFont(self, bold) + + def buttonGroup(self): + r"""buttonGroup(YRadioButton self) -> YRadioButtonGroup""" + return _yui.YRadioButton_buttonGroup(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YRadioButton self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YRadioButton_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YRadioButton self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YRadioButton_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YRadioButton self) -> YPropertySet""" + return _yui.YRadioButton_propertySet(self) + + def shortcutString(self): + r"""shortcutString(YRadioButton self) -> std::string""" + return _yui.YRadioButton_shortcutString(self) + + def setShortcutString(self, str): + r"""setShortcutString(YRadioButton self, std::string const & str)""" + return _yui.YRadioButton_setShortcutString(self, str) + + def userInputProperty(self): + r"""userInputProperty(YRadioButton self) -> char const *""" + return _yui.YRadioButton_userInputProperty(self) + +# Register YRadioButton in _yui: +_yui.YRadioButton_swigregister(YRadioButton) + +class YReplacePoint(YSingleChildContainerWidget): + r"""Proxy of C++ YReplacePoint class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + + def showChild(self): + r"""showChild(YReplacePoint self)""" + return _yui.YReplacePoint_showChild(self) + + def widgetClass(self): + r"""widgetClass(YReplacePoint self) -> char const *""" + return _yui.YReplacePoint_widgetClass(self) + __swig_destroy__ = _yui.delete_YReplacePoint + +# Register YReplacePoint in _yui: +_yui.YReplacePoint_swigregister(YReplacePoint) + +class YRichText(YWidget): + r"""Proxy of C++ YRichText class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YRichText + + def widgetClass(self): + r"""widgetClass(YRichText self) -> char const *""" + return _yui.YRichText_widgetClass(self) + + def setValue(self, newValue): + r"""setValue(YRichText self, std::string const & newValue)""" + return _yui.YRichText_setValue(self, newValue) + + def value(self): + r"""value(YRichText self) -> std::string""" + return _yui.YRichText_value(self) + + def setText(self, newText): + r"""setText(YRichText self, std::string const & newText)""" + return _yui.YRichText_setText(self, newText) + + def text(self): + r"""text(YRichText self) -> std::string""" + return _yui.YRichText_text(self) + + def plainTextMode(self): + r"""plainTextMode(YRichText self) -> bool""" + return _yui.YRichText_plainTextMode(self) + + def setPlainTextMode(self, on=True): + r"""setPlainTextMode(YRichText self, bool on=True)""" + return _yui.YRichText_setPlainTextMode(self, on) + + def autoScrollDown(self): + r"""autoScrollDown(YRichText self) -> bool""" + return _yui.YRichText_autoScrollDown(self) + + def setAutoScrollDown(self, on=True): + r"""setAutoScrollDown(YRichText self, bool on=True)""" + return _yui.YRichText_setAutoScrollDown(self, on) + + def shrinkable(self): + r"""shrinkable(YRichText self) -> bool""" + return _yui.YRichText_shrinkable(self) + + def setShrinkable(self, shrinkable=True): + r"""setShrinkable(YRichText self, bool shrinkable=True)""" + return _yui.YRichText_setShrinkable(self, shrinkable) + + def setProperty(self, propertyName, val): + r"""setProperty(YRichText self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YRichText_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YRichText self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YRichText_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YRichText self) -> YPropertySet""" + return _yui.YRichText_propertySet(self) + + def vScrollValue(self): + r"""vScrollValue(YRichText self) -> std::string""" + return _yui.YRichText_vScrollValue(self) + + def setVScrollValue(self, newValue): + r"""setVScrollValue(YRichText self, std::string const & newValue)""" + return _yui.YRichText_setVScrollValue(self, newValue) + + def hScrollValue(self): + r"""hScrollValue(YRichText self) -> std::string""" + return _yui.YRichText_hScrollValue(self) + + def setHScrollValue(self, newValue): + r"""setHScrollValue(YRichText self, std::string const & newValue)""" + return _yui.YRichText_setHScrollValue(self, newValue) + + def activateLink(self, url): + r"""activateLink(YRichText self, std::string const & url)""" + return _yui.YRichText_activateLink(self, url) + +# Register YRichText in _yui: +_yui.YRichText_swigregister(YRichText) + +class YSelectionBox(YSelectionWidget): + r"""Proxy of C++ YSelectionBox class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YSelectionBox + + def widgetClass(self): + r"""widgetClass(YSelectionBox self) -> char const *""" + return _yui.YSelectionBox_widgetClass(self) + + def shrinkable(self): + r"""shrinkable(YSelectionBox self) -> bool""" + return _yui.YSelectionBox_shrinkable(self) + + def setShrinkable(self, shrinkable=True): + r"""setShrinkable(YSelectionBox self, bool shrinkable=True)""" + return _yui.YSelectionBox_setShrinkable(self, shrinkable) + + def immediateMode(self): + r"""immediateMode(YSelectionBox self) -> bool""" + return _yui.YSelectionBox_immediateMode(self) + + def setImmediateMode(self, on=True): + r"""setImmediateMode(YSelectionBox self, bool on=True)""" + return _yui.YSelectionBox_setImmediateMode(self, on) + + def setProperty(self, propertyName, val): + r"""setProperty(YSelectionBox self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YSelectionBox_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YSelectionBox self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YSelectionBox_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YSelectionBox self) -> YPropertySet""" + return _yui.YSelectionBox_propertySet(self) + + def userInputProperty(self): + r"""userInputProperty(YSelectionBox self) -> char const *""" + return _yui.YSelectionBox_userInputProperty(self) + +# Register YSelectionBox in _yui: +_yui.YSelectionBox_swigregister(YSelectionBox) + +class YSettings(object): + r"""Proxy of C++ YSettings class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + + @staticmethod + def setProgDir(directory): + r"""setProgDir(std::string directory)""" + return _yui.YSettings_setProgDir(directory) + + @staticmethod + def progDir(): + r"""progDir() -> std::string""" + return _yui.YSettings_progDir() + + @staticmethod + def setIconDir(directory): + r"""setIconDir(std::string directory)""" + return _yui.YSettings_setIconDir(directory) + + @staticmethod + def iconDir(): + r"""iconDir() -> std::string""" + return _yui.YSettings_iconDir() + + @staticmethod + def setThemeDir(directory): + r"""setThemeDir(std::string directory)""" + return _yui.YSettings_setThemeDir(directory) + + @staticmethod + def themeDir(): + r"""themeDir() -> std::string""" + return _yui.YSettings_themeDir() + + @staticmethod + def setLocaleDir(directory): + r"""setLocaleDir(std::string directory)""" + return _yui.YSettings_setLocaleDir(directory) + + @staticmethod + def localeDir(): + r"""localeDir() -> std::string""" + return _yui.YSettings_localeDir() + + @staticmethod + def loadedUI(*args): + r""" + loadedUI(std::string ui) + loadedUI() -> std::string + """ + return _yui.YSettings_loadedUI(*args) + +# Register YSettings in _yui: +_yui.YSettings_swigregister(YSettings) + +def YSettings_setProgDir(directory): + r"""YSettings_setProgDir(std::string directory)""" + return _yui.YSettings_setProgDir(directory) + +def YSettings_progDir(): + r"""YSettings_progDir() -> std::string""" + return _yui.YSettings_progDir() + +def YSettings_setIconDir(directory): + r"""YSettings_setIconDir(std::string directory)""" + return _yui.YSettings_setIconDir(directory) + +def YSettings_iconDir(): + r"""YSettings_iconDir() -> std::string""" + return _yui.YSettings_iconDir() + +def YSettings_setThemeDir(directory): + r"""YSettings_setThemeDir(std::string directory)""" + return _yui.YSettings_setThemeDir(directory) + +def YSettings_themeDir(): + r"""YSettings_themeDir() -> std::string""" + return _yui.YSettings_themeDir() + +def YSettings_setLocaleDir(directory): + r"""YSettings_setLocaleDir(std::string directory)""" + return _yui.YSettings_setLocaleDir(directory) + +def YSettings_localeDir(): + r"""YSettings_localeDir() -> std::string""" + return _yui.YSettings_localeDir() + +def YSettings_loadedUI(*args): + r""" + YSettings_loadedUI(std::string ui) + YSettings_loadedUI() -> std::string + """ + return _yui.YSettings_loadedUI(*args) + +class YShortcut(object): + r"""Proxy of C++ YShortcut class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, shortcut_widget): + r"""__init__(YShortcut self, YWidget shortcut_widget) -> YShortcut""" + _yui.YShortcut_swiginit(self, _yui.new_YShortcut(shortcut_widget)) + __swig_destroy__ = _yui.delete_YShortcut + + def widget(self): + r"""widget(YShortcut self) -> YWidget""" + return _yui.YShortcut_widget(self) + + def widgetClass(self): + r"""widgetClass(YShortcut self) -> char const *""" + return _yui.YShortcut_widgetClass(self) + + def isButton(self): + r"""isButton(YShortcut self) -> bool""" + return _yui.YShortcut_isButton(self) + + def isWizardButton(self): + r"""isWizardButton(YShortcut self) -> bool""" + return _yui.YShortcut_isWizardButton(self) + + def isMenuItem(self): + r"""isMenuItem(YShortcut self) -> bool""" + return _yui.YShortcut_isMenuItem(self) + + def shortcutString(self): + r"""shortcutString(YShortcut self) -> std::string""" + return _yui.YShortcut_shortcutString(self) + + @staticmethod + def cleanShortcutString(*args): + r""" + cleanShortcutString() -> std::string + cleanShortcutString(std::string shortcutString) -> std::string + """ + return _yui.YShortcut_cleanShortcutString(*args) + + def preferred(self): + r"""preferred(YShortcut self) -> char""" + return _yui.YShortcut_preferred(self) + + def shortcut(self): + r"""shortcut(YShortcut self) -> char""" + return _yui.YShortcut_shortcut(self) + + def setShortcut(self, newShortcut): + r"""setShortcut(YShortcut self, char newShortcut)""" + return _yui.YShortcut_setShortcut(self, newShortcut) + + def clearShortcut(self): + r"""clearShortcut(YShortcut self)""" + return _yui.YShortcut_clearShortcut(self) + + def conflict(self): + r"""conflict(YShortcut self) -> bool""" + return _yui.YShortcut_conflict(self) + + def setConflict(self, newConflictState=True): + r"""setConflict(YShortcut self, bool newConflictState=True)""" + return _yui.YShortcut_setConflict(self, newConflictState) + + def distinctShortcutChars(self): + r"""distinctShortcutChars(YShortcut self) -> int""" + return _yui.YShortcut_distinctShortcutChars(self) + + def hasValidShortcutChar(self): + r"""hasValidShortcutChar(YShortcut self) -> bool""" + return _yui.YShortcut_hasValidShortcutChar(self) + + def debugLabel(self): + r"""debugLabel(YShortcut self) -> std::string""" + return _yui.YShortcut_debugLabel(self) + + @staticmethod + def shortcutMarker(): + r"""shortcutMarker() -> char""" + return _yui.YShortcut_shortcutMarker() + + @staticmethod + def findShortcutPos(str, start_pos=0): + r"""findShortcutPos(std::string const & str, std::string::size_type start_pos=0) -> std::string::size_type""" + return _yui.YShortcut_findShortcutPos(str, start_pos) + + @staticmethod + def findShortcut(str, start_pos=0): + r"""findShortcut(std::string const & str, std::string::size_type start_pos=0) -> char""" + return _yui.YShortcut_findShortcut(str, start_pos) + + @staticmethod + def isValid(c): + r"""isValid(char c) -> bool""" + return _yui.YShortcut_isValid(c) + + @staticmethod + def normalized(c): + r"""normalized(char c) -> char""" + return _yui.YShortcut_normalized(c) + + @staticmethod + def getShortcutString(widget): + r"""getShortcutString(YWidget widget) -> std::string""" + return _yui.YShortcut_getShortcutString(widget) + +# Register YShortcut in _yui: +_yui.YShortcut_swigregister(YShortcut) + +def YShortcut_cleanShortcutString(*args): + r""" + YShortcut_cleanShortcutString() -> std::string + YShortcut_cleanShortcutString(std::string shortcutString) -> std::string + """ + return _yui.YShortcut_cleanShortcutString(*args) + +def YShortcut_shortcutMarker(): + r"""YShortcut_shortcutMarker() -> char""" + return _yui.YShortcut_shortcutMarker() + +def YShortcut_findShortcutPos(str, start_pos=0): + r"""YShortcut_findShortcutPos(std::string const & str, std::string::size_type start_pos=0) -> std::string::size_type""" + return _yui.YShortcut_findShortcutPos(str, start_pos) + +def YShortcut_findShortcut(str, start_pos=0): + r"""YShortcut_findShortcut(std::string const & str, std::string::size_type start_pos=0) -> char""" + return _yui.YShortcut_findShortcut(str, start_pos) + +def YShortcut_isValid(c): + r"""YShortcut_isValid(char c) -> bool""" + return _yui.YShortcut_isValid(c) + +def YShortcut_normalized(c): + r"""YShortcut_normalized(char c) -> char""" + return _yui.YShortcut_normalized(c) + +def YShortcut_getShortcutString(widget): + r"""YShortcut_getShortcutString(YWidget widget) -> std::string""" + return _yui.YShortcut_getShortcutString(widget) + +class YItemShortcut(YShortcut): + r"""Proxy of C++ YItemShortcut class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, widget, item): + r"""__init__(YItemShortcut self, YWidget widget, YItem item) -> YItemShortcut""" + _yui.YItemShortcut_swiginit(self, _yui.new_YItemShortcut(widget, item)) + __swig_destroy__ = _yui.delete_YItemShortcut + + def item(self): + r"""item(YItemShortcut self) -> YItem""" + return _yui.YItemShortcut_item(self) + + def setShortcut(self, newShortcut): + r"""setShortcut(YItemShortcut self, char newShortcut)""" + return _yui.YItemShortcut_setShortcut(self, newShortcut) + + def isMenuItem(self): + r"""isMenuItem(YItemShortcut self) -> bool""" + return _yui.YItemShortcut_isMenuItem(self) + + def debugLabel(self): + r"""debugLabel(YItemShortcut self) -> std::string""" + return _yui.YItemShortcut_debugLabel(self) + +# Register YItemShortcut in _yui: +_yui.YItemShortcut_swigregister(YItemShortcut) + +class YShortcutManager(object): + r"""Proxy of C++ YShortcutManager class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, dialog): + r"""__init__(YShortcutManager self, YDialog dialog) -> YShortcutManager""" + _yui.YShortcutManager_swiginit(self, _yui.new_YShortcutManager(dialog)) + __swig_destroy__ = _yui.delete_YShortcutManager + + def checkShortcuts(self, autoResolve=True): + r"""checkShortcuts(YShortcutManager self, bool autoResolve=True)""" + return _yui.YShortcutManager_checkShortcuts(self, autoResolve) + + def conflictCount(self): + r"""conflictCount(YShortcutManager self) -> int""" + return _yui.YShortcutManager_conflictCount(self) + + def resolveAllConflicts(self): + r"""resolveAllConflicts(YShortcutManager self)""" + return _yui.YShortcutManager_resolveAllConflicts(self) + + def dialog(self): + r"""dialog(YShortcutManager self) -> YDialog""" + return _yui.YShortcutManager_dialog(self) + +# Register YShortcutManager in _yui: +_yui.YShortcutManager_swigregister(YShortcutManager) + +class YSimpleEventHandler(object): + r"""Proxy of C++ YSimpleEventHandler class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YSimpleEventHandler self) -> YSimpleEventHandler""" + _yui.YSimpleEventHandler_swiginit(self, _yui.new_YSimpleEventHandler()) + __swig_destroy__ = _yui.delete_YSimpleEventHandler + + def sendEvent(self, event_disown): + r"""sendEvent(YSimpleEventHandler self, YEvent event_disown)""" + return _yui.YSimpleEventHandler_sendEvent(self, event_disown) + + def eventPendingFor(self, widget): + r"""eventPendingFor(YSimpleEventHandler self, YWidget widget) -> bool""" + return _yui.YSimpleEventHandler_eventPendingFor(self, widget) + + def pendingEvent(self): + r"""pendingEvent(YSimpleEventHandler self) -> YEvent""" + return _yui.YSimpleEventHandler_pendingEvent(self) + + def consumePendingEvent(self): + r"""consumePendingEvent(YSimpleEventHandler self) -> YEvent""" + return _yui.YSimpleEventHandler_consumePendingEvent(self) + + def deletePendingEventsFor(self, widget): + r"""deletePendingEventsFor(YSimpleEventHandler self, YWidget widget)""" + return _yui.YSimpleEventHandler_deletePendingEventsFor(self, widget) + + def clear(self): + r"""clear(YSimpleEventHandler self)""" + return _yui.YSimpleEventHandler_clear(self) + + def blockEvents(self, block=True): + r"""blockEvents(YSimpleEventHandler self, bool block=True)""" + return _yui.YSimpleEventHandler_blockEvents(self, block) + + def unblockEvents(self): + r"""unblockEvents(YSimpleEventHandler self)""" + return _yui.YSimpleEventHandler_unblockEvents(self) + + def eventsBlocked(self): + r"""eventsBlocked(YSimpleEventHandler self) -> bool""" + return _yui.YSimpleEventHandler_eventsBlocked(self) + + def deleteEvent(self, event): + r"""deleteEvent(YSimpleEventHandler self, YEvent event)""" + return _yui.YSimpleEventHandler_deleteEvent(self, event) + +# Register YSimpleEventHandler in _yui: +_yui.YSimpleEventHandler_swigregister(YSimpleEventHandler) + +class YSlider(YIntField): + r"""Proxy of C++ YSlider class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YSlider + + def widgetClass(self): + r"""widgetClass(YSlider self) -> char const *""" + return _yui.YSlider_widgetClass(self) + +# Register YSlider in _yui: +_yui.YSlider_swigregister(YSlider) + +class YSpacing(YWidget): + r"""Proxy of C++ YSpacing class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YSpacing + + def widgetClass(self): + r"""widgetClass(YSpacing self) -> char const *""" + return _yui.YSpacing_widgetClass(self) + + def dimension(self): + r"""dimension(YSpacing self) -> YUIDimension""" + return _yui.YSpacing_dimension(self) + + def size(self, *args): + r""" + size(YSpacing self) -> int + size(YSpacing self, YUIDimension dim) -> int + """ + return _yui.YSpacing_size(self, *args) + + def preferredWidth(self): + r"""preferredWidth(YSpacing self) -> int""" + return _yui.YSpacing_preferredWidth(self) + + def preferredHeight(self): + r"""preferredHeight(YSpacing self) -> int""" + return _yui.YSpacing_preferredHeight(self) + +# Register YSpacing in _yui: +_yui.YSpacing_swigregister(YSpacing) + +class YSquash(YSingleChildContainerWidget): + r"""Proxy of C++ YSquash class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YSquash + + def widgetClass(self): + r"""widgetClass(YSquash self) -> char const *""" + return _yui.YSquash_widgetClass(self) + + def horSquash(self): + r"""horSquash(YSquash self) -> bool""" + return _yui.YSquash_horSquash(self) + + def vertSquash(self): + r"""vertSquash(YSquash self) -> bool""" + return _yui.YSquash_vertSquash(self) + + def stretchable(self, dim): + r"""stretchable(YSquash self, YUIDimension dim) -> bool""" + return _yui.YSquash_stretchable(self, dim) + +# Register YSquash in _yui: +_yui.YSquash_swigregister(YSquash) + +class YTable(YSelectionWidget): + r"""Proxy of C++ YTable class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YTable + + def widgetClass(self): + r"""widgetClass(YTable self) -> char const *""" + return _yui.YTable_widgetClass(self) + + def columns(self): + r"""columns(YTable self) -> int""" + return _yui.YTable_columns(self) + + def hasColumn(self, column): + r"""hasColumn(YTable self, int column) -> bool""" + return _yui.YTable_hasColumn(self, column) + + def header(self, column): + r"""header(YTable self, int column) -> std::string""" + return _yui.YTable_header(self, column) + + def alignment(self, column): + r"""alignment(YTable self, int column) -> YAlignmentType""" + return _yui.YTable_alignment(self, column) + + def immediateMode(self): + r"""immediateMode(YTable self) -> bool""" + return _yui.YTable_immediateMode(self) + + def setImmediateMode(self, immediateMode=True): + r"""setImmediateMode(YTable self, bool immediateMode=True)""" + return _yui.YTable_setImmediateMode(self, immediateMode) + + def keepSorting(self): + r"""keepSorting(YTable self) -> bool""" + return _yui.YTable_keepSorting(self) + + def setKeepSorting(self, keepSorting): + r"""setKeepSorting(YTable self, bool keepSorting)""" + return _yui.YTable_setKeepSorting(self, keepSorting) + + def hasMultiSelection(self): + r"""hasMultiSelection(YTable self) -> bool""" + return _yui.YTable_hasMultiSelection(self) + + def findItem(self, *args): + r""" + findItem(YTable self, std::string const & wantedItemLabel, int column) -> YItem + findItem(YTable self, std::string const & wantedItemLabel, int column, YItemConstIterator begin, YItemConstIterator end) -> YItem + """ + return _yui.YTable_findItem(self, *args) + + def cellChanged(self, cell): + r"""cellChanged(YTable self, YTableCell cell)""" + return _yui.YTable_cellChanged(self, cell) + + def setProperty(self, propertyName, val): + r"""setProperty(YTable self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YTable_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YTable self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YTable_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YTable self) -> YPropertySet""" + return _yui.YTable_propertySet(self) + + def userInputProperty(self): + r"""userInputProperty(YTable self) -> char const *""" + return _yui.YTable_userInputProperty(self) + +# Register YTable in _yui: +_yui.YTable_swigregister(YTable) + +class YTableHeader(object): + r"""Proxy of C++ YTableHeader class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YTableHeader self) -> YTableHeader""" + _yui.YTableHeader_swiginit(self, _yui.new_YTableHeader()) + __swig_destroy__ = _yui.delete_YTableHeader + + def addColumn(self, header, alignment=YAlignBegin): + r"""addColumn(YTableHeader self, std::string const & header, YAlignmentType alignment=YAlignBegin)""" + return _yui.YTableHeader_addColumn(self, header, alignment) + + def columns(self): + r"""columns(YTableHeader self) -> int""" + return _yui.YTableHeader_columns(self) + + def hasColumn(self, column): + r"""hasColumn(YTableHeader self, int column) -> bool""" + return _yui.YTableHeader_hasColumn(self, column) + + def header(self, column): + r"""header(YTableHeader self, int column) -> std::string""" + return _yui.YTableHeader_header(self, column) + + def alignment(self, column): + r"""alignment(YTableHeader self, int column) -> YAlignmentType""" + return _yui.YTableHeader_alignment(self, column) + +# Register YTableHeader in _yui: +_yui.YTableHeader_swigregister(YTableHeader) + +class YTableItem(YTreeItem): + r"""Proxy of C++ YTableItem class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YTableItem self) -> YTableItem + __init__(YTableItem self, YTableItem parent, bool isOpen=False) -> YTableItem + __init__(YTableItem self, std::string const & label_0, std::string const & label_1=std::string(), std::string const & label_2=std::string(), std::string const & label_3=std::string(), std::string const & label_4=std::string(), std::string const & label_5=std::string(), std::string const & label_6=std::string(), std::string const & label_7=std::string(), std::string const & label_8=std::string(), std::string const & label_9=std::string()) -> YTableItem + __init__(YTableItem self, YTableItem parent, std::string const & label_0, std::string const & label_1=std::string(), std::string const & label_2=std::string(), std::string const & label_3=std::string(), std::string const & label_4=std::string(), std::string const & label_5=std::string(), std::string const & label_6=std::string(), std::string const & label_7=std::string(), std::string const & label_8=std::string(), std::string const & label_9=std::string()) -> YTableItem + """ + _yui.YTableItem_swiginit(self, _yui.new_YTableItem(*args)) + __swig_destroy__ = _yui.delete_YTableItem + + def itemClass(self): + r"""itemClass(YTableItem self) -> char const *""" + return _yui.YTableItem_itemClass(self) + + def addCell(self, *args): + r""" + addCell(YTableItem self, YTableCell cell_disown) + addCell(YTableItem self, std::string const & label, std::string const & iconName=std::string(), std::string const & sortKey=std::string()) + """ + return _yui.YTableItem_addCell(self, *args) + + def addCells(self, *args): + r"""addCells(YTableItem self, std::string const & label_0, std::string const & label_1, std::string const & label_2=std::string(), std::string const & label_3=std::string(), std::string const & label_4=std::string(), std::string const & label_5=std::string(), std::string const & label_6=std::string(), std::string const & label_7=std::string(), std::string const & label_8=std::string(), std::string const & label_9=std::string())""" + return _yui.YTableItem_addCells(self, *args) + + def deleteCells(self): + r"""deleteCells(YTableItem self)""" + return _yui.YTableItem_deleteCells(self) + + def cellsBegin(self, *args): + r""" + cellsBegin(YTableItem self) -> YTableCellIterator + cellsBegin(YTableItem self) -> YTableCellConstIterator + """ + return _yui.YTableItem_cellsBegin(self, *args) + + def cellsEnd(self, *args): + r""" + cellsEnd(YTableItem self) -> YTableCellIterator + cellsEnd(YTableItem self) -> YTableCellConstIterator + """ + return _yui.YTableItem_cellsEnd(self, *args) + + def cell(self, *args): + r""" + cell(YTableItem self, int index) -> YTableCell + cell(YTableItem self, int index) -> YTableCell + """ + return _yui.YTableItem_cell(self, *args) + + def cellCount(self): + r"""cellCount(YTableItem self) -> int""" + return _yui.YTableItem_cellCount(self) + + def hasCell(self, index): + r"""hasCell(YTableItem self, int index) -> bool""" + return _yui.YTableItem_hasCell(self, index) + + def iconName(self, index): + r"""iconName(YTableItem self, int index) -> std::string""" + return _yui.YTableItem_iconName(self, index) + + def hasIconName(self, index): + r"""hasIconName(YTableItem self, int index) -> bool""" + return _yui.YTableItem_hasIconName(self, index) + + def label(self, *args): + r""" + label(YTableItem self, int index) -> std::string + label(YTableItem self) -> std::string + """ + return _yui.YTableItem_label(self, *args) + + def debugLabel(self): + r"""debugLabel(YTableItem self) -> std::string""" + return _yui.YTableItem_debugLabel(self) + +# Register YTableItem in _yui: +_yui.YTableItem_swigregister(YTableItem) + +class YTableCell(object): + r"""Proxy of C++ YTableCell class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YTableCell self, std::string const & label, std::string const & iconName="", std::string const & sortKey="") -> YTableCell + __init__(YTableCell self, YTableItem parent, int column, std::string const & label, std::string const & iconName="", std::string const & sortKey="") -> YTableCell + """ + _yui.YTableCell_swiginit(self, _yui.new_YTableCell(*args)) + __swig_destroy__ = _yui.delete_YTableCell + + def label(self): + r"""label(YTableCell self) -> std::string""" + return _yui.YTableCell_label(self) + + def setLabel(self, newLabel): + r"""setLabel(YTableCell self, std::string const & newLabel)""" + return _yui.YTableCell_setLabel(self, newLabel) + + def iconName(self): + r"""iconName(YTableCell self) -> std::string""" + return _yui.YTableCell_iconName(self) + + def hasIconName(self): + r"""hasIconName(YTableCell self) -> bool""" + return _yui.YTableCell_hasIconName(self) + + def setIconName(self, newIconName): + r"""setIconName(YTableCell self, std::string const & newIconName)""" + return _yui.YTableCell_setIconName(self, newIconName) + + def sortKey(self): + r"""sortKey(YTableCell self) -> std::string""" + return _yui.YTableCell_sortKey(self) + + def hasSortKey(self): + r"""hasSortKey(YTableCell self) -> bool""" + return _yui.YTableCell_hasSortKey(self) + + def setSortKey(self, newSortKey): + r"""setSortKey(YTableCell self, std::string const & newSortKey)""" + return _yui.YTableCell_setSortKey(self, newSortKey) + + def parent(self): + r"""parent(YTableCell self) -> YTableItem""" + return _yui.YTableCell_parent(self) + + def column(self): + r"""column(YTableCell self) -> int""" + return _yui.YTableCell_column(self) + + def itemIndex(self): + r"""itemIndex(YTableCell self) -> int""" + return _yui.YTableCell_itemIndex(self) + + def reparent(self, parent, column): + r"""reparent(YTableCell self, YTableItem parent, int column)""" + return _yui.YTableCell_reparent(self, parent, column) + +# Register YTableCell in _yui: +_yui.YTableCell_swigregister(YTableCell) + +class YTimeField(YSimpleInputField): + r"""Proxy of C++ YTimeField class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YTimeField + + def widgetClass(self): + r"""widgetClass(YTimeField self) -> char const *""" + return _yui.YTimeField_widgetClass(self) + +# Register YTimeField in _yui: +_yui.YTimeField_swigregister(YTimeField) + +class YTimezoneSelector(YWidget): + r"""Proxy of C++ YTimezoneSelector class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YTimezoneSelector + + def widgetClass(self): + r"""widgetClass(YTimezoneSelector self) -> char const *""" + return _yui.YTimezoneSelector_widgetClass(self) + + def setProperty(self, propertyName, val): + r"""setProperty(YTimezoneSelector self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YTimezoneSelector_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YTimezoneSelector self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YTimezoneSelector_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YTimezoneSelector self) -> YPropertySet""" + return _yui.YTimezoneSelector_propertySet(self) + + def currentZone(self): + r"""currentZone(YTimezoneSelector self) -> std::string""" + return _yui.YTimezoneSelector_currentZone(self) + + def setCurrentZone(self, zone, zoom): + r"""setCurrentZone(YTimezoneSelector self, std::string const & zone, bool zoom)""" + return _yui.YTimezoneSelector_setCurrentZone(self, zone, zoom) + +# Register YTimezoneSelector in _yui: +_yui.YTimezoneSelector_swigregister(YTimezoneSelector) + +class YTransText(object): + r"""Proxy of C++ YTransText class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YTransText self, std::string const & orig, std::string const & translation) -> YTransText + __init__(YTransText self, std::string const & orig) -> YTransText + __init__(YTransText self, YTransText src) -> YTransText + """ + _yui.YTransText_swiginit(self, _yui.new_YTransText(*args)) + + def orig(self): + r"""orig(YTransText self) -> std::string const &""" + return _yui.YTransText_orig(self) + + def translation(self): + r"""translation(YTransText self) -> std::string const &""" + return _yui.YTransText_translation(self) + + def trans(self): + r"""trans(YTransText self) -> std::string const &""" + return _yui.YTransText_trans(self) + + def setOrig(self, newOrig): + r"""setOrig(YTransText self, std::string const & newOrig)""" + return _yui.YTransText_setOrig(self, newOrig) + + def setTranslation(self, newTrans): + r"""setTranslation(YTransText self, std::string const & newTrans)""" + return _yui.YTransText_setTranslation(self, newTrans) + + def __lt__(self, other): + r"""__lt__(YTransText self, YTransText other) -> bool""" + return _yui.YTransText___lt__(self, other) + + def __gt__(self, other): + r"""__gt__(YTransText self, YTransText other) -> bool""" + return _yui.YTransText___gt__(self, other) + + def __eq__(self, other): + r"""__eq__(YTransText self, YTransText other) -> bool""" + return _yui.YTransText___eq__(self, other) + __swig_destroy__ = _yui.delete_YTransText + +# Register YTransText in _yui: +_yui.YTransText_swigregister(YTransText) + +class YTree(YSelectionWidget): + r"""Proxy of C++ YTree class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YTree + + def widgetClass(self): + r"""widgetClass(YTree self) -> char const *""" + return _yui.YTree_widgetClass(self) + + def rebuildTree(self): + r"""rebuildTree(YTree self)""" + return _yui.YTree_rebuildTree(self) + + def addItems(self, itemCollection): + r"""addItems(YTree self, YItemCollection itemCollection)""" + return _yui.YTree_addItems(self, itemCollection) + + def immediateMode(self): + r"""immediateMode(YTree self) -> bool""" + return _yui.YTree_immediateMode(self) + + def setImmediateMode(self, on=True): + r"""setImmediateMode(YTree self, bool on=True)""" + return _yui.YTree_setImmediateMode(self, on) + + def setProperty(self, propertyName, val): + r"""setProperty(YTree self, std::string const & propertyName, YPropertyValue val) -> bool""" + return _yui.YTree_setProperty(self, propertyName, val) + + def getProperty(self, propertyName): + r"""getProperty(YTree self, std::string const & propertyName) -> YPropertyValue""" + return _yui.YTree_getProperty(self, propertyName) + + def propertySet(self): + r"""propertySet(YTree self) -> YPropertySet""" + return _yui.YTree_propertySet(self) + + def userInputProperty(self): + r"""userInputProperty(YTree self) -> char const *""" + return _yui.YTree_userInputProperty(self) + + def hasMultiSelection(self): + r"""hasMultiSelection(YTree self) -> bool""" + return _yui.YTree_hasMultiSelection(self) + + def currentItem(self): + r"""currentItem(YTree self) -> YTreeItem""" + return _yui.YTree_currentItem(self) + + def activate(self): + r"""activate(YTree self)""" + return _yui.YTree_activate(self) + + def findItem(self, path): + r"""findItem(YTree self, std::vector< std::string,std::allocator< std::string > > const & path) -> YTreeItem""" + return _yui.YTree_findItem(self, path) + +# Register YTree in _yui: +_yui.YTree_swigregister(YTree) + +class YCodeLocation(object): + r"""Proxy of C++ YCodeLocation class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YCodeLocation self, std::string const & file_r, std::string const & func_r, int line_r) -> YCodeLocation + __init__(YCodeLocation self) -> YCodeLocation + """ + _yui.YCodeLocation_swiginit(self, _yui.new_YCodeLocation(*args)) + + def file(self): + r"""file(YCodeLocation self) -> std::string""" + return _yui.YCodeLocation_file(self) + + def func(self): + r"""func(YCodeLocation self) -> std::string""" + return _yui.YCodeLocation_func(self) + + def line(self): + r"""line(YCodeLocation self) -> int""" + return _yui.YCodeLocation_line(self) + + def asString(self): + r"""asString(YCodeLocation self) -> std::string""" + return _yui.YCodeLocation_asString(self) + __swig_destroy__ = _yui.delete_YCodeLocation + +# Register YCodeLocation in _yui: +_yui.YCodeLocation_swigregister(YCodeLocation) + +class YUIException(object): + r"""Proxy of C++ YUIException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YUIException self) -> YUIException + __init__(YUIException self, std::string const & msg_r) -> YUIException + """ + _yui.YUIException_swiginit(self, _yui.new_YUIException(*args)) + __swig_destroy__ = _yui.delete_YUIException + + def where(self): + r"""where(YUIException self) -> YCodeLocation""" + return _yui.YUIException_where(self) + + def relocate(self, newLocation): + r"""relocate(YUIException self, YCodeLocation newLocation)""" + return _yui.YUIException_relocate(self, newLocation) + + def msg(self): + r"""msg(YUIException self) -> std::string const &""" + return _yui.YUIException_msg(self) + + def setMsg(self, msg): + r"""setMsg(YUIException self, std::string const & msg)""" + return _yui.YUIException_setMsg(self, msg) + + def asString(self): + r"""asString(YUIException self) -> std::string""" + return _yui.YUIException_asString(self) + + @staticmethod + def strErrno(*args): + r""" + strErrno(int errno_r) -> std::string + strErrno(int errno_r, std::string const & msg) -> std::string + """ + return _yui.YUIException_strErrno(*args) + + @staticmethod + def log(exception, location, prefix): + r"""log(YUIException exception, YCodeLocation location, char const *const prefix)""" + return _yui.YUIException_log(exception, location, prefix) + + def what(self): + r"""what(YUIException self) -> char const *""" + return _yui.YUIException_what(self) + +# Register YUIException in _yui: +_yui.YUIException_swigregister(YUIException) + +def YUIException_strErrno(*args): + r""" + YUIException_strErrno(int errno_r) -> std::string + YUIException_strErrno(int errno_r, std::string const & msg) -> std::string + """ + return _yui.YUIException_strErrno(*args) + +def YUIException_log(exception, location, prefix): + r"""YUIException_log(YUIException exception, YCodeLocation location, char const *const prefix)""" + return _yui.YUIException_log(exception, location, prefix) + +class YUINullPointerException(YUIException): + r"""Proxy of C++ YUINullPointerException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YUINullPointerException self) -> YUINullPointerException""" + _yui.YUINullPointerException_swiginit(self, _yui.new_YUINullPointerException()) + __swig_destroy__ = _yui.delete_YUINullPointerException + +# Register YUINullPointerException in _yui: +_yui.YUINullPointerException_swigregister(YUINullPointerException) + +class YUIOutOfMemoryException(YUIException): + r"""Proxy of C++ YUIOutOfMemoryException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YUIOutOfMemoryException self) -> YUIOutOfMemoryException""" + _yui.YUIOutOfMemoryException_swiginit(self, _yui.new_YUIOutOfMemoryException()) + __swig_destroy__ = _yui.delete_YUIOutOfMemoryException + +# Register YUIOutOfMemoryException in _yui: +_yui.YUIOutOfMemoryException_swigregister(YUIOutOfMemoryException) + +class YUIInvalidWidgetException(YUIException): + r"""Proxy of C++ YUIInvalidWidgetException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YUIInvalidWidgetException self) -> YUIInvalidWidgetException""" + _yui.YUIInvalidWidgetException_swiginit(self, _yui.new_YUIInvalidWidgetException()) + __swig_destroy__ = _yui.delete_YUIInvalidWidgetException + +# Register YUIInvalidWidgetException in _yui: +_yui.YUIInvalidWidgetException_swigregister(YUIInvalidWidgetException) + +class YUIWidgetNotFoundException(YUIException): + r"""Proxy of C++ YUIWidgetNotFoundException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, idString): + r"""__init__(YUIWidgetNotFoundException self, std::string const & idString) -> YUIWidgetNotFoundException""" + _yui.YUIWidgetNotFoundException_swiginit(self, _yui.new_YUIWidgetNotFoundException(idString)) + __swig_destroy__ = _yui.delete_YUIWidgetNotFoundException + +# Register YUIWidgetNotFoundException in _yui: +_yui.YUIWidgetNotFoundException_swigregister(YUIWidgetNotFoundException) + +class YUINoDialogException(YUIException): + r"""Proxy of C++ YUINoDialogException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YUINoDialogException self) -> YUINoDialogException""" + _yui.YUINoDialogException_swiginit(self, _yui.new_YUINoDialogException()) + __swig_destroy__ = _yui.delete_YUINoDialogException + +# Register YUINoDialogException in _yui: +_yui.YUINoDialogException_swigregister(YUINoDialogException) + +class YUIDialogStackingOrderException(YUIException): + r"""Proxy of C++ YUIDialogStackingOrderException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YUIDialogStackingOrderException self) -> YUIDialogStackingOrderException""" + _yui.YUIDialogStackingOrderException_swiginit(self, _yui.new_YUIDialogStackingOrderException()) + __swig_destroy__ = _yui.delete_YUIDialogStackingOrderException + +# Register YUIDialogStackingOrderException in _yui: +_yui.YUIDialogStackingOrderException_swigregister(YUIDialogStackingOrderException) + +class YUISyntaxErrorException(YUIException): + r"""Proxy of C++ YUISyntaxErrorException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, msg): + r"""__init__(YUISyntaxErrorException self, std::string const & msg) -> YUISyntaxErrorException""" + _yui.YUISyntaxErrorException_swiginit(self, _yui.new_YUISyntaxErrorException(msg)) + __swig_destroy__ = _yui.delete_YUISyntaxErrorException + +# Register YUISyntaxErrorException in _yui: +_yui.YUISyntaxErrorException_swigregister(YUISyntaxErrorException) + +class YUIPropertyException(YUIException): + r"""Proxy of C++ YUIPropertyException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + + def property(self): + r"""property(YUIPropertyException self) -> YProperty""" + return _yui.YUIPropertyException_property(self) + + def widget(self): + r"""widget(YUIPropertyException self) -> YWidget""" + return _yui.YUIPropertyException_widget(self) + + def setWidget(self, w): + r"""setWidget(YUIPropertyException self, YWidget w)""" + return _yui.YUIPropertyException_setWidget(self, w) + +# Register YUIPropertyException in _yui: +_yui.YUIPropertyException_swigregister(YUIPropertyException) + +class YUIUnknownPropertyException(YUIPropertyException): + r"""Proxy of C++ YUIUnknownPropertyException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, propertyName, widget=None): + r"""__init__(YUIUnknownPropertyException self, std::string const & propertyName, YWidget widget=None) -> YUIUnknownPropertyException""" + _yui.YUIUnknownPropertyException_swiginit(self, _yui.new_YUIUnknownPropertyException(propertyName, widget)) + __swig_destroy__ = _yui.delete_YUIUnknownPropertyException + +# Register YUIUnknownPropertyException in _yui: +_yui.YUIUnknownPropertyException_swigregister(YUIUnknownPropertyException) + +class YUIPropertyTypeMismatchException(YUIPropertyException): + r"""Proxy of C++ YUIPropertyTypeMismatchException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, property, type, widget=None): + r"""__init__(YUIPropertyTypeMismatchException self, YProperty property, YPropertyType type, YWidget widget=None) -> YUIPropertyTypeMismatchException""" + _yui.YUIPropertyTypeMismatchException_swiginit(self, _yui.new_YUIPropertyTypeMismatchException(property, type, widget)) + __swig_destroy__ = _yui.delete_YUIPropertyTypeMismatchException + + def type(self): + r"""type(YUIPropertyTypeMismatchException self) -> YPropertyType""" + return _yui.YUIPropertyTypeMismatchException_type(self) + +# Register YUIPropertyTypeMismatchException in _yui: +_yui.YUIPropertyTypeMismatchException_swigregister(YUIPropertyTypeMismatchException) + +class YUISetReadOnlyPropertyException(YUIPropertyException): + r"""Proxy of C++ YUISetReadOnlyPropertyException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, property, widget=None): + r"""__init__(YUISetReadOnlyPropertyException self, YProperty property, YWidget widget=None) -> YUISetReadOnlyPropertyException""" + _yui.YUISetReadOnlyPropertyException_swiginit(self, _yui.new_YUISetReadOnlyPropertyException(property, widget)) + __swig_destroy__ = _yui.delete_YUISetReadOnlyPropertyException + +# Register YUISetReadOnlyPropertyException in _yui: +_yui.YUISetReadOnlyPropertyException_swigregister(YUISetReadOnlyPropertyException) + +class YUIBadPropertyArgException(YUIPropertyException): + r"""Proxy of C++ YUIBadPropertyArgException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r"""__init__(YUIBadPropertyArgException self, YProperty property, YWidget widget, std::string const & message="") -> YUIBadPropertyArgException""" + _yui.YUIBadPropertyArgException_swiginit(self, _yui.new_YUIBadPropertyArgException(*args)) + __swig_destroy__ = _yui.delete_YUIBadPropertyArgException + +# Register YUIBadPropertyArgException in _yui: +_yui.YUIBadPropertyArgException_swigregister(YUIBadPropertyArgException) + +class YUIUnsupportedWidgetException(YUIException): + r"""Proxy of C++ YUIUnsupportedWidgetException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, widgetType): + r"""__init__(YUIUnsupportedWidgetException self, std::string const & widgetType) -> YUIUnsupportedWidgetException""" + _yui.YUIUnsupportedWidgetException_swiginit(self, _yui.new_YUIUnsupportedWidgetException(widgetType)) + __swig_destroy__ = _yui.delete_YUIUnsupportedWidgetException + +# Register YUIUnsupportedWidgetException in _yui: +_yui.YUIUnsupportedWidgetException_swigregister(YUIUnsupportedWidgetException) + +class YUIInvalidDimensionException(YUIException): + r"""Proxy of C++ YUIInvalidDimensionException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YUIInvalidDimensionException self) -> YUIInvalidDimensionException""" + _yui.YUIInvalidDimensionException_swiginit(self, _yui.new_YUIInvalidDimensionException()) + __swig_destroy__ = _yui.delete_YUIInvalidDimensionException + +# Register YUIInvalidDimensionException in _yui: +_yui.YUIInvalidDimensionException_swigregister(YUIInvalidDimensionException) + +class YUIIndexOutOfRangeException(YUIException): + r"""Proxy of C++ YUIIndexOutOfRangeException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r"""__init__(YUIIndexOutOfRangeException self, int invalidIndex, int validMin, int validMax, std::string const & msg="") -> YUIIndexOutOfRangeException""" + _yui.YUIIndexOutOfRangeException_swiginit(self, _yui.new_YUIIndexOutOfRangeException(*args)) + __swig_destroy__ = _yui.delete_YUIIndexOutOfRangeException + + def invalidIndex(self): + r"""invalidIndex(YUIIndexOutOfRangeException self) -> int""" + return _yui.YUIIndexOutOfRangeException_invalidIndex(self) + + def validMin(self): + r"""validMin(YUIIndexOutOfRangeException self) -> int""" + return _yui.YUIIndexOutOfRangeException_validMin(self) + + def validMax(self): + r"""validMax(YUIIndexOutOfRangeException self) -> int""" + return _yui.YUIIndexOutOfRangeException_validMax(self) + +# Register YUIIndexOutOfRangeException in _yui: +_yui.YUIIndexOutOfRangeException_swigregister(YUIIndexOutOfRangeException) + +class YUIPluginException(YUIException): + r"""Proxy of C++ YUIPluginException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, pluginName): + r"""__init__(YUIPluginException self, std::string const & pluginName) -> YUIPluginException""" + _yui.YUIPluginException_swiginit(self, _yui.new_YUIPluginException(pluginName)) + __swig_destroy__ = _yui.delete_YUIPluginException + +# Register YUIPluginException in _yui: +_yui.YUIPluginException_swigregister(YUIPluginException) + +class YUICantLoadAnyUIException(YUIException): + r"""Proxy of C++ YUICantLoadAnyUIException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YUICantLoadAnyUIException self) -> YUICantLoadAnyUIException""" + _yui.YUICantLoadAnyUIException_swiginit(self, _yui.new_YUICantLoadAnyUIException()) + __swig_destroy__ = _yui.delete_YUICantLoadAnyUIException + +# Register YUICantLoadAnyUIException in _yui: +_yui.YUICantLoadAnyUIException_swigregister(YUICantLoadAnyUIException) + +class YUIButtonRoleMismatchException(YUIException): + r"""Proxy of C++ YUIButtonRoleMismatchException class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, msg): + r"""__init__(YUIButtonRoleMismatchException self, std::string const & msg) -> YUIButtonRoleMismatchException""" + _yui.YUIButtonRoleMismatchException_swiginit(self, _yui.new_YUIButtonRoleMismatchException(msg)) + __swig_destroy__ = _yui.delete_YUIButtonRoleMismatchException + +# Register YUIButtonRoleMismatchException in _yui: +_yui.YUIButtonRoleMismatchException_swigregister(YUIButtonRoleMismatchException) + +YUIPlugin_Qt = _yui.YUIPlugin_Qt + +YUIPlugin_NCurses = _yui.YUIPlugin_NCurses + +YUIPlugin_Gtk = _yui.YUIPlugin_Gtk + +YUIPlugin_RestAPI = _yui.YUIPlugin_RestAPI + +YUIPlugin_Ncurses_RestAPI = _yui.YUIPlugin_Ncurses_RestAPI + +YUIPlugin_Qt_RestAPI = _yui.YUIPlugin_Qt_RestAPI + +class YUILoader(object): + r"""Proxy of C++ YUILoader class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + + @staticmethod + def loadUI(withThreads=False): + r"""loadUI(bool withThreads=False)""" + return _yui.YUILoader_loadUI(withThreads) + + @staticmethod + def deleteUI(): + r"""deleteUI()""" + return _yui.YUILoader_deleteUI() + + @staticmethod + def loadRestAPIPlugin(wantedGUI, withThreads=False): + r"""loadRestAPIPlugin(std::string const & wantedGUI, bool withThreads=False)""" + return _yui.YUILoader_loadRestAPIPlugin(wantedGUI, withThreads) + + @staticmethod + def loadPlugin(name, withThreads=False): + r"""loadPlugin(std::string const & name, bool withThreads=False)""" + return _yui.YUILoader_loadPlugin(name, withThreads) + + @staticmethod + def pluginExists(pluginBaseName): + r"""pluginExists(std::string const & pluginBaseName) -> bool""" + return _yui.YUILoader_pluginExists(pluginBaseName) + + @staticmethod + def loadExternalWidgets(*args): + r"""loadExternalWidgets(std::string const & name, std::string const & symbol="_Z21createExternalWidgetsPKc")""" + return _yui.YUILoader_loadExternalWidgets(*args) + +# Register YUILoader in _yui: +_yui.YUILoader_swigregister(YUILoader) + +def YUILoader_loadUI(withThreads=False): + r"""YUILoader_loadUI(bool withThreads=False)""" + return _yui.YUILoader_loadUI(withThreads) + +def YUILoader_deleteUI(): + r"""YUILoader_deleteUI()""" + return _yui.YUILoader_deleteUI() + +def YUILoader_loadRestAPIPlugin(wantedGUI, withThreads=False): + r"""YUILoader_loadRestAPIPlugin(std::string const & wantedGUI, bool withThreads=False)""" + return _yui.YUILoader_loadRestAPIPlugin(wantedGUI, withThreads) + +def YUILoader_loadPlugin(name, withThreads=False): + r"""YUILoader_loadPlugin(std::string const & name, bool withThreads=False)""" + return _yui.YUILoader_loadPlugin(name, withThreads) + +def YUILoader_pluginExists(pluginBaseName): + r"""YUILoader_pluginExists(std::string const & pluginBaseName) -> bool""" + return _yui.YUILoader_pluginExists(pluginBaseName) + +def YUILoader_loadExternalWidgets(*args): + r"""YUILoader_loadExternalWidgets(std::string const & name, std::string const & symbol="_Z21createExternalWidgetsPKc")""" + return _yui.YUILoader_loadExternalWidgets(*args) + +YUIBuiltin_AskForExistingDirectory = _yui.YUIBuiltin_AskForExistingDirectory + +YUIBuiltin_AskForExistingFile = _yui.YUIBuiltin_AskForExistingFile + +YUIBuiltin_AskForSaveFileName = _yui.YUIBuiltin_AskForSaveFileName + +YUIBuiltin_AskForWidgetStyle = _yui.YUIBuiltin_AskForWidgetStyle + +YUIBuiltin_Beep = _yui.YUIBuiltin_Beep + +YUIBuiltin_BusyCursor = _yui.YUIBuiltin_BusyCursor + +YUIBuiltin_OpenContextMenu = _yui.YUIBuiltin_OpenContextMenu + +YUIBuiltin_ChangeWidget = _yui.YUIBuiltin_ChangeWidget + +YUIBuiltin_CloseDialog = _yui.YUIBuiltin_CloseDialog + +YUIBuiltin_CloseUI = _yui.YUIBuiltin_CloseUI + +YUIBuiltin_DumpWidgetTree = _yui.YUIBuiltin_DumpWidgetTree + +YUIBuiltin_GetDisplayInfo = _yui.YUIBuiltin_GetDisplayInfo + +YUIBuiltin_GetLanguage = _yui.YUIBuiltin_GetLanguage + +YUIBuiltin_GetProductName = _yui.YUIBuiltin_GetProductName + +YUIBuiltin_Glyph = _yui.YUIBuiltin_Glyph + +YUIBuiltin_HasSpecialWidget = _yui.YUIBuiltin_HasSpecialWidget + +YUIBuiltin_MakeScreenShot = _yui.YUIBuiltin_MakeScreenShot + +YUIBuiltin_NormalCursor = _yui.YUIBuiltin_NormalCursor + +YUIBuiltin_OpenDialog = _yui.YUIBuiltin_OpenDialog + +YUIBuiltin_OpenUI = _yui.YUIBuiltin_OpenUI + +YUIBuiltin_PollInput = _yui.YUIBuiltin_PollInput + +YUIBuiltin_QueryWidget = _yui.YUIBuiltin_QueryWidget + +YUIBuiltin_RecalcLayout = _yui.YUIBuiltin_RecalcLayout + +YUIBuiltin_Recode = _yui.YUIBuiltin_Recode + +YUIBuiltin_RedrawScreen = _yui.YUIBuiltin_RedrawScreen + +YUIBuiltin_ReplaceWidget = _yui.YUIBuiltin_ReplaceWidget + +YUIBuiltin_RunPkgSelection = _yui.YUIBuiltin_RunPkgSelection + +YUIBuiltin_SetConsoleFont = _yui.YUIBuiltin_SetConsoleFont + +YUIBuiltin_SetFocus = _yui.YUIBuiltin_SetFocus + +YUIBuiltin_SetFunctionKeys = _yui.YUIBuiltin_SetFunctionKeys + +YUIBuiltin_SetKeyboard = _yui.YUIBuiltin_SetKeyboard + +YUIBuiltin_RunInTerminal = _yui.YUIBuiltin_RunInTerminal + +YUIBuiltin_SetLanguage = _yui.YUIBuiltin_SetLanguage + +YUIBuiltin_SetProductName = _yui.YUIBuiltin_SetProductName + +YUIBuiltin_TimeoutUserInput = _yui.YUIBuiltin_TimeoutUserInput + +YUIBuiltin_UserInput = _yui.YUIBuiltin_UserInput + +YUIBuiltin_WaitForEvent = _yui.YUIBuiltin_WaitForEvent + +YUIBuiltin_WidgetExists = _yui.YUIBuiltin_WidgetExists + +YUIBuiltin_WizardCommand = _yui.YUIBuiltin_WizardCommand + +YUIBuiltin_PostponeShortcutCheck = _yui.YUIBuiltin_PostponeShortcutCheck + +YUIBuiltin_CheckShortcuts = _yui.YUIBuiltin_CheckShortcuts + +YUIBuiltin_RecordMacro = _yui.YUIBuiltin_RecordMacro + +YUIBuiltin_StopRecordMacro = _yui.YUIBuiltin_StopRecordMacro + +YUIBuiltin_PlayMacro = _yui.YUIBuiltin_PlayMacro + +YUIBuiltin_FakeUserInput = _yui.YUIBuiltin_FakeUserInput + +YUIBuiltin_WFM = _yui.YUIBuiltin_WFM + +YUIBuiltin_SCR = _yui.YUIBuiltin_SCR + +YUIWidget_Bottom = _yui.YUIWidget_Bottom + +YUIWidget_BusyIndicator = _yui.YUIWidget_BusyIndicator + +YUIWidget_ButtonBox = _yui.YUIWidget_ButtonBox + +YUIWidget_CheckBox = _yui.YUIWidget_CheckBox + +YUIWidget_CheckBoxFrame = _yui.YUIWidget_CheckBoxFrame + +YUIWidget_ComboBox = _yui.YUIWidget_ComboBox + +YUIWidget_CustomStatusItemSelector = _yui.YUIWidget_CustomStatusItemSelector + +YUIWidget_Empty = _yui.YUIWidget_Empty + +YUIWidget_Frame = _yui.YUIWidget_Frame + +YUIWidget_HBox = _yui.YUIWidget_HBox + +YUIWidget_HCenter = _yui.YUIWidget_HCenter + +YUIWidget_HSpacing = _yui.YUIWidget_HSpacing + +YUIWidget_HSquash = _yui.YUIWidget_HSquash + +YUIWidget_HStretch = _yui.YUIWidget_HStretch + +YUIWidget_HVCenter = _yui.YUIWidget_HVCenter + +YUIWidget_HVSquash = _yui.YUIWidget_HVSquash + +YUIWidget_HWeight = _yui.YUIWidget_HWeight + +YUIWidget_Heading = _yui.YUIWidget_Heading + +YUIWidget_IconButton = _yui.YUIWidget_IconButton + +YUIWidget_Image = _yui.YUIWidget_Image + +YUIWidget_InputField = _yui.YUIWidget_InputField + +YUIWidget_IntField = _yui.YUIWidget_IntField + +YUIWidget_Label = _yui.YUIWidget_Label + +YUIWidget_Left = _yui.YUIWidget_Left + +YUIWidget_LogView = _yui.YUIWidget_LogView + +YUIWidget_MarginBox = _yui.YUIWidget_MarginBox + +YUIWidget_MenuBar = _yui.YUIWidget_MenuBar + +YUIWidget_MenuButton = _yui.YUIWidget_MenuButton + +YUIWidget_MinHeight = _yui.YUIWidget_MinHeight + +YUIWidget_MinSize = _yui.YUIWidget_MinSize + +YUIWidget_MinWidth = _yui.YUIWidget_MinWidth + +YUIWidget_MultiItemSelector = _yui.YUIWidget_MultiItemSelector + +YUIWidget_MultiLineEdit = _yui.YUIWidget_MultiLineEdit + +YUIWidget_MultiSelectionBox = _yui.YUIWidget_MultiSelectionBox + +YUIWidget_PackageSelector = _yui.YUIWidget_PackageSelector + +YUIWidget_Password = _yui.YUIWidget_Password + +YUIWidget_PkgSpecial = _yui.YUIWidget_PkgSpecial + +YUIWidget_ProgressBar = _yui.YUIWidget_ProgressBar + +YUIWidget_PushButton = _yui.YUIWidget_PushButton + +YUIWidget_RadioButton = _yui.YUIWidget_RadioButton + +YUIWidget_RadioButtonGroup = _yui.YUIWidget_RadioButtonGroup + +YUIWidget_ReplacePoint = _yui.YUIWidget_ReplacePoint + +YUIWidget_RichText = _yui.YUIWidget_RichText + +YUIWidget_Right = _yui.YUIWidget_Right + +YUIWidget_SelectionBox = _yui.YUIWidget_SelectionBox + +YUIWidget_SingleItemSelector = _yui.YUIWidget_SingleItemSelector + +YUIWidget_Table = _yui.YUIWidget_Table + +YUIWidget_TextEntry = _yui.YUIWidget_TextEntry + +YUIWidget_Top = _yui.YUIWidget_Top + +YUIWidget_Tree = _yui.YUIWidget_Tree + +YUIWidget_VBox = _yui.YUIWidget_VBox + +YUIWidget_VCenter = _yui.YUIWidget_VCenter + +YUIWidget_VSpacing = _yui.YUIWidget_VSpacing + +YUIWidget_VSquash = _yui.YUIWidget_VSquash + +YUIWidget_VStretch = _yui.YUIWidget_VStretch + +YUIWidget_VWeight = _yui.YUIWidget_VWeight + +YUISpecialWidget_BarGraph = _yui.YUISpecialWidget_BarGraph + +YUISpecialWidget_Date = _yui.YUISpecialWidget_Date + +YUISpecialWidget_DateField = _yui.YUISpecialWidget_DateField + +YUISpecialWidget_DownloadProgress = _yui.YUISpecialWidget_DownloadProgress + +YUISpecialWidget_DumbTab = _yui.YUISpecialWidget_DumbTab + +YUISpecialWidget_DummySpecialWidget = _yui.YUISpecialWidget_DummySpecialWidget + +YUISpecialWidget_HMultiProgressMeter = _yui.YUISpecialWidget_HMultiProgressMeter + +YUISpecialWidget_VMultiProgressMeter = _yui.YUISpecialWidget_VMultiProgressMeter + +YUISpecialWidget_PartitionSplitter = _yui.YUISpecialWidget_PartitionSplitter + +YUISpecialWidget_PatternSelector = _yui.YUISpecialWidget_PatternSelector + +YUISpecialWidget_SimplePatchSelector = _yui.YUISpecialWidget_SimplePatchSelector + +YUISpecialWidget_Slider = _yui.YUISpecialWidget_Slider + +YUISpecialWidget_Time = _yui.YUISpecialWidget_Time + +YUISpecialWidget_TimeField = _yui.YUISpecialWidget_TimeField + +YUISpecialWidget_Wizard = _yui.YUISpecialWidget_Wizard + +YUISpecialWidget_TimezoneSelector = _yui.YUISpecialWidget_TimezoneSelector + +YUISpecialWidget_Graph = _yui.YUISpecialWidget_Graph + +YUISpecialWidget_ContextMenu = _yui.YUISpecialWidget_ContextMenu + +YUIProperty_Alive = _yui.YUIProperty_Alive + +YUIProperty_Cell = _yui.YUIProperty_Cell + +YUIProperty_ContextMenu = _yui.YUIProperty_ContextMenu + +YUIProperty_CurrentBranch = _yui.YUIProperty_CurrentBranch + +YUIProperty_CurrentButton = _yui.YUIProperty_CurrentButton + +YUIProperty_CurrentItem = _yui.YUIProperty_CurrentItem + +YUIProperty_CurrentSize = _yui.YUIProperty_CurrentSize + +YUIProperty_DebugLabel = _yui.YUIProperty_DebugLabel + +YUIProperty_EasterEgg = _yui.YUIProperty_EasterEgg + +YUIProperty_Enabled = _yui.YUIProperty_Enabled + +YUIProperty_EnabledItems = _yui.YUIProperty_EnabledItems + +YUIProperty_ExpectedSize = _yui.YUIProperty_ExpectedSize + +YUIProperty_Filename = _yui.YUIProperty_Filename + +YUIProperty_Layout = _yui.YUIProperty_Layout + +YUIProperty_HelpText = _yui.YUIProperty_HelpText + +YUIProperty_IconPath = _yui.YUIProperty_IconPath + +YUIProperty_InputMaxLength = _yui.YUIProperty_InputMaxLength + +YUIProperty_HWeight = _yui.YUIProperty_HWeight + +YUIProperty_HStretch = _yui.YUIProperty_HStretch + +YUIProperty_ID = _yui.YUIProperty_ID + +YUIProperty_Item = _yui.YUIProperty_Item + +YUIProperty_Items = _yui.YUIProperty_Items + +YUIProperty_ItemStatus = _yui.YUIProperty_ItemStatus + +YUIProperty_Label = _yui.YUIProperty_Label + +YUIProperty_Labels = _yui.YUIProperty_Labels + +YUIProperty_LastLine = _yui.YUIProperty_LastLine + +YUIProperty_MaxLines = _yui.YUIProperty_MaxLines + +YUIProperty_MaxValue = _yui.YUIProperty_MaxValue + +YUIProperty_MinValue = _yui.YUIProperty_MinValue + +YUIProperty_MultiSelection = _yui.YUIProperty_MultiSelection + +YUIProperty_Notify = _yui.YUIProperty_Notify + +YUIProperty_OpenItems = _yui.YUIProperty_OpenItems + +YUIProperty_SelectedItems = _yui.YUIProperty_SelectedItems + +YUIProperty_Text = _yui.YUIProperty_Text + +YUIProperty_Timeout = _yui.YUIProperty_Timeout + +YUIProperty_ValidChars = _yui.YUIProperty_ValidChars + +YUIProperty_Value = _yui.YUIProperty_Value + +YUIProperty_Values = _yui.YUIProperty_Values + +YUIProperty_VisibleLines = _yui.YUIProperty_VisibleLines + +YUIProperty_VisibleItems = _yui.YUIProperty_VisibleItems + +YUIProperty_VWeight = _yui.YUIProperty_VWeight + +YUIProperty_VStretch = _yui.YUIProperty_VStretch + +YUIProperty_WidgetClass = _yui.YUIProperty_WidgetClass + +YUIProperty_VScrollValue = _yui.YUIProperty_VScrollValue + +YUIProperty_HScrollValue = _yui.YUIProperty_HScrollValue + +YUIOpt_animated = _yui.YUIOpt_animated + +YUIOpt_autoWrap = _yui.YUIOpt_autoWrap + +YUIOpt_applyButton = _yui.YUIOpt_applyButton + +YUIOpt_autoScrollDown = _yui.YUIOpt_autoScrollDown + +YUIOpt_autoShortcut = _yui.YUIOpt_autoShortcut + +YUIOpt_boldFont = _yui.YUIOpt_boldFont + +YUIOpt_cancelButton = _yui.YUIOpt_cancelButton + +YUIOpt_centered = _yui.YUIOpt_centered + +YUIOpt_confirmUnsupported = _yui.YUIOpt_confirmUnsupported + +YUIOpt_customButton = _yui.YUIOpt_customButton + +YUIOpt_debugLayout = _yui.YUIOpt_debugLayout + +YUIOpt_decorated = _yui.YUIOpt_decorated + +YUIOpt_default = _yui.YUIOpt_default + +YUIOpt_defaultsize = _yui.YUIOpt_defaultsize + +YUIOpt_disabled = _yui.YUIOpt_disabled + +YUIOpt_easterEgg = _yui.YUIOpt_easterEgg + +YUIOpt_editable = _yui.YUIOpt_editable + +YUIOpt_helpButton = _yui.YUIOpt_helpButton + +YUIOpt_relNotesButton = _yui.YUIOpt_relNotesButton + +YUIOpt_hstretch = _yui.YUIOpt_hstretch + +YUIOpt_hvstretch = _yui.YUIOpt_hvstretch + +YUIOpt_immediate = _yui.YUIOpt_immediate + +YUIOpt_infocolor = _yui.YUIOpt_infocolor + +YUIOpt_invertAutoEnable = _yui.YUIOpt_invertAutoEnable + +YUIOpt_keepSorting = _yui.YUIOpt_keepSorting + +YUIOpt_keyEvents = _yui.YUIOpt_keyEvents + +YUIOpt_mainDialog = _yui.YUIOpt_mainDialog + +YUIOpt_multiSelection = _yui.YUIOpt_multiSelection + +YUIOpt_noAutoEnable = _yui.YUIOpt_noAutoEnable + +YUIOpt_notify = _yui.YUIOpt_notify + +YUIOpt_notifyContextMenu = _yui.YUIOpt_notifyContextMenu + +YUIOpt_onlineSearch = _yui.YUIOpt_onlineSearch + +YUIOpt_okButton = _yui.YUIOpt_okButton + +YUIOpt_outputField = _yui.YUIOpt_outputField + +YUIOpt_plainText = _yui.YUIOpt_plainText + +YUIOpt_recursiveSelection = _yui.YUIOpt_recursiveSelection + +YUIOpt_relaxSanityCheck = _yui.YUIOpt_relaxSanityCheck + +YUIOpt_repoMgr = _yui.YUIOpt_repoMgr + +YUIOpt_repoMode = _yui.YUIOpt_repoMode + +YUIOpt_scaleToFit = _yui.YUIOpt_scaleToFit + +YUIOpt_searchMode = _yui.YUIOpt_searchMode + +YUIOpt_shrinkable = _yui.YUIOpt_shrinkable + +YUIOpt_stepsEnabled = _yui.YUIOpt_stepsEnabled + +YUIOpt_summaryMode = _yui.YUIOpt_summaryMode + +YUIOpt_testMode = _yui.YUIOpt_testMode + +YUIOpt_tiled = _yui.YUIOpt_tiled + +YUIOpt_titleOnLeft = _yui.YUIOpt_titleOnLeft + +YUIOpt_treeEnabled = _yui.YUIOpt_treeEnabled + +YUIOpt_updateMode = _yui.YUIOpt_updateMode + +YUIOpt_vstretch = _yui.YUIOpt_vstretch + +YUIOpt_warncolor = _yui.YUIOpt_warncolor + +YUIOpt_wizardDialog = _yui.YUIOpt_wizardDialog + +YUIOpt_youMode = _yui.YUIOpt_youMode + +YUIOpt_zeroHeight = _yui.YUIOpt_zeroHeight + +YUIOpt_zeroWidth = _yui.YUIOpt_zeroWidth + +YUIOpt_key_F1 = _yui.YUIOpt_key_F1 + +YUIOpt_key_F2 = _yui.YUIOpt_key_F2 + +YUIOpt_key_F3 = _yui.YUIOpt_key_F3 + +YUIOpt_key_F4 = _yui.YUIOpt_key_F4 + +YUIOpt_key_F5 = _yui.YUIOpt_key_F5 + +YUIOpt_key_F6 = _yui.YUIOpt_key_F6 + +YUIOpt_key_F7 = _yui.YUIOpt_key_F7 + +YUIOpt_key_F8 = _yui.YUIOpt_key_F8 + +YUIOpt_key_F9 = _yui.YUIOpt_key_F9 + +YUIOpt_key_F10 = _yui.YUIOpt_key_F10 + +YUIOpt_key_F11 = _yui.YUIOpt_key_F11 + +YUIOpt_key_F12 = _yui.YUIOpt_key_F12 + +YUIOpt_key_F13 = _yui.YUIOpt_key_F13 + +YUIOpt_key_F14 = _yui.YUIOpt_key_F14 + +YUIOpt_key_F15 = _yui.YUIOpt_key_F15 + +YUIOpt_key_F16 = _yui.YUIOpt_key_F16 + +YUIOpt_key_F17 = _yui.YUIOpt_key_F17 + +YUIOpt_key_F18 = _yui.YUIOpt_key_F18 + +YUIOpt_key_F19 = _yui.YUIOpt_key_F19 + +YUIOpt_key_F20 = _yui.YUIOpt_key_F20 + +YUIOpt_key_F21 = _yui.YUIOpt_key_F21 + +YUIOpt_key_F22 = _yui.YUIOpt_key_F22 + +YUIOpt_key_F23 = _yui.YUIOpt_key_F23 + +YUIOpt_key_F24 = _yui.YUIOpt_key_F24 + +YUIOpt_key_none = _yui.YUIOpt_key_none + +YUIGlyph_ArrowLeft = _yui.YUIGlyph_ArrowLeft + +YUIGlyph_ArrowRight = _yui.YUIGlyph_ArrowRight + +YUIGlyph_ArrowUp = _yui.YUIGlyph_ArrowUp + +YUIGlyph_ArrowDown = _yui.YUIGlyph_ArrowDown + +YUIGlyph_CheckMark = _yui.YUIGlyph_CheckMark + +YUIGlyph_BulletArrowRight = _yui.YUIGlyph_BulletArrowRight + +YUIGlyph_BulletCircle = _yui.YUIGlyph_BulletCircle + +YUIGlyph_BulletSquare = _yui.YUIGlyph_BulletSquare + +YUICap_Width = _yui.YUICap_Width + +YUICap_Height = _yui.YUICap_Height + +YUICap_Depth = _yui.YUICap_Depth + +YUICap_Colors = _yui.YUICap_Colors + +YUICap_DefaultWidth = _yui.YUICap_DefaultWidth + +YUICap_DefaultHeight = _yui.YUICap_DefaultHeight + +YUICap_TextMode = _yui.YUICap_TextMode + +YUICap_HasImageSupport = _yui.YUICap_HasImageSupport + +YUICap_HasAnimationSupport = _yui.YUICap_HasAnimationSupport + +YUICap_HasIconSupport = _yui.YUICap_HasIconSupport + +YUICap_HasFullUtf8Support = _yui.YUICap_HasFullUtf8Support + +YUICap_HasWidgetStyleSupport = _yui.YUICap_HasWidgetStyleSupport + +YUICap_HasWizardDialogSupport = _yui.YUICap_HasWizardDialogSupport + +YUICap_RichTextSupportsTable = _yui.YUICap_RichTextSupportsTable + +YUICap_LeftHandedMouse = _yui.YUICap_LeftHandedMouse + +YUICap_y2debug = _yui.YUICap_y2debug + +YUISymbol_id = _yui.YUISymbol_id + +YUISymbol_opt = _yui.YUISymbol_opt + +YUISymbol_icon = _yui.YUISymbol_icon + +YUISymbol_sortKey = _yui.YUISymbol_sortKey + +YUISymbol_item = _yui.YUISymbol_item + +YUISymbol_cell = _yui.YUISymbol_cell + +YUISymbol_menu = _yui.YUISymbol_menu + +YUISymbol_header = _yui.YUISymbol_header + +YUISymbol_rgb = _yui.YUISymbol_rgb + +YUISymbol_leftMargin = _yui.YUISymbol_leftMargin + +YUISymbol_rightMargin = _yui.YUISymbol_rightMargin + +YUISymbol_topMargin = _yui.YUISymbol_topMargin + +YUISymbol_bottomMargin = _yui.YUISymbol_bottomMargin + +YUISymbol_BackgroundPixmap = _yui.YUISymbol_BackgroundPixmap + +YUISymbol_open = _yui.YUISymbol_open + +YUISymbol_closed = _yui.YUISymbol_closed + +YUISymbol_Left = _yui.YUISymbol_Left + +YUISymbol_Right = _yui.YUISymbol_Right + +YUISymbol_Center = _yui.YUISymbol_Center + +class YWidgetID(object): + r"""Proxy of C++ YWidgetID class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YWidgetID + + def isEqual(self, otherID): + r"""isEqual(YWidgetID self, YWidgetID otherID) -> bool""" + return _yui.YWidgetID_isEqual(self, otherID) + + def toString(self): + r"""toString(YWidgetID self) -> std::string""" + return _yui.YWidgetID_toString(self) + +# Register YWidgetID in _yui: +_yui.YWidgetID_swigregister(YWidgetID) + +class YStringWidgetID(YWidgetID): + r"""Proxy of C++ YStringWidgetID class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, value): + r"""__init__(YStringWidgetID self, std::string const & value) -> YStringWidgetID""" + _yui.YStringWidgetID_swiginit(self, _yui.new_YStringWidgetID(value)) + __swig_destroy__ = _yui.delete_YStringWidgetID + + def isEqual(self, otherID): + r"""isEqual(YStringWidgetID self, YWidgetID otherID) -> bool""" + return _yui.YStringWidgetID_isEqual(self, otherID) + + def toString(self): + r"""toString(YStringWidgetID self) -> std::string""" + return _yui.YStringWidgetID_toString(self) + + def value(self): + r"""value(YStringWidgetID self) -> std::string""" + return _yui.YStringWidgetID_value(self) + + def valueConstRef(self): + r"""valueConstRef(YStringWidgetID self) -> std::string const &""" + return _yui.YStringWidgetID_valueConstRef(self) + +# Register YStringWidgetID in _yui: +_yui.YStringWidgetID_swigregister(YStringWidgetID) + +class YExternalWidgetFactory(object): + r"""Proxy of C++ YExternalWidgetFactory class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined") + __repr__ = _swig_repr + +# Register YExternalWidgetFactory in _yui: +_yui.YExternalWidgetFactory_swigregister(YExternalWidgetFactory) + +class YExternalWidgets(object): + r"""Proxy of C++ YExternalWidgets class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YExternalWidgets + + @staticmethod + def externalWidgets(name): + r"""externalWidgets(std::string const & name) -> YExternalWidgets""" + return _yui.YExternalWidgets_externalWidgets(name) + + @staticmethod + def externalWidgetFactory(*args): + r""" + externalWidgetFactory() -> YExternalWidgetFactory + externalWidgetFactory(std::string const & name) -> YExternalWidgetFactory + """ + return _yui.YExternalWidgets_externalWidgetFactory(*args) + +# Register YExternalWidgets in _yui: +_yui.YExternalWidgets_swigregister(YExternalWidgets) + +def YExternalWidgets_externalWidgets(name): + r"""YExternalWidgets_externalWidgets(std::string const & name) -> YExternalWidgets""" + return _yui.YExternalWidgets_externalWidgets(name) + +def YExternalWidgets_externalWidgetFactory(*args): + r""" + YExternalWidgets_externalWidgetFactory() -> YExternalWidgetFactory + YExternalWidgets_externalWidgetFactory(std::string const & name) -> YExternalWidgetFactory + """ + return _yui.YExternalWidgets_externalWidgetFactory(*args) + +class YCBTableHeader(YTableHeader): + r"""Proxy of C++ YCBTableHeader class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self): + r"""__init__(YCBTableHeader self) -> YCBTableHeader""" + _yui.YCBTableHeader_swiginit(self, _yui.new_YCBTableHeader()) + __swig_destroy__ = _yui.delete_YCBTableHeader + + def addColumn(self, header, checkBox, alignment=YAlignBegin): + r"""addColumn(YCBTableHeader self, std::string const & header, bool checkBox, YAlignmentType alignment=YAlignBegin)""" + return _yui.YCBTableHeader_addColumn(self, header, checkBox, alignment) + + def cbColumn(self, column): + r"""cbColumn(YCBTableHeader self, int column) -> bool""" + return _yui.YCBTableHeader_cbColumn(self, column) + +# Register YCBTableHeader in _yui: +_yui.YCBTableHeader_swigregister(YCBTableHeader) + +class YCBTableCell(YTableCell): + r"""Proxy of C++ YCBTableCell class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YCBTableCell self, std::string const & label, std::string const & iconName="", std::string const & sortKey="") -> YCBTableCell + __init__(YCBTableCell self, char const * label) -> YCBTableCell + __init__(YCBTableCell self, bool const & checked) -> YCBTableCell + __init__(YCBTableCell self, YTableItem parent, int column, std::string const & label="", bool const & checked=False, std::string const & iconName="", std::string const & sortKey="") -> YCBTableCell + """ + _yui.YCBTableCell_swiginit(self, _yui.new_YCBTableCell(*args)) + __swig_destroy__ = _yui.delete_YCBTableCell + + def setChecked(self, val=True): + r"""setChecked(YCBTableCell self, bool val=True)""" + return _yui.YCBTableCell_setChecked(self, val) + + def checked(self): + r"""checked(YCBTableCell self) -> bool""" + return _yui.YCBTableCell_checked(self) + +# Register YCBTableCell in _yui: +_yui.YCBTableCell_swigregister(YCBTableCell) + +class YCBTableItem(YTableItem): + r"""Proxy of C++ YCBTableItem class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def __init__(self, *args): + r""" + __init__(YCBTableItem self) -> YCBTableItem + __init__(YCBTableItem self, YCBTableCell cell0_disown, YCBTableCell cell1_disown=None, YCBTableCell cell2_disown=None, YCBTableCell cell3_disown=None, YCBTableCell cell4_disown=None, YCBTableCell cell5_disown=None, YCBTableCell cell6_disown=None, YCBTableCell cell7_disown=None, YCBTableCell cell8_disown=None, YCBTableCell cell9_disown=None) -> YCBTableItem + """ + _yui.YCBTableItem_swiginit(self, _yui.new_YCBTableItem(*args)) + __swig_destroy__ = _yui.delete_YCBTableItem + + def itemClass(self): + r"""itemClass(YCBTableItem self) -> char const *""" + return _yui.YCBTableItem_itemClass(self) + + def addCell(self, *args): + r""" + addCell(YCBTableItem self, YCBTableCell cell_disown) + addCell(YCBTableItem self, std::string const & label, std::string const & iconName=std::string(), std::string const & sortKey=std::string()) + addCell(YCBTableItem self, bool checked) + addCell(YCBTableItem self, char const * label) + """ + return _yui.YCBTableItem_addCell(self, *args) + + def checked(self, index): + r"""checked(YCBTableItem self, int index) -> bool""" + return _yui.YCBTableItem_checked(self, index) + + def cellChanged(self): + r"""cellChanged(YCBTableItem self) -> YCBTableCell""" + return _yui.YCBTableItem_cellChanged(self) + + def setChangedColumn(self, column): + r"""setChangedColumn(YCBTableItem self, int column)""" + return _yui.YCBTableItem_setChangedColumn(self, column) + +# Register YCBTableItem in _yui: +_yui.YCBTableItem_swigregister(YCBTableItem) + +class YMGA_CBTable(YTable): + r"""Proxy of C++ YMGA_CBTable class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + __swig_destroy__ = _yui.delete_YMGA_CBTable + + def widgetClass(self): + r"""widgetClass(YMGA_CBTable self) -> char const *""" + return _yui.YMGA_CBTable_widgetClass(self) + + def isCheckBoxColumn(self, column): + r"""isCheckBoxColumn(YMGA_CBTable self, int column) -> bool""" + return _yui.YMGA_CBTable_isCheckBoxColumn(self, column) + + def setItemChecked(self, item, column, checked=True): + r"""setItemChecked(YMGA_CBTable self, YItem item, int column, bool checked=True)""" + return _yui.YMGA_CBTable_setItemChecked(self, item, column, checked) + + def setChangedItem(self, pItem): + r"""setChangedItem(YMGA_CBTable self, YCBTableItem pItem)""" + return _yui.YMGA_CBTable_setChangedItem(self, pItem) + + def changedItem(self): + r"""changedItem(YMGA_CBTable self) -> YCBTableItem""" + return _yui.YMGA_CBTable_changedItem(self) + + def nextItem(self, currentIterator): + r"""nextItem(YMGA_CBTable self, YItemIterator currentIterator) -> YItemIterator""" + return _yui.YMGA_CBTable_nextItem(self, currentIterator) + + def deleteAllItems(self): + r"""deleteAllItems(YMGA_CBTable self)""" + return _yui.YMGA_CBTable_deleteAllItems(self) + + def YItemIteratorToYItem(self, iter): + r"""YItemIteratorToYItem(YMGA_CBTable self, YItemIterator iter) -> YItem""" + return _yui.YMGA_CBTable_YItemIteratorToYItem(self, iter) + + def toCBYTableItem(self, item): + r"""toCBYTableItem(YMGA_CBTable self, YItem item) -> YCBTableItem""" + return _yui.YMGA_CBTable_toCBYTableItem(self, item) + +# Register YMGA_CBTable in _yui: +_yui.YMGA_CBTable_swigregister(YMGA_CBTable) + +class YMGAAboutDialog(object): + r"""Proxy of C++ YMGAAboutDialog class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + CLASSIC = _yui.YMGAAboutDialog_CLASSIC + + TABBED = _yui.YMGAAboutDialog_TABBED + + + def __init__(self, *args): + r"""__init__(YMGAAboutDialog self, std::string const & name, std::string const & version, std::string const & license, std::string const & authors, std::string const & description, std::string const & logo, std::string const & icon=std::string(), std::string const & credits=std::string(), std::string const & information=std::string()) -> YMGAAboutDialog""" + _yui.YMGAAboutDialog_swiginit(self, _yui.new_YMGAAboutDialog(*args)) + __swig_destroy__ = _yui.delete_YMGAAboutDialog + + def setMinSize(self, columns, lines): + r"""setMinSize(YMGAAboutDialog self, YLayoutSize_t columns, YLayoutSize_t lines)""" + return _yui.YMGAAboutDialog_setMinSize(self, columns, lines) + + def show(self, *args): + r"""show(YMGAAboutDialog self, YMGAAboutDialog::DLG_MODE type=TABBED)""" + return _yui.YMGAAboutDialog_show(self, *args) + +# Register YMGAAboutDialog in _yui: +_yui.YMGAAboutDialog_swigregister(YMGAAboutDialog) + +class YMGAMessageBox(object): + r"""Proxy of C++ YMGAMessageBox class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + B_ONE = _yui.YMGAMessageBox_B_ONE + + B_TWO = _yui.YMGAMessageBox_B_TWO + + D_NORMAL = _yui.YMGAMessageBox_D_NORMAL + + D_INFO = _yui.YMGAMessageBox_D_INFO + + D_WARNING = _yui.YMGAMessageBox_D_WARNING + + + def __init__(self, *args): + r"""__init__(YMGAMessageBox self, YMGAMessageBox::DLG_BUTTON b_num=B_ONE, YMGAMessageBox::DLG_MODE dlg_mode=D_NORMAL) -> YMGAMessageBox""" + _yui.YMGAMessageBox_swiginit(self, _yui.new_YMGAMessageBox(*args)) + __swig_destroy__ = _yui.delete_YMGAMessageBox + + def setIcon(self, icon): + r"""setIcon(YMGAMessageBox self, std::string const & icon)""" + return _yui.YMGAMessageBox_setIcon(self, icon) + + def setTitle(self, title): + r"""setTitle(YMGAMessageBox self, std::string const & title)""" + return _yui.YMGAMessageBox_setTitle(self, title) + + def setText(self, text, useRichText=False): + r"""setText(YMGAMessageBox self, std::string const & text, bool useRichText=False)""" + return _yui.YMGAMessageBox_setText(self, text, useRichText) + + def setMinSize(self, minWidth, minHeight): + r"""setMinSize(YMGAMessageBox self, YLayoutSize_t minWidth, YLayoutSize_t minHeight)""" + return _yui.YMGAMessageBox_setMinSize(self, minWidth, minHeight) + + def setButtonLabel(self, *args): + r"""setButtonLabel(YMGAMessageBox self, std::string const & label, YMGAMessageBox::DLG_BUTTON button=B_ONE)""" + return _yui.YMGAMessageBox_setButtonLabel(self, *args) + + def setDefaultButton(self, *args): + r"""setDefaultButton(YMGAMessageBox self, YMGAMessageBox::DLG_BUTTON button=B_ONE)""" + return _yui.YMGAMessageBox_setDefaultButton(self, *args) + + def show(self): + r"""show(YMGAMessageBox self) -> YMGAMessageBox::DLG_BUTTON""" + return _yui.YMGAMessageBox_show(self) + +# Register YMGAMessageBox in _yui: +_yui.YMGAMessageBox_swigregister(YMGAMessageBox) + +class YMGAWidgetFactory(YExternalWidgetFactory): + r"""Proxy of C++ YMGAWidgetFactory class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + + def __init__(self, *args, **kwargs): + raise AttributeError("No constructor defined - class is abstract") + __repr__ = _swig_repr + + def createAboutDialog(self, *args): + r"""createAboutDialog(YMGAWidgetFactory self, std::string const & appname, std::string const & appversion, std::string const & applicense, std::string const & appauthors, std::string const & appdescription, std::string const & applogo, std::string const & appicon=std::string(), std::string const & appcredits=std::string(), std::string const & appinfo=std::string()) -> YMGAAboutDialog""" + return _yui.YMGAWidgetFactory_createAboutDialog(self, *args) + + def createCBTable(self, parent, header_disown): + r"""createCBTable(YMGAWidgetFactory self, YWidget parent, YTableHeader header_disown) -> YMGA_CBTable""" + return _yui.YMGAWidgetFactory_createCBTable(self, parent, header_disown) + + def createMenuBar(self, parent): + r"""createMenuBar(YMGAWidgetFactory self, YWidget parent) -> YMGAMenuBar *""" + return _yui.YMGAWidgetFactory_createMenuBar(self, parent) + + def createDialogBox(self, *args): + r"""createDialogBox(YMGAWidgetFactory self, YMGAMessageBox::DLG_BUTTON button_number=B_ONE, YMGAMessageBox::DLG_MODE dialog_mode=D_NORMAL) -> YMGAMessageBox""" + return _yui.YMGAWidgetFactory_createDialogBox(self, *args) + + def createMessageBox(self, title, text, useRichText, btn_label): + r"""createMessageBox(YMGAWidgetFactory self, std::string const & title, std::string const & text, bool useRichText, std::string const & btn_label) -> YMGAMessageBox""" + return _yui.YMGAWidgetFactory_createMessageBox(self, title, text, useRichText, btn_label) + + def createInfoBox(self, title, text, useRichText, btn_label): + r"""createInfoBox(YMGAWidgetFactory self, std::string const & title, std::string const & text, bool useRichText, std::string const & btn_label) -> YMGAMessageBox""" + return _yui.YMGAWidgetFactory_createInfoBox(self, title, text, useRichText, btn_label) + + def createWarningBox(self, title, text, useRichText, btn_label): + r"""createWarningBox(YMGAWidgetFactory self, std::string const & title, std::string const & text, bool useRichText, std::string const & btn_label) -> YMGAMessageBox""" + return _yui.YMGAWidgetFactory_createWarningBox(self, title, text, useRichText, btn_label) + + @staticmethod + def getYMGAWidgetFactory(instance): + r"""getYMGAWidgetFactory(YExternalWidgetFactory instance) -> YMGAWidgetFactory""" + return _yui.YMGAWidgetFactory_getYMGAWidgetFactory(instance) + + @staticmethod + def getYWidgetEvent(event): + r"""getYWidgetEvent(YEvent event) -> YWidgetEvent""" + return _yui.YMGAWidgetFactory_getYWidgetEvent(event) + + @staticmethod + def getYKeyEvent(event): + r"""getYKeyEvent(YEvent event) -> YKeyEvent""" + return _yui.YMGAWidgetFactory_getYKeyEvent(event) + + @staticmethod + def getYMenuEvent(event): + r"""getYMenuEvent(YEvent event) -> YMenuEvent""" + return _yui.YMGAWidgetFactory_getYMenuEvent(event) + + @staticmethod + def getYCancelEvent(event): + r"""getYCancelEvent(YEvent event) -> YCancelEvent""" + return _yui.YMGAWidgetFactory_getYCancelEvent(event) + + @staticmethod + def getYDebugEvent(event): + r"""getYDebugEvent(YEvent event) -> YDebugEvent""" + return _yui.YMGAWidgetFactory_getYDebugEvent(event) + + @staticmethod + def getYTimeoutEvent(event): + r"""getYTimeoutEvent(YEvent event) -> YTimeoutEvent""" + return _yui.YMGAWidgetFactory_getYTimeoutEvent(event) + + @staticmethod + def toYMGAMenuItem(item): + r"""toYMGAMenuItem(YItem item) -> YMGAMenuItem *""" + return _yui.YMGAWidgetFactory_toYMGAMenuItem(item) + + @staticmethod + def toYMenuSeparator(item): + r"""toYMenuSeparator(YItem item) -> YMenuSeparator *""" + return _yui.YMGAWidgetFactory_toYMenuSeparator(item) + +# Register YMGAWidgetFactory in _yui: +_yui.YMGAWidgetFactory_swigregister(YMGAWidgetFactory) + +def YMGAWidgetFactory_getYMGAWidgetFactory(instance): + r"""YMGAWidgetFactory_getYMGAWidgetFactory(YExternalWidgetFactory instance) -> YMGAWidgetFactory""" + return _yui.YMGAWidgetFactory_getYMGAWidgetFactory(instance) + +def YMGAWidgetFactory_getYWidgetEvent(event): + r"""YMGAWidgetFactory_getYWidgetEvent(YEvent event) -> YWidgetEvent""" + return _yui.YMGAWidgetFactory_getYWidgetEvent(event) + +def YMGAWidgetFactory_getYKeyEvent(event): + r"""YMGAWidgetFactory_getYKeyEvent(YEvent event) -> YKeyEvent""" + return _yui.YMGAWidgetFactory_getYKeyEvent(event) + +def YMGAWidgetFactory_getYMenuEvent(event): + r"""YMGAWidgetFactory_getYMenuEvent(YEvent event) -> YMenuEvent""" + return _yui.YMGAWidgetFactory_getYMenuEvent(event) + +def YMGAWidgetFactory_getYCancelEvent(event): + r"""YMGAWidgetFactory_getYCancelEvent(YEvent event) -> YCancelEvent""" + return _yui.YMGAWidgetFactory_getYCancelEvent(event) + +def YMGAWidgetFactory_getYDebugEvent(event): + r"""YMGAWidgetFactory_getYDebugEvent(YEvent event) -> YDebugEvent""" + return _yui.YMGAWidgetFactory_getYDebugEvent(event) + +def YMGAWidgetFactory_getYTimeoutEvent(event): + r"""YMGAWidgetFactory_getYTimeoutEvent(YEvent event) -> YTimeoutEvent""" + return _yui.YMGAWidgetFactory_getYTimeoutEvent(event) + +def YMGAWidgetFactory_toYMGAMenuItem(item): + r"""YMGAWidgetFactory_toYMGAMenuItem(YItem item) -> YMGAMenuItem *""" + return _yui.YMGAWidgetFactory_toYMGAMenuItem(item) + +def YMGAWidgetFactory_toYMenuSeparator(item): + r"""YMGAWidgetFactory_toYMenuSeparator(YItem item) -> YMenuSeparator *""" + return _yui.YMGAWidgetFactory_toYMenuSeparator(item) + +class YItemCollection(object): + r"""Proxy of C++ std::vector< YItem * > class.""" + + thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") + __repr__ = _swig_repr + + def iterator(self): + r"""iterator(YItemCollection self) -> SwigPyIterator""" + return _yui.YItemCollection_iterator(self) + def __iter__(self): + return self.iterator() + + def __nonzero__(self): + r"""__nonzero__(YItemCollection self) -> bool""" + return _yui.YItemCollection___nonzero__(self) + + def __bool__(self): + r"""__bool__(YItemCollection self) -> bool""" + return _yui.YItemCollection___bool__(self) + + def __len__(self): + r"""__len__(YItemCollection self) -> std::vector< YItem * >::size_type""" + return _yui.YItemCollection___len__(self) + + def __getslice__(self, i, j): + r"""__getslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j) -> YItemCollection""" + return _yui.YItemCollection___getslice__(self, i, j) + + def __setslice__(self, *args): + r""" + __setslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j) + __setslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j, YItemCollection v) + """ + return _yui.YItemCollection___setslice__(self, *args) + + def __delslice__(self, i, j): + r"""__delslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j)""" + return _yui.YItemCollection___delslice__(self, i, j) + + def __delitem__(self, *args): + r""" + __delitem__(YItemCollection self, std::vector< YItem * >::difference_type i) + __delitem__(YItemCollection self, PySliceObject * slice) + """ + return _yui.YItemCollection___delitem__(self, *args) + + def __getitem__(self, *args): + r""" + __getitem__(YItemCollection self, PySliceObject * slice) -> YItemCollection + __getitem__(YItemCollection self, std::vector< YItem * >::difference_type i) -> YItem + """ + return _yui.YItemCollection___getitem__(self, *args) + + def __setitem__(self, *args): + r""" + __setitem__(YItemCollection self, PySliceObject * slice, YItemCollection v) + __setitem__(YItemCollection self, PySliceObject * slice) + __setitem__(YItemCollection self, std::vector< YItem * >::difference_type i, YItem x) + """ + return _yui.YItemCollection___setitem__(self, *args) + + def pop(self): + r"""pop(YItemCollection self) -> YItem""" + return _yui.YItemCollection_pop(self) + + def append(self, x): + r"""append(YItemCollection self, YItem x)""" + return _yui.YItemCollection_append(self, x) + + def empty(self): + r"""empty(YItemCollection self) -> bool""" + return _yui.YItemCollection_empty(self) + + def size(self): + r"""size(YItemCollection self) -> std::vector< YItem * >::size_type""" + return _yui.YItemCollection_size(self) + + def swap(self, v): + r"""swap(YItemCollection self, YItemCollection v)""" + return _yui.YItemCollection_swap(self, v) + + def begin(self): + r"""begin(YItemCollection self) -> std::vector< YItem * >::iterator""" + return _yui.YItemCollection_begin(self) + + def end(self): + r"""end(YItemCollection self) -> std::vector< YItem * >::iterator""" + return _yui.YItemCollection_end(self) + + def rbegin(self): + r"""rbegin(YItemCollection self) -> std::vector< YItem * >::reverse_iterator""" + return _yui.YItemCollection_rbegin(self) + + def rend(self): + r"""rend(YItemCollection self) -> std::vector< YItem * >::reverse_iterator""" + return _yui.YItemCollection_rend(self) + + def clear(self): + r"""clear(YItemCollection self)""" + return _yui.YItemCollection_clear(self) + + def get_allocator(self): + r"""get_allocator(YItemCollection self) -> std::vector< YItem * >::allocator_type""" + return _yui.YItemCollection_get_allocator(self) + + def pop_back(self): + r"""pop_back(YItemCollection self)""" + return _yui.YItemCollection_pop_back(self) + + def erase(self, *args): + r""" + erase(YItemCollection self, std::vector< YItem * >::iterator pos) -> std::vector< YItem * >::iterator + erase(YItemCollection self, std::vector< YItem * >::iterator first, std::vector< YItem * >::iterator last) -> std::vector< YItem * >::iterator + """ + return _yui.YItemCollection_erase(self, *args) + + def __init__(self, *args): + r""" + __init__(YItemCollection self) -> YItemCollection + __init__(YItemCollection self, YItemCollection other) -> YItemCollection + __init__(YItemCollection self, std::vector< YItem * >::size_type size) -> YItemCollection + __init__(YItemCollection self, std::vector< YItem * >::size_type size, YItem value) -> YItemCollection + """ + _yui.YItemCollection_swiginit(self, _yui.new_YItemCollection(*args)) + + def push_back(self, x): + r"""push_back(YItemCollection self, YItem x)""" + return _yui.YItemCollection_push_back(self, x) + + def front(self): + r"""front(YItemCollection self) -> YItem""" + return _yui.YItemCollection_front(self) + + def back(self): + r"""back(YItemCollection self) -> YItem""" + return _yui.YItemCollection_back(self) + + def assign(self, n, x): + r"""assign(YItemCollection self, std::vector< YItem * >::size_type n, YItem x)""" + return _yui.YItemCollection_assign(self, n, x) + + def resize(self, *args): + r""" + resize(YItemCollection self, std::vector< YItem * >::size_type new_size) + resize(YItemCollection self, std::vector< YItem * >::size_type new_size, YItem x) + """ + return _yui.YItemCollection_resize(self, *args) + + def insert(self, *args): + r""" + insert(YItemCollection self, std::vector< YItem * >::iterator pos, YItem x) -> std::vector< YItem * >::iterator + insert(YItemCollection self, std::vector< YItem * >::iterator pos, std::vector< YItem * >::size_type n, YItem x) + """ + return _yui.YItemCollection_insert(self, *args) + + def reserve(self, n): + r"""reserve(YItemCollection self, std::vector< YItem * >::size_type n)""" + return _yui.YItemCollection_reserve(self, n) + + def capacity(self): + r"""capacity(YItemCollection self) -> std::vector< YItem * >::size_type""" + return _yui.YItemCollection_capacity(self) + __swig_destroy__ = _yui.delete_YItemCollection + +# Register YItemCollection in _yui: +_yui.YItemCollection_swigregister(YItemCollection) + + +def toYWidgetEvent(event): + r"""toYWidgetEvent(YEvent event) -> YWidgetEvent""" + return _yui.toYWidgetEvent(event) + +def toYKeyEvent(event): + r"""toYKeyEvent(YEvent event) -> YKeyEvent""" + return _yui.toYKeyEvent(event) + +def toYMenuEvent(event): + r"""toYMenuEvent(YEvent event) -> YMenuEvent""" + return _yui.toYMenuEvent(event) + +def toYCancelEvent(event): + r"""toYCancelEvent(YEvent event) -> YCancelEvent""" + return _yui.toYCancelEvent(event) + +def toYDebugEvent(event): + r"""toYDebugEvent(YEvent event) -> YDebugEvent""" + return _yui.toYDebugEvent(event) + +def toYTimeoutEvent(event): + r"""toYTimeoutEvent(YEvent event) -> YTimeoutEvent""" + return _yui.toYTimeoutEvent(event) + +def toYTreeItem(item): + r"""toYTreeItem(YItem item) -> YTreeItem""" + return _yui.toYTreeItem(item) + +def toYTableItem(item): + r"""toYTableItem(YItem item) -> YTableItem""" + return _yui.toYTableItem(item) + +def toYItem(iter): + r"""toYItem(YItemIterator iter) -> YItem""" + return _yui.toYItem(iter) + +def toYTableCell(iter): + r"""toYTableCell(YTableCellIterator iter) -> YTableCell""" + return _yui.toYTableCell(iter) + +def incrYItemIterator(currentIterator): + r"""incrYItemIterator(YItemIterator currentIterator) -> YItemIterator""" + return _yui.incrYItemIterator(currentIterator) + +def beginYItemCollection(coll): + r"""beginYItemCollection(YItemCollection coll) -> YItemIterator""" + return _yui.beginYItemCollection(coll) + +def endYItemCollection(coll): + r"""endYItemCollection(YItemCollection coll) -> YItemIterator""" + return _yui.endYItemCollection(coll) + +def incrYTableCellIterator(currentIterator): + r"""incrYTableCellIterator(YTableCellIterator currentIterator) -> YTableCellIterator""" + return _yui.incrYTableCellIterator(currentIterator) + + diff --git a/test/test_combobox.py b/test/test_combobox.py new file mode 100644 index 0000000..3be6ad0 --- /dev/null +++ b/test/test_combobox.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_combobox(backend_name=None): + """Test ComboBox widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + if backend.value == 'ncurses': + print("\nNCurses ComboBox Instructions:") + print("1. Use TAB to navigate to ComboBox") + print("2. Press SPACE to expand dropdown") + print("3. Use UP/DOWN arrows to navigate") + print("4. Press ENTER to select") + print("5. Selected value should be displayed") + print("6. Press F10 or Q to quit") + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Create dialog focused on ComboBox testing + dialog = factory.createMainDialog() + vbox = factory.createVBox(dialog) + + factory.createHeading(vbox, "ComboBox Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + factory.createLabel(vbox, "Test selecting and displaying values") + + # Test ComboBox with initial selection + factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + combo = factory.createComboBox(hbox, "Choose fruit:", False) + combo.addItem("Apple") + combo.addItem("Banana") + combo.addItem("Orange") + combo.addItem("Grape") + combo.addItem("Mango") + + # Set initial value to test display + combo.setValue("Banana") + + factory.createLabel(hbox, " - ") + combo = factory.createComboBox(hbox, "Choose option:", False) + combo.addItem("Option 1") + combo.addItem("Option 2") + combo.addItem("Option 3") + + # Simple buttons + factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + ok_button = factory.createPushButton(hbox, "OK") + cancel_button = factory.createPushButton(hbox, "Cancel") + + print("\nOpening ComboBox test dialog...") + + # Store reference to check final value + dialog._test_combo = combo + + # Open dialog + dialog.open() + + # Show final result + print(f"\nFinal ComboBox value: '{dialog._test_combo.value()}'") + + except Exception as e: + print(f"Error testing ComboBox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_combobox(sys.argv[1]) + else: + test_combobox() diff --git a/test/test_multi_backend.py b/test/test_multi_backend.py new file mode 100644 index 0000000..f3d00f6 --- /dev/null +++ b/test/test_multi_backend.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + +import os +import sys +import time + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_backend(backend_name=None): + """Test a specific backend""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Create dialog + dialog = factory.createMainDialog() + vbox = factory.createVBox(dialog) + + # Add widgets - SAME LAYOUT FOR ALL BACKENDS + factory.createHeading(vbox, f"manatools AUI {backend.value.upper()} Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + + if backend.value == 'ncurses': + factory.createLabel(vbox, "ComboBox Test: Use SPACE to expand") + factory.createLabel(vbox, "Then use ARROWS and ENTER to select") + + # Input fields + input_field = factory.createInputField(vbox, "Username:") + + # ComboBox - NEW WIDGET + combo = factory.createComboBox(vbox, "Select option:", False) + combo.addItem("Option 1") + combo.addItem("Option 2") + combo.addItem("Option 3") + combo.addItem("Option 4") + combo.addItem("Option 5") + combo.addItem("Option 6") + + # Checkboxes + checkbox = factory.createCheckBox(vbox, "Enable features") + + # Buttons + hbox = factory.createHBox(vbox) + ok_button = factory.createPushButton(hbox, "OK") + cancel_button = factory.createPushButton(hbox, "Cancel") + + print("Opening dialog...") + + # Store references + dialog._test_combo = combo + + # Open dialog + dialog.open() + + # Show results after dialog closes + print(f"Dialog closed.") + if hasattr(dialog, '_test_combo'): + print(f"Combo value: '{dialog._test_combo.value()}'") + + except Exception as e: + print(f"Error with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +def test_all_backends(): + """Test all available backends""" + backends_to_test = [] + + # Check which backends are available + try: + import PyQt5.QtWidgets + backends_to_test.append('qt') + print("✓ Qt backend available") + except ImportError: + print("✗ Qt backend not available") + + try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk + backends_to_test.append('gtk') + print("✓ GTK backend available") + except (ImportError, ValueError) as e: + print(f"✗ GTK backend not available: {e}") + + try: + import curses + backends_to_test.append('ncurses') + print("✓ NCurses backend available") + except ImportError as e: + print(f"✗ NCurses backend not available: {e}") + + print(f"\nAvailable backends: {backends_to_test}") + + for backend in backends_to_test: + print(f"\n{'='*60}") + print(f"Testing {backend.upper()} backend") + print(f"{'='*60}") + + if backend == 'ncurses': + print("\nNCurses ComboBox: Use SPACE to expand, arrows to navigate, ENTER to select") + input("Press Enter to start...") + + test_backend(backend) + + if backend != 'ncurses' and backends_to_test.index(backend) < len(backends_to_test) - 1: + input("Press Enter to test next backend...") + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_backend(sys.argv[1]) + else: + test_all_backends() From 40d1b35f5280089328d063e94816b7d1f4db226a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 9 Nov 2025 18:10:49 +0100 Subject: [PATCH 002/523] Added YTimeOutEvent --- manatools/aui/yui_common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index ffc2a65..fca45f9 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -84,6 +84,7 @@ def serial(self): return self._serial class YWidgetEvent(YEvent): + """Event generated by widgets""" def __init__(self, widget=None, reason=YEventReason.Activated, event_type=YEventType.WidgetEvent): super().__init__(event_type, widget, reason) @@ -110,6 +111,11 @@ def item(self): def id(self): return self._id +class YTimeoutEvent(YEvent): + """Event generated on timeout""" + def __init__(self): + super().__init__() + class YCancelEvent(YEvent): def __init__(self): super().__init__(YEventType.CancelEvent) From 8d81da01de4fa4fabaaf11df6147072eff4fc035 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 9 Nov 2025 18:11:21 +0100 Subject: [PATCH 003/523] Implemented dialog waitForEvent --- manatools/aui/yui_qt.py | 92 +++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 3ccab46..0021808 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -105,22 +105,27 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._color_mode = color_mode self._is_open = False self._qwidget = None + self._event_result = None + self._qt_event_loop = None YDialogQt._open_dialogs.append(self) def widgetClass(self): return "YDialog" def open(self): - if not self._qwidget: - self._create_backend_widget() - - self._qwidget.show() - self._is_open = True - - # Start Qt event loop if not already running - app = QtWidgets.QApplication.instance() - if app: - app.exec_() + """ + Finalize and show the dialog in a non-blocking way. + + Matches libyui semantics: open() should only finalize and make visible. + If the application expects blocking behavior it should call waitForEvent() + which will start a nested event loop as required. + """ + if not self._is_open: + if not self._qwidget: + self._create_backend_widget() + + self._qwidget.show() + self._is_open = True def isOpen(self): return self._is_open @@ -165,8 +170,64 @@ def _create_backend_widget(self): self._qwidget.closeEvent = self._on_close_event def _on_close_event(self, event): + # Post a cancel event so waitForEvent returns a YCancelEvent when the user + # closes the window with the window manager 'X' button. + try: + self._post_event(YCancelEvent()) + except Exception: + pass + # Ensure dialog is destroyed and accept the close self.destroy() event.accept() + + def _post_event(self, event): + """Internal: post an event to this dialog and quit local event loop if running.""" + self._event_result = event + if self._qt_event_loop is not None and self._qt_event_loop.isRunning(): + self._qt_event_loop.quit() + + def waitForEvent(self, timeout_millisec=0): + """ + Ensure dialog is finalized/open, then run a nested Qt QEventLoop until an + event is posted or timeout occurs. Returns a YEvent (YWidgetEvent, YTimeoutEvent, ...). + + If the application called open() previously this will just block until an event. + If open() was not called, it will finalize and show the dialog here (so creation + followed by immediate waitForEvent behaves like libyui). + """ + # Ensure dialog is created and visible (finalize if needed) + if not self._qwidget: + self.open() + + # give Qt a chance to process pending show/layout events + app = QtWidgets.QApplication.instance() + if app: + app.processEvents() + + self._event_result = None + loop = QtCore.QEventLoop() + self._qt_event_loop = loop + + timer = None + if timeout_millisec and timeout_millisec > 0: + timer = QtCore.QTimer() + timer.setSingleShot(True) + def on_timeout(): + # post timeout event and quit + self._event_result = YTimeoutEvent() + if loop.isRunning(): + loop.quit() + timer.timeout.connect(on_timeout) + timer.start(timeout_millisec) + + loop.exec_() + + # cleanup + if timer and timer.isActive(): + timer.stop() + self._qt_event_loop = None + return self._event_result if self._event_result is not None else YEvent() + class YVBoxQt(YWidget): def __init__(self, parent=None): @@ -297,8 +358,15 @@ def _create_backend_widget(self): self._backend_widget.clicked.connect(self._on_clicked) def _on_clicked(self): - print(f"Button clicked: {self._label}") - # In a real implementation, this would send a YEvent + # Post a YWidgetEvent to the containing dialog (walk parents) + dlg = self._parent + while dlg is not None and not isinstance(dlg, YDialogQt): + dlg = dlg.parent() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + else: + # fallback logging for now + print(f"Button clicked (no dialog found): {self._label}") class YCheckBoxQt(YWidget): def __init__(self, parent=None, label="", is_checked=False): From e5fe1686947f8a8ad40263c13fd943329784968d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 9 Nov 2025 19:07:33 +0100 Subject: [PATCH 004/523] Implemented dialog waitForEvent --- manatools/aui/yui_gtk.py | 117 ++++++++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 13 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 018cc36..fb4e028 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -4,7 +4,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject +from gi.repository import Gtk, GObject, GLib import threading from .yui_common import * @@ -93,20 +93,22 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._color_mode = color_mode self._is_open = False self._window = None + self._event_result = None + self._glib_loop = None YDialogGtk._open_dialogs.append(self) def widgetClass(self): return "YDialog" def open(self): - if not self._window: - self._create_backend_widget() - - self._window.show_all() - self._is_open = True - - # Start GTK main loop - Gtk.main() + # Finalize and show the dialog in a non-blocking way. + # Matching libyui semantics: open() should finalize and make visible, + # but must NOT start a global blocking Gtk.main() here. + if not self._is_open: + if not self._window: + self._create_backend_widget() + self._window.show_all() + self._is_open = True def isOpen(self): return self._is_open @@ -121,9 +123,67 @@ def destroy(self, doThrow=True): # Stop GTK main loop if no dialogs left if not YDialogGtk._open_dialogs: - Gtk.main_quit() + try: + # Only quit the global Gtk main loop if it's actually running + if hasattr(Gtk, "main_level") and Gtk.main_level() > 0: + Gtk.main_quit() + except Exception: + # be defensive: do not raise from cleanup + pass return True - + + def _post_event(self, event): + """Internal: post an event to this dialog and quit local GLib.MainLoop if running.""" + self._event_result = event + if self._glib_loop is not None and self._glib_loop.is_running(): + try: + self._glib_loop.quit() + except Exception: + pass + + def waitForEvent(self, timeout_millisec=0): + """ + Run a GLib.MainLoop until an event is posted or timeout occurs. + Returns a YEvent (YWidgetEvent, YTimeoutEvent, ...). + """ + # Ensure dialog is finalized/open (finalize if caller didn't call open()). + if not self.isOpen(): + self.open() + + # Let GTK process pending events (show/layout) before entering nested loop. + while Gtk.events_pending(): + Gtk.main_iteration() + + self._event_result = None + self._glib_loop = GLib.MainLoop() + + def on_timeout(): + # post timeout event and quit loop + self._event_result = YTimeoutEvent() + try: + if self._glib_loop.is_running(): + self._glib_loop.quit() + except Exception: + pass + return False # don't repeat + + self._timeout_id = None + if timeout_millisec and timeout_millisec > 0: + self._timeout_id = GLib.timeout_add(timeout_millisec, on_timeout) + + # run nested loop + self._glib_loop.run() + + # cleanup + if self._timeout_id: + try: + GLib.source_remove(self._timeout_id) + except Exception: + pass + self._timeout_id = None + self._glib_loop = None + return self._event_result if self._event_result is not None else YEvent() + @classmethod def deleteTopmostDialog(cls, doThrow=True): if cls._open_dialogs: @@ -148,10 +208,34 @@ def _create_backend_widget(self): self._window.add(self._child.get_backend_widget()) self._backend_widget = self._window + # Connect to both "delete-event" (window manager close) and "destroy" + # so we can post a YCancelEvent and stop any nested wait loop. + self._window.connect("delete-event", self._on_delete_event) self._window.connect("destroy", self._on_destroy) def _on_destroy(self, widget): - self.destroy() + # normal widget destruction: ensure internal state cleaned + try: + # If no nested loop running, remove dialog and quit global loop if needed + self.destroy() + except Exception: + pass + + def _on_delete_event(self, widget, event): + # User clicked the window manager close (X) button: + # post a YCancelEvent so waitForEvent can return YCancelEvent. + try: + self._post_event(YCancelEvent()) + except Exception: + pass + # Destroy the window and stop further handling + try: + self.destroy() + except Exception: + pass + # Returning False allows the default handler to destroy the window; + # we already destroyed it, so return False to continue. + return False class YVBoxGtk(YWidget): def __init__(self, parent=None): @@ -279,7 +363,14 @@ def _create_backend_widget(self): self._backend_widget.connect("clicked", self._on_clicked) def _on_clicked(self, button): - print(f"Button clicked: {self._label}") + # Post a YWidgetEvent to the containing dialog (walk parents) + dlg = self._parent + while dlg is not None and not isinstance(dlg, YDialogGtk): + dlg = dlg.parent() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + else: + print(f"Button clicked (no dialog found): {self._label}") class YCheckBoxGtk(YWidget): def __init__(self, parent=None, label="", is_checked=False): From c247a0ed6746deaac3184490ac7d73cece234759 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 9 Nov 2025 19:07:53 +0100 Subject: [PATCH 005/523] Implemented dialog waitForEvent --- manatools/aui/yui_curses.py | 177 ++++++++++++++++++++++++------------ 1 file changed, 118 insertions(+), 59 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 39df57d..4c9f843 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -137,6 +137,7 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._focused_widget = None self._last_draw_time = 0 self._draw_interval = 0.1 # seconds + self._event_result = None YDialogCurses._open_dialogs.append(self) def widgetClass(self): @@ -155,7 +156,9 @@ def open(self): self._focused_widget = focusable[0] self._focused_widget._focused = True - self._run_event_loop() + # open() must be non-blocking (finalize and show). Event loop is + # started by waitForEvent() to match libyui semantics. + return True def isOpen(self): return self._is_open @@ -188,60 +191,10 @@ def _create_backend_widget(self): self._backend_widget = curses.newwin(0, 0, 0, 0) def _run_event_loop(self): - from manatools.aui.yui import YUI - ui = YUI.ui() - - while self._is_open: - try: - # Draw only if needed (throttle redraws) - current_time = time.time() - if current_time - self._last_draw_time >= self._draw_interval: - self._draw_dialog() - self._last_draw_time = current_time - - # Non-blocking input - ui._stdscr.nodelay(True) - key = ui._stdscr.getch() - - if key == -1: - time.sleep(0.01) # Small sleep to prevent CPU spinning - continue - - # Handle global keys - if key == curses.KEY_F10 or key == ord('q') or key == ord('Q'): - print("Quit requested") - break - elif key == curses.KEY_RESIZE: - # Handle terminal resize - force redraw - self._last_draw_time = 0 - continue - - # Handle tab navigation - if key == ord('\t'): - self._cycle_focus(forward=True) - self._last_draw_time = 0 # Force redraw - continue - elif key == curses.KEY_BTAB: # Shift+Tab - self._cycle_focus(forward=False) - self._last_draw_time = 0 # Force redraw - continue - - # Send key event to focused widget - if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): - handled = self._focused_widget._handle_key(key) - if handled: - self._last_draw_time = 0 # Force redraw - - except KeyboardInterrupt: - print("Keyboard interrupt") - break - except Exception as e: - # Don't crash on curses errors - print(f"Curses error: {e}") - time.sleep(0.1) - - self.destroy() - print("NCurses dialog closed") + # Backwards-compatible helper: run an indefinite loop until dialog closed. + # Implemented via waitForEvent with no timeout (block until an event that + # may close/destroy the dialog). + self.waitForEvent(timeout_millisec=0) def _draw_dialog(self): """Draw the entire dialog (called by event loop)""" @@ -343,6 +296,105 @@ def find_in_widget(widget): return focusable + + def _post_event(self, event): + """Post an event to this dialog; waitForEvent will return it.""" + self._event_result = event + # If dialog is not open anymore, ensure cleanup + if isinstance(event, YCancelEvent): + # Mark closed so loop can clean up + self._is_open = False + + def waitForEvent(self, timeout_millisec=0): + """ + Run the ncurses event loop until an event is posted or timeout occurs. + timeout_millisec == 0 -> block indefinitely until an event (no timeout). + Returns a YEvent (YWidgetEvent, YTimeoutEvent, YCancelEvent, ...). + """ + from manatools.aui.yui import YUI + ui = YUI.ui() + + # Ensure dialog is open/finalized + if not self._is_open: + self.open() + + self._event_result = None + deadline = None + if timeout_millisec and timeout_millisec > 0: + deadline = time.time() + (timeout_millisec / 1000.0) + + # Main nested loop: iterate until event posted or timeout + while self._is_open and self._event_result is None: + try: + # Draw only if needed (throttle redraws) + current_time = time.time() + if current_time - self._last_draw_time >= self._draw_interval: + self._draw_dialog() + self._last_draw_time = current_time + + # Non-blocking input + ui._stdscr.nodelay(True) + key = ui._stdscr.getch() + + if key == -1: + # no input; check timeout + if deadline and time.time() >= deadline: + self._event_result = YTimeoutEvent() + break + time.sleep(0.01) + continue + + # Handle global keys + if key == curses.KEY_F10 or key == ord('q') or key == ord('Q'): + # Post cancel event + self._post_event(YCancelEvent()) + break + elif key == curses.KEY_RESIZE: + # Handle terminal resize - force redraw + self._last_draw_time = 0 + continue + + # Handle tab navigation + if key == ord('\t'): + self._cycle_focus(forward=True) + self._last_draw_time = 0 # Force redraw + continue + elif key == curses.KEY_BTAB: # Shift+Tab + self._cycle_focus(forward=False) + self._last_draw_time = 0 # Force redraw + continue + + # Send key event to focused widget + if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): + handled = self._focused_widget._handle_key(key) + if handled: + self._last_draw_time = 0 # Force redraw + + except KeyboardInterrupt: + # treat as cancel + self._post_event(YCancelEvent()) + break + except Exception as e: + # Don't crash on curses errors + # keep running unless fatal + time.sleep(0.1) + + # If dialog was closed without explicit event, produce CancelEvent + if self._event_result is None: + if not self._is_open: + self._event_result = YCancelEvent() + elif deadline and time.time() >= deadline: + self._event_result = YTimeoutEvent() + + # cleanup if dialog closed + if not self._is_open: + try: + self.destroy() + except Exception: + pass + + return self._event_result if self._event_result is not None else YEvent() + class YVBoxCurses(YWidget): def __init__(self, parent=None): super().__init__(parent) @@ -586,10 +638,17 @@ def _handle_key(self, key): return False if key == ord('\n') or key == ord(' '): - # Button pressed - print(f"Button '{self._label}' pressed") - return True - + # Button pressed -> post widget event to containing dialog + dlg = getattr(self, '_parent', None) + # walk up parents until dialog found + while dlg is not None and not isinstance(dlg, YDialogCurses): + dlg = getattr(dlg, '_parent', None) + if dlg is not None: + try: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + pass + return True return False class YCheckBoxCurses(YWidget): From 5ca87444c5d758dd5ef45880f02af0cbdda0e513 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 9 Nov 2025 19:08:26 +0100 Subject: [PATCH 006/523] Added test hello world and combobx --- test/test_combobox.py | 13 ++++++++--- test/test_hello_world.py | 48 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 test/test_hello_world.py diff --git a/test/test_combobox.py b/test/test_combobox.py index 3be6ad0..50ac270 100644 --- a/test/test_combobox.py +++ b/test/test_combobox.py @@ -15,7 +15,8 @@ def test_combobox(backend_name=None): print("Using auto-detection") try: - from manatools.aui.yui import YUI, YUI_ui + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui # Force re-detection YUI._instance = None @@ -74,8 +75,14 @@ def test_combobox(backend_name=None): # Store reference to check final value dialog._test_combo = combo - # Open dialog - dialog.open() + while True: + event = dialog.waitForEvent() + if event.eventType() == yui.YEventType.CancelEvent: + dialog.destroy() + break + if event.widget() == cancel_button: + dialog.destroy() + break # Show final result print(f"\nFinal ComboBox value: '{dialog._test_combo.value()}'") diff --git a/test/test_hello_world.py b/test/test_hello_world.py new file mode 100644 index 0000000..fda5289 --- /dev/null +++ b/test/test_hello_world.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_hello_world(backend_name=None): + """Test simple dialog with hello world""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox( dialog ) + factory.createLabel( vbox, "Hello, World!" ) + factory.createPushButton( vbox, "OK" ) + dialog.open() + event = dialog.waitForEvent() + dialog.destroy() + + + except Exception as e: + print(f"Error testing ComboBox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_hello_world(sys.argv[1]) + else: + test_hello_world() From 14b1b5e8ce85b1046f5348861452bc5a3fcd1f00 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 9 Nov 2025 20:22:03 +0100 Subject: [PATCH 007/523] Implemented YWidget findDialog --- manatools/aui/yui_common.py | 7 +++++++ manatools/aui/yui_curses.py | 5 +---- manatools/aui/yui_gtk.py | 4 +--- manatools/aui/yui_qt.py | 4 +--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index fca45f9..8810be0 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -189,6 +189,13 @@ def parent(self): def hasParent(self): return self._parent is not None + def findDialog(self): + """Find the parent dialog of this widget.""" + parent = getattr(self, '_parent', None) + while parent is not None and parent.widgetClass() != 'YDialog': + parent = getattr(parent, '_parent', None) + return parent + def setEnabled(self, enabled=True): self._enabled = enabled if self._backend_widget: diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 4c9f843..11010b5 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -639,10 +639,7 @@ def _handle_key(self, key): if key == ord('\n') or key == ord(' '): # Button pressed -> post widget event to containing dialog - dlg = getattr(self, '_parent', None) - # walk up parents until dialog found - while dlg is not None and not isinstance(dlg, YDialogCurses): - dlg = getattr(dlg, '_parent', None) + dlg = self.findDialog() if dlg is not None: try: dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index fb4e028..9cee4e0 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -364,9 +364,7 @@ def _create_backend_widget(self): def _on_clicked(self, button): # Post a YWidgetEvent to the containing dialog (walk parents) - dlg = self._parent - while dlg is not None and not isinstance(dlg, YDialogGtk): - dlg = dlg.parent() + dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) else: diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 0021808..697302c 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -359,9 +359,7 @@ def _create_backend_widget(self): def _on_clicked(self): # Post a YWidgetEvent to the containing dialog (walk parents) - dlg = self._parent - while dlg is not None and not isinstance(dlg, YDialogQt): - dlg = dlg.parent() + dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) else: From 3a682119081cdbf83f205708b4558b9879037e95 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 19:27:54 +0100 Subject: [PATCH 008/523] Fixed ncurses combobox item selection --- manatools/aui/yui_curses.py | 42 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 11010b5..3b3b9d2 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -840,18 +840,9 @@ def _handle_key(self, key): handled = True - if key == ord('\n') or key == ord(' '): - # Toggle expanded state - self._expanded = not self._expanded - if self._expanded and self._items: - # Set hover index to current value if exists - self._hover_index = 0 - if self._value: - for i, item in enumerate(self._items): - if item.label() == self._value: - self._hover_index = i - break - elif self._expanded: + # If currently expanded, give expanded-list handling priority so Enter + # selects the hovered item instead of simply toggling expansion. + if self._expanded: # Handle navigation in expanded list if key == curses.KEY_UP: if self._hover_index > 0: @@ -863,13 +854,36 @@ def _handle_key(self, key): # Select hovered item if self._items and 0 <= self._hover_index < len(self._items): selected_item = self._items[self._hover_index] - self.setValue(selected_item.label()) # Use setValue to update display + self.setValue(selected_item.label()) # update internal value/selection self._expanded = False + # force parent dialog redraw if present + dlg = self.findDialog() + if dlg is not None: + try: + # notify dialog to redraw immediately + dlg._last_draw_time = 0 + # post a widget event for selection change + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + # selection made -> handled elif key == 27: # ESC key self._expanded = False else: handled = False else: - handled = False + # Not expanded: Enter/Space expands the list + if key == ord('\n') or key == ord(' '): + self._expanded = not self._expanded + if self._expanded and self._items: + # Set hover index to current value if exists + self._hover_index = 0 + if self._value: + for i, item in enumerate(self._items): + if item.label() == self._value: + self._hover_index = i + break + else: + handled = False return handled From 0bc7849d3462bd41ea4e4888a4c092446ae05b1f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 19:28:17 +0100 Subject: [PATCH 009/523] Added information on what is selected --- test/test_combobox.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/test/test_combobox.py b/test/test_combobox.py index 50ac270..24a4f2b 100644 --- a/test/test_combobox.py +++ b/test/test_combobox.py @@ -59,13 +59,13 @@ def test_combobox(backend_name=None): combo.setValue("Banana") factory.createLabel(hbox, " - ") - combo = factory.createComboBox(hbox, "Choose option:", False) - combo.addItem("Option 1") - combo.addItem("Option 2") - combo.addItem("Option 3") + combo1 = factory.createComboBox(hbox, "Choose option:", False) + combo1.addItem("Option 1") + combo1.addItem("Option 2") + combo1.addItem("Option 3") # Simple buttons - factory.createLabel(vbox, "") + selected = factory.createLabel(vbox, "") hbox = factory.createHBox(vbox) ok_button = factory.createPushButton(hbox, "OK") cancel_button = factory.createPushButton(hbox, "Cancel") @@ -73,19 +73,28 @@ def test_combobox(backend_name=None): print("\nOpening ComboBox test dialog...") # Store reference to check final value - dialog._test_combo = combo + dialog._foo = combo while True: event = dialog.waitForEvent() - if event.eventType() == yui.YEventType.CancelEvent: - dialog.destroy() - break - if event.widget() == cancel_button: - dialog.destroy() - break + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == cancel_button: + dialog.destroy() + break + elif wdg == combo: + selected.setText(f"Selected: '{combo.value()}'") + elif wdg == combo1: + selected.setText(f"Selected: '{combo1.value()}'") + elif wdg == ok_button: + selected.setText(f"OK clicked.") # Show final result - print(f"\nFinal ComboBox value: '{dialog._test_combo.value()}'") + print(f"\nFinal ComboBox value: '{combo.value()}' {combo1.value()}") except Exception as e: print(f"Error testing ComboBox with backend {backend_name}: {e}") From 9a4353cff61bc028ddd02dbde3069b0daebd28be Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 19:38:03 +0100 Subject: [PATCH 010/523] added qt combobox selection event --- manatools/aui/yui_qt.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 697302c..4ec1abe 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -401,6 +401,7 @@ def __init__(self, parent=None, label="", editable=False): self._label = label self._editable = editable self._value = "" + self._selected_items = [] def widgetClass(self): return "YComboBox" @@ -416,6 +417,12 @@ def setValue(self, text): self._combo_widget.setCurrentIndex(index) elif self._editable: self._combo_widget.setEditText(text) + # update selected_items to keep internal state consistent + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break def editable(self): return self._editable @@ -440,6 +447,8 @@ def _create_backend_widget(self): combo.addItem(item.label()) combo.currentTextChanged.connect(self._on_text_changed) + # also handle index change (safer for some input methods) + combo.currentIndexChanged.connect(lambda idx: self._on_text_changed(combo.currentText())) layout.addWidget(combo) self._backend_widget = container @@ -453,3 +462,10 @@ def _on_text_changed(self, text): if item.label() == text: self._selected_items.append(item) break + # Post selection-changed event to containing dialog + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass \ No newline at end of file From f8cd643bbc7bdef7271bc9643c0e7cd7407f9a0a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 20:01:54 +0100 Subject: [PATCH 011/523] Fixed gtk combobox item changed event --- manatools/aui/yui_gtk.py | 109 ++++++++++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 18 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 9cee4e0..0e5947c 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -405,6 +405,7 @@ def __init__(self, parent=None, label="", editable=False): self._label = label self._editable = editable self._value = "" + self._selected_items = [] def widgetClass(self): return "YComboBox" @@ -413,54 +414,126 @@ def value(self): return self._value def setValue(self, text): + # Always update internal value self._value = text + # If backend combo already exists, update it immediately if hasattr(self, '_combo_widget') and self._combo_widget: - if self._editable: - self._combo_widget.get_child().set_text(text) - else: - # Find and select the item - for i, item in enumerate(self._items): + try: + if self._editable: + # For editable ComboBoxText with entry + entry = self._combo_widget.get_child() + if entry: + entry.set_text(text) + else: + # Find and select the item + for i, item in enumerate(self._items): + if item.label() == text: + self._combo_widget.set_active(i) + break + # Update selected_items to reflect new value + self._selected_items = [] + for item in self._items: if item.label() == text: - self._combo_widget.set_active(i) + self._selected_items.append(item) break - + except Exception: + # be defensive if widget not fully initialized + pass + def editable(self): return self._editable def _create_backend_widget(self): hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - + if self._label: label = Gtk.Label(label=self._label) label.set_xalign(0.0) hbox.pack_start(label, False, False, 0) - + if self._editable: # Create a ComboBoxText that is editable combo = Gtk.ComboBoxText.new_with_entry() - combo.get_child().connect("changed", self._on_text_changed) + entry = combo.get_child() + if entry: + entry.connect("changed", self._on_text_changed) else: combo = Gtk.ComboBoxText() combo.connect("changed", self._on_changed) - + # Add items to combo box for item in self._items: combo.append_text(item.label()) - + + # If a value was set prior to widget creation, apply it now + if self._value: + try: + if self._editable: + entry = combo.get_child() + if entry: + entry.set_text(self._value) + else: + for i, item in enumerate(self._items): + if item.label() == self._value: + combo.set_active(i) + break + # update selected_items + self._selected_items = [] + for item in self._items: + if item.label() == self._value: + self._selected_items.append(item) + break + except Exception: + pass + hbox.pack_start(combo, True, True, 0) self._backend_widget = hbox self._combo_widget = combo - + def _on_text_changed(self, entry): - self._value = entry.get_text() - + # editable combo: update value and notify dialog + try: + text = entry.get_text() + except Exception: + text = "" + self._value = text + # update selected items (may be none for free text) + self._selected_items = [] + for item in self._items: + if item.label() == self._value: + self._selected_items.append(item) + break + # Post selection-changed event + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + def _on_changed(self, combo): - active_id = combo.get_active() - if active_id >= 0: - self._value = combo.get_active_text() + # non-editable combo: selection changed via index + try: + active_id = combo.get_active() + if active_id >= 0: + val = combo.get_active_text() + else: + val = "" + except Exception: + val = "" + + if val: + self._value = val # Update selected items self._selected_items = [] for item in self._items: if item.label() == self._value: self._selected_items.append(item) break + # Post selection-changed event to containing dialog + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass From 9d8e6570693d967dfb856b98c50bece6f2ab8597 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 20:02:12 +0100 Subject: [PATCH 012/523] fixed main loop --- test/test_multi_backend.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/test/test_multi_backend.py b/test/test_multi_backend.py index f3d00f6..c27b4e8 100644 --- a/test/test_multi_backend.py +++ b/test/test_multi_backend.py @@ -16,7 +16,8 @@ def test_backend(backend_name=None): print("Using auto-detection") try: - from manatools.aui.yui import YUI, YUI_ui + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui # Force re-detection YUI._instance = None @@ -56,22 +57,32 @@ def test_backend(backend_name=None): checkbox = factory.createCheckBox(vbox, "Enable features") # Buttons + selected = factory.createLabel(vbox, "") hbox = factory.createHBox(vbox) ok_button = factory.createPushButton(hbox, "OK") cancel_button = factory.createPushButton(hbox, "Cancel") print("Opening dialog...") - # Store references - dialog._test_combo = combo - - # Open dialog - dialog.open() + while True: + event = dialog.waitForEvent() + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == cancel_button: + dialog.destroy() + break + elif wdg == combo: + selected.setText(f"Selected: '{combo.value()}'") + elif wdg == ok_button: + selected.setText(f"OK clicked.") + # Show results after dialog closes print(f"Dialog closed.") - if hasattr(dialog, '_test_combo'): - print(f"Combo value: '{dialog._test_combo.value()}'") except Exception as e: print(f"Error with backend {backend_name}: {e}") From 040f23ce16f3ff8646f9f2d8b35573ab0ed17b7d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 20:13:32 +0100 Subject: [PATCH 013/523] sent YWidgetEvent when checkbox is changed --- manatools/aui/yui_gtk.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 0e5947c..ab5eb31 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -396,8 +396,15 @@ def _create_backend_widget(self): self._backend_widget.connect("toggled", self._on_toggled) def _on_toggled(self, button): + # Update internal state self._is_checked = button.get_active() - print(f"Checkbox toggled: {self._label} = {self._is_checked}") + + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"Checkbox toggled (no dialog found): {self._label} = {self._is_checked}") class YComboBoxGtk(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): From f9542f7b7b4ce96b470b60e477659fb59ce064d9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 20:14:12 +0100 Subject: [PATCH 014/523] manage checkbox and imput field --- test/test_multi_backend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_multi_backend.py b/test/test_multi_backend.py index c27b4e8..22e704a 100644 --- a/test/test_multi_backend.py +++ b/test/test_multi_backend.py @@ -78,8 +78,9 @@ def test_backend(backend_name=None): elif wdg == combo: selected.setText(f"Selected: '{combo.value()}'") elif wdg == ok_button: - selected.setText(f"OK clicked.") - + selected.setText(f"OK clicked. - {input_field.value()}") + elif wdg == checkbox: + selected.setText(f"{checkbox.label()} - {checkbox.value()}") # Show results after dialog closes print(f"Dialog closed.") From d90e0b985d1379d0fd150c4859af716e598e9eaf Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 20:17:49 +0100 Subject: [PATCH 015/523] added qt checkbox change value event --- manatools/aui/yui_qt.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 4ec1abe..dad400a 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -392,8 +392,16 @@ def _create_backend_widget(self): self._backend_widget.stateChanged.connect(self._on_state_changed) def _on_state_changed(self, state): + # Update internal state + # state is QtCore.Qt.CheckState (Unchecked=0, PartiallyChecked=1, Checked=2) self._is_checked = (state == QtCore.Qt.Checked) - print(f"Checkbox toggled: {self._label} = {self._is_checked}") + + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"CheckBox state changed (no dialog found): {self._label} = {self._is_checked}") class YComboBoxQt(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): From b3cf7cbdda3b9b62d2e37df058e3ab4e8136393b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 20:21:34 +0100 Subject: [PATCH 016/523] added ncurses check box changed value event --- manatools/aui/yui_curses.py | 44 +++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 3b3b9d2..fd4c6c3 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -670,27 +670,49 @@ def label(self): return self._label def _create_backend_widget(self): - self._backend_widget = None + # In curses, there's no actual backend widget, just internal state + pass def _draw(self, window, y, x, width, height): + """Draw the checkbox with its label""" try: - checkbox = "[X]" if self._is_checked else "[ ]" - text = f"{checkbox} {self._label}" + # Draw checkbox symbol: [X] or [ ] + checkbox_symbol = "[X]" if self._is_checked else "[ ]" + text = f"{checkbox_symbol} {self._label}" - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - window.addstr(y, x, text, attr) + # Truncate if too wide + if len(text) > width: + text = text[:width-3] + "..." + + # Draw with highlighting if focused + if self._focused: + window.attron(curses.A_REVERSE) + + window.addstr(y, x, text) + + if self._focused: + window.attroff(curses.A_REVERSE) except curses.error: pass def _handle_key(self, key): - if not self._focused: - return False - - if key == ord(' ') or key == ord('\n'): - self._is_checked = not self._is_checked + """Handle keyboard input for checkbox (Space to toggle)""" + # Space or Enter to toggle + if key in (ord(' '), ord('\n'), curses.KEY_ENTER): + self._toggle() return True - return False + + def _toggle(self): + """Toggle checkbox state and post event""" + self._is_checked = not self._is_checked + + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"CheckBox toggled (no dialog found): {self._label} = {self._is_checked}") class YComboBoxCurses(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): From 462d0a38115b2994b6e294622eae583eaaa6cbc9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 22:59:50 +0100 Subject: [PATCH 017/523] removed unused code --- test/test_combobox.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_combobox.py b/test/test_combobox.py index 24a4f2b..cc1a6a1 100644 --- a/test/test_combobox.py +++ b/test/test_combobox.py @@ -71,10 +71,7 @@ def test_combobox(backend_name=None): cancel_button = factory.createPushButton(hbox, "Cancel") print("\nOpening ComboBox test dialog...") - - # Store reference to check final value - dialog._foo = combo - + while True: event = dialog.waitForEvent() typ = event.eventType() From 14abc81ca210fc862580c11f3076c186eab3db77 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 10 Nov 2025 23:00:03 +0100 Subject: [PATCH 018/523] added Application Title e Icon to be managed yet --- manatools/aui/yui_curses.py | 21 +++++++++++++++++++-- manatools/aui/yui_gtk.py | 25 ++++++++++++++++++++++--- manatools/aui/yui_qt.py | 24 +++++++++++++++++++++--- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index fd4c6c3..92d8930 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -76,9 +76,10 @@ def yApp(self): class YApplicationCurses: def __init__(self): + self._application_title = "manatools Curses Application" + self._product_name = "manatools YUI Curses" self._icon_base_path = "" - self._product_name = "YUI Curses" - + def iconBasePath(self): return self._icon_base_path @@ -91,6 +92,22 @@ def setProductName(self, product_name): def productName(self): return self._product_name + def setApplicationTitle(self, title): + """Set the application title.""" + self._application_title = title + + def applicationTitle(self): + """Get the application title.""" + return self._application_title + + def setApplicationIcon(self, Icon): + """Set the application title.""" + self._icon = Icon + + def applicationIcon(self): + """Get the application title.""" + return self.__icon + class YWidgetFactoryCurses: def __init__(self): pass diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index ab5eb31..9472bf0 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -31,9 +31,11 @@ def yApp(self): class YApplicationGtk: def __init__(self): - self._icon_base_path = "/usr/share/icons" - self._product_name = "YUI GTK" - + self._application_title = "manatools GTK Application" + self._product_name = "manatools YUI GTK" + self._icon_base_path = None + self._icon = "" + def iconBasePath(self): return self._icon_base_path @@ -45,6 +47,23 @@ def setProductName(self, product_name): def productName(self): return self._product_name + + def setApplicationTitle(self, title): + """Set the application title.""" + self._application_title = title + + def applicationTitle(self): + """Get the application title.""" + return self._application_title + + def setApplicationIcon(self, Icon): + """Set the application title.""" + self._icon = Icon + + def applicationIcon(self): + """Get the application title.""" + return self.__icon + class YWidgetFactoryGtk: def __init__(self): diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index dad400a..1f11603 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -34,9 +34,11 @@ def yApp(self): class YApplicationQt: def __init__(self): - self._icon_base_path = "/usr/share/icons" - self._product_name = "YUI Qt" - + self._application_title = "manatools Qt Application" + self._product_name = "manatools YUI Qt" + self._icon_base_path = None + self._icon = "" + def iconBasePath(self): return self._icon_base_path @@ -48,6 +50,22 @@ def setProductName(self, product_name): def productName(self): return self._product_name + + def setApplicationTitle(self, title): + """Set the application title.""" + self._application_title = title + + def applicationTitle(self): + """Get the application title.""" + return self._application_title + + def setApplicationIcon(self, Icon): + """Set the application title.""" + self._icon = Icon + + def applicationIcon(self): + """Get the application title.""" + return self.__icon class YWidgetFactoryQt: def __init__(self): From cd0ee6f63d351304702b6f5413d19335f29564bc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 11 Nov 2025 20:55:28 +0100 Subject: [PATCH 019/523] Added Qt YSelectionBox --- manatools/aui/yui_qt.py | 99 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 1f11603..e1170f8 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -494,4 +494,101 @@ def _on_text_changed(self, text): if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) except Exception: - pass \ No newline at end of file + pass + +class YSelectionBoxQt(YSelectionWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + self._selected_items = [] + + def widgetClass(self): + return "YSelectionBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_list_widget') and self._list_widget: + # Find and select the item with matching text + for i in range(self._list_widget.count()): + item = self._list_widget.item(i) + if item.text() == text: + self._list_widget.setCurrentItem(item) + break + # Update selected_items to keep internal state consistent + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break + + def label(self): + return self._label + + def selectedItems(self): + """Get list of selected items""" + return self._selected_items + + def selectItem(self, item, selected=True): + """Select or deselect a specific item""" + if hasattr(self, '_list_widget') and self._list_widget: + for i in range(self._list_widget.count()): + list_item = self._list_widget.item(i) + if list_item.text() == item.label(): + if selected: + self._list_widget.setCurrentItem(list_item) + if item not in self._selected_items: + self._selected_items.append(item) + else: + if item in self._selected_items: + self._selected_items.remove(item) + break + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + + if self._label: + label = QtWidgets.QLabel(self._label) + layout.addWidget(label) + + list_widget = QtWidgets.QListWidget() + list_widget.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + + # Add items to list widget + for item in self._items: + list_widget.addItem(item.label()) + + list_widget.itemSelectionChanged.connect(self._on_selection_changed) + layout.addWidget(list_widget) + + self._backend_widget = container + self._list_widget = list_widget + + def _on_selection_changed(self): + """Handle selection change in the list widget""" + if hasattr(self, '_list_widget') and self._list_widget: + # Update selected items + self._selected_items = [] + selected_indices = [index.row() for index in self._list_widget.selectedIndexes()] + + for idx in selected_indices: + if idx < len(self._items): + self._selected_items.append(self._items[idx]) + + # Update value to first selected item + if self._selected_items: + self._value = self._selected_items[0].label() + + # Post selection-changed event to containing dialog + try: + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass \ No newline at end of file From 308c619de3f8ca950d15b545a612a87bd5a27cbd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 11 Nov 2025 20:55:50 +0100 Subject: [PATCH 020/523] Added a YSelctionBox test case --- test/test_selctionbox.py | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 test/test_selctionbox.py diff --git a/test/test_selctionbox.py b/test/test_selctionbox.py new file mode 100644 index 0000000..4c4b7e8 --- /dev/null +++ b/test/test_selctionbox.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_selectionbox(backend_name=None): + """Test ComboBox widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + + +############### + dialog = factory.createPopupDialog() + vbox = factory.createVBox( dialog ) + hbox = factory.createHBox( vbox ) + selBox = factory.createSelectionBox( hbox, "Menu" ) + + selBox.addItem( "Pizza Margherita" ) + selBox.addItem( "Pizza Capricciosa" ) + selBox.addItem( "Pizza Funghi" ) + selBox.addItem( "Pizza Prosciutto" ) + selBox.addItem( "Pizza Quattro Stagioni" ) + selBox.addItem( "Calzone" ) + + checkBox = factory.createCheckBox( hbox, "Notify on change", selBox.notify() ) + + hbox = factory.createHBox( vbox ) + factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) + valueField = factory.createLabel(hbox, "") + #valueField.setStretchable( yui.YD_HORIZ, True ) # // allow stretching over entire dialog width + + valueButton = factory.createPushButton( hbox, "Value" ) + #factory.createVSpacing( vbox, 0.3 ) + + #rightAlignment = factory.createRight( vbox ) TODO + closeButton = factory.createPushButton( vbox, "Close" ) + + # + # Event loop + # + valueField.setText( "???" ) + while True: + event = dialog.waitForEvent() + if not event: + print("Empty") + next + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == closeButton: + dialog.destroy() + break + elif (wdg == valueButton): + item = selBox.selectedItem() + valueField.setText( item.label() if item else "" ) + elif (wdg == checkBox): + selBox.setNotify( checkBox.value() ) + elif (wdg == selBox): # selBox will only send events with setNotify() TODO + valueField.setText(selBox.value()) + + except Exception as e: + print(f"Error testing ComboBox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_selectionbox(sys.argv[1]) + else: + test_selectionbox() + + + + From 0f8348e8b897c4f7b6b3d08917f07276ddec38d5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 11 Nov 2025 21:03:32 +0100 Subject: [PATCH 021/523] Added single/multi selection management --- manatools/aui/yui_qt.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index e1170f8..fa59d9a 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -502,6 +502,7 @@ def __init__(self, parent=None, label=""): self._label = label self._value = "" self._selected_items = [] + self._multi_selection = False def widgetClass(self): return "YSelectionBox" @@ -546,6 +547,26 @@ def selectItem(self, item, selected=True): if item in self._selected_items: self._selected_items.remove(item) break + + def setMultiSelection(self, enabled): + """Enable or disable multi-selection.""" + self._multi_selection = bool(enabled) + if hasattr(self, '_list_widget') and self._list_widget: + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi_selection else QtWidgets.QAbstractItemView.SingleSelection + self._list_widget.setSelectionMode(mode) + # if disabling multi-selection, collapse to the first selected item + if not self._multi_selection: + selected = self._list_widget.selectedItems() + if len(selected) > 1: + first = selected[0] + self._list_widget.clearSelection() + first.setSelected(True) + # update internal state to reflect change + self._on_selection_changed() + + def multiSelection(self): + """Return whether multi-selection is enabled.""" + return bool(self._multi_selection) def _create_backend_widget(self): container = QtWidgets.QWidget() @@ -557,7 +578,8 @@ def _create_backend_widget(self): layout.addWidget(label) list_widget = QtWidgets.QListWidget() - list_widget.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi_selection else QtWidgets.QAbstractItemView.SingleSelection + list_widget.setSelectionMode(mode) # Add items to list widget for item in self._items: From 4d912c18926fe3b98210e609eacdffbce3cf1378 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 11 Nov 2025 21:14:29 +0100 Subject: [PATCH 022/523] Added YSelectionBox for Gtk --- manatools/aui/yui_gtk.py | 175 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 9472bf0..57b16fa 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -101,6 +101,9 @@ def createPasswordField(self, parent, label): def createComboBox(self, parent, label, editable=False): return YComboBoxGtk(parent, label, editable) + + def createSelectionBox(self, parent, label): + return YSelectionBoxGtk(parent, label) # GTK Widget Implementations class YDialogGtk(YSingleChildContainerWidget): @@ -563,3 +566,175 @@ def _on_changed(self, combo): dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) except Exception: pass + +class YSelectionBoxGtk(YSelectionWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + self._selected_items = [] + self._multi_selection = False + self._treeview = None + self._liststore = None + self._backend_widget = None + + def widgetClass(self): + return "YSelectionBox" + + def label(self): + return self._label + + def value(self): + return self._value + + def setValue(self, text): + """Select first item matching text.""" + self._value = text + # Update internal selected_items + self._selected_items = [it for it in self._items if it.label() == text] + if self._treeview is None: + return + # Select matching row in the TreeView + sel = self._treeview.get_selection() + sel.unselect_all() + for i, it in enumerate(self._items): + if it.label() == text: + sel.select_path(Gtk.TreePath.new_from_string(str(i))) + break + # notify via handler + self._on_selection_changed(sel) + + def selectedItems(self): + return list(self._selected_items) + + def selectItem(self, item, selected=True): + """Programmatically select/deselect a specific item.""" + # Update internal state even if widget not yet created + if selected: + if not self._multi_selection: + self._selected_items = [item] + self._value = item.label() + else: + if item not in self._selected_items: + self._selected_items.append(item) + else: + if item in self._selected_items: + self._selected_items.remove(item) + self._value = self._selected_items[0].label() if self._selected_items else "" + + if self._treeview is None: + return + + # Reflect change in UI + sel = self._treeview.get_selection() + # find index + idx = None + for i, it in enumerate(self._items): + if it is item or it.label() == item.label(): + idx = i + break + if idx is None: + return + path = Gtk.TreePath.new_from_string(str(idx)) + if selected: + sel.select_path(path) + else: + sel.unselect_path(path) + # notify via handler + self._on_selection_changed(sel) + + def setMultiSelection(self, enabled): + self._multi_selection = bool(enabled) + if self._treeview is None: + return + sel = self._treeview.get_selection() + mode = Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE + sel.set_mode(mode) + # If disabling multi-selection, ensure only first remains selected + if not self._multi_selection: + paths, model = sel.get_selected_rows() + if len(paths) > 1: + first = paths[0] + sel.unselect_all() + sel.select_path(first) + self._on_selection_changed(sel) + + def multiSelection(self): + return bool(self._multi_selection) + + def _create_backend_widget(self): + # Container with optional label and a TreeView for (multi-)selection + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + if self._label: + lbl = Gtk.Label(label=self._label) + lbl.set_xalign(0.0) + vbox.pack_start(lbl, False, False, 0) + + # ListStore with one string column + self._liststore = Gtk.ListStore(str) + for it in self._items: + self._liststore.append([it.label()]) + + treeview = Gtk.TreeView(model=self._liststore) + renderer = Gtk.CellRendererText() + col = Gtk.TreeViewColumn("", renderer, text=0) + treeview.append_column(col) + treeview.set_headers_visible(False) + + sel = treeview.get_selection() + mode = Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE + sel.set_mode(mode) + sel.connect("changed", self._on_selection_changed) + + # If a value was previously set, apply it + if self._value: + for i, it in enumerate(self._items): + if it.label() == self._value: + sel.select_path(Gtk.TreePath.new_from_string(str(i))) + break + + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + sw.add(treeview) + vbox.pack_start(sw, True, True, 0) + + self._backend_widget = vbox + self._treeview = treeview + + def _on_selection_changed(self, selection): + # Selection may be either Gtk.TreeSelection (from signal) or Gtk.TreeSelection object passed + if isinstance(selection, Gtk.TreeSelection): + sel = selection + else: + # If called programmatically with a non-selection, try to fetch current selection + if self._treeview is None: + return + sel = self._treeview.get_selection() + + paths, model = sel.get_selected_rows() + self._selected_items = [] + for p in paths: + try: + idx = p.get_indices()[0] + except Exception: + # fallback for single-index string paths + try: + idx = int(str(p)) + except Exception: + continue + if 0 <= idx < len(self._items): + self._selected_items.append(self._items[idx]) + + if self._selected_items: + self._value = self._selected_items[0].label() + else: + self._value = "" + + # Post selection-changed event to containing dialog if notifications enabled + try: + if getattr(self, "notify", lambda: True)(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass \ No newline at end of file From ce9f67a4a4b583fdb8df5408f46df32e390d1101 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 11 Nov 2025 21:28:58 +0100 Subject: [PATCH 023/523] Added YSelectionBoxCurses --- manatools/aui/yui_curses.py | 222 ++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 92d8930..bd00e6d 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -114,6 +114,9 @@ def __init__(self): def createMainDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): return YDialogCurses(YDialogType.YMainDialog, color_mode) + + def createPopupDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): + return YDialogCurses(YDialogType.YMainDialog, color_mode) def createVBox(self, parent): return YVBoxCurses(parent) @@ -138,6 +141,9 @@ def createCheckBox(self, parent, label, is_checked=False): def createComboBox(self, parent, label, editable=False): return YComboBoxCurses(parent, label, editable) + + def createSelectionBox(self, parent, label): + return YSelectionBoxCurses(parent, label) # Curses Widget Implementations @@ -926,3 +932,219 @@ def _handle_key(self, key): handled = False return handled + +class YSelectionBoxCurses(YSelectionWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + self._selected_items = [] + self._multi_selection = False + + # UI state for drawing/navigation + self._height = 6 # visible rows for items (excluding optional label) + self._scroll_offset = 0 + self._hover_index = 0 # index into self._items (global) + self._can_focus = True + self._focused = False + + def widgetClass(self): + return "YSelectionBox" + + def label(self): + return self._label + + def value(self): + return self._value + + def setValue(self, text): + """Select first item matching text.""" + self._value = text + # update selected_items + self._selected_items = [it for it in self._items if it.label() == text][:1] + # update hover to first matching index + for idx, it in enumerate(self._items): + if it.label() == text: + self._hover_index = idx + # adjust scroll offset to make hovered visible + self._ensure_hover_visible() + break + + def selectedItems(self): + return list(self._selected_items) + + def selectItem(self, item, selected=True): + """Programmatically select/deselect an item.""" + # find index + idx = None + for i, it in enumerate(self._items): + if it is item or it.label() == item.label(): + idx = i + break + if idx is None: + return + + if selected: + if not self._multi_selection: + self._selected_items = [self._items[idx]] + self._value = self._items[idx].label() + else: + if self._items[idx] not in self._selected_items: + self._selected_items.append(self._items[idx]) + else: + if self._items[idx] in self._selected_items: + self._selected_items.remove(self._items[idx]) + self._value = self._selected_items[0].label() if self._selected_items else "" + + # ensure hover and scroll reflect this item + self._hover_index = idx + self._ensure_hover_visible() + + # notify dialog + try: + if getattr(self, "notify", lambda: True)(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + + def setMultiSelection(self, enabled): + self._multi_selection = bool(enabled) + # if disabling multi-selection, reduce to first selected item + if not self._multi_selection and len(self._selected_items) > 1: + first = self._selected_items[0] + self._selected_items = [first] + self._value = first.label() + + def multiSelection(self): + return bool(self._multi_selection) + + def _ensure_hover_visible(self): + """Adjust scroll offset so that hover_index is visible in the box.""" + visible = self._visible_row_count() + if visible <= 0: + return + if self._hover_index < self._scroll_offset: + self._scroll_offset = self._hover_index + elif self._hover_index >= self._scroll_offset + visible: + self._scroll_offset = self._hover_index - visible + 1 + + def _visible_row_count(self): + # Use configured height, but don't exceed number of items + return min(self._height, max(0, len(self._items))) + + def _create_backend_widget(self): + # No curses backend widget object; drawing handled in _draw. + # Keep height heuristic: try to show up to 6 items or fewer if not available. + self._height = min(6, max(1, len(self._items))) + # reset scroll/hover if out of range + if self._hover_index >= len(self._items): + self._hover_index = max(0, len(self._items) - 1) + self._ensure_hover_visible() + + def _draw(self, window, y, x, width, height): + """Draw label (optional) and visible portion of items.""" + try: + line = y + # draw label if present + if self._label: + lbl = self._label + try: + window.addstr(line, x, lbl[:width], curses.A_BOLD) + except curses.error: + pass + line += 1 + + visible = self._visible_row_count() + # ensure visible fits in provided height + visible = min(visible, max(0, height - (1 if self._label else 0))) + for i in range(visible): + item_idx = self._scroll_offset + i + if item_idx >= len(self._items): + break + item = self._items[item_idx] + text = item.label() + checkbox = "*" if item in self._selected_items else " " + # Display selection marker for multi or single similarly + display = f"[{checkbox}] {text}" + # truncate + if len(display) > width: + display = display[:max(0, width - 3)] + "..." + attr = curses.A_NORMAL + if self._focused and item_idx == self._hover_index: + attr |= curses.A_REVERSE + try: + window.addstr(line + i, x, display.ljust(width), attr) + except curses.error: + pass + + # if focused and there are more items than visible, show scrollbar hint + if self._focused and len(self._items) > visible and width > 0: + try: + # show simple up/down markers at rightmost column + if self._scroll_offset > 0: + window.addch(y + (1 if self._label else 0), x + width - 1, '^') + if (self._scroll_offset + visible) < len(self._items): + window.addch(y + (1 if self._label else 0) + visible - 1, x + width - 1, 'v') + except curses.error: + pass + + except curses.error: + pass + + def _handle_key(self, key): + """Handle navigation and selection keys when focused.""" + if not self._focused: + return False + + handled = True + if key == curses.KEY_UP: + if self._hover_index > 0: + self._hover_index -= 1 + self._ensure_hover_visible() + elif key == curses.KEY_DOWN: + if self._hover_index < max(0, len(self._items) - 1): + self._hover_index += 1 + self._ensure_hover_visible() + elif key == curses.KEY_PPAGE: # PageUp + step = self._visible_row_count() or 1 + self._hover_index = max(0, self._hover_index - step) + self._ensure_hover_visible() + elif key == curses.KEY_NPAGE: # PageDown + step = self._visible_row_count() or 1 + self._hover_index = min(max(0, len(self._items) - 1), self._hover_index + step) + self._ensure_hover_visible() + elif key == curses.KEY_HOME: + self._hover_index = 0 + self._ensure_hover_visible() + elif key == curses.KEY_END: + self._hover_index = max(0, len(self._items) - 1) + self._ensure_hover_visible() + elif key in (ord(' '), ord('\n')): # toggle/select + if 0 <= self._hover_index < len(self._items): + item = self._items[self._hover_index] + if self._multi_selection: + # toggle membership + if item in self._selected_items: + self._selected_items.remove(item) + else: + self._selected_items.append(item) + # update primary value to first selected or empty + self._value = self._selected_items[0].label() if self._selected_items else "" + else: + # single selection: set as sole selected + self._selected_items = [item] + self._value = item.label() + # notify dialog of selection change + try: + if getattr(self, "notify", lambda: True)(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + else: + handled = False + + return handled From e99122803c389efeac974aa97bcd9ae7c9e2d314 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 11 Nov 2025 21:29:30 +0100 Subject: [PATCH 024/523] added a vbox test case, hbox must be fixed --- test/test_selctionbox-vbox.py | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 test/test_selctionbox-vbox.py diff --git a/test/test_selctionbox-vbox.py b/test/test_selctionbox-vbox.py new file mode 100644 index 0000000..15de8c4 --- /dev/null +++ b/test/test_selctionbox-vbox.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_selectionbox(backend_name=None): + """Test ComboBox widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + + +############### + dialog = factory.createPopupDialog() + vbox = factory.createVBox( dialog ) + hbox = factory.createHBox( vbox ) + selBox = factory.createSelectionBox( vbox, "Menu" ) + + selBox.addItem( "Pizza Margherita" ) + selBox.addItem( "Pizza Capricciosa" ) + selBox.addItem( "Pizza Funghi" ) + selBox.addItem( "Pizza Prosciutto" ) + selBox.addItem( "Pizza Quattro Stagioni" ) + selBox.addItem( "Calzone" ) + + checkBox = factory.createCheckBox( hbox, "Notify on change", selBox.notify() ) + + hbox = factory.createHBox( vbox ) + factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) + valueField = factory.createLabel(vbox, "") + #valueField.setStretchable( yui.YD_HORIZ, True ) # // allow stretching over entire dialog width + + valueButton = factory.createPushButton( hbox, "Value" ) + #factory.createVSpacing( vbox, 0.3 ) + + #rightAlignment = factory.createRight( vbox ) TODO + closeButton = factory.createPushButton( vbox, "Close" ) + + # + # Event loop + # + valueField.setText( "???" ) + while True: + event = dialog.waitForEvent() + if not event: + print("Empty") + next + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == closeButton: + dialog.destroy() + break + elif (wdg == valueButton): + item = selBox.selectedItem() + valueField.setText( item.label() if item else "" ) + elif (wdg == checkBox): + selBox.setNotify( checkBox.value() ) + elif (wdg == selBox): # selBox will only send events with setNotify() TODO + valueField.setText(selBox.value()) + + except Exception as e: + print(f"Error testing ComboBox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_selectionbox(sys.argv[1]) + else: + test_selectionbox() + + + + From 100d77012c2c18bdfa138c5fafbe3b7b3b0d158c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 12 Nov 2025 21:33:16 +0100 Subject: [PATCH 025/523] Improved layout vbox and hbox management for Gtk --- manatools/aui/yui_gtk.py | 97 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 57b16fa..df75743 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -266,14 +266,59 @@ def __init__(self, parent=None): def widgetClass(self): return "YVBox" + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + def _create_backend_widget(self): self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) for child in self._children: widget = child.get_backend_widget() - expand = child.stretchable(YUIDimension.YD_VERT) + expand = bool(child.stretchable(YUIDimension.YD_VERT)) + print( f"VBoxGtk: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug fill = True padding = 0 + + # Ensure GTK will actually expand/fill the child when requested. + # Some widgets need explicit vexpand/valign (and sensible horiz settings) + # to take the extra space; be defensive for widgets that may not + # expose those properties. + try: + if expand: + if hasattr(widget, "set_vexpand"): + widget.set_vexpand(True) + if hasattr(widget, "set_valign"): + widget.set_valign(Gtk.Align.FILL) + # When a child expands vertically, usually we want it to fill + # horizontally as well so it doesn't collapse to minimal width. + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(True) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.FILL) + else: + if hasattr(widget, "set_vexpand"): + widget.set_vexpand(False) + if hasattr(widget, "set_valign"): + widget.set_valign(Gtk.Align.START) + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(False) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.START) + except Exception: + # be defensive — don't fail UI creation on exotic widgets + pass + self._backend_widget.pack_start(widget, expand, fill, padding) class YHBoxGtk(YWidget): @@ -282,15 +327,47 @@ def __init__(self, parent=None): def widgetClass(self): return "YHBox" - + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + def _create_backend_widget(self): self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - + for child in self._children: widget = child.get_backend_widget() - expand = child.stretchable(YUIDimension.YD_HORIZ) + expand = bool(child.stretchable(YUIDimension.YD_HORIZ)) + print( f"HBoxGtk: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug fill = True padding = 0 + # Ensure GTK will actually expand/fill the child when requested. + # Some widgets need explicit hexpand/halign to take the extra space. + try: + if expand: + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(True) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.FILL) + else: + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(False) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.START) + except Exception: + # be defensive — don't fail UI creation on exotic widgets + pass + self._backend_widget.pack_start(widget, expand, fill, padding) class YLabelGtk(YWidget): @@ -382,9 +459,19 @@ def setLabel(self, label): def _create_backend_widget(self): self._backend_widget = Gtk.Button(label=self._label) + # Prevent button from being stretched horizontally by default. + try: + if hasattr(self._backend_widget, "set_hexpand"): + self._backend_widget.set_hexpand(False) + if hasattr(self._backend_widget, "set_halign"): + self._backend_widget.set_halign(Gtk.Align.START) + except Exception: + pass self._backend_widget.connect("clicked", self._on_clicked) def _on_clicked(self, button): + if self.notify() is False: + return # Post a YWidgetEvent to the containing dialog (walk parents) dlg = self.findDialog() if dlg is not None: @@ -577,6 +664,8 @@ def __init__(self, parent=None, label=""): self._treeview = None self._liststore = None self._backend_widget = None + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) def widgetClass(self): return "YSelectionBox" From dacfecddb6a7a7e17079bf9d3832dca40d68ca88 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 12 Nov 2025 21:33:54 +0100 Subject: [PATCH 026/523] added back strechable setting --- test/test_selctionbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_selctionbox.py b/test/test_selctionbox.py index 4c4b7e8..9f79b1e 100644 --- a/test/test_selctionbox.py +++ b/test/test_selctionbox.py @@ -47,7 +47,7 @@ def test_selectionbox(backend_name=None): hbox = factory.createHBox( vbox ) factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) valueField = factory.createLabel(hbox, "") - #valueField.setStretchable( yui.YD_HORIZ, True ) # // allow stretching over entire dialog width + valueField.setStretchable( yui.YUIDimension.YD_HORIZ, True ) # // allow stretching over entire dialog width valueButton = factory.createPushButton( hbox, "Value" ) #factory.createVSpacing( vbox, 0.3 ) From 4dbdfd1da4a4a09b0fad8611773fffca203daa58 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 12 Nov 2025 23:09:25 +0100 Subject: [PATCH 027/523] Managed event sending when notify is setting --- manatools/aui/yui_curses.py | 51 ++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index bd00e6d..f5a4467 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -730,12 +730,13 @@ def _toggle(self): """Toggle checkbox state and post event""" self._is_checked = not self._is_checked - # Post a YWidgetEvent to the containing dialog - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - else: - print(f"CheckBox toggled (no dialog found): {self._label} = {self._is_checked}") + if self.notify(): + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"CheckBox toggled (no dialog found): {self._label} = {self._is_checked}") class YComboBoxCurses(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): @@ -901,16 +902,17 @@ def _handle_key(self, key): selected_item = self._items[self._hover_index] self.setValue(selected_item.label()) # update internal value/selection self._expanded = False - # force parent dialog redraw if present - dlg = self.findDialog() - if dlg is not None: - try: - # notify dialog to redraw immediately - dlg._last_draw_time = 0 - # post a widget event for selection change - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass + if self.notify(): + # force parent dialog redraw if present + dlg = self.findDialog() + if dlg is not None: + try: + # notify dialog to redraw immediately + dlg._last_draw_time = 0 + # post a widget event for selection change + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass # selection made -> handled elif key == 27: # ESC key self._expanded = False @@ -1000,14 +1002,15 @@ def selectItem(self, item, selected=True): self._hover_index = idx self._ensure_hover_visible() - # notify dialog - try: - if getattr(self, "notify", lambda: True)(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass + if self.notify(): + # notify dialog + try: + if getattr(self, "notify", lambda: True)(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) From 1c815ee85d3b66fab31ba1e27cc51788730d95fc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 12 Nov 2025 23:10:35 +0100 Subject: [PATCH 028/523] Managed event sending when notify is setting --- manatools/aui/yui_gtk.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index df75743..51d2c53 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -508,12 +508,13 @@ def _on_toggled(self, button): # Update internal state self._is_checked = button.get_active() - # Post a YWidgetEvent to the containing dialog - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - else: - print(f"Checkbox toggled (no dialog found): {self._label} = {self._is_checked}") + if self.notify(): + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"Checkbox toggled (no dialog found): {self._label} = {self._is_checked}") class YComboBoxGtk(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): @@ -619,13 +620,14 @@ def _on_text_changed(self, entry): if item.label() == self._value: self._selected_items.append(item) break - # Post selection-changed event - try: - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass + if self.notify(): + # Post selection-changed event + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass def _on_changed(self, combo): # non-editable combo: selection changed via index From c378b23e5ee84edfeca453f77d5a1dc88b0b725e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 08:17:29 +0100 Subject: [PATCH 029/523] Improving layout on Qt and event notifications --- manatools/aui/yui_qt.py | 73 +++++++++++++++++++++++++++++++--------- test/test_selctionbox.py | 4 ++- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index fa59d9a..7f9f686 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -253,7 +253,21 @@ def __init__(self, parent=None): def widgetClass(self): return "YVBox" - + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + def _create_backend_widget(self): self._backend_widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(self._backend_widget) @@ -263,6 +277,7 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 + print( f"YVBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) class YHBoxQt(YWidget): @@ -271,7 +286,21 @@ def __init__(self, parent=None): def widgetClass(self): return "YHBox" - + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + def _create_backend_widget(self): self._backend_widget = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(self._backend_widget) @@ -281,6 +310,7 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 + print( f"YHBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) class YLabelQt(YWidget): @@ -373,6 +403,15 @@ def setLabel(self, label): def _create_backend_widget(self): self._backend_widget = QtWidgets.QPushButton(self._label) + # Set size policy to prevent unwanted expansion + try: + sp = self._backend_widget.sizePolicy() + # Prefer minimal size in both dimensions + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) + #sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + self._backend_widget.setSizePolicy(sp) + except Exception: + pass self._backend_widget.clicked.connect(self._on_clicked) def _on_clicked(self): @@ -414,12 +453,13 @@ def _on_state_changed(self, state): # state is QtCore.Qt.CheckState (Unchecked=0, PartiallyChecked=1, Checked=2) self._is_checked = (state == QtCore.Qt.Checked) - # Post a YWidgetEvent to the containing dialog - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - else: - print(f"CheckBox state changed (no dialog found): {self._label} = {self._is_checked}") + if self.notify(): + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"CheckBox state changed (no dialog found): {self._label} = {self._is_checked}") class YComboBoxQt(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): @@ -488,13 +528,14 @@ def _on_text_changed(self, text): if item.label() == text: self._selected_items.append(item) break - # Post selection-changed event to containing dialog - try: - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass + if self.notify(): + # Post selection-changed event to containing dialog + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass class YSelectionBoxQt(YSelectionWidget): def __init__(self, parent=None, label=""): @@ -503,6 +544,8 @@ def __init__(self, parent=None, label=""): self._value = "" self._selected_items = [] self._multi_selection = False + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) def widgetClass(self): return "YSelectionBox" diff --git a/test/test_selctionbox.py b/test/test_selctionbox.py index 9f79b1e..eece606 100644 --- a/test/test_selctionbox.py +++ b/test/test_selctionbox.py @@ -53,7 +53,9 @@ def test_selectionbox(backend_name=None): #factory.createVSpacing( vbox, 0.3 ) #rightAlignment = factory.createRight( vbox ) TODO - closeButton = factory.createPushButton( vbox, "Close" ) + hbox = factory.createHBox( vbox ) + closeButton = factory.createPushButton( hbox, "Close" ) + factory.createLabel(hbox, " ") # spacer # # Event loop From ec310cfcf3f75f43adda40578d8e7e4897b90b3e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 09:16:44 +0100 Subject: [PATCH 030/523] improved ncurses layout management --- manatools/aui/yui_curses.py | 166 +++++++++++++++++++++++++++++------- 1 file changed, 133 insertions(+), 33 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index f5a4467..b465458 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -424,29 +424,61 @@ def __init__(self, parent=None): def widgetClass(self): return "YVBox" - + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + def _create_backend_widget(self): self._backend_widget = None def _draw(self, window, y, x, width, height): - current_y = y + # Calculate total fixed height and number of stretchable children + fixed_height = 0 + stretchable_count = 0 + child_heights = [] for child in self._children: + min_height = getattr(child, '_height', 1) + if child.stretchable(YUIDimension.YD_VERT): + stretchable_count += 1 + child_heights.append(None) # placeholder for stretchable + else: + fixed_height += min_height + child_heights.append(min_height) + + # Calculate available height for stretchable children + spacing = len(self._children) - 1 + available_height = max(0, height - fixed_height - spacing) + stretch_height = available_height // stretchable_count if stretchable_count else 0 + + # Assign heights + for idx, child in enumerate(self._children): + if child_heights[idx] is None: + # Stretchable child + child_heights[idx] = max(1, stretch_height) + + # Draw children + current_y = y + for idx, child in enumerate(self._children): if not hasattr(child, '_draw'): continue - - # Get child height - only consider direct children - child_height = getattr(child, '_height', 1) - - # Check space - if current_y + child_height > y + height: + ch = child_heights[idx] + if current_y + ch > y + height: break - - # Draw ONLY the direct child - # The child will handle drawing its own children recursively - child._draw(window, current_y, x, width, child_height) - - # Move to next position - current_y += child_height + 1 # +1 for spacing + child._draw(window, current_y, x, width, ch) + current_y += ch + if idx < len(self._children) - 1: + current_y += 1 # spacing class YHBoxCurses(YWidget): def __init__(self, parent=None): @@ -458,27 +490,93 @@ def widgetClass(self): def _create_backend_widget(self): self._backend_widget = None - - def _draw(self, window, y, x, width, height): - # HBox draws its OWN children horizontally + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + + def _child_min_width(self, child, max_width): + # Best-effort minimal width heuristic + try: + if hasattr(child, "minWidth"): + return min(max_width, max(1, int(child.minWidth()))) + except Exception: + pass + # Heuristics based on common attributes + try: + cls = child.widgetClass() if hasattr(child, "widgetClass") else "" + if cls in ("YLabel", "YPushButton", "YCheckBox"): + text = getattr(child, "_text", None) + if text is None: + text = getattr(child, "_label", "") + pad = 4 if cls == "YPushButton" else 0 + return min(max_width, max(1, len(str(text)) + pad)) + except Exception: + pass + return max(1, min(10, max_width)) # safe default + + def _draw(self, window, y, x, width, height): num_children = len(self._children) - if num_children == 0: + if num_children == 0 or width <= 0 or height <= 0: return - - # Calculate equal widths for horizontal layout - child_width = width // num_children - current_x = x - + + spacing = max(0, num_children - 1) + available = max(0, width - spacing) + + # Allocate fixed width for non-stretchable children + widths = [0] * num_children + stretchables = [] + fixed_total = 0 for i, child in enumerate(self._children): - if hasattr(child, '_draw'): - # Last child gets remaining space - actual_width = child_width if i < num_children - 1 else (x + width) - current_x - - # Draw the child - HBox handles its children's positioning - child._draw(window, y, current_x, actual_width, height) - - # Move to next horizontal position - current_x += child_width + if child.stretchable(YUIDimension.YD_HORIZ): + stretchables.append(i) + else: + w = self._child_min_width(child, available) + widths[i] = w + fixed_total += w + + # Remaining width goes to stretchable children + remaining = max(0, available - fixed_total) + if stretchables: + per = remaining // len(stretchables) + extra = remaining % len(stretchables) + for k, idx in enumerate(stretchables): + widths[idx] = max(1, per + (1 if k < extra else 0)) + else: + # No stretchables: distribute leftover evenly + if fixed_total < available: + leftover = available - fixed_total + per = leftover // num_children + extra = leftover % num_children + for i in range(num_children): + base = widths[i] if widths[i] else 1 + widths[i] = base + per + (1 if i < extra else 0) + else: + # If even fixed widths overflow, clamp proportionally + pass # widths already reflect minimal values + + # Draw children + cx = x + for i, child in enumerate(self._children): + w = widths[i] + if w <= 0: + continue + if hasattr(child, "_draw"): + ch = min(height, getattr(child, "_height", height)) + child._draw(window, y, cx, w, ch) + cx += w + if i < num_children - 1: + cx += 1 # one-column spacing class YLabelCurses(YWidget): def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): @@ -949,6 +1047,8 @@ def __init__(self, parent=None, label=""): self._hover_index = 0 # index into self._items (global) self._can_focus = True self._focused = False + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) def widgetClass(self): return "YSelectionBox" From 6f0a93de08cc193116af1a97e7bbe13b06fc2efd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 09:17:43 +0100 Subject: [PATCH 031/523] cleaned up test code --- test/test_selctionbox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_selctionbox.py b/test/test_selctionbox.py index eece606..8ff8a0a 100644 --- a/test/test_selctionbox.py +++ b/test/test_selctionbox.py @@ -45,7 +45,8 @@ def test_selectionbox(backend_name=None): checkBox = factory.createCheckBox( hbox, "Notify on change", selBox.notify() ) hbox = factory.createHBox( vbox ) - factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) + label = factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) + label.setStretchable( yui.YUIDimension.YD_HORIZ, True ) valueField = factory.createLabel(hbox, "") valueField.setStretchable( yui.YUIDimension.YD_HORIZ, True ) # // allow stretching over entire dialog width @@ -60,7 +61,7 @@ def test_selectionbox(backend_name=None): # # Event loop # - valueField.setText( "???" ) + #valueField.setText( "???" ) while True: event = dialog.waitForEvent() if not event: From cc56f19153cbcf0ec2fce8de1800682c7daae093 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 10:07:42 +0100 Subject: [PATCH 032/523] fixed selection box scrolling --- manatools/aui/yui_curses.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index b465458..8d92307 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -1050,6 +1050,10 @@ def __init__(self, parent=None, label=""): self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) + # Track last computed visible rows during last _draw call so + # navigation/ensure logic uses actual available space. + self._current_visible_rows = None + def widgetClass(self): return "YSelectionBox" @@ -1125,7 +1129,10 @@ def multiSelection(self): def _ensure_hover_visible(self): """Adjust scroll offset so that hover_index is visible in the box.""" - visible = self._visible_row_count() + # Prefer the visible row count computed during the last _draw call + # (which takes the actual available height into account). Fallback + # to the configured visible row count if no draw happened yet. + visible = self._current_visible_rows if self._current_visible_rows is not None else self._visible_row_count() if visible <= 0: return if self._hover_index < self._scroll_offset: @@ -1145,6 +1152,8 @@ def _create_backend_widget(self): if self._hover_index >= len(self._items): self._hover_index = max(0, len(self._items) - 1) self._ensure_hover_visible() + # reset the cached visible rows so future navigation uses the next draw's value + self._current_visible_rows = None def _draw(self, window, y, x, width, height): """Draw label (optional) and visible portion of items.""" @@ -1162,6 +1171,10 @@ def _draw(self, window, y, x, width, height): visible = self._visible_row_count() # ensure visible fits in provided height visible = min(visible, max(0, height - (1 if self._label else 0))) + # remember actual visible rows for navigation logic (_ensure_hover_visible) + self._current_visible_rows = visible + # ensure visible fits in provided height + visible = min(visible, max(0, height - (1 if self._label else 0))) for i in range(visible): item_idx = self._scroll_offset + i if item_idx >= len(self._items): @@ -1192,7 +1205,7 @@ def _draw(self, window, y, x, width, height): window.addch(y + (1 if self._label else 0) + visible - 1, x + width - 1, 'v') except curses.error: pass - + # keep _current_visible_rows until next draw; navigation will use it except curses.error: pass From 4c789239b872d13fa3d2fe9c06a7cf25d4cb300c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 11:43:42 +0100 Subject: [PATCH 033/523] Fixed selection box vertical strecthing --- manatools/aui/yui_curses.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 8d92307..b4d67fe 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -926,7 +926,7 @@ def _draw_expanded_list(self, window): return try: - list_height = min(len(self._items), 6) # Max 6 items visible + list_height = min(len(self._items), 6) # Max 6 items visible # Calculate dropdown position - right below the combo box dropdown_y = self._combo_y + 1 @@ -1042,7 +1042,11 @@ def __init__(self, parent=None, label=""): self._multi_selection = False # UI state for drawing/navigation - self._height = 6 # visible rows for items (excluding optional label) + # actual minimal height for layout (keep small so parent can expand it) + self._height = 1 + # preferred rows used for paging when no draw happened yet + self._preferred_rows = 6 + self._scroll_offset = 0 self._hover_index = 0 # index into self._items (global) self._can_focus = True @@ -1141,16 +1145,17 @@ def _ensure_hover_visible(self): self._scroll_offset = self._hover_index - visible + 1 def _visible_row_count(self): - # Use configured height, but don't exceed number of items - return min(self._height, max(0, len(self._items))) + # Return preferred visible rows for navigation (PageUp/PageDown step). + # Use preferred_rows (default 6) rather than forcing the layout minimum. + return max(1, getattr(self, "_preferred_rows", 6)) def _create_backend_widget(self): # No curses backend widget object; drawing handled in _draw. - # Keep height heuristic: try to show up to 6 items or fewer if not available. - self._height = min(6, max(1, len(self._items))) + # Keep minimal layout height small so parent can give more space. + self._height = len(self._items) + (1 if self._label else 0) # reset scroll/hover if out of range if self._hover_index >= len(self._items): - self._hover_index = max(0, len(self._items) - 1) + self._hover_index = max(0, len(self._items) - 1) self._ensure_hover_visible() # reset the cached visible rows so future navigation uses the next draw's value self._current_visible_rows = None @@ -1169,12 +1174,16 @@ def _draw(self, window, y, x, width, height): line += 1 visible = self._visible_row_count() - # ensure visible fits in provided height - visible = min(visible, max(0, height - (1 if self._label else 0))) + # compute how many rows we can actually draw given provided height. + available_rows = max(0, height - (1 if self._label else 0)) + if self.stretchable(YUIDimension.YD_VERT): + # If widget is stretchable vertically, use all available rows (up to number of items) + visible = min(len(self._items), available_rows) + else: + # Otherwise prefer configured height but don't exceed available rows or items + visible = min(len(self._items), self._visible_row_count(), available_rows) # remember actual visible rows for navigation logic (_ensure_hover_visible) self._current_visible_rows = visible - # ensure visible fits in provided height - visible = min(visible, max(0, height - (1 if self._label else 0))) for i in range(visible): item_idx = self._scroll_offset + i if item_idx >= len(self._items): From b5126e49397ea0fdbf6b9362c7280037cd5e2274 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 11:56:32 +0100 Subject: [PATCH 034/523] Better fixing gtk item selection --- manatools/aui/yui_gtk.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 51d2c53..c3389d8 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -802,19 +802,20 @@ def _on_selection_changed(self, selection): return sel = self._treeview.get_selection() - paths, model = sel.get_selected_rows() + # Robustly build selected items by checking each known row path. + # This avoids corner cases with path types returned by get_selected_rows() + # and ensures indices align with self._items. self._selected_items = [] - for p in paths: + if self._treeview is None or self._liststore is None: + return + for i, it in enumerate(self._items): try: - idx = p.get_indices()[0] + path = Gtk.TreePath.new_from_string(str(i)) + if sel.path_is_selected(path): + self._selected_items.append(it) except Exception: - # fallback for single-index string paths - try: - idx = int(str(p)) - except Exception: - continue - if 0 <= idx < len(self._items): - self._selected_items.append(self._items[idx]) + # ignore malformed paths or selection APIs we can't query + continue if self._selected_items: self._value = self._selected_items[0].label() From 07aced1700793af842c0a08a5478279cc9ec0049 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 11:57:12 +0100 Subject: [PATCH 035/523] Added a check box to test multiselection too --- test/test_selctionbox.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/test/test_selctionbox.py b/test/test_selctionbox.py index 8ff8a0a..d553d9a 100644 --- a/test/test_selctionbox.py +++ b/test/test_selctionbox.py @@ -31,9 +31,9 @@ def test_selectionbox(backend_name=None): ############### dialog = factory.createPopupDialog() - vbox = factory.createVBox( dialog ) - hbox = factory.createHBox( vbox ) - selBox = factory.createSelectionBox( hbox, "Menu" ) + mainVbox = factory.createVBox( dialog ) + hbox = factory.createHBox( mainVbox ) + selBox = factory.createSelectionBox( hbox, "Choose your pizza" ) selBox.addItem( "Pizza Margherita" ) selBox.addItem( "Pizza Capricciosa" ) @@ -42,19 +42,23 @@ def test_selectionbox(backend_name=None): selBox.addItem( "Pizza Quattro Stagioni" ) selBox.addItem( "Calzone" ) - checkBox = factory.createCheckBox( hbox, "Notify on change", selBox.notify() ) + vbox = factory.createVBox( hbox ) + notifyCheckBox = factory.createCheckBox( vbox, "Notify on change", selBox.notify() ) + notifyCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + multiSelectionCheckBox = factory.createCheckBox( vbox, "Multi-selection", selBox.multiSelection() ) + multiSelectionCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) - hbox = factory.createHBox( vbox ) + hbox = factory.createHBox( mainVbox ) + valueButton = factory.createPushButton( hbox, "Value" ) label = factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) label.setStretchable( yui.YUIDimension.YD_HORIZ, True ) valueField = factory.createLabel(hbox, "") valueField.setStretchable( yui.YUIDimension.YD_HORIZ, True ) # // allow stretching over entire dialog width - valueButton = factory.createPushButton( hbox, "Value" ) #factory.createVSpacing( vbox, 0.3 ) #rightAlignment = factory.createRight( vbox ) TODO - hbox = factory.createHBox( vbox ) + hbox = factory.createHBox( mainVbox ) closeButton = factory.createPushButton( hbox, "Close" ) factory.createLabel(hbox, " ") # spacer @@ -76,13 +80,23 @@ def test_selectionbox(backend_name=None): if wdg == closeButton: dialog.destroy() break - elif (wdg == valueButton): - item = selBox.selectedItem() - valueField.setText( item.label() if item else "" ) - elif (wdg == checkBox): - selBox.setNotify( checkBox.value() ) + elif (wdg == valueButton): + if selBox.multiSelection(): + labels = [item.label() for item in selBox.selectedItems()] + valueField.setText( ", ".join(labels) ) + else: + item = selBox.selectedItem() + valueField.setText( item.label() if item else "" ) + elif (wdg == notifyCheckBox): + selBox.setNotify( notifyCheckBox.value() ) + elif (wdg == multiSelectionCheckBox): + selBox.setMultiSelection( multiSelectionCheckBox.value() ) elif (wdg == selBox): # selBox will only send events with setNotify() TODO - valueField.setText(selBox.value()) + if selBox.multiSelection(): + labels = [item.label() for item in selBox.selectedItems()] + valueField.setText( ", ".join(labels) ) + else: + valueField.setText(selBox.value()) except Exception as e: print(f"Error testing ComboBox with backend {backend_name}: {e}") From 9a54738ce0e23641fb7d5b72e7b7ca61073d1bc5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 11:57:41 +0100 Subject: [PATCH 036/523] move forward --- sow/TODO.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 246fe3f..76d8e4a 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -8,9 +8,9 @@ Next is the starting todo list. Missing Widgets comparing libyui: - [ ] YComboBox (on going) - [ ] YSelectionBox - [ ] YMultiSelectionBox + [X] YComboBox + [X] YSelectionBox + [X] YMultiSelectionBox [ ] YTree [ ] YTable [ ] YProgressBar From 9879c67f9859aed6a080b0496c8439249392cbdb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 12:08:25 +0100 Subject: [PATCH 037/523] closing widthout changes dropdown combo if we change widget --- manatools/aui/yui_curses.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index b4d67fe..c5f7dac 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -297,12 +297,20 @@ def _cycle_focus(self, forward=True): else: new_index = (current_index - 1) % len(focusable) - # Update focus + # If the currently focused widget is an expanded combo, collapse it + # so tabbing away closes the dropdown but does not change selection. if self._focused_widget: + try: + if getattr(self._focused_widget, "_expanded", False): + self._focused_widget._expanded = False + except Exception: + pass self._focused_widget._focused = False self._focused_widget = focusable[new_index] self._focused_widget._focused = True + # Force redraw on focus change + self._last_draw_time = 0 def _find_focusable_widgets(self): """Find all widgets that can receive focus""" From 321d340cf05f80b2cdbd22c8e9f676549a339110 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 12:32:52 +0100 Subject: [PATCH 038/523] better drawing expanded combo --- manatools/aui/yui_curses.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index c5f7dac..5b01e03 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -934,16 +934,16 @@ def _draw_expanded_list(self, window): return try: - list_height = min(len(self._items), 6) # Max 6 items visible - + # Make sure we don't draw outside screen + screen_height, screen_width = window.getmaxyx() + + list_height = min(len(self._items), screen_height) + # Calculate dropdown position - right below the combo box dropdown_y = self._combo_y + 1 dropdown_x = self._combo_x + (len(self._label) + 1 if self._label else 0) dropdown_width = self._combo_width - (len(self._label) + 1 if self._label else 0) - - # Make sure we don't draw outside screen - screen_height, screen_width = window.getmaxyx() - + # If not enough space below, draw above if dropdown_y + list_height >= screen_height: dropdown_y = max(1, self._combo_y - list_height - 1) From 4c37536df12c872114d0c0c0a52a197ecf53a786 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 13:06:10 +0100 Subject: [PATCH 039/523] combo boxes dropdown items on top --- manatools/aui/yui_curses.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 5b01e03..796d37f 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -212,13 +212,7 @@ def currentDialog(cls, doThrow=True): def _create_backend_widget(self): # Use the main screen self._backend_widget = curses.newwin(0, 0, 0, 0) - - def _run_event_loop(self): - # Backwards-compatible helper: run an indefinite loop until dialog closed. - # Implemented via waitForEvent with no timeout (block until an event that - # may close/destroy the dialog). - self.waitForEvent(timeout_millisec=0) - + def _draw_dialog(self): """Draw the entire dialog (called by event loop)""" if not hasattr(self, '_backend_widget') or not self._backend_widget: @@ -229,7 +223,7 @@ def _draw_dialog(self): # Clear screen self._backend_widget.clear() - + # Draw border self._backend_widget.border() @@ -258,8 +252,12 @@ def _draw_dialog(self): if self._focused_widget: focus_text = f" Focus: {getattr(self._focused_widget, '_label', 'Unknown')} " if len(focus_text) < width: - self._backend_widget.addstr(height - 2, 2, focus_text, curses.A_REVERSE) + self._backend_widget.addstr(height - 1, 2, focus_text, curses.A_REVERSE) + #if the focused widget has an expnded list (menus, combos,...), draw it on top + if hasattr(self._focused_widget, "_draw_expanded_list"): + self._focused_widget._draw_expanded_list(self._backend_widget) + # Refresh main window first self._backend_widget.refresh() except curses.error as e: From 83c7d659af73771139fec69c9e00e597169a7a30 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 14:56:08 +0100 Subject: [PATCH 040/523] updated --- sow/TODO.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 76d8e4a..9acb55a 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -24,4 +24,9 @@ Missing Widgets comparing libyui: [ ] YReplacePoint [ ] YRadioButton, YRadioButtonGroup -To check how to manage YEvents and YItems. +To check how to manage YEvents [X] and YItems [ ]. + +Nice to have: improvements outside YUI API + [ ] window title + [ ] window icons + [ ] selected YItem(s) in event From 055814c3ccb26d9cb454badd82ac202dd4a3ff17 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 14:57:13 +0100 Subject: [PATCH 041/523] Managed Qt application title --- manatools/aui/yui_qt.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 7f9f686..16a16f9 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -54,6 +54,13 @@ def productName(self): def setApplicationTitle(self, title): """Set the application title.""" self._application_title = title + # also keep Qt's application name in sync so dialogs can read it without importing YUI + try: + app = QtWidgets.QApplication.instance() + if app: + app.setApplicationName(title) + except Exception: + pass def applicationTitle(self): """Get the application title.""" @@ -64,8 +71,8 @@ def setApplicationIcon(self, Icon): self._icon = Icon def applicationIcon(self): - """Get the application title.""" - return self.__icon + """Get the application icon.""" + return self._icon class YWidgetFactoryQt: def __init__(self): @@ -174,9 +181,34 @@ def currentDialog(cls, doThrow=True): def _create_backend_widget(self): self._qwidget = QtWidgets.QMainWindow() - self._qwidget.setWindowTitle("YUI Qt Dialog") - self._qwidget.resize(600, 400) + # Determine window title:from YApplicationQt instance stored on the YUI backend + title = "Manatools YUI Qt Dialog" + try: + from . import yui as yui_mod + appobj = None + # YUI._backend may hold the backend instance (YUIQt) + backend = getattr(yui_mod.YUI, "_backend", None) + if backend: + if hasattr(backend, "application"): + appobj = backend.application() + # fallback: YUI._instance might be set and expose application/yApp + if not appobj: + inst = getattr(yui_mod.YUI, "_instance", None) + if inst: + if hasattr(inst, "application"): + appobj = inst.application() + if appobj and hasattr(appobj, "applicationTitle"): + atitle = appobj.applicationTitle() + if atitle: + title = atitle + except Exception: + # ignore and keep default + pass + + self._qwidget.setWindowTitle(title) + self._qwidget.resize(600, 400) + central_widget = QtWidgets.QWidget() self._qwidget.setCentralWidget(central_widget) From c2d2ac53f44cb261b4450cfab95a9bb3b2d44a93 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 15:12:56 +0100 Subject: [PATCH 042/523] Added Gtk application title --- manatools/aui/yui_gtk.py | 50 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index c3389d8..69a0de4 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -51,6 +51,30 @@ def productName(self): def setApplicationTitle(self, title): """Set the application title.""" self._application_title = title + try: + # Try to update a running Gtk.Application so dialogs can read it via app.get_application_name() + app = None + try: + if hasattr(Gtk.Application, "get_default"): + app = Gtk.Application.get_default() + except Exception: + app = None + if app: + # Prefer the setter if available + if hasattr(app, "set_application_name"): + try: + app.set_application_name(title) + except Exception: + pass + # as fallback try setting a property that might be used by specific apps + if hasattr(app, "set_application_id"): + try: + # don't override a real application id, but try best-effort + app.set_application_id(str(title)) + except Exception: + pass + except Exception: + pass def applicationTitle(self): """Get the application title.""" @@ -222,7 +246,31 @@ def currentDialog(cls, doThrow=True): return cls._open_dialogs[-1] def _create_backend_widget(self): - self._window = Gtk.Window(title="YUI GTK Dialog") + # Determine window title:from YApplicationQt instance stored on the YUI backend + title = "Manatools YUI GTK Dialog" + + try: + from . import yui as yui_mod + appobj = None + # YUI._backend may hold the backend instance (YUIGtk) + backend = getattr(yui_mod.YUI, "_backend", None) + if backend: + if hasattr(backend, "application"): + appobj = backend.application() + # fallback: YUI._instance might be set and expose application/yApp + if not appobj: + inst = getattr(yui_mod.YUI, "_instance", None) + if inst: + if hasattr(inst, "application"): + appobj = inst.application() + if appobj and hasattr(appobj, "applicationTitle"): + atitle = appobj.applicationTitle() + if atitle: + title = atitle + except Exception: + # ignore and keep default + pass + self._window = Gtk.Window(title=title) self._window.set_default_size(600, 400) self._window.set_border_width(10) From 97f57bcb0bf339a842e77cb15888f443f0069884 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 15:22:35 +0100 Subject: [PATCH 043/523] Added ncurses application title --- manatools/aui/yui_curses.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 796d37f..c03006e 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -228,7 +228,28 @@ def _draw_dialog(self): self._backend_widget.border() # Draw title - title = " YUI NCurses Dialog " + title = " manatools YUI NCurses Dialog " + try: + from . import yui as yui_mod + appobj = None + # YUI._backend may hold the backend instance (YUIQt) + backend = getattr(yui_mod.YUI, "_backend", None) + if backend: + if hasattr(backend, "application"): + appobj = backend.application() + # fallback: YUI._instance might be set and expose application/yApp + if not appobj: + inst = getattr(yui_mod.YUI, "_instance", None) + if inst: + if hasattr(inst, "application"): + appobj = inst.application() + if appobj and hasattr(appobj, "applicationTitle"): + atitle = appobj.applicationTitle() + if atitle: + title = atitle + except Exception: + # ignore and keep default + pass title_x = max(0, (width - len(title)) // 2) self._backend_widget.addstr(0, title_x, title, curses.A_BOLD) From 18ddf7dc09d794cdfada59d98d481e85a1eea770 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 18:58:34 +0100 Subject: [PATCH 044/523] Added application title to ncurses too --- manatools/aui/yui_curses.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index c03006e..ae12c26 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -39,7 +39,6 @@ def _init_curses(self): curses.init_pair(2, curses.COLOR_YELLOW, -1) curses.init_pair(3, curses.COLOR_GREEN, -1) curses.init_pair(4, curses.COLOR_RED, -1) - except Exception as e: print(f"Error initializing curses: {e}") self._cleanup_curses() @@ -95,6 +94,20 @@ def productName(self): def setApplicationTitle(self, title): """Set the application title.""" self._application_title = title + # Update terminal/window title for xterm-like terminals when stdout is a TTY + escape_sequences = [ + f"\033]0;{title}\007", # Standard + f"\033]1;{title}\007", # Icon name + f"\033]2;{title}\007", # Window title + f"\033]30;{title}\007", # Konsole variant 1 + f"\033]31;{title}\007", # Konsole variant 2 + ] + try: + for seq in escape_sequences: + sys.stdout.write(seq) + sys.stdout.flush() + except Exception: + pass def applicationTitle(self): """Get the application title.""" @@ -247,6 +260,8 @@ def _draw_dialog(self): atitle = appobj.applicationTitle() if atitle: title = atitle + if appobj: + appobj.setApplicationTitle(title) except Exception: # ignore and keep default pass From 514a7638770e8225f7c87c6946ac043d7a16ec9a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 19:20:39 +0100 Subject: [PATCH 045/523] App setApplicationTitle force the QT mainwindow to change the title --- manatools/aui/yui_qt.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 16a16f9..a14c4b5 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -10,13 +10,12 @@ class YUIQt: def __init__(self): self._widget_factory = YWidgetFactoryQt() self._optional_widget_factory = None - self._application = YApplicationQt() - # Ensure QApplication exists self._qapp = QtWidgets.QApplication.instance() if not self._qapp: self._qapp = QtWidgets.QApplication(sys.argv) - + self._application = YApplicationQt() + def widgetFactory(self): return self._widget_factory @@ -58,7 +57,15 @@ def setApplicationTitle(self, title): try: app = QtWidgets.QApplication.instance() if app: + print(f"YApplicationQt: setting QApplication applicationName to '{title}'") app.setApplicationName(title) + top_level_widgets = app.topLevelWidgets() + + for widget in top_level_widgets: + if isinstance(widget, QtWidgets.QMainWindow): + main_window = widget + main_window.setWindowTitle(title) + break except Exception: pass From 2a43f48cc37404990b4dde9282d9ba107f9ffdf4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 20:49:26 +0100 Subject: [PATCH 046/523] Added gtk currentDialog/topMostDialog to get the active dialog --- manatools/aui/yui_gtk.py | 48 ++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 69a0de4..64fba2a 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -52,27 +52,21 @@ def setApplicationTitle(self, title): """Set the application title.""" self._application_title = title try: - # Try to update a running Gtk.Application so dialogs can read it via app.get_application_name() - app = None + + # update the top most YDialogGtk windows created, i.e. the current one try: - if hasattr(Gtk.Application, "get_default"): - app = Gtk.Application.get_default() + # YDialogGtk is defined in this module; update its open dialogs' windows + dlg =YDialogGtk.currentDialog(doThrow=False) + try: + win = getattr(dlg, "_window", None) + if win: + win.set_title(title) + print(f"YApplicationGtk: set YDialogGtk window title to '{title}'") + except Exception: + pass except Exception: - app = None - if app: - # Prefer the setter if available - if hasattr(app, "set_application_name"): - try: - app.set_application_name(title) - except Exception: - pass - # as fallback try setting a property that might be used by specific apps - if hasattr(app, "set_application_id"): - try: - # don't override a real application id, but try best-effort - app.set_application_id(str(title)) - except Exception: - pass + pass + except Exception: pass @@ -146,6 +140,22 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM def widgetClass(self): return "YDialog" + @staticmethod + def currentDialog(doThrow=True): + open_dialog = YDialogGtk._open_dialogs[-1] if YDialogGtk._open_dialogs else None + if not open_dialog and doThrow: + raise YUINoDialogException("No dialog is currently open") + return open_dialog + + @staticmethod + def topmostDialog(doThrow=True): + ''' same as currentDialog ''' + return YDialogGtk.currentDialog(doThrow=doThrow) + + def isTopmostDialog(self): + '''Return whether this dialog is the topmost open dialog.''' + return YDialogGtk._open_dialogs[-1] == self if YDialogGtk._open_dialogs else False + def open(self): # Finalize and show the dialog in a non-blocking way. # Matching libyui semantics: open() should finalize and make visible, From d4a1d77737fe01d5dd6ba0a9b2d98d2f52a95d47 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 20:50:17 +0100 Subject: [PATCH 047/523] Added Qt currentDialog/topMostDialog to get the active dialog --- manatools/aui/yui_qt.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index a14c4b5..93a8dbf 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -144,6 +144,23 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM def widgetClass(self): return "YDialog" + @staticmethod + def currentDialog(doThrow=True): + '''Return the currently open dialog (topmost), or raise if none.''' + open_dialog = YDialogQt._open_dialogs[-1] if YDialogQt._open_dialogs else None + if not open_dialog and doThrow: + raise YUINoDialogException("No dialog is currently open") + return open_dialog + + @staticmethod + def topmostDialog(doThrow=True): + ''' same as currentDialog ''' + return YDialogQt.currentDialog(doThrow=doThrow) + + def isTopmostDialog(self): + '''Return whether this dialog is the topmost open dialog.''' + return YDialogQt._open_dialogs[-1] == self if YDialogQt._open_dialogs else False + def open(self): """ Finalize and show the dialog in a non-blocking way. From 3955ad5423faf14cbea220fb873b4d5e85c0feca Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 15 Nov 2025 20:51:22 +0100 Subject: [PATCH 048/523] Added curses currentDialog/topMostDialog to get the active dialog --- manatools/aui/yui_curses.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index ae12c26..08be83b 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -179,6 +179,22 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM def widgetClass(self): return "YDialog" + @staticmethod + def currentDialog(doThrow=True): + open_dialog = YDialogCurses._open_dialogs[-1] if YDialogCurses._open_dialogs else None + if not open_dialog and doThrow: + raise YUINoDialogException("No dialog is currently open") + return open_dialog + + @staticmethod + def topmostDialog(doThrow=True): + ''' same as currentDialog ''' + return YDialogCurses.currentDialog(doThrow=doThrow) + + def isTopmostDialog(self): + '''Return whether this dialog is the topmost open dialog.''' + return YDialogCurses._open_dialogs[-1] == self if YDialogCurses._open_dialogs else False + def open(self): if not self._window: self._create_backend_widget() From 3be5ff9eb439e9473d7dea217d2938b4fc971028 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 17:23:19 +0100 Subject: [PATCH 049/523] Move to gtk4 --- manatools/aui/yui_gtk.py | 705 +++++++++++++++++++++++---------------- 1 file changed, 412 insertions(+), 293 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 64fba2a..553ba40 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -1,10 +1,9 @@ """ -GTK backend implementation for YUI +GTK4 backend implementation for YUI (converted from GTK3) """ - import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GObject, GLib +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GLib import threading from .yui_common import * @@ -49,24 +48,21 @@ def productName(self): return self._product_name def setApplicationTitle(self, title): - """Set the application title.""" + """Set the application title and try to update dialogs/windows.""" self._application_title = title try: - - # update the top most YDialogGtk windows created, i.e. the current one + # update the top most YDialogGtk window if available try: - # YDialogGtk is defined in this module; update its open dialogs' windows - dlg =YDialogGtk.currentDialog(doThrow=False) - try: + dlg = YDialogGtk.currentDialog(doThrow=False) + if dlg: win = getattr(dlg, "_window", None) if win: - win.set_title(title) - print(f"YApplicationGtk: set YDialogGtk window title to '{title}'") - except Exception: - pass + try: + win.set_title(title) + except Exception: + pass except Exception: pass - except Exception: pass @@ -75,12 +71,10 @@ def applicationTitle(self): return self._application_title def setApplicationIcon(self, Icon): - """Set the application title.""" self._icon = Icon def applicationIcon(self): - """Get the application title.""" - return self.__icon + return self._icon class YWidgetFactoryGtk: @@ -123,7 +117,8 @@ def createComboBox(self, parent, label, editable=False): def createSelectionBox(self, parent, label): return YSelectionBoxGtk(parent, label) -# GTK Widget Implementations + +# GTK4 Widget Implementations class YDialogGtk(YSingleChildContainerWidget): _open_dialogs = [] @@ -158,12 +153,17 @@ def isTopmostDialog(self): def open(self): # Finalize and show the dialog in a non-blocking way. - # Matching libyui semantics: open() should finalize and make visible, - # but must NOT start a global blocking Gtk.main() here. if not self._is_open: if not self._window: self._create_backend_widget() - self._window.show_all() + # in Gtk4, show_all is not recommended; use present() or show + try: + self._window.present() + except Exception: + try: + self._window.show() + except Exception: + pass self._is_open = True def isOpen(self): @@ -171,20 +171,24 @@ def isOpen(self): def destroy(self, doThrow=True): if self._window: - self._window.destroy() + try: + self._window.destroy() + except Exception: + try: + self._window.close() + except Exception: + pass self._window = None self._is_open = False if self in YDialogGtk._open_dialogs: YDialogGtk._open_dialogs.remove(self) - # Stop GTK main loop if no dialogs left + # Stop GLib main loop if no dialogs left (nested loops only) if not YDialogGtk._open_dialogs: try: - # Only quit the global Gtk main loop if it's actually running - if hasattr(Gtk, "main_level") and Gtk.main_level() > 0: - Gtk.main_quit() + if self._glib_loop and self._glib_loop.is_running(): + self._glib_loop.quit() except Exception: - # be defensive: do not raise from cleanup pass return True @@ -206,9 +210,15 @@ def waitForEvent(self, timeout_millisec=0): if not self.isOpen(): self.open() - # Let GTK process pending events (show/layout) before entering nested loop. - while Gtk.events_pending(): - Gtk.main_iteration() + # Let GTK/GLib process pending events (show/layout) before entering nested loop. + # Gtk.events_pending()/Gtk.main_iteration() do not exist in GTK4; use MainContext iteration. + try: + ctx = GLib.MainContext.default() + while ctx.pending(): + ctx.iteration(False) + except Exception: + # be defensive if API differs on some bindings + pass self._event_result = None self._glib_loop = GLib.MainLoop() @@ -256,67 +266,96 @@ def currentDialog(cls, doThrow=True): return cls._open_dialogs[-1] def _create_backend_widget(self): - # Determine window title:from YApplicationQt instance stored on the YUI backend + # Determine window title from YApplicationGtk instance stored on the YUI backend title = "Manatools YUI GTK Dialog" - try: from . import yui as yui_mod appobj = None # YUI._backend may hold the backend instance (YUIGtk) backend = getattr(yui_mod.YUI, "_backend", None) - if backend: - if hasattr(backend, "application"): - appobj = backend.application() + if backend and hasattr(backend, "application"): + appobj = backend.application() # fallback: YUI._instance might be set and expose application/yApp if not appobj: inst = getattr(yui_mod.YUI, "_instance", None) - if inst: - if hasattr(inst, "application"): - appobj = inst.application() + if inst and hasattr(inst, "application"): + appobj = inst.application() if appobj and hasattr(appobj, "applicationTitle"): atitle = appobj.applicationTitle() if atitle: title = atitle except Exception: - # ignore and keep default pass + + # Create Gtk4 Window self._window = Gtk.Window(title=title) - self._window.set_default_size(600, 400) - self._window.set_border_width(10) - + try: + self._window.set_default_size(600, 400) + except Exception: + pass + + # Content container with margins (window.set_child used in Gtk4) + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + content.set_margin_start(10) + content.set_margin_end(10) + content.set_margin_top(10) + content.set_margin_bottom(10) + if self._child: - self._window.add(self._child.get_backend_widget()) - + child_widget = self._child.get_backend_widget() + # ensure child is shown properly + try: + content.append(child_widget) + except Exception: + try: + content.add(child_widget) + except Exception: + pass + + try: + self._window.set_child(content) + except Exception: + # fallback for older bindings + try: + self._window.add(content) + except Exception: + pass + self._backend_widget = self._window - # Connect to both "delete-event" (window manager close) and "destroy" - # so we can post a YCancelEvent and stop any nested wait loop. - self._window.connect("delete-event", self._on_delete_event) - self._window.connect("destroy", self._on_destroy) + # Connect destroy/close handlers + try: + # Gtk4: use 'close-request' if available, otherwise 'destroy' + if hasattr(self._window, "connect"): + try: + self._window.connect("close-request", self._on_delete_event) + except Exception: + try: + self._window.connect("destroy", self._on_destroy) + except Exception: + pass + except Exception: + pass def _on_destroy(self, widget): - # normal widget destruction: ensure internal state cleaned try: - # If no nested loop running, remove dialog and quit global loop if needed self.destroy() except Exception: pass - def _on_delete_event(self, widget, event): - # User clicked the window manager close (X) button: - # post a YCancelEvent so waitForEvent can return YCancelEvent. + def _on_delete_event(self, *args): + # close-request handler in Gtk4: post cancel event and destroy try: self._post_event(YCancelEvent()) except Exception: pass - # Destroy the window and stop further handling try: self.destroy() except Exception: pass - # Returning False allows the default handler to destroy the window; - # we already destroyed it, so return False to continue. + # returning False/True not used in this simplified handler return False + class YVBoxGtk(YWidget): def __init__(self, parent=None): super().__init__(parent) @@ -325,17 +364,12 @@ def widgetClass(self): return "YVBox" # Returns the stretchability of the layout box: - # * The layout box is stretchable if one of the children is stretchable in - # * this dimension or if one of the child widgets has a layout weight in - # * this dimension. def stretchable(self, dim): for child in self._children: - widget = child.get_backend_widget() expand = bool(child.stretchable(dim)) weight = bool(child.weight(dim)) if expand or weight: return True - # No child is stretchable in this dimension return False def _create_backend_widget(self): @@ -344,22 +378,15 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() expand = bool(child.stretchable(YUIDimension.YD_VERT)) - print( f"VBoxGtk: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug fill = True padding = 0 - # Ensure GTK will actually expand/fill the child when requested. - # Some widgets need explicit vexpand/valign (and sensible horiz settings) - # to take the extra space; be defensive for widgets that may not - # expose those properties. try: if expand: if hasattr(widget, "set_vexpand"): widget.set_vexpand(True) if hasattr(widget, "set_valign"): widget.set_valign(Gtk.Align.FILL) - # When a child expands vertically, usually we want it to fill - # horizontally as well so it doesn't collapse to minimal width. if hasattr(widget, "set_hexpand"): widget.set_hexpand(True) if hasattr(widget, "set_halign"): @@ -374,10 +401,16 @@ def _create_backend_widget(self): if hasattr(widget, "set_halign"): widget.set_halign(Gtk.Align.START) except Exception: - # be defensive — don't fail UI creation on exotic widgets pass - self._backend_widget.pack_start(widget, expand, fill, padding) + # Gtk4: use append instead of pack_start + try: + self._backend_widget.append(widget) + except Exception: + try: + self._backend_widget.add(widget) + except Exception: + pass class YHBoxGtk(YWidget): def __init__(self, parent=None): @@ -386,18 +419,12 @@ def __init__(self, parent=None): def widgetClass(self): return "YHBox" - # Returns the stretchability of the layout box: - # * The layout box is stretchable if one of the children is stretchable in - # * this dimension or if one of the child widgets has a layout weight in - # * this dimension. def stretchable(self, dim): for child in self._children: - widget = child.get_backend_widget() expand = bool(child.stretchable(dim)) weight = bool(child.weight(dim)) if expand or weight: return True - # No child is stretchable in this dimension return False def _create_backend_widget(self): @@ -406,11 +433,8 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() expand = bool(child.stretchable(YUIDimension.YD_HORIZ)) - print( f"HBoxGtk: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug fill = True padding = 0 - # Ensure GTK will actually expand/fill the child when requested. - # Some widgets need explicit hexpand/halign to take the extra space. try: if expand: if hasattr(widget, "set_hexpand"): @@ -423,10 +447,15 @@ def _create_backend_widget(self): if hasattr(widget, "set_halign"): widget.set_halign(Gtk.Align.START) except Exception: - # be defensive — don't fail UI creation on exotic widgets pass - self._backend_widget.pack_start(widget, expand, fill, padding) + try: + self._backend_widget.append(widget) + except Exception: + try: + self._backend_widget.add(widget) + except Exception: + pass class YLabelGtk(YWidget): def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): @@ -444,15 +473,26 @@ def text(self): def setText(self, new_text): self._text = new_text if self._backend_widget: - self._backend_widget.set_text(new_text) + try: + self._backend_widget.set_text(new_text) + except Exception: + pass def _create_backend_widget(self): self._backend_widget = Gtk.Label(label=self._text) - self._backend_widget.set_xalign(0.0) # Left align + try: + # alignment API in Gtk4 differs; fall back to setting xalign if available + if hasattr(self._backend_widget, "set_xalign"): + self._backend_widget.set_xalign(0.0) + except Exception: + pass if self._is_heading: - markup = f"{self._text}" - self._backend_widget.set_markup(markup) + try: + markup = f"{self._text}" + self._backend_widget.set_markup(markup) + except Exception: + pass class YInputFieldGtk(YWidget): def __init__(self, parent=None, label="", password_mode=False): @@ -470,7 +510,10 @@ def value(self): def setValue(self, text): self._value = text if hasattr(self, '_entry_widget') and self._entry_widget: - self._entry_widget.set_text(text) + try: + self._entry_widget.set_text(text) + except Exception: + pass def label(self): return self._label @@ -480,24 +523,44 @@ def _create_backend_widget(self): if self._label: label = Gtk.Label(label=self._label) - label.set_xalign(0.0) - hbox.pack_start(label, False, False, 0) + try: + if hasattr(label, "set_xalign"): + label.set_xalign(0.0) + except Exception: + pass + try: + hbox.append(label) + except Exception: + hbox.add(label) if self._password_mode: entry = Gtk.Entry() - entry.set_visibility(False) + try: + entry.set_visibility(False) + except Exception: + pass else: entry = Gtk.Entry() - entry.set_text(self._value) - entry.connect("changed", self._on_changed) + try: + entry.set_text(self._value) + entry.connect("changed", self._on_changed) + except Exception: + pass - hbox.pack_start(entry, True, True, 0) + try: + hbox.append(entry) + except Exception: + hbox.add(entry) + self._backend_widget = hbox self._entry_widget = entry def _on_changed(self, entry): - self._value = entry.get_text() + try: + self._value = entry.get_text() + except Exception: + self._value = "" class YPushButtonGtk(YWidget): def __init__(self, parent=None, label=""): @@ -513,7 +576,10 @@ def label(self): def setLabel(self, label): self._label = label if self._backend_widget: - self._backend_widget.set_label(label) + try: + self._backend_widget.set_label(label) + except Exception: + pass def _create_backend_widget(self): self._backend_widget = Gtk.Button(label=self._label) @@ -525,17 +591,20 @@ def _create_backend_widget(self): self._backend_widget.set_halign(Gtk.Align.START) except Exception: pass - self._backend_widget.connect("clicked", self._on_clicked) + try: + self._backend_widget.connect("clicked", self._on_clicked) + except Exception: + pass def _on_clicked(self, button): if self.notify() is False: return - # Post a YWidgetEvent to the containing dialog (walk parents) dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) else: - print(f"Button clicked (no dialog found): {self._label}") + # silent fallback + pass class YCheckBoxGtk(YWidget): def __init__(self, parent=None, label="", is_checked=False): @@ -552,27 +621,32 @@ def value(self): def setValue(self, checked): self._is_checked = checked if self._backend_widget: - self._backend_widget.set_active(checked) + try: + self._backend_widget.set_active(checked) + except Exception: + pass def label(self): return self._label def _create_backend_widget(self): self._backend_widget = Gtk.CheckButton(label=self._label) - self._backend_widget.set_active(self._is_checked) - self._backend_widget.connect("toggled", self._on_toggled) + try: + self._backend_widget.set_active(self._is_checked) + self._backend_widget.connect("toggled", self._on_toggled) + except Exception: + pass def _on_toggled(self, button): - # Update internal state - self._is_checked = button.get_active() + try: + self._is_checked = button.get_active() + except Exception: + self._is_checked = bool(self._is_checked) if self.notify(): - # Post a YWidgetEvent to the containing dialog dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - else: - print(f"Checkbox toggled (no dialog found): {self._label} = {self._is_checked}") class YComboBoxGtk(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): @@ -581,6 +655,7 @@ def __init__(self, parent=None, label="", editable=False): self._editable = editable self._value = "" self._selected_items = [] + self._combo_widget = None def widgetClass(self): return "YComboBox" @@ -589,30 +664,24 @@ def value(self): return self._value def setValue(self, text): - # Always update internal value self._value = text - # If backend combo already exists, update it immediately - if hasattr(self, '_combo_widget') and self._combo_widget: + if self._combo_widget: try: + # try entry child for editable combos + child = None if self._editable: - # For editable ComboBoxText with entry - entry = self._combo_widget.get_child() - if entry: - entry.set_text(text) + child = self._combo_widget.get_child() + if child and hasattr(child, "set_text"): + child.set_text(text) else: - # Find and select the item - for i, item in enumerate(self._items): - if item.label() == text: - self._combo_widget.set_active(i) - break - # Update selected_items to reflect new value - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break - except Exception: - # be defensive if widget not fully initialized + # attempt to set active by matching text if API available + if hasattr(self._combo_widget, "set_active_id"): + # Gtk.DropDown uses ids in models; we keep simple and try to match by text + # fallback: rebuild model and select programmatically below + pass + # update selected_items + self._selected_items = [it for it in self._items if it.label() == text][:1] + except Exception: pass def editable(self): @@ -623,96 +692,157 @@ def _create_backend_widget(self): if self._label: label = Gtk.Label(label=self._label) - label.set_xalign(0.0) - hbox.pack_start(label, False, False, 0) + try: + if hasattr(label, "set_xalign"): + label.set_xalign(0.0) + except Exception: + pass + try: + hbox.append(label) + except Exception: + hbox.add(label) + # For Gtk4 there is no ComboBoxText; try DropDown for non-editable, + # and Entry for editable combos (simple fallback). if self._editable: - # Create a ComboBoxText that is editable - combo = Gtk.ComboBoxText.new_with_entry() - entry = combo.get_child() - if entry: - entry.connect("changed", self._on_text_changed) + entry = Gtk.Entry() + entry.set_text(self._value) + entry.connect("changed", self._on_text_changed) + self._combo_widget = entry + try: + hbox.append(entry) + except Exception: + hbox.add(entry) else: - combo = Gtk.ComboBoxText() - combo.connect("changed", self._on_changed) - - # Add items to combo box - for item in self._items: - combo.append_text(item.label()) - - # If a value was set prior to widget creation, apply it now - if self._value: + # Build a simple Gtk.DropDown backed by a Gtk.StringList (if available) try: - if self._editable: - entry = combo.get_child() - if entry: - entry.set_text(self._value) + if hasattr(Gtk, "StringList") and hasattr(Gtk, "DropDown"): + model = Gtk.StringList() + for it in self._items: + model.append(it.label()) + dropdown = Gtk.DropDown.new(model, None) + # select initial value + if self._value: + for idx, it in enumerate(self._items): + if it.label() == self._value: + dropdown.set_selected(idx) + break + dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) + self._combo_widget = dropdown + hbox.append(dropdown) else: - for i, item in enumerate(self._items): - if item.label() == self._value: - combo.set_active(i) - break - # update selected_items - self._selected_items = [] - for item in self._items: - if item.label() == self._value: - self._selected_items.append(item) - break + # fallback: simple Gtk.Button that cycles items on click (very simple) + btn = Gtk.Button(label=self._value or (self._items[0].label() if self._items else "")) + btn.connect("clicked", self._on_fallback_button_clicked) + self._combo_widget = btn + hbox.append(btn) except Exception: - pass + # final fallback: entry + entry = Gtk.Entry() + entry.set_text(self._value) + entry.connect("changed", self._on_text_changed) + self._combo_widget = entry + hbox.append(entry) - hbox.pack_start(combo, True, True, 0) self._backend_widget = hbox - self._combo_widget = combo + + def _on_fallback_button_clicked(self, btn): + # naive cycle through items + if not self._items: + return + current = btn.get_label() + labels = [it.label() for it in self._items] + try: + idx = labels.index(current) + idx = (idx + 1) % len(labels) + except Exception: + idx = 0 + new = labels[idx] + btn.set_label(new) + self.setValue(new) + if self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) def _on_text_changed(self, entry): - # editable combo: update value and notify dialog try: text = entry.get_text() except Exception: text = "" self._value = text - # update selected items (may be none for free text) - self._selected_items = [] - for item in self._items: - if item.label() == self._value: - self._selected_items.append(item) - break + self._selected_items = [it for it in self._items if it.label() == self._value][:1] if self.notify(): - # Post selection-changed event + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def _on_changed_dropdown(self, dropdown): + try: + # Prefer using the selected index to get a reliable label + idx = None try: - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + idx = dropdown.get_selected() except Exception: - pass + idx = None - def _on_changed(self, combo): - # non-editable combo: selection changed via index - try: - active_id = combo.get_active() - if active_id >= 0: - val = combo.get_active_text() + if isinstance(idx, int) and 0 <= idx < len(self._items): + self._value = self._items[idx].label() else: - val = "" + # Fallback: try to extract text from the selected-item object + val = None + try: + val = dropdown.get_selected_item() + except Exception: + val = None + + self._value = "" + if isinstance(val, str): + self._value = val + elif val is not None: + # Try common accessor names that GTK objects may expose + for meth in ("get_string", "get_text", "get_value", "get_label", "get_name", "to_string"): + try: + fn = getattr(val, meth, None) + if callable(fn): + v = fn() + if isinstance(v, str) and v: + self._value = v + break + except Exception: + continue + # Try properties if available + if not self._value: + try: + props = getattr(val, "props", None) + if props: + for attr in ("string", "value", "label", "name", "text"): + try: + pv = getattr(props, attr) + if isinstance(pv, str) and pv: + self._value = pv + break + except Exception: + pass + except Exception: + pass + # final fallback to str() + if not self._value: + try: + self._value = str(val) + except Exception: + self._value = "" + + # update selected_items using reliable labels + self._selected_items = [it for it in self._items if it.label() == self._value][:1] except Exception: - val = "" - - if val: - self._value = val - # Update selected items - self._selected_items = [] - for item in self._items: - if item.label() == self._value: - self._selected_items.append(item) - break - # Post selection-changed event to containing dialog - try: - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass + pass + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + class YSelectionBoxGtk(YSelectionWidget): def __init__(self, parent=None, label=""): @@ -721,8 +851,7 @@ def __init__(self, parent=None, label=""): self._value = "" self._selected_items = [] self._multi_selection = False - self._treeview = None - self._liststore = None + self._listbox = None self._backend_widget = None self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) @@ -739,26 +868,28 @@ def value(self): def setValue(self, text): """Select first item matching text.""" self._value = text - # Update internal selected_items self._selected_items = [it for it in self._items if it.label() == text] - if self._treeview is None: + if self._listbox is None: return - # Select matching row in the TreeView - sel = self._treeview.get_selection() - sel.unselect_all() - for i, it in enumerate(self._items): - if it.label() == text: - sel.select_path(Gtk.TreePath.new_from_string(str(i))) + # find and select corresponding row + children = self._listbox.get_children() + for i, row in enumerate(children): + if i >= len(self._items): + continue + if self._items[i].label() == text: + try: + row.set_selectable(True) + row.set_selected(True) + except Exception: + pass break - # notify via handler - self._on_selection_changed(sel) + # notify + self._on_selection_changed() def selectedItems(self): return list(self._selected_items) def selectItem(self, item, selected=True): - """Programmatically select/deselect a specific item.""" - # Update internal state even if widget not yet created if selected: if not self._multi_selection: self._selected_items = [item] @@ -771,120 +902,108 @@ def selectItem(self, item, selected=True): self._selected_items.remove(item) self._value = self._selected_items[0].label() if self._selected_items else "" - if self._treeview is None: + if self._listbox is None: return - # Reflect change in UI - sel = self._treeview.get_selection() - # find index - idx = None + # reflect change in UI + children = self._listbox.get_children() for i, it in enumerate(self._items): if it is item or it.label() == item.label(): - idx = i + try: + row = children[i] + row.set_selected(selected) + except Exception: + pass break - if idx is None: - return - path = Gtk.TreePath.new_from_string(str(idx)) - if selected: - sel.select_path(path) - else: - sel.unselect_path(path) - # notify via handler - self._on_selection_changed(sel) + self._on_selection_changed() def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) - if self._treeview is None: + if self._listbox is None: return - sel = self._treeview.get_selection() - mode = Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE - sel.set_mode(mode) - # If disabling multi-selection, ensure only first remains selected - if not self._multi_selection: - paths, model = sel.get_selected_rows() - if len(paths) > 1: - first = paths[0] - sel.unselect_all() - sel.select_path(first) - self._on_selection_changed(sel) + # Gtk.ListBox selection handling is manual; we simply keep _multi_selection flag + # and adjust selection behaviour in _on_row_selected. def multiSelection(self): return bool(self._multi_selection) def _create_backend_widget(self): - # Container with optional label and a TreeView for (multi-)selection vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) if self._label: lbl = Gtk.Label(label=self._label) - lbl.set_xalign(0.0) - vbox.pack_start(lbl, False, False, 0) + try: + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + except Exception: + pass + try: + vbox.append(lbl) + except Exception: + vbox.add(lbl) - # ListStore with one string column - self._liststore = Gtk.ListStore(str) + # Use Gtk.ListBox inside a ScrolledWindow for Gtk4 + listbox = Gtk.ListBox() + listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE) + # populate rows for it in self._items: - self._liststore.append([it.label()]) - - treeview = Gtk.TreeView(model=self._liststore) - renderer = Gtk.CellRendererText() - col = Gtk.TreeViewColumn("", renderer, text=0) - treeview.append_column(col) - treeview.set_headers_visible(False) - - sel = treeview.get_selection() - mode = Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE - sel.set_mode(mode) - sel.connect("changed", self._on_selection_changed) - - # If a value was previously set, apply it - if self._value: - for i, it in enumerate(self._items): - if it.label() == self._value: - sel.select_path(Gtk.TreePath.new_from_string(str(i))) - break + row = Gtk.ListBoxRow() + lbl = Gtk.Label(label=it.label()) + try: + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + except Exception: + pass + try: + row.set_child(lbl) + except Exception: + try: + row.add(lbl) + except Exception: + pass + listbox.append(row) + + # connect selection signal + try: + listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + except Exception: + pass sw = Gtk.ScrolledWindow() - sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - sw.add(treeview) - vbox.pack_start(sw, True, True, 0) + # policy APIs changed in Gtk4: use set_overlay_scrolling and set_min_content_height if needed + try: + sw.set_child(listbox) + except Exception: + try: + sw.add(listbox) + except Exception: + pass + + try: + vbox.append(sw) + except Exception: + vbox.add(sw) self._backend_widget = vbox - self._treeview = treeview + self._listbox = listbox - def _on_selection_changed(self, selection): - # Selection may be either Gtk.TreeSelection (from signal) or Gtk.TreeSelection object passed - if isinstance(selection, Gtk.TreeSelection): - sel = selection - else: - # If called programmatically with a non-selection, try to fetch current selection - if self._treeview is None: - return - sel = self._treeview.get_selection() - - # Robustly build selected items by checking each known row path. - # This avoids corner cases with path types returned by get_selected_rows() - # and ensures indices align with self._items. + def _on_row_selected(self, listbox, row): + # Build selected items list from listbox rows' selected state self._selected_items = [] - if self._treeview is None or self._liststore is None: - return - for i, it in enumerate(self._items): + children = listbox.get_children() + for i, r in enumerate(children): try: - path = Gtk.TreePath.new_from_string(str(i)) - if sel.path_is_selected(path): - self._selected_items.append(it) + if r.get_selected(): + if i < len(self._items): + self._selected_items.append(self._items[i]) except Exception: - # ignore malformed paths or selection APIs we can't query - continue + pass if self._selected_items: self._value = self._selected_items[0].label() else: self._value = "" - # Post selection-changed event to containing dialog if notifications enabled - try: - if getattr(self, "notify", lambda: True)(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass \ No newline at end of file + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) From 4f6a3757c483e215560d90582677ad38a7bf8be5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 18:00:34 +0100 Subject: [PATCH 050/523] FIxing SelectionBox gtk --- manatools/aui/yui_gtk.py | 144 +++++++++++++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 22 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 553ba40..59faa90 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -853,6 +853,11 @@ def __init__(self, parent=None, label=""): self._multi_selection = False self._listbox = None self._backend_widget = None + # keep a stable list of rows we create so we don't rely on ListBox container APIs + # (GTK4 bindings may not expose get_children()) + self._rows = [] + # Preferred visible rows for layout/paging; parent can give more space when stretchable + self._preferred_rows = 6 self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) @@ -871,18 +876,20 @@ def setValue(self, text): self._selected_items = [it for it in self._items if it.label() == text] if self._listbox is None: return - # find and select corresponding row - children = self._listbox.get_children() - for i, row in enumerate(children): + # find and select corresponding row using the cached rows list + for i, row in enumerate(getattr(self, "_rows", [])): if i >= len(self._items): continue - if self._items[i].label() == text: - try: + try: + if self._items[i].label() == text: row.set_selectable(True) row.set_selected(True) - except Exception: - pass - break + else: + # ensure others are not selected in single-selection mode + if not self._multi_selection: + row.set_selected(False) + except Exception: + pass # notify self._on_selection_changed() @@ -906,11 +913,11 @@ def selectItem(self, item, selected=True): return # reflect change in UI - children = self._listbox.get_children() + rows = getattr(self, "_rows", []) for i, it in enumerate(self._items): if it is item or it.label() == item.label(): try: - row = children[i] + row = rows[i] row.set_selected(selected) except Exception: pass @@ -944,10 +951,17 @@ def _create_backend_widget(self): # Use Gtk.ListBox inside a ScrolledWindow for Gtk4 listbox = Gtk.ListBox() listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE) + # allow listbox to expand if parent allocates more space + try: + listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + pass # populate rows + self._rows = [] for it in self._items: row = Gtk.ListBoxRow() - lbl = Gtk.Label(label=it.label()) + lbl = Gtk.Label(label=it.label() or "") try: if hasattr(lbl, "set_xalign"): lbl.set_xalign(0.0) @@ -960,15 +974,37 @@ def _create_backend_widget(self): row.add(lbl) except Exception: pass + # If this item matches current value, mark selected + try: + if self._value and it.label() == self._value: + row.set_selectable(True) + row.set_selected(True) + except Exception: + pass + self._rows.append(row) listbox.append(row) # connect selection signal try: - listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + listbox.connect("row-activated", self._on_row_activated) except Exception: pass sw = Gtk.ScrolledWindow() + # allow scrolled window to expand vertically and horizontally + try: + sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + # give a reasonable minimum content height so layout initially shows several rows; + # Gtk4 expects pixels — try a conservative estimate (rows * ~20px) + min_h = int(getattr(self, "_preferred_rows", 6) * 20) + try: + # some Gtk4 bindings expose set_min_content_height + sw.set_min_content_height(min_h) + except Exception: + pass + except Exception: + pass # policy APIs changed in Gtk4: use set_overlay_scrolling and set_min_content_height if needed try: sw.set_child(listbox) @@ -978,6 +1014,13 @@ def _create_backend_widget(self): except Exception: pass + # also request vexpand on the outer vbox so parent layout sees it can grow + try: + vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + pass + try: vbox.append(sw) except Exception: @@ -986,22 +1029,79 @@ def _create_backend_widget(self): self._backend_widget = vbox self._listbox = listbox - def _on_row_selected(self, listbox, row): - # Build selected items list from listbox rows' selected state + def _on_row_activated(self, listbox, row): + """ + Update internal selected items list. + + The 'row' argument may be the affected row (or None). Use it when available, + otherwise probe all cached rows with robust checks. + """ self._selected_items = [] - children = listbox.get_children() - for i, r in enumerate(children): + + # Helper to test if a row is selected in a robust way and to set selection. + def _row_is_selected(r): try: - if r.get_selected(): - if i < len(self._items): - self._selected_items.append(self._items[i]) + return bool(r.get_selected()) except Exception: - pass + # fallback to an internal flag if the binding doesn't expose get_selected() + return bool(getattr(r, "_selected_flag", False)) + + def _set_row_selected(r, val): + try: + r.set_selected(bool(val)) + except Exception: + try: + setattr(r, "_selected_flag", bool(val)) + except Exception: + pass + + if row is not None: + try: + idx = self._rows.index(row) + except Exception: + idx = None + + if idx is not None: + if not self._multi_selection: + # single-selection: clear others, select this one + for i, r in enumerate(getattr(self, "_rows", [])): + try: + _set_row_selected(r, i == idx) + except Exception: + pass + if idx < len(self._items): + self._selected_items = [self._items[idx]] + else: + # multi-selection: toggle this row selection + try: + cur = _row_is_selected(row) + _set_row_selected(row, not cur) + except Exception: + pass + # rebuild selected list + for i, r in enumerate(getattr(self, "_rows", [])): + try: + if _row_is_selected(r) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + else: + # fallback: scan all rows + for i, r in enumerate(getattr(self, "_rows", [])): + try: + if _row_is_selected(r) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + # normalize value: first selected label or None if self._selected_items: - self._value = self._selected_items[0].label() + try: + self._value = self._selected_items[0].label() + except Exception: + self._value = None else: - self._value = "" + self._value = None if self.notify(): dlg = self.findDialog() From 4f129482ffa9cb8668249d4340ed67d39898b05b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 18:57:56 +0100 Subject: [PATCH 051/523] Fixing multiselection in selection box --- manatools/aui/yui_gtk.py | 224 ++++++++++++++++++++++++++++----------- 1 file changed, 161 insertions(+), 63 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 59faa90..384b5b2 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -926,10 +926,47 @@ def selectItem(self, item, selected=True): def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) + # If listbox already created, update its selection mode at runtime. if self._listbox is None: return - # Gtk.ListBox selection handling is manual; we simply keep _multi_selection flag - # and adjust selection behaviour in _on_row_selected. + try: + mode = Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE + self._listbox.set_selection_mode(mode) + except Exception: + pass + # Rewire signals: disconnect previous handlers and connect appropriate one. + try: + # Disconnect any previously stored handlers + try: + for key, hid in list(getattr(self, "_signal_handlers", {}).items()): + if hid and isinstance(hid, int): + try: + self._listbox.disconnect(hid) + except Exception: + pass + self._signal_handlers = {} + except Exception: + self._signal_handlers = {} + + # Connect new handler based on mode + if self._multi_selection: + try: + hid = self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) + self._signal_handlers['selected-rows-changed'] = hid + except Exception: + try: + hid = self._listbox.connect("row-selected", lambda lb, row: self._on_selected_rows_changed(lb)) + self._signal_handlers['row-selected_for_multi'] = hid + except Exception: + pass + else: + try: + hid = self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + self._signal_handlers['row-selected'] = hid + except Exception: + pass + except Exception: + pass def multiSelection(self): return bool(self._multi_selection) @@ -974,6 +1011,13 @@ def _create_backend_widget(self): row.add(lbl) except Exception: pass + + # Make every row selectable so users can multi-select if mode allows. + try: + row.set_selectable(True) + except Exception: + pass + # If this item matches current value, mark selected try: if self._value and it.label() == self._value: @@ -984,12 +1028,6 @@ def _create_backend_widget(self): self._rows.append(row) listbox.append(row) - # connect selection signal - try: - listbox.connect("row-activated", self._on_row_activated) - except Exception: - pass - sw = Gtk.ScrolledWindow() # allow scrolled window to expand vertically and horizontally try: @@ -1026,81 +1064,141 @@ def _create_backend_widget(self): except Exception: vbox.add(sw) - self._backend_widget = vbox - self._listbox = listbox - - def _on_row_activated(self, listbox, row): - """ - Update internal selected items list. - - The 'row' argument may be the affected row (or None). Use it when available, - otherwise probe all cached rows with robust checks. - """ - self._selected_items = [] - - # Helper to test if a row is selected in a robust way and to set selection. - def _row_is_selected(r): + # connect selection signal: choose appropriate signal per selection mode + # store handler ids so we can disconnect later if selection mode changes at runtime + self._signal_handlers = {} + try: + # ensure any previous handlers are disconnected (defensive) try: - return bool(r.get_selected()) + for hid in list(self._signal_handlers.values()): + if hid and isinstance(hid, int): + try: + listbox.disconnect(hid) + except Exception: + pass except Exception: - # fallback to an internal flag if the binding doesn't expose get_selected() - return bool(getattr(r, "_selected_flag", False)) + pass - def _set_row_selected(r, val): - try: - r.set_selected(bool(val)) - except Exception: + if self._multi_selection: + # Prefer a bulk selection-changed signal when available in bindings try: - setattr(r, "_selected_flag", bool(val)) + hid = listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) + self._signal_handlers['selected-rows-changed'] = hid except Exception: - pass + # fallback to row-selected if the other signal isn't available + hid = listbox.connect("row-selected", lambda lb, row: self._on_selected_rows_changed(lb)) + self._signal_handlers['row-selected_for_multi'] = hid + else: + # single selection: react to row-selected and enforce single selection semantics + hid = listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + self._signal_handlers['row-selected'] = hid + except Exception: + pass - if row is not None: - try: - idx = self._rows.index(row) - except Exception: - idx = None + self._backend_widget = vbox + self._listbox = listbox - if idx is not None: - if not self._multi_selection: - # single-selection: clear others, select this one - for i, r in enumerate(getattr(self, "_rows", [])): - try: - _set_row_selected(r, i == idx) - except Exception: - pass - if idx < len(self._items): - self._selected_items = [self._items[idx]] - else: - # multi-selection: toggle this row selection + def _row_is_selected(self, r): + """Robust helper to detect whether a ListBoxRow is selected.""" + try: + return bool(r.get_selected()) + except Exception: + pass + try: + props = getattr(r, "props", None) + if props and hasattr(props, "selected"): + return bool(getattr(props, "selected")) + except Exception: + pass + return bool(getattr(r, "_selected_flag", False)) + + def _on_row_selected(self, listbox, row): + """ + Handler for single-selection mode. Ensure only the provided row is selected + and update internal state accordingly. + """ + try: + # If a row was provided, enforce single-selection: deselect others + if row is not None: + for r in getattr(self, "_rows", []): try: - cur = _row_is_selected(row) - _set_row_selected(row, not cur) + r.set_selected(r is row) except Exception: - pass - # rebuild selected list - for i, r in enumerate(getattr(self, "_rows", [])): + # fallback flag try: - if _row_is_selected(r) and i < len(self._items): - self._selected_items.append(self._items[i]) + setattr(r, "_selected_flag", (r is row)) except Exception: pass - else: - # fallback: scan all rows + + # rebuild selected_items scanning cached rows (defensive) + self._selected_items = [] for i, r in enumerate(getattr(self, "_rows", [])): try: - if _row_is_selected(r) and i < len(self._items): + if self._row_is_selected(r) and i < len(self._items): self._selected_items.append(self._items[i]) except Exception: pass - # normalize value: first selected label or None - if self._selected_items: + self._value = self._selected_items[0].label() if self._selected_items else None + except Exception: + # be defensive + self._selected_items = [] + self._value = None + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def _on_selected_rows_changed(self, listbox): + """ + Handler for multi-selection (or bulk selection change). Rebuild selected list + using either ListBox APIs (if available) or by scanning cached rows. + """ + try: + # Try to use any available API that returns selected rows + sel_rows = None try: - self._value = self._selected_items[0].label() + # Some bindings may provide get_selected_rows() + sel_rows = listbox.get_selected_rows() except Exception: - self._value = None - else: + sel_rows = None + + self._selected_items = [] + if sel_rows: + # sel_rows may be list of Row objects or Paths; try to match by identity + for r in sel_rows: + try: + # if r is a ListBoxRow already + if isinstance(r, type(self._rows[0])) if self._rows else False: + try: + idx = self._rows.index(r) + if idx < len(self._items): + self._selected_items.append(self._items[idx]) + except Exception: + pass + else: + # fallback: scan cached rows to find selected ones + for i, cr in enumerate(getattr(self, "_rows", [])): + try: + if self._row_is_selected(cr) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + except Exception: + pass + else: + # Generic fallback: scan cached rows and collect selected ones + for i, r in enumerate(getattr(self, "_rows", [])): + try: + if self._row_is_selected(r) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + + self._value = self._selected_items[0].label() if self._selected_items else None + except Exception: + self._selected_items = [] self._value = None if self.notify(): From 013a68e1c353176b61330893a54c81ad4c81b83a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 19:40:06 +0100 Subject: [PATCH 052/523] Managed signal connection and disconnection fro multi selection --- manatools/aui/yui_gtk.py | 48 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 384b5b2..56761bb 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -951,7 +951,7 @@ def setMultiSelection(self, enabled): # Connect new handler based on mode if self._multi_selection: try: - hid = self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) + hid = self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) self._signal_handlers['selected-rows-changed'] = hid except Exception: try: @@ -1079,19 +1079,12 @@ def _create_backend_widget(self): except Exception: pass - if self._multi_selection: - # Prefer a bulk selection-changed signal when available in bindings - try: - hid = listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) - self._signal_handlers['selected-rows-changed'] = hid - except Exception: - # fallback to row-selected if the other signal isn't available - hid = listbox.connect("row-selected", lambda lb, row: self._on_selected_rows_changed(lb)) - self._signal_handlers['row-selected_for_multi'] = hid - else: - # single selection: react to row-selected and enforce single selection semantics + # Use row-selected for both single and multi modes; handler will toggle for multi + try: hid = listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) self._signal_handlers['row-selected'] = hid + except Exception: + pass except Exception: pass @@ -1114,23 +1107,35 @@ def _row_is_selected(self, r): def _on_row_selected(self, listbox, row): """ - Handler for single-selection mode. Ensure only the provided row is selected - and update internal state accordingly. + Handler for row selection. In single-selection mode behaves as before + (select provided row and deselect others). In multi-selection mode toggles + the provided row and rebuilds the selected items list. """ try: - # If a row was provided, enforce single-selection: deselect others if row is not None: - for r in getattr(self, "_rows", []): + if self._multi_selection: + # toggle selection state for this row try: - r.set_selected(r is row) + cur = self._row_is_selected(row) + try: + row.set_selected(not cur) + except Exception: + # fallback: store a flag when set_selected isn't available + setattr(row, "_selected_flag", not cur) except Exception: - # fallback flag + pass + else: + # single-selection: select provided row and deselect others + for r in getattr(self, "_rows", []): try: - setattr(r, "_selected_flag", (r is row)) + r.set_selected(r is row) except Exception: - pass + try: + setattr(r, "_selected_flag", (r is row)) + except Exception: + pass - # rebuild selected_items scanning cached rows (defensive) + # rebuild selected_items scanning cached rows (works for both modes) self._selected_items = [] for i, r in enumerate(getattr(self, "_rows", [])): try: @@ -1161,6 +1166,7 @@ def _on_selected_rows_changed(self, listbox): try: # Some bindings may provide get_selected_rows() sel_rows = listbox.get_selected_rows() + print(f"Using get_selected_rows() {len(sel_rows)} API") except Exception: sel_rows = None From 84a5b8b5a154f0f1776a8674025d90d7377bad44 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 22:00:40 +0100 Subject: [PATCH 053/523] Moving to PySide6 --- manatools/aui/yui_qt.py | 47 +++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 93a8dbf..59f444c 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -3,7 +3,7 @@ """ import sys -from PyQt5 import QtWidgets, QtCore, QtGui +from PySide6 import QtWidgets, QtCore, QtGui from .yui_common import * class YUIQt: @@ -175,9 +175,9 @@ def open(self): self._qwidget.show() self._is_open = True - + def isOpen(self): - return self._is_open + return self._is_open def destroy(self, doThrow=True): if self._qwidget: @@ -294,7 +294,8 @@ def on_timeout(): timer.timeout.connect(on_timeout) timer.start(timeout_millisec) - loop.exec_() + # PySide6 / Qt6 uses exec() + loop.exec() # cleanup if timer and timer.isActive(): @@ -461,13 +462,25 @@ def _create_backend_widget(self): self._backend_widget = QtWidgets.QPushButton(self._label) # Set size policy to prevent unwanted expansion try: - sp = self._backend_widget.sizePolicy() - # Prefer minimal size in both dimensions - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) - #sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) - self._backend_widget.setSizePolicy(sp) + try: + sp = self._backend_widget.sizePolicy() + # PySide6 may expect enum class; try both styles defensively + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Minimum) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) + except Exception: + pass + self._backend_widget.setSizePolicy(sp) + except Exception: + try: + # fallback: set using convenience form (two args) + self._backend_widget.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + except Exception: + pass except Exception: - pass + pass self._backend_widget.clicked.connect(self._on_clicked) def _on_clicked(self): @@ -494,7 +507,15 @@ def value(self): def setValue(self, checked): self._is_checked = checked if self._backend_widget: - self._backend_widget.setChecked(checked) + try: + # avoid emitting signals while programmatically changing state + self._backend_widget.blockSignals(True) + self._backend_widget.setChecked(checked) + finally: + try: + self._backend_widget.blockSignals(False) + except Exception: + pass def label(self): return self._label @@ -507,8 +528,8 @@ def _create_backend_widget(self): def _on_state_changed(self, state): # Update internal state # state is QtCore.Qt.CheckState (Unchecked=0, PartiallyChecked=1, Checked=2) - self._is_checked = (state == QtCore.Qt.Checked) - + self._is_checked = (QtCore.Qt.CheckState(state) == QtCore.Qt.CheckState.Checked) + if self.notify(): # Post a YWidgetEvent to the containing dialog dlg = self.findDialog() From 8c1c6714560296ed2b08f498cf6452114355b4b6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 22:04:33 +0100 Subject: [PATCH 054/523] Removed a print for debug --- manatools/aui/yui_qt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 59f444c..ba12bf2 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -57,7 +57,6 @@ def setApplicationTitle(self, title): try: app = QtWidgets.QApplication.instance() if app: - print(f"YApplicationQt: setting QApplication applicationName to '{title}'") app.setApplicationName(title) top_level_widgets = app.topLevelWidgets() From 6f303f99ae367e46d320138656dae94cc1741219 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 22:40:49 +0100 Subject: [PATCH 055/523] added application icon --- manatools/aui/yui_qt.py | 122 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index ba12bf2..c395711 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -4,6 +4,7 @@ import sys from PySide6 import QtWidgets, QtCore, QtGui +import os from .yui_common import * class YUIQt: @@ -36,7 +37,9 @@ def __init__(self): self._application_title = "manatools Qt Application" self._product_name = "manatools YUI Qt" self._icon_base_path = None - self._icon = "" + self._icon = "" + # cached QIcon resolved from _icon (None if not resolved) + self._qt_icon = None def iconBasePath(self): return self._icon_base_path @@ -68,6 +71,84 @@ def setApplicationTitle(self, title): except Exception: pass + def _resolve_qicon(self, icon_spec): + """Resolve icon_spec (path or theme name) into a QtGui.QIcon or None. + If iconBasePath is set, prefer that as absolute path base. + """ + if not icon_spec: + return None + # if we have a base path and the spec is not absolute, try that first + try: + if self._icon_base_path: + cand = icon_spec + if not os.path.isabs(cand): + cand = os.path.join(self._icon_base_path, icon_spec) + if os.path.exists(cand): + return QtGui.QIcon(cand) + # if icon_spec looks like an absolute path, try it + if os.path.isabs(icon_spec) and os.path.exists(icon_spec): + return QtGui.QIcon(icon_spec) + except Exception: + pass + # fallback to theme lookup + try: + theme_icon = QtGui.QIcon.fromTheme(icon_spec) + if not theme_icon.isNull(): + return theme_icon + except Exception: + pass + return None + + def setApplicationIcon(self, Icon): + """Set application icon spec (theme name or path). Try to apply it to QApplication and active dialogs. + + If iconBasePath is set, icon is considered relative to that path and will force local file usage. + """ + try: + self._icon = Icon + except Exception: + self._icon = "" + # resolve into a QIcon and cache + try: + self._qt_icon = self._resolve_qicon(self._icon) + except Exception: + self._qt_icon = None + + # apply to global QApplication + try: + app = QtWidgets.QApplication.instance() + if app and self._qt_icon: + try: + app.setWindowIcon(self._qt_icon) + except Exception: + pass + except Exception: + pass + + # apply to any open YDialogQt windows (if backend used) + try: + # avoid importing the whole module if not available + from . import yui as yui_mod + # try to update dialogs known in YDialogQt._open_dialogs + dlg_cls = getattr(yui_mod, "YDialogQt", None) + if dlg_cls is None: + # fallback to import local symbol if module structure different + dlg_cls = globals().get("YDialogQt", None) + if dlg_cls is not None: + for dlg in getattr(dlg_cls, "_open_dialogs", []) or []: + try: + w = getattr(dlg, "_qwidget", None) + if w and self._qt_icon: + try: + w.setWindowIcon(self._qt_icon) + except Exception: + pass + except Exception: + pass + except Exception: + # best-effort; ignore failures + pass + def applicationTitle(self): """Get the application title.""" return self._application_title @@ -225,11 +306,48 @@ def _create_backend_widget(self): atitle = appobj.applicationTitle() if atitle: title = atitle + # try to obtain a resolved QIcon from the application backend if available + app_qicon = None + if appobj: + # prefer cached Qt icon if set by setApplicationIcon + app_qicon = getattr(appobj, "_qt_icon", None) + # otherwise try to resolve applicationIcon string on the fly + if not app_qicon: + try: + icon_spec = appobj.applicationIcon() + if icon_spec: + # use the application's iconBasePath if present + base = getattr(appobj, "_icon_base_path", None) + if base and not os.path.isabs(icon_spec): + p = os.path.join(base, icon_spec) + if os.path.exists(p): + app_qicon = QtGui.QIcon(p) + if not app_qicon: + q = QtGui.QIcon.fromTheme(icon_spec) + if not q.isNull(): + app_qicon = q + except Exception: + pass + # if we have a qicon, set it on the QApplication and the new window + if app_qicon: + try: + qapp = QtWidgets.QApplication.instance() + if qapp: + qapp.setWindowIcon(app_qicon) + except Exception: + pass + # store resolved qicon locally to apply to this window + _resolved_qicon = app_qicon except Exception: # ignore and keep default - pass + _resolved_qicon = None self._qwidget.setWindowTitle(title) + try: + if _resolved_qicon: + self._qwidget.setWindowIcon(_resolved_qicon) + except Exception: + pass self._qwidget.resize(600, 400) central_widget = QtWidgets.QWidget() From 503a6104f48d5d6c10e9f49cef30e44c6fe42064 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 22:58:05 +0100 Subject: [PATCH 056/523] First attempt to load a system icon --- manatools/aui/yui_gtk.py | 140 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 56761bb..187d8ac 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -5,8 +5,13 @@ gi.require_version('Gtk', '4.0') from gi.repository import Gtk, GLib import threading +import os +try: + from gi.repository import GdkPixbuf +except Exception: + GdkPixbuf = None from .yui_common import * - + class YUIGtk: def __init__(self): self._widget_factory = YWidgetFactoryGtk() @@ -34,6 +39,48 @@ def __init__(self): self._product_name = "manatools YUI GTK" self._icon_base_path = None self._icon = "" + # cached resolved GdkPixbuf.Pixbuf (or None) + self._gtk_icon_pixbuf = None + + def _resolve_pixbuf(self, icon_spec): + """Resolve icon_spec into a GdkPixbuf.Pixbuf if possible. + Prefer local path resolved against iconBasePath if set, else try theme lookup. + """ + if not icon_spec: + return None + # try explicit path (icon_base_path forced) + try: + # if base path set and icon_spec not absolute, try join + if self._icon_base_path: + cand = icon_spec if os.path.isabs(icon_spec) else os.path.join(self._icon_base_path, icon_spec) + if os.path.exists(cand) and GdkPixbuf is not None: + try: + return GdkPixbuf.Pixbuf.new_from_file(cand) + except Exception: + pass + # try absolute path + if os.path.isabs(icon_spec) and os.path.exists(icon_spec) and GdkPixbuf is not None: + try: + return GdkPixbuf.Pixbuf.new_from_file(icon_spec) + except Exception: + pass + except Exception: + pass + + # fallback: try icon theme lookup + try: + theme = Gtk.IconTheme.get_default() + # request a reasonable size (48) - theme will scale as needed + if theme and theme.has_icon(icon_spec) and GdkPixbuf is not None: + try: + pix = theme.load_icon(icon_spec, 48, 0) + if pix is not None: + return pix + except Exception: + pass + except Exception: + pass + return None def iconBasePath(self): return self._icon_base_path @@ -69,10 +116,68 @@ def setApplicationTitle(self, title): def applicationTitle(self): """Get the application title.""" return self._application_title - + def setApplicationIcon(self, Icon): - self._icon = Icon + """Set application icon spec (theme name or path). If iconBasePath is set, prefer local file.""" + try: + self._icon = Icon or "" + except Exception: + self._icon = "" + # resolve and cache a GdkPixbuf if possible + try: + self._gtk_icon_pixbuf = self._resolve_pixbuf(self._icon) + except Exception: + self._gtk_icon_pixbuf = None + # Try to set a global default icon for windows (best-effort) + try: + if self._gtk_icon_pixbuf is not None: + # Gtk.Window.set_default_icon/from_file may be available + try: + Gtk.Window.set_default_icon(self._gtk_icon_pixbuf) + except Exception: + try: + # try using file path if we resolved one from disk + if self._icon_base_path: + cand = self._icon if os.path.isabs(self._icon) else os.path.join(self._icon_base_path, self._icon) + if os.path.exists(cand): + Gtk.Window.set_default_icon_from_file(cand) + else: + # if _icon was an absolute file + if os.path.isabs(self._icon) and os.path.exists(self._icon): + Gtk.Window.set_default_icon_from_file(self._icon) + except Exception: + pass + except Exception: + pass + + # Apply icon to any open YDialogGtk windows + try: + for dlg in getattr(YDialogGtk, "_open_dialogs", []) or []: + try: + win = getattr(dlg, "_window", None) + if win: + if self._gtk_icon_pixbuf is not None: + try: + # try direct pixbuf assignment + win.set_icon(self._gtk_icon_pixbuf) + except Exception: + try: + # try setting icon name as fallback + win.set_icon_name(self._icon) + except Exception: + pass + else: + # if we have only a name, try icon name + try: + win.set_icon_name(self._icon) + except Exception: + pass + except Exception: + pass + except Exception: + pass + def applicationIcon(self): return self._icon @@ -284,11 +389,40 @@ def _create_backend_widget(self): atitle = appobj.applicationTitle() if atitle: title = atitle + # try to obtain resolved pixbuf from application and store for window icon + _resolved_pixbuf = None + try: + _resolved_pixbuf = getattr(appobj, "_gtk_icon_pixbuf", None) + except Exception: + _resolved_pixbuf = None except Exception: pass # Create Gtk4 Window self._window = Gtk.Window(title=title) + # set window icon if available + try: + if _resolved_pixbuf is not None: + try: + self._window.set_icon(_resolved_pixbuf) + except Exception: + try: + # fallback to name if pixbuf not accepted + icname = getattr(appobj, "applicationIcon", lambda : None)() + if icname: + self._window.set_icon_name(icname) + except Exception: + pass + else: + try: + # try setting icon name if application provided it + icname = getattr(appobj, "applicationIcon", lambda : None)() + if icname: + self._window.set_icon_name(icname) + except Exception: + pass + except Exception: + pass try: self._window.set_default_size(600, 400) except Exception: From 9157b7e778d2c6569423046b066a936f52f49e43 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 16 Nov 2025 23:00:38 +0100 Subject: [PATCH 057/523] added set e get applicaiton icon to be compliant --- manatools/aui/yui_curses.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 08be83b..312bdd3 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -78,6 +78,7 @@ def __init__(self): self._application_title = "manatools Curses Application" self._product_name = "manatools YUI Curses" self._icon_base_path = "" + self._icon = "" def iconBasePath(self): return self._icon_base_path @@ -91,6 +92,14 @@ def setProductName(self, product_name): def productName(self): return self._product_name + def setApplicationIcon(self, Icon): + """Set the application icon.""" + self._icon = Icon + + def applicationIcon(self): + """Get the application icon.""" + return self._icon + def setApplicationTitle(self, title): """Set the application title.""" self._application_title = title From b50c378aa2df8bf4cb116eaf76162aba923bccfe Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 19 Nov 2025 19:34:36 +0100 Subject: [PATCH 058/523] Added start implementation of YAlignment --- manatools/aui/yui_curses.py | 104 ++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 312bdd3..9cc66eb 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -167,6 +167,28 @@ def createComboBox(self, parent, label, editable=False): def createSelectionBox(self, parent, label): return YSelectionBoxCurses(parent, label) + # Alignment helpers + def createLeft(self, parent): + return YAlignmentCurses(parent, horAlign="Left", vertAlign=None) + + def createRight(self, parent): + return YAlignmentCurses(parent, horAlign="Right", vertAlign=None) + + def createTop(self, parent): + return YAlignmentCurses(parent, horAlign=None, vertAlign="Top") + + def createBottom(self, parent): + return YAlignmentCurses(parent, horAlign=None, vertAlign="Bottom") + + def createHCenter(self, parent): + return YAlignmentCurses(parent, horAlign="HCenter", vertAlign=None) + + def createVCenter(self, parent): + return YAlignmentCurses(parent, horAlign=None, vertAlign="VCenter") + + def createHVCenter(self, parent): + return YAlignmentCurses(parent, horAlign="HCenter", vertAlign="VCenter") + # Curses Widget Implementations class YDialogCurses(YSingleChildContainerWidget): @@ -1340,3 +1362,85 @@ def _handle_key(self, key): handled = False return handled + +class YAlignmentCurses(YSingleChildContainerWidget): + """ + Single-child alignment container for ncurses. It becomes stretchable on the + requested axes, and positions the child inside its draw area accordingly. + """ + def __init__(self, parent=None, horAlign=None, vertAlign=None): + super().__init__(parent) + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._backend_widget = None # not used by curses + self._height = 1 + + def widgetClass(self): + return "YAlignment" + + def stretchable(self, dim): + if dim == YUIDimension.YD_HORIZ: + return str(self._halign_spec).lower() in ("right", "hcenter", "hvcenter") + if dim == YUIDimension.YD_VERT: + return str(self._valign_spec).lower() in ("vcenter", "hvcenter") + return False + + def setAlignment(self, horAlign=None, vertAlign=None): + self._halign_spec = horAlign + self._valign_spec = vertAlign + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + + def _create_backend_widget(self): + self._backend_widget = None + self._height = max(1, getattr(self._child, "_height", 1) if self._child else 1) + + def _child_min_width(self, child, max_width): + # Heuristic minimal width similar to YHBoxCurses + try: + cls = child.widgetClass() if hasattr(child, "widgetClass") else "" + if cls in ("YLabel", "YPushButton", "YCheckBox"): + text = getattr(child, "_text", None) + if text is None: + text = getattr(child, "_label", "") + pad = 4 if cls == "YPushButton" else 0 + return min(max_width, max(1, len(str(text)) + pad)) + except Exception: + pass + return max(1, min(10, max_width)) + + def _draw(self, window, y, x, width, height): + if not self._child or not hasattr(self._child, "_draw"): + return + try: + # width to give to the child: minimal needed (so it can be pushed) + ch_min_w = self._child_min_width(self._child, width) + # Horizontal position + hs = str(self._halign_spec).lower() if self._halign_spec else "left" + if hs in ("right",): + cx = x + max(0, width - ch_min_w) + elif hs in ("hcenter", "center", "centre", "hvcenter"): + cx = x + max(0, (width - ch_min_w) // 2) + else: + cx = x + # Vertical position (single line widgets mostly) + vs = str(self._valign_spec).lower() if self._valign_spec else "top" + if vs in ("vcenter", "center", "centre", "hvcenter"): + cy = y + max(0, (height - 1) // 2) + elif vs in ("bottom", "end"): + cy = y + max(0, height - 1) + else: + cy = y + self._child._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, getattr(self._child, "_height", 1))) + except Exception: + pass From 865ae8cd51b92e53aacbe6357e7282de342182a9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 19 Nov 2025 19:35:09 +0100 Subject: [PATCH 059/523] Started implementation of YAligment --- manatools/aui/yui_qt.py | 151 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index c395711..21b8ea7 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -207,6 +207,28 @@ def createProgressBar(self, parent, label, max_value=100): def createComboBox(self, parent, label, editable=False): return YComboBoxQt(parent, label, editable) + # Alignment helpers + def createLeft(self, parent): + return YAlignmentQt(parent, horAlign="Left", vertAlign=None) + + def createRight(self, parent): + return YAlignmentQt(parent, horAlign="Right", vertAlign=None) + + def createTop(self, parent): + return YAlignmentQt(parent, horAlign=None, vertAlign="Top") + + def createBottom(self, parent): + return YAlignmentQt(parent, horAlign=None, vertAlign="Bottom") + + def createHCenter(self, parent): + return YAlignmentQt(parent, horAlign="HCenter", vertAlign=None) + + def createVCenter(self, parent): + return YAlignmentQt(parent, horAlign=None, vertAlign="VCenter") + + def createHVCenter(self, parent): + return YAlignmentQt(parent, horAlign="HCenter", vertAlign="VCenter") + # Qt Widget Implementations class YDialogQt(YSingleChildContainerWidget): _open_dialogs = [] @@ -850,4 +872,131 @@ def _on_selection_changed(self): if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) except Exception: - pass \ No newline at end of file + pass + +class YAlignmentQt(YSingleChildContainerWidget): + """ + Single-child alignment container for Qt6. Uses a QWidget + QGridLayout, + applying Qt.Alignment flags to the child. The container expands along + axes needed by Right/HCenter/VCenter/HVCenter to allow alignment. + """ + def __init__(self, parent=None, horAlign=None, vertAlign=None): + super().__init__(parent) + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._backend_widget = None + self._layout = None + + def widgetClass(self): + return "YAlignment" + + def _to_qt_align(self, spec, axis="h"): + if spec is None: + return None + s = str(getattr(spec, "name", spec)).lower() + if axis == "h": + if s in ("left", "begin", "start"): + return QtCore.Qt.AlignmentFlag.AlignLeft + if s in ("right", "end"): + return QtCore.Qt.AlignmentFlag.AlignRight + if s in ("hcenter", "center", "centre", "hvcenter"): + return QtCore.Qt.AlignmentFlag.AlignHCenter + else: + if s in ("top", "begin", "start"): + return QtCore.Qt.AlignmentFlag.AlignTop + if s in ("bottom", "end"): + return QtCore.Qt.AlignmentFlag.AlignBottom + if s in ("vcenter", "center", "centre", "hvcenter"): + return QtCore.Qt.AlignmentFlag.AlignVCenter + return None + + def stretchable(self, dim): + if dim == YUIDimension.YD_HORIZ: + return str(self._halign_spec).lower() in ("right", "hcenter", "hvcenter") + if dim == YUIDimension.YD_VERT: + return str(self._valign_spec).lower() in ("vcenter", "hvcenter") + return False + + def setAlignment(self, horAlign=None, vertAlign=None): + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._reapply_alignment() + + def _reapply_alignment(self): + if not (self._layout and self._child): + return + try: + w = self._child.get_backend_widget() + if w: + self._layout.removeWidget(w) + flags = QtCore.Qt.AlignmentFlag(0) + ha = self._to_qt_align(self._halign_spec, "h") + va = self._to_qt_align(self._valign_spec, "v") + if ha: + flags |= ha + if va: + flags |= va + self._layout.addWidget(w, 0, 0, flags) + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + if self._backend_widget: + self._attach_child_backend() + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + if self._backend_widget: + self._attach_child_backend() + + def _attach_child_backend(self): + if not (self._backend_widget and self._layout and self._child): + return + try: + w = self._child.get_backend_widget() + if w: + # clear previous + try: + self._layout.removeWidget(w) + except Exception: + pass + flags = QtCore.Qt.AlignmentFlag(0) + ha = self._to_qt_align(self._halign_spec, "h") + va = self._to_qt_align(self._valign_spec, "v") + if ha: + flags |= ha + if va: + flags |= va + self._layout.addWidget(w, 0, 0, flags) + except Exception: + pass + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + grid = QtWidgets.QGridLayout(container) + grid.setContentsMargins(0, 0, 0, 0) + grid.setSpacing(0) + + # Size policy: expand along axes needed for alignment to work + sp = container.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) + else QtWidgets.QSizePolicy.Policy.Preferred) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) + else QtWidgets.QSizePolicy.Policy.Preferred) + except Exception: + pass + container.setSizePolicy(sp) + + self._backend_widget = container + self._layout = grid + + if getattr(self, "_child", None): + self._attach_child_backend() \ No newline at end of file From 87063b772d60f28434123a369515e97670898969 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 19 Nov 2025 19:48:37 +0100 Subject: [PATCH 060/523] fixed foxus on aligned widgets --- manatools/aui/yui_curses.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 9cc66eb..3f68c91 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -1394,12 +1394,38 @@ def addChild(self, child): super().addChild(child) except Exception: self._child = child + # Ensure child is visible to traversal (dialog looks at widget._children) + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + if child not in self._children: + self._children.append(child) + # keep parent pointer consistent + try: + setattr(child, "_parent", self) + except Exception: + pass + except Exception: + pass def setChild(self, child): try: super().setChild(child) except Exception: self._child = child + # Mirror to _children so focus traversal finds it + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + # replace existing children with this single child to avoid stale entries + if self._children != [child]: + self._children = [child] + try: + setattr(child, "_parent", self) + except Exception: + pass + except Exception: + pass def _create_backend_widget(self): self._backend_widget = None From 521d95332b2e4b8e743c5df0f6376961adf7062d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 19 Nov 2025 20:54:39 +0100 Subject: [PATCH 061/523] first attempt to realize YAlignment --- manatools/aui/yui_gtk.py | 156 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 187d8ac..1d689f8 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -222,6 +222,28 @@ def createComboBox(self, parent, label, editable=False): def createSelectionBox(self, parent, label): return YSelectionBoxGtk(parent, label) + # Alignment helpers + def createLeft(self, parent): + return YAlignmentGtk(parent, horAlign="Left", vertAlign=None) + + def createRight(self, parent): + return YAlignmentGtk(parent, horAlign="Right", vertAlign=None) + + def createTop(self, parent): + return YAlignmentGtk(parent, horAlign=None, vertAlign="Top") + + def createBottom(self, parent): + return YAlignmentGtk(parent, horAlign=None, vertAlign="Bottom") + + def createHCenter(self, parent): + return YAlignmentGtk(parent, horAlign="HCenter", vertAlign=None) + + def createVCenter(self, parent): + return YAlignmentGtk(parent, horAlign=None, vertAlign="VCenter") + + def createHVCenter(self, parent): + return YAlignmentGtk(parent, horAlign="HCenter", vertAlign="VCenter") + # GTK4 Widget Implementations class YDialogGtk(YSingleChildContainerWidget): @@ -1345,3 +1367,137 @@ def _on_selected_rows_changed(self, listbox): dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) +class YAlignmentGtk(YSingleChildContainerWidget): + """ + Single-child container that aligns its child using Gtk.Align. + Works reliably inside HBox/VBox: right/center expand horizontally to push content. + """ + def __init__(self, parent=None, horAlign=None, vertAlign=None): + super().__init__(parent) + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._backend_widget = None + + def widgetClass(self): + return "YAlignment" + + def _to_gtk_align(self, spec, axis="h"): + # Accept strings ("Left","Right","HCenter","Top","Bottom","VCenter","HVCenter") + if spec is None: + return None + try: + s = str(getattr(spec, "name", spec)).lower() + except Exception: + s = str(spec).lower() + if axis == "h": + if s in ("left", "begin", "start"): + return Gtk.Align.START + if s in ("right", "end"): + return Gtk.Align.END + if s in ("hcenter", "center", "centre", "hvcenter"): + return Gtk.Align.CENTER + else: + if s in ("top", "begin", "start"): + return Gtk.Align.START + if s in ("bottom", "end"): + return Gtk.Align.END + if s in ("vcenter", "center", "centre", "hvcenter"): + return Gtk.Align.CENTER + if s == "fill": + return Gtk.Align.FILL + return None + + def stretchable(self, dim): + # Expand horizontally when Right/HCenter/HVCenter; vertically for VCenter/HVCenter + if dim == YUIDimension.YD_HORIZ: + return str(self._halign_spec).lower() in ("right", "hcenter", "hvcenter") + if dim == YUIDimension.YD_VERT: + return str(self._valign_spec).lower() in ("vcenter", "hvcenter") + return False + + def setAlignment(self, horAlign=None, vertAlign=None): + self._halign_spec = horAlign + self._valign_spec = vertAlign + # Re-apply if backend exists + if self._backend_widget and self._child: + try: + cw = self._child.get_backend_widget() + hal = self._to_gtk_align(self._halign_spec, "h") + val = self._to_gtk_align(self._valign_spec, "v") + if hal is not None and hasattr(cw, "set_halign"): + cw.set_halign(hal) + if val is not None and hasattr(cw, "set_valign"): + cw.set_valign(val) + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + # If backend already created, attach immediately + if self._backend_widget: + self._attach_child_backend() + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + if self._backend_widget: + self._attach_child_backend() + + def _attach_child_backend(self): + if not self._child or not self._backend_widget: + return + try: + for ch in list(getattr(self._backend_widget, "get_children", lambda: [])()) or []: + # clean previous child if re-attaching + try: + self._backend_widget.remove(ch) + except Exception: + pass + except Exception: + pass + try: + cw = self._child.get_backend_widget() + if cw: + # apply alignment to child + hal = self._to_gtk_align(self._halign_spec, "h") + val = self._to_gtk_align(self._valign_spec, "v") + try: + print(f"YAlignmentGtk: attaching child {self._child.widgetClass()} with halign={hal}, valign={val}") + if hal is not None and hasattr(cw, "set_halign"): + print("YAlignmentGtk: setting child halign") + cw.set_halign(hal) + if val is not None and hasattr(cw, "set_valign"): + print("YAlignmentGtk: setting child valign") + cw.set_valign(val) + except Exception: + print("YAlignmentGtk: failed to set child alignment") + pass + try: + self._backend_widget.append(cw) + except Exception: + self._backend_widget.add(cw) + except Exception: + pass + + def _create_backend_widget(self): + # Wrapper box; expansion hints depend on target alignment + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + try: + box.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + box.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + # fill along expanding axis + if hasattr(box, "set_halign"): + box.set_halign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_HORIZ) else Gtk.Align.START) + if hasattr(box, "set_valign"): + box.set_valign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_VERT) else Gtk.Align.START) + except Exception: + pass + self._backend_widget = box + # attach child if already present + if getattr(self, "_child", None): + self._attach_child_backend() From 04a32ac3bc1a7e8754ffcbf1d86e2995c7eb99f9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 12:38:02 +0100 Subject: [PATCH 062/523] Added a test case for YAligment --- test/test_aligment.py | 77 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/test_aligment.py diff --git a/test/test_aligment.py b/test/test_aligment.py new file mode 100644 index 0000000..7ea1bca --- /dev/null +++ b/test/test_aligment.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_hello_world(backend_name=None): + """Test simple dialog with hello world""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox( dialog ) + factory.createLabel(vbox, "Testing aligment Left and Right into HBox") + hbox = factory.createHBox( vbox ) + leftAlignment = factory.createLeft( hbox ) + left = factory.createPushButton( leftAlignment, "Left" ) + rightAlignment = factory.createRight( hbox ) + factory.createPushButton( rightAlignment, "Right" ) + + factory.createLabel(vbox, "Testing aligment Top and Bottom into HBox") + hbox = factory.createHBox( vbox ) + topAlignment = factory.createTop( hbox ) + factory.createPushButton( topAlignment, "Top" ) + factory.createLabel(hbox, "separator") + bottomAlignment = factory.createBottom( hbox ) + factory.createPushButton( bottomAlignment, "Bottom" ) + + factory.createLabel(vbox, "Testing aligment HCenter into HBox") + hbox = factory.createHBox( vbox ) + align = factory.createHCenter( hbox ) + factory.createPushButton( align, "HCenter" ) + + factory.createLabel(vbox, "Testing aligment VCenter into HBox") + hbox = factory.createHBox( vbox ) + align = factory.createVCenter( hbox ) + factory.createPushButton( align, "VCenter" ) + + factory.createLabel(vbox, "Testing aligment HVCenter into HBox") + hbox = factory.createHBox( vbox ) + align = factory.createVCenter( hbox ) + factory.createPushButton( align, "HVCenter" ) + + factory.createPushButton( vbox, "OK" ) + dialog.open() + event = dialog.waitForEvent() + dialog.destroy() + + + except Exception as e: + print(f"Error testing ComboBox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_hello_world(sys.argv[1]) + else: + test_hello_world() From 4dc0454f53d3ab96c69d26f8ba96f0938a1ee4b2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 12:40:29 +0100 Subject: [PATCH 063/523] fixed code information for this test case --- test/test_aligment.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/test_aligment.py b/test/test_aligment.py index 7ea1bca..6de2197 100644 --- a/test/test_aligment.py +++ b/test/test_aligment.py @@ -6,8 +6,7 @@ # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -def test_hello_world(backend_name=None): - """Test simple dialog with hello world""" +def test_Alignment(backend_name=None): if backend_name: print(f"Setting backend to: {backend_name}") os.environ['YUI_BACKEND'] = backend_name @@ -66,12 +65,12 @@ def test_hello_world(backend_name=None): except Exception as e: - print(f"Error testing ComboBox with backend {backend_name}: {e}") + print(f"Error testing Alignment with backend {backend_name}: {e}") import traceback traceback.print_exc() if __name__ == "__main__": if len(sys.argv) > 1: - test_hello_world(sys.argv[1]) + test_Alignment(sys.argv[1]) else: - test_hello_world() + test_Alignment() From c8911d6d0fb55aa20c0448a1f6eb3f5a025e6741 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 14:44:34 +0100 Subject: [PATCH 064/523] Top and Bottom seem to align correctly --- manatools/aui/yui_gtk.py | 351 ++++++++++++++++++++++++++++++--------- 1 file changed, 270 insertions(+), 81 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 1d689f8..70287f1 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -3,13 +3,11 @@ """ import gi gi.require_version('Gtk', '4.0') -from gi.repository import Gtk, GLib +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo import threading import os -try: - from gi.repository import GdkPixbuf -except Exception: - GdkPixbuf = None from .yui_common import * class YUIGtk: @@ -587,6 +585,7 @@ def _create_backend_widget(self): self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) for child in self._children: + print("HBox child: ", child.widgetClass()) widget = child.get_backend_widget() expand = bool(child.stretchable(YUIDimension.YD_HORIZ)) fill = True @@ -1367,22 +1366,54 @@ def _on_selected_rows_changed(self, listbox): dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo + +# patch_alignment_fix.py + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo + class YAlignmentGtk(YSingleChildContainerWidget): """ - Single-child container that aligns its child using Gtk.Align. - Works reliably inside HBox/VBox: right/center expand horizontally to push content. + GTK4 implementation of YAlignment. + + - Uses a Gtk.Box as a lightweight container that requests expansion when + needed so child halign/valign can take effect (matches the small GTK sample). + - Applies halign/valign hints to the child's backend widget. + - Defers attaching the child if its backend is not yet created (GLib.idle_add). + - Supports an optional repeating background pixbuf painted in the draw signal. """ def __init__(self, parent=None, horAlign=None, vertAlign=None): super().__init__(parent) self._halign_spec = horAlign self._valign_spec = vertAlign + self._background_pixbuf = None + self._signal_id = None self._backend_widget = None + # schedule guard for deferred attach + self._attach_scheduled = False + # Track if we've already attached a child + self._child_attached = False def widgetClass(self): return "YAlignment" def _to_gtk_align(self, spec, axis="h"): - # Accept strings ("Left","Right","HCenter","Top","Bottom","VCenter","HVCenter") + """Convert a spec (string/enum) to Gtk.Align or None.""" if spec is None: return None try: @@ -1390,114 +1421,272 @@ def _to_gtk_align(self, spec, axis="h"): except Exception: s = str(spec).lower() if axis == "h": - if s in ("left", "begin", "start"): + if "left" in s or s in ("start", "begin"): return Gtk.Align.START - if s in ("right", "end"): + if "right" in s or "end" in s: return Gtk.Align.END - if s in ("hcenter", "center", "centre", "hvcenter"): + if "center" in s or "hcenter" in s or "hvcenter" in s: return Gtk.Align.CENTER else: - if s in ("top", "begin", "start"): + if "top" in s or s in ("start", "begin"): return Gtk.Align.START - if s in ("bottom", "end"): + if "bottom" in s or "end" in s: return Gtk.Align.END - if s in ("vcenter", "center", "centre", "hvcenter"): + if "center" in s or "vcenter" in s or "hvcenter" in s: return Gtk.Align.CENTER - if s == "fill": + if "fill" in s or "expand" in s: return Gtk.Align.FILL return None def stretchable(self, dim): - # Expand horizontally when Right/HCenter/HVCenter; vertically for VCenter/HVCenter - if dim == YUIDimension.YD_HORIZ: - return str(self._halign_spec).lower() in ("right", "hcenter", "hvcenter") - if dim == YUIDimension.YD_VERT: - return str(self._valign_spec).lower() in ("vcenter", "hvcenter") + """Report whether this alignment should expand in given dimension. + + Parents (HBox/VBox) use this to distribute space. + """ + try: + if dim == YUIDimension.YD_HORIZ: + align = self._to_gtk_align(self._halign_spec, "h") + return align in (Gtk.Align.CENTER, Gtk.Align.END) + if dim == YUIDimension.YD_VERT: + align = self._to_gtk_align(self._valign_spec, "v") + return align in (Gtk.Align.CENTER, Gtk.Align.END) + except Exception: + pass return False - def setAlignment(self, horAlign=None, vertAlign=None): - self._halign_spec = horAlign - self._valign_spec = vertAlign - # Re-apply if backend exists - if self._backend_widget and self._child: + def setBackgroundPixmap(self, filename): + """Set a repeating background pixbuf and connect draw handler.""" + # disconnect previous handler + if self._signal_id and self._backend_widget: try: - cw = self._child.get_backend_widget() - hal = self._to_gtk_align(self._halign_spec, "h") - val = self._to_gtk_align(self._valign_spec, "v") - if hal is not None and hasattr(cw, "set_halign"): - cw.set_halign(hal) - if val is not None and hasattr(cw, "set_valign"): - cw.set_valign(val) + self._backend_widget.disconnect(self._signal_id) except Exception: pass + self._signal_id = None + + # release previous pixbuf if present + self._background_pixbuf = None + + if filename: + try: + self._background_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + if self._backend_widget: + self._signal_id = self._backend_widget.connect("draw", self._on_draw) + self._backend_widget.queue_draw() # Trigger redraw + except Exception as e: + print(f"Failed to load background image: {e}") + self._background_pixbuf = None + + def _on_draw(self, widget, cr): + """Draw callback that tiles the background pixbuf.""" + if not self._background_pixbuf: + return False + try: + # Get actual allocation + width = widget.get_allocated_width() + height = widget.get_allocated_height() + + Gdk.cairo_set_source_pixbuf(cr, self._background_pixbuf, 0, 0) + # set repeat + pat = cr.get_source() + pat.set_extend(cairo.Extend.REPEAT) + cr.rectangle(0, 0, width, height) + cr.fill() + except Exception as e: + print(f"Error drawing background: {e}") + return False def addChild(self, child): + """Keep base behavior and ensure we attempt to attach child's backend.""" try: super().addChild(child) except Exception: self._child = child - # If backend already created, attach immediately - if self._backend_widget: - self._attach_child_backend() + self._child_attached = False + self._schedule_attach_child() def setChild(self, child): + """Keep base behavior and ensure we attempt to attach child's backend.""" try: super().setChild(child) except Exception: self._child = child - if self._backend_widget: - self._attach_child_backend() + self._child_attached = False + self._schedule_attach_child() - def _attach_child_backend(self): - if not self._child or not self._backend_widget: + def _schedule_attach_child(self): + """Schedule a single idle callback to attach child backend later.""" + if self._attach_scheduled or self._child_attached: return + self._attach_scheduled = True + + def _idle_cb(): + self._attach_scheduled = False + try: + self._ensure_child_attached() + except Exception as e: + print(f"Error attaching child: {e}") + return False + try: - for ch in list(getattr(self._backend_widget, "get_children", lambda: [])()) or []: - # clean previous child if re-attaching - try: - self._backend_widget.remove(ch) - except Exception: - pass + GLib.idle_add(_idle_cb) except Exception: - pass + # fallback: call synchronously if idle_add not available + _idle_cb() + + def _ensure_child_attached(self): + """Attach child's backend to our container, apply alignment hints.""" + if self._backend_widget is None: + self._create_backend_widget() + return + + # choose child reference (support _child or _children storage) + child = getattr(self, "_child", None) + if child is None: + try: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + except Exception: + child = None + if child is None: + return + + # get child's backend widget try: - cw = self._child.get_backend_widget() - if cw: - # apply alignment to child - hal = self._to_gtk_align(self._halign_spec, "h") - val = self._to_gtk_align(self._valign_spec, "v") - try: - print(f"YAlignmentGtk: attaching child {self._child.widgetClass()} with halign={hal}, valign={val}") - if hal is not None and hasattr(cw, "set_halign"): - print("YAlignmentGtk: setting child halign") - cw.set_halign(hal) - if val is not None and hasattr(cw, "set_valign"): - print("YAlignmentGtk: setting child valign") - cw.set_valign(val) - except Exception: - print("YAlignmentGtk: failed to set child alignment") - pass - try: - self._backend_widget.append(cw) - except Exception: - self._backend_widget.add(cw) + cw = child.get_backend_widget() except Exception: - pass + cw = None - def _create_backend_widget(self): - # Wrapper box; expansion hints depend on target alignment - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + if cw is None: + # child backend not yet ready; schedule again + if not self._child_attached: + self._schedule_attach_child() + return + + # convert specs -> Gtk.Align + hal = self._to_gtk_align(self._halign_spec, "h") + val = self._to_gtk_align(self._valign_spec, "v") + + # Apply alignment and expansion hints to child + try: + # Set horizontal alignment and expansion + if hasattr(cw, "set_halign"): + if hal is not None: + cw.set_halign(hal) + else: + cw.set_halign(Gtk.Align.FILL) + + # Request expansion for alignment to work properly + cw.set_hexpand(True) + + # Set vertical alignment and expansion + if hasattr(cw, "set_valign"): + if val is not None: + cw.set_valign(val) + else: + cw.set_valign(Gtk.Align.FILL) + + # Request expansion for alignment to work properly + cw.set_vexpand(True) + + except Exception as e: + print(f"Error setting alignment properties: {e}") + + # If the child widget is already parented to us, nothing to do + parent_of_cw = None try: - box.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) - box.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - # fill along expanding axis - if hasattr(box, "set_halign"): - box.set_halign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_HORIZ) else Gtk.Align.START) - if hasattr(box, "set_valign"): - box.set_valign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_VERT) else Gtk.Align.START) + if hasattr(cw, 'get_parent'): + parent_of_cw = cw.get_parent() except Exception: - pass + parent_of_cw = None + + if parent_of_cw == self._backend_widget: + self._child_attached = True + return + + # Remove any existing children from our container + try: + # In GTK4, we need to remove all existing children + while True: + child_widget = self._backend_widget.get_first_child() + if child_widget is None: + break + self._backend_widget.remove(child_widget) + except Exception as e: + print(f"Error removing existing children: {e}") + + # Append child to our box - this is the critical fix for GTK4 + try: + self._backend_widget.append(cw) + self._child_attached = True + print(f"Successfully attached child {child.widgetClass()} {child.debugLabel()} to alignment container") + except Exception as e: + print(f"Error appending child: {e}") + # Try alternative method for GTK4 + try: + self._backend_widget.set_child(cw) + self._child_attached = True + print(f"Successfully set child {child.widgetClass()} {child.debugLabel()} using set_child()") + except Exception as e2: + print(f"Error setting child: {e2}") + + def _create_backend_widget(self): + """Create a Box container oriented to allow alignment to work. + + In GTK4, we use a simple Box that expands in both directions + to provide space for the child widget to align within. + """ + try: + # Use a box that can expand in both directions + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + + # Make the box expand to fill available space + box.set_hexpand(True) + box.set_vexpand(True) + + # Set the box to fill its allocation so child has space to align + box.set_halign(Gtk.Align.FILL) + box.set_valign(Gtk.Align.FILL) + + except Exception as e: + print(f"Error creating backend widget: {e}") + box = Gtk.Box() + self._backend_widget = box - # attach child if already present - if getattr(self, "_child", None): - self._attach_child_backend() + + # Connect draw handler if we have a background pixbuf + if self._background_pixbuf and not self._signal_id: + try: + self._signal_id = box.connect("draw", self._on_draw) + except Exception as e: + print(f"Error connecting draw signal: {e}") + self._signal_id = None + + # Mark that backend is ready and attempt to attach child + self._ensure_child_attached() + + def get_backend_widget(self): + """Return the backend GTK widget.""" + if self._backend_widget is None: + self._create_backend_widget() + return self._backend_widget + + def setSize(self, width, height): + """Set size of the alignment widget.""" + if self._backend_widget: + if width > 0 and height > 0: + self._backend_widget.set_size_request(width, height) + else: + self._backend_widget.set_size_request(-1, -1) + + def setEnabled(self, enabled): + """Set widget enabled state.""" + if self._backend_widget: + self._backend_widget.set_sensitive(enabled) + super().setEnabled(enabled) + + def setVisible(self, visible): + """Set widget visibility.""" + if self._backend_widget: + self._backend_widget.set_visible(visible) + super().setVisible(visible) From 599c6cfb7f86ee4bb0ec1d283ca834eb9bd96f51 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 16:18:49 +0100 Subject: [PATCH 065/523] Forced the check on YAlignmentType --- manatools/aui/yui_gtk.py | 95 ++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 70287f1..8035272 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -222,25 +222,29 @@ def createSelectionBox(self, parent, label): # Alignment helpers def createLeft(self, parent): - return YAlignmentGtk(parent, horAlign="Left", vertAlign=None) + return YAlignmentGtk(parent, horAlign=YAlignmentType.YAlignBegin, vertAlign=YAlignmentType.YAlignUnchanged) def createRight(self, parent): - return YAlignmentGtk(parent, horAlign="Right", vertAlign=None) + return YAlignmentGtk(parent, horAlign=YAlignmentType.YAlignEnd, vertAlign=YAlignmentType.YAlignUnchanged) def createTop(self, parent): - return YAlignmentGtk(parent, horAlign=None, vertAlign="Top") + return YAlignmentGtk(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignBegin) def createBottom(self, parent): - return YAlignmentGtk(parent, horAlign=None, vertAlign="Bottom") + return YAlignmentGtk(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignEnd) def createHCenter(self, parent): - return YAlignmentGtk(parent, horAlign="HCenter", vertAlign=None) + return YAlignmentGtk(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignUnchanged) def createVCenter(self, parent): - return YAlignmentGtk(parent, horAlign=None, vertAlign="VCenter") + return YAlignmentGtk(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignCenter) def createHVCenter(self, parent): - return YAlignmentGtk(parent, horAlign="HCenter", vertAlign="VCenter") + return YAlignmentGtk(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignCenter) + + def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: YAlignmentType): + """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" + return YAlignmentGtk(parent, horAlign=horAlignment, vertAlign=vertAlignment) # GTK4 Widget Implementations @@ -1397,7 +1401,7 @@ class YAlignmentGtk(YSingleChildContainerWidget): - Defers attaching the child if its backend is not yet created (GLib.idle_add). - Supports an optional repeating background pixbuf painted in the draw signal. """ - def __init__(self, parent=None, horAlign=None, vertAlign=None): + def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): super().__init__(parent) self._halign_spec = horAlign self._valign_spec = vertAlign @@ -1412,32 +1416,55 @@ def __init__(self, parent=None, horAlign=None, vertAlign=None): def widgetClass(self): return "YAlignment" - def _to_gtk_align(self, spec, axis="h"): - """Convert a spec (string/enum) to Gtk.Align or None.""" - if spec is None: - return None - try: - s = str(getattr(spec, "name", spec)).lower() - except Exception: - s = str(spec).lower() - if axis == "h": - if "left" in s or s in ("start", "begin"): + def _to_gtk_halign(self): + """Convert Horizontal YAlignmentType to Gtk.Align or None.""" + if self._halign_spec: + if self._halign_spec == YAlignmentType.YAlignBegin: return Gtk.Align.START - if "right" in s or "end" in s: - return Gtk.Align.END - if "center" in s or "hcenter" in s or "hvcenter" in s: + if self._halign_spec == YAlignmentType.YAlignCenter: return Gtk.Align.CENTER - else: - if "top" in s or s in ("start", "begin"): - return Gtk.Align.START - if "bottom" in s or "end" in s: + if self._halign_spec == YAlignmentType.YAlignEnd: return Gtk.Align.END - if "center" in s or "vcenter" in s or "hvcenter" in s: + return None + + def _to_gtk_valign(self): + """Convert Vertical YAlignmentType to Gtk.Align or None.""" + if self._valign_spec: + if self._valign_spec == YAlignmentType.YAlignBegin: + return Gtk.Align.START + if self._valign_spec == YAlignmentType.YAlignCenter: return Gtk.Align.CENTER - if "fill" in s or "expand" in s: - return Gtk.Align.FILL + if self._valign_spec == YAlignmentType.YAlignEnd: + return Gtk.Align.END return None + + #def _to_gtk_align(self, spec, axis="h"): + # """Convert a spec (string/enum) to Gtk.Align or None.""" + # if spec is None: + # return None + # try: + # s = str(getattr(spec, "name", spec)).lower() + # except Exception: + # s = str(spec).lower() + # if axis == "h": + # if "left" in s or s in ("start", "begin"): + # return Gtk.Align.START + # if "right" in s or "end" in s: + # return Gtk.Align.END + # if "center" in s or "hcenter" in s or "hvcenter" in s: + # return Gtk.Align.CENTER + # else: + # if "top" in s or s in ("start", "begin"): + # return Gtk.Align.START + # if "bottom" in s or "end" in s: + # return Gtk.Align.END + # if "center" in s or "vcenter" in s or "hvcenter" in s: + # return Gtk.Align.CENTER + # if "fill" in s or "expand" in s: + # return Gtk.Align.FILL + # return None + def stretchable(self, dim): """Report whether this alignment should expand in given dimension. @@ -1445,11 +1472,11 @@ def stretchable(self, dim): """ try: if dim == YUIDimension.YD_HORIZ: - align = self._to_gtk_align(self._halign_spec, "h") - return align in (Gtk.Align.CENTER, Gtk.Align.END) + align = self._to_gtk_halign() + return align in (Gtk.Align.CENTER, Gtk.Align.END) #TODO: verify if dim == YUIDimension.YD_VERT: - align = self._to_gtk_align(self._valign_spec, "v") - return align in (Gtk.Align.CENTER, Gtk.Align.END) + align = self._to_gtk_valign() + return align in (Gtk.Align.CENTER, Gtk.Align.END) #TODO: verify except Exception: pass return False @@ -1564,8 +1591,8 @@ def _ensure_child_attached(self): return # convert specs -> Gtk.Align - hal = self._to_gtk_align(self._halign_spec, "h") - val = self._to_gtk_align(self._valign_spec, "v") + hal = self._to_gtk_halign() + val = self._to_gtk_valign() # Apply alignment and expansion hints to child try: From 66dd1c0150ad79bdff5b0bfbb13f5645e704dfec Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 16:47:41 +0100 Subject: [PATCH 066/523] Removed unused code --- manatools/aui/yui_gtk.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 8035272..e39f83a 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -1438,33 +1438,6 @@ def _to_gtk_valign(self): return Gtk.Align.END return None - - #def _to_gtk_align(self, spec, axis="h"): - # """Convert a spec (string/enum) to Gtk.Align or None.""" - # if spec is None: - # return None - # try: - # s = str(getattr(spec, "name", spec)).lower() - # except Exception: - # s = str(spec).lower() - # if axis == "h": - # if "left" in s or s in ("start", "begin"): - # return Gtk.Align.START - # if "right" in s or "end" in s: - # return Gtk.Align.END - # if "center" in s or "hcenter" in s or "hvcenter" in s: - # return Gtk.Align.CENTER - # else: - # if "top" in s or s in ("start", "begin"): - # return Gtk.Align.START - # if "bottom" in s or "end" in s: - # return Gtk.Align.END - # if "center" in s or "vcenter" in s or "hvcenter" in s: - # return Gtk.Align.CENTER - # if "fill" in s or "expand" in s: - # return Gtk.Align.FILL - # return None - def stretchable(self, dim): """Report whether this alignment should expand in given dimension. From 3061c0dd0e726be1f59d52d5ccc3658608b86c07 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 17:12:07 +0100 Subject: [PATCH 067/523] Managed YAlignmentType correctly --- manatools/aui/yui_qt.py | 86 ++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 21b8ea7..e06fcb3 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -207,27 +207,32 @@ def createProgressBar(self, parent, label, max_value=100): def createComboBox(self, parent, label, editable=False): return YComboBoxQt(parent, label, editable) + # Alignment helpers # Alignment helpers def createLeft(self, parent): - return YAlignmentQt(parent, horAlign="Left", vertAlign=None) + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignBegin, vertAlign=YAlignmentType.YAlignUnchanged) def createRight(self, parent): - return YAlignmentQt(parent, horAlign="Right", vertAlign=None) + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignEnd, vertAlign=YAlignmentType.YAlignUnchanged) def createTop(self, parent): - return YAlignmentQt(parent, horAlign=None, vertAlign="Top") + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignBegin) def createBottom(self, parent): - return YAlignmentQt(parent, horAlign=None, vertAlign="Bottom") + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignEnd) def createHCenter(self, parent): - return YAlignmentQt(parent, horAlign="HCenter", vertAlign=None) + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignUnchanged) def createVCenter(self, parent): - return YAlignmentQt(parent, horAlign=None, vertAlign="VCenter") + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignCenter) def createHVCenter(self, parent): - return YAlignmentQt(parent, horAlign="HCenter", vertAlign="VCenter") + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignCenter) + + def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: YAlignmentType): + """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" + return YAlignmentQt(parent, horAlign=horAlignment, vertAlign=vertAlignment) # Qt Widget Implementations class YDialogQt(YSingleChildContainerWidget): @@ -880,7 +885,7 @@ class YAlignmentQt(YSingleChildContainerWidget): applying Qt.Alignment flags to the child. The container expands along axes needed by Right/HCenter/VCenter/HVCenter to allow alignment. """ - def __init__(self, parent=None, horAlign=None, vertAlign=None): + def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): super().__init__(parent) self._halign_spec = horAlign self._valign_spec = vertAlign @@ -890,34 +895,45 @@ def __init__(self, parent=None, horAlign=None, vertAlign=None): def widgetClass(self): return "YAlignment" - def _to_qt_align(self, spec, axis="h"): - if spec is None: - return None - s = str(getattr(spec, "name", spec)).lower() - if axis == "h": - if s in ("left", "begin", "start"): + def _to_qt_halign(self): + """Convert Horizontal YAlignmentType to QtCore.Qt.AlignmentFlag or None.""" + if self._halign_spec: + if self._halign_spec == YAlignmentType.YAlignBegin: return QtCore.Qt.AlignmentFlag.AlignLeft - if s in ("right", "end"): - return QtCore.Qt.AlignmentFlag.AlignRight - if s in ("hcenter", "center", "centre", "hvcenter"): + if self._halign_spec == YAlignmentType.YAlignCenter: return QtCore.Qt.AlignmentFlag.AlignHCenter - else: - if s in ("top", "begin", "start"): + if self._halign_spec == YAlignmentType.YAlignEnd: + return QtCore.Qt.AlignmentFlag.AlignRight + return None + + def _to_qt_valign(self): + """Convert Vertical YAlignmentType to QtCore.Qt.AlignmentFlag or None.""" + if self._valign_spec: + if self._valign_spec == YAlignmentType.YAlignBegin: return QtCore.Qt.AlignmentFlag.AlignTop - if s in ("bottom", "end"): - return QtCore.Qt.AlignmentFlag.AlignBottom - if s in ("vcenter", "center", "centre", "hvcenter"): + if self._valign_spec == YAlignmentType.YAlignCenter: return QtCore.Qt.AlignmentFlag.AlignVCenter + if self._valign_spec == YAlignmentType.YAlignEnd: + return QtCore.Qt.AlignmentFlag.AlignBottom return None - def stretchable(self, dim): - if dim == YUIDimension.YD_HORIZ: - return str(self._halign_spec).lower() in ("right", "hcenter", "hvcenter") - if dim == YUIDimension.YD_VERT: - return str(self._valign_spec).lower() in ("vcenter", "hvcenter") + + def stretchable(self, dim: YUIDimension): + ''' Returns the stretchability of the layout box: + * The layout box is stretchable if the child is stretchable in + * this dimension or if the child widget has a layout weight in + * this dimension. + ''' + if self._child: + widget = self._child.get_backend_widget() + expand = bool(self._child.stretchable(dim)) + weight = bool(self._child.weight(dim)) + if expand or weight: + return True + return False - def setAlignment(self, horAlign=None, vertAlign=None): + def setAlignment(self, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): self._halign_spec = horAlign self._valign_spec = vertAlign self._reapply_alignment() @@ -930,8 +946,8 @@ def _reapply_alignment(self): if w: self._layout.removeWidget(w) flags = QtCore.Qt.AlignmentFlag(0) - ha = self._to_qt_align(self._halign_spec, "h") - va = self._to_qt_align(self._valign_spec, "v") + ha = self._to_qt_halign() + va = self._to_qt_valign() if ha: flags |= ha if va: @@ -968,8 +984,8 @@ def _attach_child_backend(self): except Exception: pass flags = QtCore.Qt.AlignmentFlag(0) - ha = self._to_qt_align(self._halign_spec, "h") - va = self._to_qt_align(self._valign_spec, "v") + ha = self._to_qt_halign() + va = self._to_qt_valign() if ha: flags |= ha if va: @@ -986,11 +1002,11 @@ def _create_backend_widget(self): # Size policy: expand along axes needed for alignment to work sp = container.sizePolicy() - try: + try: sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) - else QtWidgets.QSizePolicy.Policy.Preferred) + else QtWidgets.QSizePolicy.Policy.Fixed) sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) - else QtWidgets.QSizePolicy.Policy.Preferred) + else QtWidgets.QSizePolicy.Policy.Fixed) except Exception: pass container.setSizePolicy(sp) From e9e5fe37653072bf67e107866181a0bbd8bf2e3c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 18:09:37 +0100 Subject: [PATCH 068/523] Managed YAlignmentType --- manatools/aui/yui_curses.py | 53 ++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 3f68c91..90b561f 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -169,26 +169,29 @@ def createSelectionBox(self, parent, label): # Alignment helpers def createLeft(self, parent): - return YAlignmentCurses(parent, horAlign="Left", vertAlign=None) + return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignBegin, vertAlign=YAlignmentType.YAlignUnchanged) def createRight(self, parent): - return YAlignmentCurses(parent, horAlign="Right", vertAlign=None) + return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignEnd, vertAlign=YAlignmentType.YAlignUnchanged) def createTop(self, parent): - return YAlignmentCurses(parent, horAlign=None, vertAlign="Top") + return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignBegin) def createBottom(self, parent): - return YAlignmentCurses(parent, horAlign=None, vertAlign="Bottom") + return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignEnd) def createHCenter(self, parent): - return YAlignmentCurses(parent, horAlign="HCenter", vertAlign=None) + return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignUnchanged) def createVCenter(self, parent): - return YAlignmentCurses(parent, horAlign=None, vertAlign="VCenter") + return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignCenter) def createHVCenter(self, parent): - return YAlignmentCurses(parent, horAlign="HCenter", vertAlign="VCenter") + return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignCenter) + def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: YAlignmentType): + """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" + return YAlignmentCurses(parent, horAlign=horAlignment, vertAlign=vertAlignment) # Curses Widget Implementations class YDialogCurses(YSingleChildContainerWidget): @@ -829,7 +832,7 @@ def _create_backend_widget(self): def _draw(self, window, y, x, width, height): try: # Center the button label within available width - button_text = f" {self._label} " + button_text = f"[ {self._label} ]" text_x = x + max(0, (width - len(button_text)) // 2) # Only draw if we have enough space @@ -1368,7 +1371,7 @@ class YAlignmentCurses(YSingleChildContainerWidget): Single-child alignment container for ncurses. It becomes stretchable on the requested axes, and positions the child inside its draw area accordingly. """ - def __init__(self, parent=None, horAlign=None, vertAlign=None): + def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): super().__init__(parent) self._halign_spec = horAlign self._valign_spec = vertAlign @@ -1378,17 +1381,19 @@ def __init__(self, parent=None, horAlign=None, vertAlign=None): def widgetClass(self): return "YAlignment" - def stretchable(self, dim): - if dim == YUIDimension.YD_HORIZ: - return str(self._halign_spec).lower() in ("right", "hcenter", "hvcenter") - if dim == YUIDimension.YD_VERT: - return str(self._valign_spec).lower() in ("vcenter", "hvcenter") + def stretchable(self, dim: YUIDimension): + ''' Returns the stretchability of the layout box: + * The layout box is stretchable if the child is stretchable in + * this dimension or if the child widget has a layout weight in + * this dimension. + ''' + if self._child: + expand = bool(self._child.stretchable(dim)) + weight = bool(self._child.weight(dim)) + if expand or weight: + return True return False - def setAlignment(self, horAlign=None, vertAlign=None): - self._halign_spec = horAlign - self._valign_spec = vertAlign - def addChild(self, child): try: super().addChild(child) @@ -1432,7 +1437,7 @@ def _create_backend_widget(self): self._height = max(1, getattr(self._child, "_height", 1) if self._child else 1) def _child_min_width(self, child, max_width): - # Heuristic minimal width similar to YHBoxCurses + # Heuristic minimal width similar to YHBoxCurses TODO: verify with widget information instead of hardcoded classes try: cls = child.widgetClass() if hasattr(child, "widgetClass") else "" if cls in ("YLabel", "YPushButton", "YCheckBox"): @@ -1452,18 +1457,16 @@ def _draw(self, window, y, x, width, height): # width to give to the child: minimal needed (so it can be pushed) ch_min_w = self._child_min_width(self._child, width) # Horizontal position - hs = str(self._halign_spec).lower() if self._halign_spec else "left" - if hs in ("right",): + if self._halign_spec == YAlignmentType.YAlignEnd: cx = x + max(0, width - ch_min_w) - elif hs in ("hcenter", "center", "centre", "hvcenter"): + elif self._halign_spec == YAlignmentType.YAlignCenter: cx = x + max(0, (width - ch_min_w) // 2) else: cx = x # Vertical position (single line widgets mostly) - vs = str(self._valign_spec).lower() if self._valign_spec else "top" - if vs in ("vcenter", "center", "centre", "hvcenter"): + if self._valign_spec == YAlignmentType.YAlignCenter: cy = y + max(0, (height - 1) // 2) - elif vs in ("bottom", "end"): + elif self._valign_spec == YAlignmentType.YAlignEnd: cy = y + max(0, height - 1) else: cy = y From 1e46f42dea7415d04b9daf1389fd0e479f22cea4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 18:20:27 +0100 Subject: [PATCH 069/523] Strechable if the child is and removed wrong imports --- manatools/aui/yui_gtk.py | 60 ++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index e39f83a..e2992b5 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -1371,25 +1371,6 @@ def _on_selected_rows_changed(self, listbox): if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) -import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Gdk', '4.0') -from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib -import cairo - -# patch_alignment_fix.py - -import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Gdk', '4.0') -from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib -import cairo - -import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Gdk', '4.0') -from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib -import cairo class YAlignmentGtk(YSingleChildContainerWidget): """ @@ -1438,20 +1419,33 @@ def _to_gtk_valign(self): return Gtk.Align.END return None - def stretchable(self, dim): - """Report whether this alignment should expand in given dimension. - - Parents (HBox/VBox) use this to distribute space. - """ - try: - if dim == YUIDimension.YD_HORIZ: - align = self._to_gtk_halign() - return align in (Gtk.Align.CENTER, Gtk.Align.END) #TODO: verify - if dim == YUIDimension.YD_VERT: - align = self._to_gtk_valign() - return align in (Gtk.Align.CENTER, Gtk.Align.END) #TODO: verify - except Exception: - pass + #def stretchable(self, dim): + # """Report whether this alignment should expand in given dimension. + # + # Parents (HBox/VBox) use this to distribute space. + # """ + # try: + # if dim == YUIDimension.YD_HORIZ: + # align = self._to_gtk_halign() + # return align in (Gtk.Align.CENTER, Gtk.Align.END) #TODO: verify + # if dim == YUIDimension.YD_VERT: + # align = self._to_gtk_valign() + # return align == Gtk.Align.CENTER #TODO: verify + # except Exception: + # pass + # return False + + def stretchable(self, dim: YUIDimension): + ''' Returns the stretchability of the layout box: + * The layout box is stretchable if the child is stretchable in + * this dimension or if the child widget has a layout weight in + * this dimension. + ''' + if self._child: + expand = bool(self._child.stretchable(dim)) + weight = bool(self._child.weight(dim)) + if expand or weight: + return True return False def setBackgroundPixmap(self, filename): From a0b2d6c6a56c9d146b9301f7f5c3b89d434e6326 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 22 Nov 2025 19:02:33 +0100 Subject: [PATCH 070/523] restored right and left aligned behavior --- manatools/aui/yui_qt.py | 89 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index e06fcb3..2ce9abd 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -478,6 +478,24 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 + + # If the child requests horizontal stretch, set its QSizePolicy to Expanding + try: + if expand == 1: + sp = widget.sizePolicy() + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + widget.setSizePolicy(sp) + except Exception: + pass + + + print( f"YVBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) @@ -511,6 +529,21 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 + + # If the child requests horizontal stretch, set its QSizePolicy to Expanding + try: + if expand == 1: + sp = widget.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + widget.setSizePolicy(sp) + except Exception: + pass print( f"YHBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) @@ -920,17 +953,30 @@ def _to_qt_valign(self): def stretchable(self, dim: YUIDimension): ''' Returns the stretchability of the layout box: - * The layout box is stretchable if the child is stretchable in - * this dimension or if the child widget has a layout weight in - * this dimension. + * The layout box is stretchable if the alignment spec requests expansion + * (Right/HCenter/HVCenter for horizontal, VCenter/HVCenter for vertical) + * OR if the child itself requests stretchability or has a layout weight. ''' - if self._child: - widget = self._child.get_backend_widget() - expand = bool(self._child.stretchable(dim)) - weight = bool(self._child.weight(dim)) - if expand or weight: - return True + # Expand if alignment spec requests it + try: + if dim == YUIDimension.YD_HORIZ: + if self._halign_spec in (YAlignmentType.YAlignEnd, YAlignmentType.YAlignCenter): + return True + if dim == YUIDimension.YD_VERT: + if self._valign_spec in (YAlignmentType.YAlignCenter,): + return True + except Exception: + pass + # Otherwise honor child's own stretchability/weight + try: + if self._child: + expand = bool(self._child.stretchable(dim)) + weight = bool(self._child.weight(dim)) + if expand or weight: + return True + except Exception: + pass return False def setAlignment(self, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): @@ -990,6 +1036,31 @@ def _attach_child_backend(self): flags |= ha if va: flags |= va + # If the child requests horizontal stretch, set its QSizePolicy to Expanding + try: + if self._child and self._child.stretchable(YUIDimension.YD_HORIZ): + sp = w.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + w.setSizePolicy(sp) + # If child requests vertical stretch, set vertical policy + if self._child and self._child.stretchable(YUIDimension.YD_VERT): + sp = w.sizePolicy() + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + w.setSizePolicy(sp) + except Exception: + pass self._layout.addWidget(w, 0, 0, flags) except Exception: pass From 8de57ed8710666913f0fde85c4a03da96a264e31 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 23 Nov 2025 18:06:05 +0100 Subject: [PATCH 071/523] Added setDisable for back compatibility --- manatools/aui/yui_common.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 8810be0..2d153d1 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -197,10 +197,18 @@ def findDialog(self): return parent def setEnabled(self, enabled=True): + ''' + Enable or disable the widget. i.e. make it accept or reject user input. + Derived backend classes must implement _set_backend_enabled to apply + the change to the actual backend widget. + ''' self._enabled = enabled if self._backend_widget: self._set_backend_enabled(enabled) + def setDisabled(self): + self.setEnabled(False) + def isEnabled(self): return self._enabled From 489d3d4bfb80befcef72ed2928c966135025a341 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 23 Nov 2025 18:33:35 +0100 Subject: [PATCH 072/523] Added a commento because notify here is not back compatible --- manatools/aui/yui_common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 2d153d1..e9f8b50 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -136,6 +136,8 @@ def __init__(self, parent=None): self._stretchable_vert = False self._weight_horiz = 0 self._weight_vert = 0 + # NOTE: Notify property should be False for back compatibility, + # but backends has been implemented as True self._notify = True self._auto_shortcut = False self._function_key = 0 From 2b01a1d198ec0f9f4f806ca7c0cb5d73bda08b70 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 23 Nov 2025 18:34:35 +0100 Subject: [PATCH 073/523] Added enable/disable widgets --- manatools/aui/yui_gtk.py | 195 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index e2992b5..fd3a172 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -513,6 +513,32 @@ def _on_delete_event(self, *args): # returning False/True not used in this simplified handler return False + def setVisible(self, visible): + """Set widget visibility.""" + if self._backend_widget: + try: + self._backend_widget.set_visible(visible) + except Exception: + pass + super().setVisible(visible) + + def _set_backend_enabled(self, enabled): + """Enable/disable the dialog window backend.""" + try: + if self._window is not None: + try: + self._window.set_sensitive(enabled) + except Exception: + # fallback: propagate to child content + try: + child = getattr(self, "_window", None) + if child and hasattr(child, "set_sensitive"): + child.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + class YVBoxGtk(YWidget): def __init__(self, parent=None): @@ -570,6 +596,26 @@ def _create_backend_widget(self): except Exception: pass + def _set_backend_enabled(self, enabled): + """Enable/disable the VBox and propagate to children.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate logical enabled state to child widgets so they update their backends + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + class YHBoxGtk(YWidget): def __init__(self, parent=None): super().__init__(parent) @@ -616,6 +662,25 @@ def _create_backend_widget(self): except Exception: pass + def _set_backend_enabled(self, enabled): + """Enable/disable the HBox and propagate to children.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + class YLabelGtk(YWidget): def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): super().__init__(parent) @@ -653,6 +718,17 @@ def _create_backend_widget(self): except Exception: pass + def _set_backend_enabled(self, enabled): + """Enable/disable the label widget backend.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + class YInputFieldGtk(YWidget): def __init__(self, parent=None, label="", password_mode=False): super().__init__(parent) @@ -721,6 +797,25 @@ def _on_changed(self, entry): except Exception: self._value = "" + def _set_backend_enabled(self, enabled): + """Enable/disable the input field (entry and container).""" + try: + if getattr(self, "_entry_widget", None) is not None: + try: + self._entry_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + class YPushButtonGtk(YWidget): def __init__(self, parent=None, label=""): super().__init__(parent) @@ -765,6 +860,17 @@ def _on_clicked(self, button): # silent fallback pass + def _set_backend_enabled(self, enabled): + """Enable/disable the push button backend.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + class YCheckBoxGtk(YWidget): def __init__(self, parent=None, label="", is_checked=False): super().__init__(parent) @@ -807,6 +913,17 @@ def _on_toggled(self, button): if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + def _set_backend_enabled(self, enabled): + """Enable/disable the check button backend.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + class YComboBoxGtk(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): super().__init__(parent) @@ -905,6 +1022,27 @@ def _create_backend_widget(self): self._backend_widget = hbox + def _set_backend_enabled(self, enabled): + """Enable/disable the combobox/backing widget and its entry/dropdown.""" + try: + # prefer to enable the primary control if present + ctl = getattr(self, "_combo_widget", None) + if ctl is not None: + try: + ctl.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + def _on_fallback_button_clicked(self, btn): # naive cycle through items if not self._items: @@ -1250,6 +1388,34 @@ def _create_backend_widget(self): self._backend_widget = vbox self._listbox = listbox + def _set_backend_enabled(self, enabled): + """Enable/disable the selection box and its listbox/rows.""" + try: + if getattr(self, "_listbox", None) is not None: + try: + self._listbox.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate logical enabled state to child items/widgets + try: + for c in list(getattr(self, "_rows", []) or []): + try: + c.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + def _row_is_selected(self, r): """Robust helper to detect whether a ListBoxRow is selected.""" try: @@ -1682,5 +1848,32 @@ def setEnabled(self, enabled): def setVisible(self, visible): """Set widget visibility.""" if self._backend_widget: - self._backend_widget.set_visible(visible) + try: + self._backend_widget.set_visible(visible) + except Exception: + pass super().setVisible(visible) + + def _set_backend_enabled(self, enabled): + """Enable/disable the alignment container and its child (if any).""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate to logical child so child's backend updates too + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass From 56e463c62725b8ef7d2ed327e12aee3172851bdd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 23 Nov 2025 18:46:05 +0100 Subject: [PATCH 074/523] Added enable/disable widgets --- manatools/aui/yui_qt.py | 190 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 184 insertions(+), 6 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 2ce9abd..7b25ef5 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -387,6 +387,32 @@ def _create_backend_widget(self): self._backend_widget = self._qwidget self._qwidget.closeEvent = self._on_close_event + def _set_backend_enabled(self, enabled): + """Enable/disable the dialog window and propagate to logical child widgets.""" + try: + if getattr(self, "_qwidget", None) is not None: + try: + self._qwidget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate logical enabled state to contained YWidget(s) + try: + if getattr(self, "_child", None): + try: + self._child.setEnabled(enabled) + except Exception: + pass + else: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + def _on_close_event(self, event): # Post a cancel event so waitForEvent returns a YCancelEvent when the user # closes the window with the window manager 'X' button. @@ -499,6 +525,25 @@ def _create_backend_widget(self): print( f"YVBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) + def _set_backend_enabled(self, enabled): + """Enable/disable the VBox container and propagate to children.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + class YHBoxQt(YWidget): def __init__(self, parent=None): super().__init__(parent) @@ -547,6 +592,25 @@ def _create_backend_widget(self): print( f"YHBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) + def _set_backend_enabled(self, enabled): + """Enable/disable the HBox container and propagate to children.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + class YLabelQt(YWidget): def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): super().__init__(parent) @@ -573,6 +637,17 @@ def _create_backend_widget(self): font.setPointSize(font.pointSize() + 2) self._backend_widget.setFont(font) + def _set_backend_enabled(self, enabled): + """Enable/disable the QLabel backend.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + class YInputFieldQt(YWidget): def __init__(self, parent=None, label="", password_mode=False): super().__init__(parent) @@ -615,7 +690,26 @@ def _create_backend_widget(self): self._backend_widget = container self._entry_widget = entry - + + def _set_backend_enabled(self, enabled): + """Enable/disable the input field: entry and container.""" + try: + if getattr(self, "_entry_widget", None) is not None: + try: + self._entry_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + def _on_text_changed(self, text): self._value = text @@ -659,7 +753,18 @@ def _create_backend_widget(self): except Exception: pass self._backend_widget.clicked.connect(self._on_clicked) - + + def _set_backend_enabled(self, enabled): + """Enable/disable the QPushButton backend.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + def _on_clicked(self): # Post a YWidgetEvent to the containing dialog (walk parents) dlg = self.findDialog() @@ -701,7 +806,18 @@ def _create_backend_widget(self): self._backend_widget = QtWidgets.QCheckBox(self._label) self._backend_widget.setChecked(self._is_checked) self._backend_widget.stateChanged.connect(self._on_state_changed) - + + def _set_backend_enabled(self, enabled): + """Enable/disable the QCheckBox backend.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + def _on_state_changed(self, state): # Update internal state # state is QtCore.Qt.CheckState (Unchecked=0, PartiallyChecked=1, Checked=2) @@ -773,7 +889,26 @@ def _create_backend_widget(self): self._backend_widget = container self._combo_widget = combo - + + def _set_backend_enabled(self, enabled): + """Enable/disable the combobox and its container.""" + try: + if getattr(self, "_combo_widget", None) is not None: + try: + self._combo_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + def _on_text_changed(self, text): self._value = text # Update selected items @@ -887,7 +1022,26 @@ def _create_backend_widget(self): self._backend_widget = container self._list_widget = list_widget - + + def _set_backend_enabled(self, enabled): + """Enable/disable the selection box and its list widget; propagate where applicable.""" + try: + if getattr(self, "_list_widget", None) is not None: + try: + self._list_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + def _on_selection_changed(self): """Handle selection change in the list widget""" if hasattr(self, '_list_widget') and self._list_widget: @@ -1086,4 +1240,28 @@ def _create_backend_widget(self): self._layout = grid if getattr(self, "_child", None): - self._attach_child_backend() \ No newline at end of file + self._attach_child_backend() + + def _set_backend_enabled(self, enabled): + """Enable/disable the alignment container and propagate to its logical child.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate to logical child + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass From 8d235eb5ffb2734df612ab74d207f5794254ac3a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 23 Nov 2025 19:01:26 +0100 Subject: [PATCH 075/523] Added enable/disable widgets --- manatools/aui/yui_curses.py | 211 +++++++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 6 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 90b561f..9cd2f19 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -276,6 +276,32 @@ def _create_backend_widget(self): # Use the main screen self._backend_widget = curses.newwin(0, 0, 0, 0) + def _set_backend_enabled(self, enabled): + """Enable/disable the dialog and propagate to contained widgets.""" + try: + # propagate logical enabled state to entire subtree + if getattr(self, "_child", None): + try: + self._child.setEnabled(enabled) + except Exception: + # fallback: traverse _children if present + pass + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + # If disabling and dialog had focused widget, clear focus + if not enabled: + try: + if getattr(self, "_focused_widget", None): + self._focused_widget._focused = False + self._focused_widget = None + except Exception: + pass + except Exception: + pass + def _draw_dialog(self): """Draw the entire dialog (called by event loop)""" if not hasattr(self, '_backend_widget') or not self._backend_widget: @@ -533,7 +559,18 @@ def stretchable(self, dim): def _create_backend_widget(self): self._backend_widget = None - + + def _set_backend_enabled(self, enabled): + """Enable/disable VBox and propagate to logical children.""" + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + def _draw(self, window, y, x, width, height): # Calculate total fixed height and number of stretchable children fixed_height = 0 @@ -583,6 +620,17 @@ def widgetClass(self): def _create_backend_widget(self): self._backend_widget = None + def _set_backend_enabled(self, enabled): + """Enable/disable HBox and propagate to logical children.""" + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + # Returns the stretchability of the layout box: # * The layout box is stretchable if one of the children is stretchable in # * this dimension or if one of the child widgets has a layout weight in @@ -691,7 +739,16 @@ def setText(self, new_text): def _create_backend_widget(self): self._backend_widget = None - + + def _set_backend_enabled(self, enabled): + """Enable/disable label: labels are not focusable; just keep enabled state for drawing.""" + try: + # labels don't accept focus; nothing to change except state used by draw + # draw() will consult self._enabled from base class + pass + except Exception: + pass + def _draw(self, window, y, x, width, height): try: attr = 0 @@ -730,7 +787,32 @@ def label(self): def _create_backend_widget(self): self._backend_widget = None - + + def _set_backend_enabled(self, enabled): + """Enable/disable the input field: affect focusability and focused state.""" + try: + # Save/restore _can_focus when toggling + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + # disable focusable behavior + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = False + self._can_focus = False + # if currently focused, remove focus + if getattr(self, "_focused", False): + self._focused = False + else: + # restore previous focusability + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + def _draw(self, window, y, x, width, height): try: # Draw label @@ -828,7 +910,28 @@ def setLabel(self, label): def _create_backend_widget(self): self._backend_widget = None - + + def _set_backend_enabled(self, enabled): + """Enable/disable push button: update focusability and collapse focus if disabling.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + def _draw(self, window, y, x, width, height): try: # Center the button label within available width @@ -885,7 +988,28 @@ def label(self): def _create_backend_widget(self): # In curses, there's no actual backend widget, just internal state pass - + + def _set_backend_enabled(self, enabled): + """Enable/disable checkbox: update focusability and collapse focus if disabling.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + def _draw(self, window, y, x, width, height): """Draw the checkbox with its label""" try: @@ -963,7 +1087,34 @@ def editable(self): def _create_backend_widget(self): self._backend_widget = None - + + def _set_backend_enabled(self, enabled): + """Enable/disable combobox: affect focusability, expanded state and focused state.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + # collapse expanded dropdown if any + try: + if getattr(self, "_expanded", False): + self._expanded = False + except Exception: + pass + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + def _draw(self, window, y, x, width, height): # Store position and dimensions for dropdown drawing self._combo_y = y @@ -1252,6 +1403,37 @@ def _create_backend_widget(self): # reset the cached visible rows so future navigation uses the next draw's value self._current_visible_rows = None + def _set_backend_enabled(self, enabled): + """Enable/disable selection box: affect focusability and propagate to row items.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + # propagate logical enabled state to contained items (if they are YWidget) + try: + for it in list(getattr(self, "_items", []) or []): + if hasattr(it, "setEnabled"): + try: + it.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + except Exception: + pass + def _draw(self, window, y, x, width, height): """Draw label (optional) and visible portion of items.""" try: @@ -1436,6 +1618,23 @@ def _create_backend_widget(self): self._backend_widget = None self._height = max(1, getattr(self._child, "_height", 1) if self._child else 1) + def _set_backend_enabled(self, enabled): + """Enable/disable alignment container and propagate to its logical child.""" + try: + # propagate to logical child so it updates its own focusability/state + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None and hasattr(child, "setEnabled"): + try: + child.setEnabled(enabled) + except Exception: + pass + # nothing else to do for curses backend (no real widget object) + except Exception: + pass + def _child_min_width(self, child, max_width): # Heuristic minimal width similar to YHBoxCurses TODO: verify with widget information instead of hardcoded classes try: From dfaf6cddb884766dc08df0bde53451331cfd7674 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 23 Nov 2025 19:24:19 +0100 Subject: [PATCH 076/523] fixed drawing disabled widget as not focusing --- manatools/aui/yui_curses.py | 175 ++++++++++++++++++++---------------- 1 file changed, 96 insertions(+), 79 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 9cd2f19..a7311d5 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -279,12 +279,12 @@ def _create_backend_widget(self): def _set_backend_enabled(self, enabled): """Enable/disable the dialog and propagate to contained widgets.""" try: - # propagate logical enabled state to entire subtree + # propagate logical enabled state to entire subtree using setEnabled on children + # so each widget's hook executes and updates its state. if getattr(self, "_child", None): try: self._child.setEnabled(enabled) except Exception: - # fallback: traverse _children if present pass for c in list(getattr(self, "_children", []) or []): try: @@ -299,6 +299,11 @@ def _set_backend_enabled(self, enabled): self._focused_widget = None except Exception: pass + # Force a redraw so disabled/enabled visual state appears immediately + try: + self._last_draw_time = 0 + except Exception: + pass except Exception: pass @@ -753,10 +758,13 @@ def _draw(self, window, y, x, width, height): try: attr = 0 if self._is_heading: - attr = curses.A_BOLD - + attr |= curses.A_BOLD + # dim if disabled + if not self.isEnabled(): + attr |= curses.A_DIM + # Truncate text to fit available width - display_text = self._text[:width-1] + display_text = self._text[:max(0, width-1)] window.addstr(y, x, display_text, attr) except curses.error: pass @@ -820,20 +828,22 @@ def _draw(self, window, y, x, width, height): label_text = self._label if len(label_text) > width // 3: label_text = label_text[:width // 3] - window.addstr(y, x, label_text) + lbl_attr = curses.A_BOLD if self._is_heading else curses.A_NORMAL + if not self.isEnabled(): + lbl_attr |= curses.A_DIM + window.addstr(y, x, label_text, lbl_attr) x += len(label_text) + 1 width -= len(label_text) + 1 - - # Calculate available space for input + if width <= 0: return - + # Prepare display value if self._password_mode and self._value: display_value = '*' * len(self._value) else: display_value = self._value - + # Handle scrolling for long values if len(display_value) > width: if self._cursor_pos >= width: @@ -841,27 +851,30 @@ def _draw(self, window, y, x, width, height): display_value = display_value[start_pos:start_pos + width] else: display_value = display_value[:width] - + # Draw input field background + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + field_bg = ' ' * width - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL window.addstr(y, x, field_bg, attr) - + # Draw text if display_value: window.addstr(y, x, display_value, attr) - - # Show cursor if focused - if self._focused: + + # Show cursor if focused and enabled + if self._focused and self.isEnabled(): cursor_display_pos = min(self._cursor_pos, width - 1) if cursor_display_pos < len(display_value): window.chgat(y, x + cursor_display_pos, 1, curses.A_REVERSE | curses.A_BOLD) - except curses.error: pass - + def _handle_key(self, key): - if not self._focused: + if not self._focused or not self.isEnabled(): return False handled = True @@ -937,22 +950,24 @@ def _draw(self, window, y, x, width, height): # Center the button label within available width button_text = f"[ {self._label} ]" text_x = x + max(0, (width - len(button_text)) // 2) - + # Only draw if we have enough space if text_x + len(button_text) <= x + width: - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - if self._focused: - attr |= curses.A_BOLD - + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + if self._focused: + attr |= curses.A_BOLD + window.addstr(y, text_x, button_text, attr) except curses.error: - # Ignore drawing errors (out of bounds) pass - + def _handle_key(self, key): - if not self._focused: + if not self._focused or not self.isEnabled(): return False - + if key == ord('\n') or key == ord(' '): # Button pressed -> post widget event to containing dialog dlg = self.findDialog() @@ -961,7 +976,7 @@ def _handle_key(self, key): dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) except Exception: pass - return True + return True return False class YCheckBoxCurses(YWidget): @@ -1011,29 +1026,33 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): - """Draw the checkbox with its label""" try: - # Draw checkbox symbol: [X] or [ ] checkbox_symbol = "[X]" if self._is_checked else "[ ]" text = f"{checkbox_symbol} {self._label}" - - # Truncate if too wide if len(text) > width: - text = text[:width-3] + "..." - - # Draw with highlighting if focused - if self._focused: + text = text[:max(0, width - 3)] + "..." + + if self._focused and self.isEnabled(): window.attron(curses.A_REVERSE) - + elif not self.isEnabled(): + # indicate disabled with dim attribute + window.attron(curses.A_DIM) + window.addstr(y, x, text) - - if self._focused: + + if self._focused and self.isEnabled(): window.attroff(curses.A_REVERSE) + elif not self.isEnabled(): + try: + window.attroff(curses.A_DIM) + except Exception: + pass except curses.error: pass - + def _handle_key(self, key): - """Handle keyboard input for checkbox (Space to toggle)""" + if not self.isEnabled(): + return False # Space or Enter to toggle if key in (ord(' '), ord('\n'), curses.KEY_ENTER): self._toggle() @@ -1120,49 +1139,54 @@ def _draw(self, window, y, x, width, height): self._combo_y = y self._combo_x = x self._combo_width = width - + try: # Calculate available space for combo box label_space = len(self._label) + 1 if self._label else 0 combo_space = width - label_space - - if combo_space <= 3: # Need at least space for " ▼ " + + if combo_space <= 3: return - + # Draw label if self._label: label_text = self._label if len(label_text) > label_space - 1: label_text = label_text[:label_space - 1] - window.addstr(y, x, label_text) + lbl_attr = curses.A_NORMAL + if not self.isEnabled(): + lbl_attr |= curses.A_DIM + window.addstr(y, x, label_text, lbl_attr) x += len(label_text) + 1 - - # Prepare display value - always show current value + + # Prepare display value display_value = self._value if self._value else "Select..." - max_display_width = combo_space - 3 # Reserve space for " ▼ " + max_display_width = combo_space - 3 if len(display_value) > max_display_width: display_value = display_value[:max_display_width] + "..." - + # Draw combo box background + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + combo_bg = " " * combo_space - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL window.addstr(y, x, combo_bg, attr) - - # Draw combo box content + combo_text = f" {display_value} ▼" if len(combo_text) > combo_space: combo_text = combo_text[:combo_space] - + window.addstr(y, x, combo_text, attr) - - # Draw expanded list if active - if self._expanded: + + # Draw expanded list if active and enabled + if self._expanded and self.isEnabled(): self._draw_expanded_list(window) - except curses.error: - # Ignore drawing errors pass + def _draw_expanded_list(self, window): """Draw the expanded dropdown list at correct position""" if not self._expanded or not self._items: @@ -1222,9 +1246,8 @@ def _draw_expanded_list(self, window): pass def _handle_key(self, key): - if not self._focused: - return False - + if not self._focused or not self.isEnabled(): + return False handled = True # If currently expanded, give expanded-list handling priority so Enter @@ -1435,29 +1458,28 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): - """Draw label (optional) and visible portion of items.""" try: line = y # draw label if present if self._label: lbl = self._label + lbl_attr = curses.A_BOLD + if not self.isEnabled(): + lbl_attr |= curses.A_DIM try: - window.addstr(line, x, lbl[:width], curses.A_BOLD) + window.addstr(line, x, lbl[:width], lbl_attr) except curses.error: pass line += 1 visible = self._visible_row_count() - # compute how many rows we can actually draw given provided height. available_rows = max(0, height - (1 if self._label else 0)) if self.stretchable(YUIDimension.YD_VERT): - # If widget is stretchable vertically, use all available rows (up to number of items) visible = min(len(self._items), available_rows) else: - # Otherwise prefer configured height but don't exceed available rows or items visible = min(len(self._items), self._visible_row_count(), available_rows) - # remember actual visible rows for navigation logic (_ensure_hover_visible) self._current_visible_rows = visible + for i in range(visible): item_idx = self._scroll_offset + i if item_idx >= len(self._items): @@ -1465,38 +1487,33 @@ def _draw(self, window, y, x, width, height): item = self._items[item_idx] text = item.label() checkbox = "*" if item in self._selected_items else " " - # Display selection marker for multi or single similarly display = f"[{checkbox}] {text}" - # truncate if len(display) > width: display = display[:max(0, width - 3)] + "..." attr = curses.A_NORMAL - if self._focused and item_idx == self._hover_index: + if not self.isEnabled(): + attr |= curses.A_DIM + if self._focused and item_idx == self._hover_index and self.isEnabled(): attr |= curses.A_REVERSE try: window.addstr(line + i, x, display.ljust(width), attr) except curses.error: pass - # if focused and there are more items than visible, show scrollbar hint - if self._focused and len(self._items) > visible and width > 0: + if self._focused and len(self._items) > visible and width > 0 and self.isEnabled(): try: - # show simple up/down markers at rightmost column if self._scroll_offset > 0: window.addch(y + (1 if self._label else 0), x + width - 1, '^') if (self._scroll_offset + visible) < len(self._items): window.addch(y + (1 if self._label else 0) + visible - 1, x + width - 1, 'v') except curses.error: pass - # keep _current_visible_rows until next draw; navigation will use it except curses.error: pass def _handle_key(self, key): - """Handle navigation and selection keys when focused.""" - if not self._focused: + if not self._focused or not self.isEnabled(): return False - handled = True if key == curses.KEY_UP: if self._hover_index > 0: From 9124acfd712fa55915852627ef42e40b4959505e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 10:32:28 +0100 Subject: [PATCH 077/523] Added parent in constructor, NOTE THAT is not back compatible but easy to manage with strong type checking --- manatools/aui/yui_common.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index e9f8b50..ec556d0 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -4,6 +4,7 @@ from enum import Enum import uuid +from typing import Optional # Enums class YUIDimension(Enum): @@ -389,11 +390,15 @@ def setData(self, new_data): self._data = new_data class YTreeItem(YItem): - def __init__(self, label, is_open=False, icon_name=""): + def __init__(self, label: str, parent: Optional["YTreeItem"] = None, is_open: bool = False, icon_name: str = ""): + ''' YTreeItem represents an item in a tree structure. + It can have child items and can be expanded or collapsed.''' super().__init__(label, False, icon_name) self._children = [] self._is_open = is_open - self._parent_item = None + self._parent_item = parent + if parent: + parent.addChild(self) def hasChildren(self): return len(self._children) > 0 From 392aaf76aaaf258d4885b6f4de78b3684b7593e2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 10:34:05 +0100 Subject: [PATCH 078/523] Adding YTree implementation for Qt BE --- manatools/aui/yui_qt.py | 227 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 7b25ef5..c79a45f 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -233,6 +233,11 @@ def createHVCenter(self, parent): def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: YAlignmentType): """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" return YAlignmentQt(parent, horAlign=horAlignment, vertAlign=vertAlignment) + + def createTree(self, parent, label, multiselection=False, recursiveselection = False): + """Create a Tree widget.""" + return YTreeQt(parent, label, multiselection, recursiveselection) + # Qt Widget Implementations class YDialogQt(YSingleChildContainerWidget): @@ -1265,3 +1270,225 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + +class YTreeQt(YSelectionWidget): + """ + Qt backend for YTree (based on YTree.h semantics). + + - Uses QTreeWidget to display hierarchical items stored in self._items. + - Supports multiSelection and immediateMode. + - Rebuild tree from internal items with rebuildTree(). + - currentItem() returns the YTreeItem wrapper for the focused/selected QTreeWidgetItem. + - activate() simulates user activation of the current item (posts an Activated event). + """ + def __init__(self, parent=None, label="", multiSelection=False, recursiveSelection=False): + super().__init__(parent) + self._label = label + self._multi = bool(multiSelection) + self._recursive = bool(recursiveSelection) + self._immediate = False + self._backend_widget = None + self._tree_widget = None + # mappings between QTreeWidgetItem and logical YTreeItem (python objects in self._items) + self._qitem_to_item = {} + self._item_to_qitem = {} + + def widgetClass(self): + return "YTree" + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + if self._label: + lbl = QtWidgets.QLabel(self._label) + layout.addWidget(lbl) + + tree = QtWidgets.QTreeWidget() + tree.setHeaderHidden(True) + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi else QtWidgets.QAbstractItemView.SingleSelection + tree.setSelectionMode(mode) + tree.itemSelectionChanged.connect(self._on_selection_changed) + tree.itemActivated.connect(self._on_item_activated) + + layout.addWidget(tree) + self._backend_widget = container + self._tree_widget = tree + + # populate if items already present + try: + self.rebuildTree() + except Exception: + pass + + def rebuildTree(self): + """Rebuild the QTreeWidget from self._items (calls helper recursively).""" + if self._tree_widget is None: + # ensure backend exists + self._create_backend_widget() + # clear existing + self._qitem_to_item.clear() + self._item_to_qitem.clear() + self._tree_widget.clear() + + def _add_recursive(parent_qitem, item): + # item expected to provide label() and possibly children() iterable + text = "" + try: + text = item.label() + except Exception: + try: + text = str(item) + except Exception: + text = "" + qitem = QtWidgets.QTreeWidgetItem([text]) + # preserve mapping + self._qitem_to_item[qitem] = item + self._item_to_qitem[item] = qitem + # attach to parent or top-level + if parent_qitem is None: + self._tree_widget.addTopLevelItem(qitem) + else: + parent_qitem.addChild(qitem) + + # recurse on children if available + try: + children = getattr(item, "children", None) + if callable(children): + childs = children() + else: + childs = children or [] + except Exception: + childs = [] + # many YTreeItem implementations may expose _children or similar; try common patterns + if not childs: + try: + childs = getattr(item, "_children", []) or [] + except Exception: + childs = [] + + for c in childs: + _add_recursive(qitem, c) + + return qitem + + for it in list(getattr(self, "_items", []) or []): + try: + _add_recursive(None, it) + except Exception: + pass + + # expand top-level by default to show items (mirror libyui reasonable behavior) + try: + self._tree_widget.expandAll() + except Exception: + pass + + def currentItem(self): + """Return the logical YTreeItem corresponding to the current/focused QTreeWidgetItem.""" + if not self._tree_widget: + return None + try: + qcur = self._tree_widget.currentItem() + if qcur is None: + # fallback to first selected item if current not set + sel = self._tree_widget.selectedItems() + qcur = sel[0] if sel else None + if qcur is None: + return None + return self._qitem_to_item.get(qcur, None) + except Exception: + return None + + def activate(self): + """Simulate activation of the current item (post Activated event).""" + item = self.currentItem() + if item is None: + return False + try: + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + return True + except Exception: + return False + + def immediateMode(self): + return bool(self._immediate) + + def setImmediateMode(self, on=True): + self._immediate = bool(on) + + # selection change handler + def _on_selection_changed(self): + # if immediate mode, post selection-changed event immediately + try: + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + + # item activated (double click / Enter) + def _on_item_activated(self, qitem, column): + try: + # map to logical item + item = self._qitem_to_item.get(qitem, None) + if item is None: + return + # post activated event + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + pass + + def addItem(self, item): + '''Add a YItem redefinition from YSelectionWidget to manage YTreeItems.''' + if isinstance(item, str): + item = YTreeItem(item) + self._items.append(item) + else: + super().addItem(item) + + # property API hooks (minimal implementation) + def setProperty(self, propertyName, val): + try: + if propertyName == "immediateMode": + self.setImmediateMode(bool(val)) + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "immediateMode": + return self.immediateMode() + except Exception: + pass + return None + + def _set_backend_enabled(self, enabled): + """Enable/disable the tree widget and propagate to logical items/widgets.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # logical propagation to child YWidgets (if any) + try: + for c in list(getattr(self, "_items", []) or []): + try: + if hasattr(c, "setEnabled"): + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass From f708b3d03e551b128b1a1b771011bd6cbcd25659 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 11:36:07 +0100 Subject: [PATCH 079/523] improved item selection --- manatools/aui/yui_qt.py | 49 ++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index c79a45f..4b3e02e 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -1280,13 +1280,14 @@ class YTreeQt(YSelectionWidget): - Rebuild tree from internal items with rebuildTree(). - currentItem() returns the YTreeItem wrapper for the focused/selected QTreeWidgetItem. - activate() simulates user activation of the current item (posts an Activated event). + - recursiveSelection if it should select children recursively """ def __init__(self, parent=None, label="", multiSelection=False, recursiveSelection=False): super().__init__(parent) self._label = label self._multi = bool(multiSelection) self._recursive = bool(recursiveSelection) - self._immediate = False + self._immediate = self.notify() self._backend_widget = None self._tree_widget = None # mappings between QTreeWidgetItem and logical YTreeItem (python objects in self._items) @@ -1418,17 +1419,49 @@ def activate(self): def immediateMode(self): return bool(self._immediate) - def setImmediateMode(self, on=True): - self._immediate = bool(on) + def setImmediateMode(self, on:bool=True): + self._immediate = on + self.steNotify(on) # selection change handler def _on_selection_changed(self): - # if immediate mode, post selection-changed event immediately + """Update logical selection list and emit selection-changed event when needed.""" try: - if self._immediate and self.notify(): - dlg = self.findDialog() - if dlg is not None and self.notify(): - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + # map QTreeWidget selected QTreeWidgetItem -> logical YTreeItem + sel_qitems = self._tree_widget.selectedItems() if self._tree_widget is not None else [] + new_selected = [] + for qitem in sel_qitems: + itm = self._qitem_to_item.get(qitem, None) + if itm is not None: + new_selected.append(itm) + + # Update internal selected items list (logical selection used by base class) + try: + # clear previous selection flags for all known items + for it in list(getattr(self, "_items", []) or []): + try: + it.setSelected(False) + except Exception: + pass + # set selection flag for newly selected items + for it in new_selected: + try: + it.setSelected(True) + except Exception: + pass + except Exception: + pass + + self._selected_items = new_selected + + # immediate mode: notify container/dialog + try: + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass except Exception: pass From 1ae7390c7332fefe6b392b8e0a906534841350a8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 11:45:45 +0100 Subject: [PATCH 080/523] Added per item open management --- manatools/aui/yui_qt.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 4b3e02e..df4a684 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -1354,6 +1354,14 @@ def _add_recursive(parent_qitem, item): else: parent_qitem.addChild(qitem) + # set expanded state according to the logical item's _is_open flag + try: + is_open = bool(getattr(item, "_is_open", False)) + # setExpanded ensures the node shows as expanded/collapsed + qitem.setExpanded(is_open) + except Exception: + pass + # recurse on children if available try: children = getattr(item, "children", None) @@ -1380,12 +1388,7 @@ def _add_recursive(parent_qitem, item): _add_recursive(None, it) except Exception: pass - - # expand top-level by default to show items (mirror libyui reasonable behavior) - try: - self._tree_widget.expandAll() - except Exception: - pass + # do not call expandAll(); expansion is controlled per-item by _is_open def currentItem(self): """Return the logical YTreeItem corresponding to the current/focused QTreeWidgetItem.""" From f323217a1eb93e3193bf57f3669699b623dfbac8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 12:04:42 +0100 Subject: [PATCH 081/523] added check for multiselection active --- manatools/aui/yui_qt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index df4a684..cbacfae 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -1419,6 +1419,10 @@ def activate(self): except Exception: return False + def hasMultiSelection(self): + """Return True if the tree allows selecting multiple items at once.""" + return bool(self._multi) + def immediateMode(self): return bool(self._immediate) From bb4229fbfce02c284cb7ccb7f4d538135c2b16d7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 12:21:35 +0100 Subject: [PATCH 082/523] added recursive selection --- manatools/aui/yui_qt.py | 72 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index cbacfae..a18dac8 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -1287,12 +1287,16 @@ def __init__(self, parent=None, label="", multiSelection=False, recursiveSelecti self._label = label self._multi = bool(multiSelection) self._recursive = bool(recursiveSelection) + if self._recursive: + self._multi = True # recursive selection implies multi-selection self._immediate = self.notify() self._backend_widget = None self._tree_widget = None # mappings between QTreeWidgetItem and logical YTreeItem (python objects in self._items) self._qitem_to_item = {} self._item_to_qitem = {} + # guard to avoid recursion when programmatically changing selection + self._suppress_selection_handler = False def widgetClass(self): return "YTree" @@ -1430,14 +1434,76 @@ def setImmediateMode(self, on:bool=True): self._immediate = on self.steNotify(on) + def _collect_descendant_qitems(self, qitem): + """Return a list of qitem and all descendant QTreeWidgetItem objects.""" + out = [] + if qitem is None: + return out + stack = [qitem] + while stack: + cur = stack.pop() + out.append(cur) + try: + for i in range(cur.childCount()): + stack.append(cur.child(i)) + except Exception: + pass + return out + # selection change handler def _on_selection_changed(self): """Update logical selection list and emit selection-changed event when needed.""" + # Defensive guard: when we change selection programmatically we don't want to re-enter here. + if self._suppress_selection_handler: + return + try: - # map QTreeWidget selected QTreeWidgetItem -> logical YTreeItem - sel_qitems = self._tree_widget.selectedItems() if self._tree_widget is not None else [] + if not self._tree_widget: + return + + sel_qitems = list(self._tree_widget.selectedItems()) + + # If recursive selection is enabled and multi-selection is allowed, + # ensure children of selected parents become selected too. + if self._recursive and self._multi: + # compute desired set = selected qitems + all their descendants + desired_set = set() + for q in sel_qitems: + for dq in self._collect_descendant_qitems(q): + desired_set.add(dq) + + # if current selection differs, update QTreeWidget selection programmatically + current_set = set(sel_qitems) + if desired_set != current_set: + try: + self._suppress_selection_handler = True + self._tree_widget.clearSelection() + for q in desired_set: + try: + q.setSelected(True) + except Exception: + pass + finally: + self._suppress_selection_handler = False + # refresh sel_qitems to the new expanded selection + sel_qitems = list(self._tree_widget.selectedItems()) + + # For recursive selection but single-selection mode: keep UI selection as-is but + # include descendants in logical selection list (best-effort). + logical_qitems = [] + if self._recursive and not self._multi: + # include selected items and their descendants in logical list + for q in sel_qitems: + logical_qitems.append(q) + for dq in self._collect_descendant_qitems(q): + if dq is not q: + logical_qitems.append(dq) + else: + logical_qitems = sel_qitems + + # Map qitems -> logical YTreeItem objects new_selected = [] - for qitem in sel_qitems: + for qitem in logical_qitems: itm = self._qitem_to_item.get(qitem, None) if itm is not None: new_selected.append(itm) From 7306037761640477ea5bc6886da165782cb414be Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 12:27:35 +0100 Subject: [PATCH 083/523] removing selection is propagated in recursive selection --- manatools/aui/yui_qt.py | 43 +++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index a18dac8..8971321 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -1274,8 +1274,6 @@ def _set_backend_enabled(self, enabled): class YTreeQt(YSelectionWidget): """ Qt backend for YTree (based on YTree.h semantics). - - - Uses QTreeWidget to display hierarchical items stored in self._items. - Supports multiSelection and immediateMode. - Rebuild tree from internal items with rebuildTree(). - currentItem() returns the YTreeItem wrapper for the focused/selected QTreeWidgetItem. @@ -1297,6 +1295,8 @@ def __init__(self, parent=None, label="", multiSelection=False, recursiveSelecti self._item_to_qitem = {} # guard to avoid recursion when programmatically changing selection self._suppress_selection_handler = False + # remember last selected QTreeWidgetItem set to detect added/removed selections + self._last_selected_qitems = set() def widgetClass(self): return "YTree" @@ -1462,18 +1462,31 @@ def _on_selection_changed(self): return sel_qitems = list(self._tree_widget.selectedItems()) + current_set = set(sel_qitems) # If recursive selection is enabled and multi-selection is allowed, - # ensure children of selected parents become selected too. + # adjust selection so that selecting a parent selects all descendants + # and deselecting a parent deselects all descendants. if self._recursive and self._multi: - # compute desired set = selected qitems + all their descendants - desired_set = set() - for q in sel_qitems: + added = current_set - self._last_selected_qitems + removed = self._last_selected_qitems - current_set + + # start desired_set from current_set + desired_set = set(current_set) + + # For every newly added item, ensure its descendants are selected + for q in list(added): for dq in self._collect_descendant_qitems(q): desired_set.add(dq) - # if current selection differs, update QTreeWidget selection programmatically - current_set = set(sel_qitems) + # For every removed item, ensure its descendants are deselected + for q in list(removed): + for dq in self._collect_descendant_qitems(q): + if dq in desired_set: + desired_set.discard(dq) + + # If desired_set differs from what's currently selected in the widget, + # apply the change programmatically. if desired_set != current_set: try: self._suppress_selection_handler = True @@ -1485,14 +1498,14 @@ def _on_selection_changed(self): pass finally: self._suppress_selection_handler = False - # refresh sel_qitems to the new expanded selection + # refresh sel_qitems and current_set after modification sel_qitems = list(self._tree_widget.selectedItems()) + current_set = set(sel_qitems) - # For recursive selection but single-selection mode: keep UI selection as-is but - # include descendants in logical selection list (best-effort). + # Build logical_qitems: if recursive + single select, include descendants in logical list; + # otherwise logical_qitems mirrors current UI selection. logical_qitems = [] if self._recursive and not self._multi: - # include selected items and their descendants in logical list for q in sel_qitems: logical_qitems.append(q) for dq in self._collect_descendant_qitems(q): @@ -1527,6 +1540,12 @@ def _on_selection_changed(self): self._selected_items = new_selected + # remember last selected QTreeWidgetItem set for next invocation + try: + self._last_selected_qitems = set(self._tree_widget.selectedItems()) + except Exception: + self._last_selected_qitems = set() + # immediate mode: notify container/dialog try: if self._immediate and self.notify(): From b386b55b75c131a8926b6d3801a98b8d19850642 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 12:32:43 +0100 Subject: [PATCH 084/523] testing YTree --- test/test_multiselection_tree.py | 99 ++++++++++++++++++++++++++ test/test_recursiveselection_tree.py | 100 +++++++++++++++++++++++++++ test/test_tree.py | 93 +++++++++++++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 test/test_multiselection_tree.py create mode 100644 test/test_recursiveselection_tree.py create mode 100644 test/test_tree.py diff --git a/test/test_multiselection_tree.py b/test/test_multiselection_tree.py new file mode 100644 index 0000000..45d8cb1 --- /dev/null +++ b/test/test_multiselection_tree.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_tree(backend_name=None): + """Test ComboBox widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Create dialog focused on ComboBox testing + dialog = factory.createMainDialog() + vbox = factory.createVBox(dialog) + + factory.createHeading(vbox, "Tree Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + factory.createLabel(vbox, "Test selecting and displaying values") + + # Test ComboBox with initial selection + factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + tree = factory.createTree(hbox, "Select:", multiselection=True) + + for i in range(5): + item = yui.YTreeItem(f"Item {i+1}", is_open=(i==0)) + for j in range(3): + subitem = yui.YTreeItem(f"SubItem {i+1}.{j+1}", parent=item) + if i==1 and j == 1: + for k in range(2): + yui.YTreeItem(f"SubItem {i+1}.{j+1}.{k+1}", parent=subitem) + + tree.addItem(item) + + selected = factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + ok_button = factory.createPushButton(hbox, "OK") + cancel_button = factory.createPushButton(hbox, "Cancel") + + print("\nOpening ComboBox test dialog...") + + while True: + event = dialog.waitForEvent() + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + reason = event.reason() + if wdg == cancel_button: + dialog.destroy() + break + elif wdg == tree: + if reason == yui.YEventReason.SelectionChanged: + if tree.hasMultiSelection(): + labels = [item.label() for item in tree.selectedItems()] + selected.setText(f"Selected: {labels}") + elif tree.selectedItem() is not None: + selected.setText(f"Selected: '{tree.selectedItem().label()}'") + else: + selected.setText("Selected: None") + elif reason == yui.YEventReason.Activated: + selected.setText(f"Activated: '{tree.selectedItem().label()}'") + elif wdg == ok_button: + selected.setText(f"OK clicked.") + + # Show final result + print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") + + except Exception as e: + print(f"Error testing Tree with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_tree(sys.argv[1]) + else: + test_tree() diff --git a/test/test_recursiveselection_tree.py b/test/test_recursiveselection_tree.py new file mode 100644 index 0000000..198a873 --- /dev/null +++ b/test/test_recursiveselection_tree.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_tree(backend_name=None): + """Test ComboBox widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Create dialog focused on ComboBox testing + dialog = factory.createMainDialog() + vbox = factory.createVBox(dialog) + + factory.createHeading(vbox, "Tree Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + factory.createLabel(vbox, "Test selecting and displaying values") + + # Test ComboBox with initial selection + factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + tree = factory.createTree(hbox, "Select:", multiselection=False, recursiveselection=True) + + for i in range(5): + item = yui.YTreeItem(f"Item {i+1}", is_open=(i==0)) + for j in range(3): + subitem = yui.YTreeItem(f"SubItem {i+1}.{j+1}", parent=item) + if i==1 and j == 1: + for k in range(2): + yui.YTreeItem(f"SubItem {i+1}.{j+1}.{k+1}", parent=subitem) + + tree.addItem(item) + + selected = factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + ok_button = factory.createPushButton(hbox, "OK") + cancel_button = factory.createPushButton(hbox, "Cancel") + + print("\nOpening ComboBox test dialog...") + + while True: + event = dialog.waitForEvent() + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + reason = event.reason() + if wdg == cancel_button: + dialog.destroy() + break + elif wdg == tree: + if reason == yui.YEventReason.SelectionChanged: + if tree.hasMultiSelection(): + labels = [item.label() for item in tree.selectedItems()] + selected.setText(f"Selected: {labels}") + elif tree.selectedItem() is not None: + selected.setText(f"Selected: '{tree.selectedItem().label()}'") + else: + selected.setText("Selected: None") + elif reason == yui.YEventReason.Activated: + if tree.selectedItem() is not None: + selected.setText(f"Activated: '{tree.selectedItem().label()}'") + elif wdg == ok_button: + selected.setText(f"OK clicked.") + + # Show final result + print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") + + except Exception as e: + print(f"Error testing Tree with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_tree(sys.argv[1]) + else: + test_tree() diff --git a/test/test_tree.py b/test/test_tree.py new file mode 100644 index 0000000..e2a5329 --- /dev/null +++ b/test/test_tree.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_tree(backend_name=None): + """Test ComboBox widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Create dialog focused on ComboBox testing + dialog = factory.createMainDialog() + vbox = factory.createVBox(dialog) + + factory.createHeading(vbox, "Tree Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + factory.createLabel(vbox, "Test selecting and displaying values") + + # Test ComboBox with initial selection + factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + tree = factory.createTree(hbox, "Select:") + + for i in range(5): + item = yui.YTreeItem(f"Item {i+1}", is_open=(i==0)) + for j in range(3): + subitem = yui.YTreeItem(f"SubItem {i+1}.{j+1}", parent=item) + if i==1 and j == 1: + for k in range(2): + yui.YTreeItem(f"SubItem {i+1}.{j+1}.{k+1}", parent=subitem) + + tree.addItem(item) + + selected = factory.createLabel(vbox, "") + hbox = factory.createHBox(vbox) + ok_button = factory.createPushButton(hbox, "OK") + cancel_button = factory.createPushButton(hbox, "Cancel") + + print("\nOpening ComboBox test dialog...") + + while True: + event = dialog.waitForEvent() + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + reason = event.reason() + if wdg == cancel_button: + dialog.destroy() + break + elif wdg == tree: + if reason == yui.YEventReason.SelectionChanged: + selected.setText(f"Selected: '{tree.selectedItem().label()}'") + elif reason == yui.YEventReason.Activated: + selected.setText(f"Activated: '{tree.selectedItem().label()}'") + elif wdg == ok_button: + selected.setText(f"OK clicked.") + + # Show final result + print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") + + except Exception as e: + print(f"Error testing Tree with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_tree(sys.argv[1]) + else: + test_tree() From 6e6f8f2ac2b54ff1a794962fe66a19e33658f367 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 12:47:57 +0100 Subject: [PATCH 085/523] First attempt to implement YTree for GTK BE --- manatools/aui/yui_gtk.py | 455 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index fd3a172..54fad08 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -246,6 +246,9 @@ def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: Y """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" return YAlignmentGtk(parent, horAlign=horAlignment, vertAlign=vertAlignment) + def createTree(self, parent, label, multiselection=False, recursiveselection = False): + """Create a Tree widget.""" + return YTreeGtk(parent, label, multiselection, recursiveselection) # GTK4 Widget Implementations class YDialogGtk(YSingleChildContainerWidget): @@ -1877,3 +1880,455 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + +class YTreeGtk(YSelectionWidget): + def __init__(self, parent=None, label="", multiselection=False, recursiveselection=False): + super().__init__(parent) + self._label = label + self._multi = bool(multiselection) + self._recursive = bool(recursiveselection) + if self._recursive: + # recursive selection implies multi-selection semantics + self._multi = True + self._immediate = self.notify() + self._backend_widget = None + self._treeview = None + self._treestore = None + # guard to avoid re-entrancy when changing selection programmatically + self._suppress_selection_handler = False + # remember last selected set (by path string) for delta detection + self._last_selected_paths = set() + + def widgetClass(self): + return "YTree" + + def _create_backend_widget(self): + """Create Gtk widgets: optional label + TreeView backed by TreeStore.""" + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + if self._label: + lbl = Gtk.Label(label=self._label) + try: + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + except Exception: + pass + try: + vbox.append(lbl) + except Exception: + try: + vbox.add(lbl) + except Exception: + pass + + # TreeStore with two columns: display string and python object (YTreeItem) + try: + # first column: str, second: python object + self._treestore = Gtk.TreeStore(str, object) + except Exception: + # fallback: try object column via GObject.TYPE_PYOBJECT + try: + self._treestore = Gtk.TreeStore(str, GObject.TYPE_PYOBJECT) + except Exception: + self._treestore = Gtk.TreeStore(str, object) + + # TreeView + tree = Gtk.TreeView(model=self._treestore) + # single text column + try: + renderer = Gtk.CellRendererText() + col = Gtk.TreeViewColumn.new_with_attributes("Name", renderer, text=0) + tree.append_column(col) + except Exception: + try: + renderer = Gtk.CellRendererText() + col = Gtk.TreeViewColumn("Name") + col.pack_start(renderer, True) + col.add_attribute(renderer, "text", 0) + tree.append_column(col) + except Exception: + pass + + # selection mode + try: + sel = tree.get_selection() + mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE + sel.set_mode(mode) + # connect handler + try: + sel.connect("changed", self._on_selection_changed) + except Exception: + # alternative API + try: + tree.get_selection().connect("changed", self._on_selection_changed) + except Exception: + pass + except Exception: + pass + + # row-activated (double click / Enter) + try: + tree.connect("row-activated", self._on_row_activated) + except Exception: + pass + + # store references + try: + vbox.append(tree) + except Exception: + try: + vbox.add(tree) + except Exception: + pass + + self._backend_widget = vbox + self._treeview = tree + + # If items were already present, populate model + try: + if getattr(self, "_items", None): + self.rebuildTree() + except Exception: + pass + + def rebuildTree(self): + """Build TreeStore from self._items and apply per-item expanded state.""" + if self._treestore is None or self._treeview is None: + # ensure backend exists + self._create_backend_widget() + + # clear existing store + try: + self._treestore.clear() + except Exception: + # recreate if clear not available + try: + self._treestore = Gtk.TreeStore(str, object) + self._treeview.set_model(self._treestore) + except Exception: + pass + + def _add_recursive(parent_iter, item): + try: + label = item.label() if hasattr(item, "label") else str(item) + except Exception: + label = str(item) + try: + new_iter = self._treestore.append(parent_iter, [label, item]) + except Exception: + # fallback: append with only label then set object separately + new_iter = self._treestore.append(parent_iter, [label, None]) + try: + self._treestore.set_value(new_iter, 1, item) + except Exception: + pass + + # set expanded/collapsed according to item._is_open / isOpen() + try: + is_open = bool(getattr(item, "_is_open", False)) + except Exception: + try: + is_open = bool(item.isOpen()) + except Exception: + is_open = False + + # If treeview and path APIs available, expand row + try: + path = self._treestore.get_path(new_iter) + if is_open: + try: + self._treeview.expand_row(path, False) + except Exception: + # some bindings expect path string + try: + self._treeview.expand_row(path.to_string(), False) + except Exception: + pass + except Exception: + pass + + # recurse children + try: + childs = [] + try: + if callable(getattr(item, "children", None)): + childs = item.children() + else: + childs = getattr(item, "_children", []) or [] + except Exception: + childs = getattr(item, "_children", []) or [] + for c in childs: + _add_recursive(new_iter, c) + except Exception: + pass + + return new_iter + + try: + for it in list(getattr(self, "_items", []) or []): + try: + _add_recursive(None, it) + except Exception: + pass + except Exception: + pass + + # reset last selected paths + self._last_selected_paths = set() + + def _iter_to_path_str(self, treeiter): + try: + path = self._treestore.get_path(treeiter) + try: + return path.to_string() + except Exception: + return str(path) + except Exception: + return None + + def _collect_descendant_iters(self, treeiter): + """Return list of iter for treeiter and all its descendants.""" + out = [] + if treeiter is None: + return out + # depth-first traversal + stack = [treeiter] + while stack: + cur = stack.pop() + out.append(cur) + try: + child = self._treestore.iter_children(cur) + while child is not None: + stack.append(child) + try: + child = self._treestore.iter_next(child) + except Exception: + # iter_next returns False sometimes + break + except Exception: + pass + return out + + def _on_selection_changed(self, selection): + """Sync selection from TreeView to logical _selected_items and handle recursive propagation.""" + if self._suppress_selection_handler: + return + + try: + # collect currently selected iters (as path strings and iters) + sel_paths = [] + sel_iters = [] + + # preferred API: selection.selected_foreach(callback, user_data) + try: + def _cb(model, path, treeiter, ud=None): + sel_paths.append(path.to_string() if hasattr(path, "to_string") else str(path)) + sel_iters.append(treeiter) + selection.selected_foreach(_cb) + except Exception: + # fallback: try selection.get_selected_rows() + try: + rows = selection.get_selected_rows() + for p in rows: + try: + it = self._treestore.get_iter(p) + sel_paths.append(p.to_string() if hasattr(p, "to_string") else str(p)) + sel_iters.append(it) + except Exception: + pass + except Exception: + # final fallback: nothing + pass + + current_set = set(sel_paths) + added = current_set - self._last_selected_paths + removed = self._last_selected_paths - current_set + + # If recursive + multi: selecting a parent selects descendants; deselecting a parent deselects descendants + if self._recursive and self._multi: + desired_paths = set(current_set) + # for each added path, add all descendant paths + for idx, it in enumerate(sel_iters): + try: + if sel_paths[idx] in added: + for dq in self._collect_descendant_iters(it): + pstr = self._iter_to_path_str(dq) + if pstr: + desired_paths.add(pstr) + except Exception: + pass + # for each removed path, remove its descendants from desired set + for pstr in list(removed): + try: + # find iter for this path + try: + path_obj = Gtk.TreePath.new_from_string(pstr) + except Exception: + path_obj = None + if path_obj: + try: + rem_iter = self._treestore.get_iter(path_obj) + except Exception: + rem_iter = None + else: + rem_iter = None + if rem_iter is not None: + for dq in self._collect_descendant_iters(rem_iter): + dp = self._iter_to_path_str(dq) + if dp and dp in desired_paths: + desired_paths.discard(dp) + except Exception: + pass + + # if desired differs from current, apply programmatically + if desired_paths != current_set: + try: + self._suppress_selection_handler = True + selection.unselect_all() + for pstr in desired_paths: + try: + path_obj = Gtk.TreePath.new_from_string(pstr) + selection.select_path(path_obj) + except Exception: + try: + selection.select_path(pstr) + except Exception: + pass + finally: + self._suppress_selection_handler = False + # rebuild selected lists after modification + sel_paths = [] + sel_iters = [] + try: + selection.selected_foreach(lambda m, p, it, ud=None: (sel_paths.append(p.to_string() if hasattr(p, "to_string") else str(p)), sel_iters.append(it))) + except Exception: + pass + + # Build logical selected YTreeItem list from sel_iters (or descendants if recursive+single) + logical_iters = [] + if self._recursive and not self._multi: + # include selected iters and their descendants + for it in sel_iters: + logical_iters.append(it) + try: + for dq in self._collect_descendant_iters(it): + if dq is not it: + logical_iters.append(dq) + except Exception: + pass + else: + logical_iters = sel_iters + + new_selected = [] + # clear previous selected flag on all known items + try: + for it in list(getattr(self, "_items", []) or []): + try: + it.setSelected(False) + except Exception: + pass + except Exception: + pass + + for it in logical_iters: + try: + itm = self._treestore.get_value(it, 1) + except Exception: + try: + # alternative accessor + itm = self._treestore[it][1] + except Exception: + itm = None + if itm is not None: + try: + itm.setSelected(True) + except Exception: + pass + new_selected.append(itm) + + self._selected_items = new_selected + + # store last selected paths + self._last_selected_paths = set(sel_paths) + + # notify immediate mode + try: + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + + except Exception: + pass + + def _on_row_activated(self, treeview, path, column): + """Row activated (double click / Enter) handler.""" + try: + # Post an Activated event to containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + pass + + def currentItem(self): + """Return first selected logical item or None.""" + try: + sel = self._selected_items + return sel[0] if sel else None + except Exception: + return None + + def getSelectedItem(self): + return self.currentItem() + + def getSelectedItems(self): + return list(self._selected_items) + + def activate(self): + """Simulate activation of current item.""" + try: + itm = self.currentItem() + if itm is None: + return False + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + return True + except Exception: + return False + + def immediateMode(self): + return bool(self._immediate) + + def setImmediateMode(self, on=True): + self._immediate = bool(on) + + def hasMultiSelection(self): + return bool(self._multi) + + def _set_backend_enabled(self, enabled): + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate logical enabled state to items (if they are YWidgets) + try: + for it in list(getattr(self, "_items", []) or []): + try: + if hasattr(it, "setEnabled"): + it.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def get_backend_widget(self): + if self._backend_widget is None: + self._create_backend_widget() + return self._backend_widget \ No newline at end of file From 193ed4c70c00e1368f7af97be6361f399f9bece1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 15:29:26 +0100 Subject: [PATCH 086/523] Avoid deprecated (sigh) Gtk.TreeView --- manatools/aui/yui_gtk.py | 610 ++++++++++++++++++++++----------------- 1 file changed, 346 insertions(+), 264 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 54fad08..b69e313 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -1882,6 +1882,13 @@ def _set_backend_enabled(self, enabled): pass class YTreeGtk(YSelectionWidget): + """ + Stable Gtk4 implementation of a tree using Gtk.ListBox + ScrolledWindow. + + - Renders visible nodes (respecting YTreeItem._is_open). + - Supports multiselection and recursiveSelection (select/deselect parents -> children). + - Preserves stretching: the ScrolledWindow/ListBox expand to fill container. + """ def __init__(self, parent=None, label="", multiselection=False, recursiveselection=False): super().__init__(parent) self._label = label @@ -1892,391 +1899,468 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti self._multi = True self._immediate = self.notify() self._backend_widget = None - self._treeview = None - self._treestore = None - # guard to avoid re-entrancy when changing selection programmatically + self._listbox = None + # cached rows and mappings + self._rows = [] # ordered list of Gtk.ListBoxRow + self._row_to_item = {} # row -> YTreeItem + self._item_to_row = {} # YTreeItem -> row + self._visible_items = [] # list of (item, depth) self._suppress_selection_handler = False - # remember last selected set (by path string) for delta detection - self._last_selected_paths = set() + self._last_selected_ids = set() + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) def widgetClass(self): return "YTree" def _create_backend_widget(self): - """Create Gtk widgets: optional label + TreeView backed by TreeStore.""" - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + if self._label: - lbl = Gtk.Label(label=self._label) try: + lbl = Gtk.Label(label=self._label) if hasattr(lbl, "set_xalign"): lbl.set_xalign(0.0) - except Exception: - pass - try: vbox.append(lbl) except Exception: - try: - vbox.add(lbl) - except Exception: - pass + pass - # TreeStore with two columns: display string and python object (YTreeItem) + # ListBox (flat, shows only visible nodes). Put into ScrolledWindow so it won't grow parent on expand. + listbox = Gtk.ListBox() try: - # first column: str, second: python object - self._treestore = Gtk.TreeStore(str, object) + mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE + listbox.set_selection_mode(mode) + # Let listbox expand in available area + listbox.set_vexpand(True) + listbox.set_hexpand(True) except Exception: - # fallback: try object column via GObject.TYPE_PYOBJECT - try: - self._treestore = Gtk.TreeStore(str, GObject.TYPE_PYOBJECT) - except Exception: - self._treestore = Gtk.TreeStore(str, object) + pass - # TreeView - tree = Gtk.TreeView(model=self._treestore) - # single text column + sw = Gtk.ScrolledWindow() try: - renderer = Gtk.CellRendererText() - col = Gtk.TreeViewColumn.new_with_attributes("Name", renderer, text=0) - tree.append_column(col) + sw.set_child(listbox) except Exception: try: - renderer = Gtk.CellRendererText() - col = Gtk.TreeViewColumn("Name") - col.pack_start(renderer, True) - col.add_attribute(renderer, "text", 0) - tree.append_column(col) + sw.add(listbox) except Exception: pass - # selection mode + # Make scrolled window expand to fill container (so tree respects parent stretching) try: - sel = tree.get_selection() - mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE - sel.set_mode(mode) - # connect handler - try: - sel.connect("changed", self._on_selection_changed) - except Exception: - # alternative API - try: - tree.get_selection().connect("changed", self._on_selection_changed) - except Exception: - pass + sw.set_vexpand(True) + sw.set_hexpand(True) + vbox.set_vexpand(True) + vbox.set_hexpand(True) except Exception: pass - # row-activated (double click / Enter) + # connect selection signal; use defensive handler that scans rows try: - tree.connect("row-activated", self._on_row_activated) + listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) except Exception: pass - # store references + self._backend_widget = vbox + self._listbox = listbox + try: - vbox.append(tree) + vbox.append(sw) except Exception: try: - vbox.add(tree) + vbox.add(sw) except Exception: pass - self._backend_widget = vbox - self._treeview = tree - - # If items were already present, populate model + # populate if items already exist try: if getattr(self, "_items", None): self.rebuildTree() except Exception: pass - def rebuildTree(self): - """Build TreeStore from self._items and apply per-item expanded state.""" - if self._treestore is None or self._treeview is None: - # ensure backend exists - self._create_backend_widget() + def _make_row(self, item, depth): + """Create a ListBoxRow for item with indentation and (optional) toggle button.""" + row = Gtk.ListBoxRow() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - # clear existing store + # indentation spacer try: - self._treestore.clear() + indent = Gtk.Box() + indent.set_size_request(depth * 12, 1) + hbox.append(indent) except Exception: - # recreate if clear not available - try: - self._treestore = Gtk.TreeStore(str, object) - self._treeview.set_model(self._treestore) - except Exception: - pass + pass - def _add_recursive(parent_iter, item): - try: - label = item.label() if hasattr(item, "label") else str(item) - except Exception: - label = str(item) + # toggle if item has children + has_children = False + try: + childs = [] + if callable(getattr(item, "children", None)): + childs = item.children() or [] + else: + childs = getattr(item, "_children", []) or [] + has_children = len(childs) > 0 + except Exception: + has_children = False + + if has_children: try: - new_iter = self._treestore.append(parent_iter, [label, item]) - except Exception: - # fallback: append with only label then set object separately - new_iter = self._treestore.append(parent_iter, [label, None]) + btn = Gtk.Button(label="▾" if bool(getattr(item, "_is_open", False)) else "▸") try: - self._treestore.set_value(new_iter, 1, item) + btn.set_relief(Gtk.ReliefStyle.NONE) except Exception: pass - - # set expanded/collapsed according to item._is_open / isOpen() - try: - is_open = bool(getattr(item, "_is_open", False)) + btn.set_focus_on_click(False) + btn.connect("clicked", lambda b, it=item: self._on_toggle_clicked(it)) + hbox.append(btn) except Exception: + # fallback spacer try: - is_open = bool(item.isOpen()) + spacer = Gtk.Box() + spacer.set_size_request(14, 1) + hbox.append(spacer) except Exception: - is_open = False - - # If treeview and path APIs available, expand row + pass + else: try: - path = self._treestore.get_path(new_iter) - if is_open: - try: - self._treeview.expand_row(path, False) - except Exception: - # some bindings expect path string - try: - self._treeview.expand_row(path.to_string(), False) - except Exception: - pass + spacer = Gtk.Box() + spacer.set_size_request(14, 1) + hbox.append(spacer) except Exception: pass - # recurse children + # label + try: + lbl = Gtk.Label(label=item.label() if hasattr(item, "label") else str(item)) + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + # ensure label expands to take remaining space try: - childs = [] - try: - if callable(getattr(item, "children", None)): - childs = item.children() - else: - childs = getattr(item, "_children", []) or [] - except Exception: - childs = getattr(item, "_children", []) or [] - for c in childs: - _add_recursive(new_iter, c) + lbl.set_hexpand(True) except Exception: pass + hbox.append(lbl) + except Exception: + pass - return new_iter + try: + row.set_child(hbox) + except Exception: + try: + row.add(hbox) + except Exception: + pass try: - for it in list(getattr(self, "_items", []) or []): - try: - _add_recursive(None, it) - except Exception: - pass + row.set_selectable(True) except Exception: pass - # reset last selected paths - self._last_selected_paths = set() + return row - def _iter_to_path_str(self, treeiter): + def _on_toggle_clicked(self, item): + """Toggle _is_open and rebuild, preserving selection.""" try: - path = self._treestore.get_path(treeiter) + cur = bool(getattr(item, "_is_open", False)) + try: + item._is_open = not cur + except Exception: + try: + item.setOpen(not cur) + except Exception: + pass + # preserve selection ids try: - return path.to_string() + self._last_selected_ids = set(id(i) for i in self._selected_items) except Exception: - return str(path) + self._last_selected_ids = set() + self.rebuildTree() except Exception: - return None + pass - def _collect_descendant_iters(self, treeiter): - """Return list of iter for treeiter and all its descendants.""" - out = [] - if treeiter is None: - return out - # depth-first traversal - stack = [treeiter] + def _collect_all_descendants(self, item): + """Return set of all descendant items (recursive).""" + out = set() + stack = [] + try: + for c in getattr(item, "_children", []) or []: + stack.append(c) + except Exception: + pass while stack: cur = stack.pop() - out.append(cur) + out.add(cur) try: - child = self._treestore.iter_children(cur) - while child is not None: - stack.append(child) - try: - child = self._treestore.iter_next(child) - except Exception: - # iter_next returns False sometimes - break + for ch in getattr(cur, "_children", []) or []: + stack.append(ch) except Exception: pass return out - def _on_selection_changed(self, selection): - """Sync selection from TreeView to logical _selected_items and handle recursive propagation.""" - if self._suppress_selection_handler: - return - + def rebuildTree(self): + """Flatten visible items according to _is_open and populate the ListBox.""" + if self._backend_widget is None or self._listbox is None: + self._create_backend_widget() try: - # collect currently selected iters (as path strings and iters) - sel_paths = [] - sel_iters = [] - - # preferred API: selection.selected_foreach(callback, user_data) + # clear listbox rows try: - def _cb(model, path, treeiter, ud=None): - sel_paths.append(path.to_string() if hasattr(path, "to_string") else str(path)) - sel_iters.append(treeiter) - selection.selected_foreach(_cb) + for r in list(self._listbox.get_children()): + try: + self._listbox.remove(r) + except Exception: + try: + self._listbox.unbind_model() + except Exception: + pass except Exception: - # fallback: try selection.get_selected_rows() + # fallback ignore + pass + + self._rows = [] + self._row_to_item.clear() + self._item_to_row.clear() + self._visible_items = [] + + # Depth-first traversal producing visible nodes only when ancestors are open + def _visit(nodes, depth=0): + for n in nodes: + self._visible_items.append((n, depth)) + try: + is_open = bool(getattr(n, "_is_open", False)) + except Exception: + is_open = False + if is_open: + try: + childs = [] + if callable(getattr(n, "children", None)): + childs = n.children() or [] + else: + childs = getattr(n, "_children", []) or [] + except Exception: + childs = getattr(n, "_children", []) or [] + if childs: + _visit(childs, depth + 1) + + roots = list(getattr(self, "_items", []) or []) + _visit(roots, 0) + + # create rows + for item, depth in self._visible_items: try: - rows = selection.get_selected_rows() - for p in rows: + row = self._make_row(item, depth) + self._listbox.append(row) + self._rows.append(row) + self._row_to_item[row] = item + self._item_to_row[item] = row + except Exception: + pass + + # restore previous selection (visible rows only) + try: + if self._last_selected_ids: + self._suppress_selection_handler = True + try: + self._listbox.unselect_all() + except Exception: + pass + for row, item in list(self._row_to_item.items()): try: - it = self._treestore.get_iter(p) - sel_paths.append(p.to_string() if hasattr(p, "to_string") else str(p)) - sel_iters.append(it) + if id(item) in self._last_selected_ids: + try: + row.set_selected(True) + except Exception: + pass except Exception: pass + self._suppress_selection_handler = False + except Exception: + self._suppress_selection_handler = False + + # rebuild logical selected items from rows + self._selected_items = [] + for row in self._rows: + try: + if getattr(row, "get_selected", None): + sel = row.get_selected() + else: + sel = bool(getattr(row, "_selected_flag", False)) + if sel: + it = self._row_to_item.get(row, None) + if it is not None: + self._selected_items.append(it) except Exception: - # final fallback: nothing pass - current_set = set(sel_paths) - added = current_set - self._last_selected_paths - removed = self._last_selected_paths - current_set + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + pass + + def _on_row_selected(self, listbox, row): + """Handle selection change; implement recursive selection propagation.""" + if self._suppress_selection_handler: + return + try: + # collect currently selected items (by id) + cur_selected_ids = set() + selected_rows = [] + try: + # prefer scanning rows API + for r in self._rows: + try: + sel = False + if getattr(r, "get_selected", None): + sel = r.get_selected() + else: + sel = bool(getattr(r, "_selected_flag", False)) + if sel: + selected_rows.append(r) + it = self._row_to_item.get(r, None) + if it is not None: + cur_selected_ids.add(id(it)) + except Exception: + pass + except Exception: + pass + + added = cur_selected_ids - self._last_selected_ids + removed = self._last_selected_ids - cur_selected_ids - # If recursive + multi: selecting a parent selects descendants; deselecting a parent deselects descendants + # Recursive + multi: selecting parent selects all descendants; deselecting parent deselects descendants if self._recursive and self._multi: - desired_paths = set(current_set) - # for each added path, add all descendant paths - for idx, it in enumerate(sel_iters): + desired_ids = set(cur_selected_ids) + # handle added -> add descendants + for r in list(selected_rows): try: - if sel_paths[idx] in added: - for dq in self._collect_descendant_iters(it): - pstr = self._iter_to_path_str(dq) - if pstr: - desired_paths.add(pstr) + it = self._row_to_item.get(r, None) + if it is None: + continue + if id(it) in added: + for d in self._collect_all_descendants(it): + desired_ids.add(id(d)) except Exception: pass - # for each removed path, remove its descendants from desired set - for pstr in list(removed): + # handle removed -> remove descendants + for rid in list(removed): try: - # find iter for this path - try: - path_obj = Gtk.TreePath.new_from_string(pstr) - except Exception: - path_obj = None - if path_obj: - try: - rem_iter = self._treestore.get_iter(path_obj) - except Exception: - rem_iter = None - else: - rem_iter = None - if rem_iter is not None: - for dq in self._collect_descendant_iters(rem_iter): - dp = self._iter_to_path_str(dq) - if dp and dp in desired_paths: - desired_paths.discard(dp) + # find item object by id among previous items (we can search self._items tree) + def _find_by_id(target_id, nodes): + for n in nodes: + if id(n) == target_id: + return n + try: + chs = [] + if callable(getattr(n, "children", None)): + chs = n.children() or [] + else: + chs = getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + res = _find_by_id(target_id, chs) + if res: + return res + return None + obj = _find_by_id(rid, list(getattr(self, "_items", []) or [])) + if obj is not None: + for d in self._collect_all_descendants(obj): + if id(d) in desired_ids: + desired_ids.discard(id(d)) except Exception: pass - # if desired differs from current, apply programmatically - if desired_paths != current_set: + # apply desired_ids to visible rows + if desired_ids != cur_selected_ids: try: self._suppress_selection_handler = True - selection.unselect_all() - for pstr in desired_paths: + try: + self._listbox.unselect_all() + except Exception: + pass + for row, it in list(self._row_to_item.items()): try: - path_obj = Gtk.TreePath.new_from_string(pstr) - selection.select_path(path_obj) + if id(it) in desired_ids: + try: + row.set_selected(True) + except Exception: + pass except Exception: - try: - selection.select_path(pstr) - except Exception: - pass + pass finally: self._suppress_selection_handler = False - # rebuild selected lists after modification - sel_paths = [] - sel_iters = [] - try: - selection.selected_foreach(lambda m, p, it, ud=None: (sel_paths.append(p.to_string() if hasattr(p, "to_string") else str(p)), sel_iters.append(it))) - except Exception: - pass - # Build logical selected YTreeItem list from sel_iters (or descendants if recursive+single) - logical_iters = [] - if self._recursive and not self._multi: - # include selected iters and their descendants - for it in sel_iters: - logical_iters.append(it) + # recompute selection sets after applying + cur_selected_ids = set() try: - for dq in self._collect_descendant_iters(it): - if dq is not it: - logical_iters.append(dq) + for r in self._rows: + try: + sel = False + if getattr(r, "get_selected", None): + sel = r.get_selected() + else: + sel = bool(getattr(r, "_selected_flag", False)) + if sel: + it = self._row_to_item.get(r, None) + if it is not None: + cur_selected_ids.add(id(it)) + except Exception: + pass except Exception: pass - else: - logical_iters = sel_iters + # Build logical selection list and set YItem selected flags new_selected = [] - # clear previous selected flag on all known items try: - for it in list(getattr(self, "_items", []) or []): - try: - it.setSelected(False) - except Exception: - pass + # clear previous selection flags on all items + def _clear_flags(nodes): + for n in nodes: + try: + n.setSelected(False) + except Exception: + pass + try: + childs = [] + if callable(getattr(n, "children", None)): + childs = n.children() or [] + else: + childs = getattr(n, "_children", []) or [] + except Exception: + childs = getattr(n, "_children", []) or [] + if childs: + _clear_flags(childs) + _clear_flags(list(getattr(self, "_items", []) or [])) except Exception: pass - for it in logical_iters: + for r in self._rows: try: - itm = self._treestore.get_value(it, 1) + sel = False + if getattr(r, "get_selected", None): + sel = r.get_selected() + else: + sel = bool(getattr(r, "_selected_flag", False)) + if sel: + it = self._row_to_item.get(r, None) + if it is not None: + try: + it.setSelected(True) + except Exception: + pass + new_selected.append(it) except Exception: - try: - # alternative accessor - itm = self._treestore[it][1] - except Exception: - itm = None - if itm is not None: - try: - itm.setSelected(True) - except Exception: - pass - new_selected.append(itm) + pass self._selected_items = new_selected + self._last_selected_ids = set(id(i) for i in self._selected_items) - # store last selected paths - self._last_selected_paths = set(sel_paths) - - # notify immediate mode - try: - if self._immediate and self.notify(): - dlg = self.findDialog() - if dlg: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - - except Exception: - pass - - def _on_row_activated(self, treeview, path, column): - """Row activated (double click / Enter) handler.""" - try: - # Post an Activated event to containing dialog - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) except Exception: pass def currentItem(self): - """Return first selected logical item or None.""" try: - sel = self._selected_items - return sel[0] if sel else None + return self._selected_items[0] if self._selected_items else None except Exception: return None @@ -2287,7 +2371,6 @@ def getSelectedItems(self): return list(self._selected_items) def activate(self): - """Simulate activation of current item.""" try: itm = self.currentItem() if itm is None: @@ -2317,7 +2400,6 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass - # propagate logical enabled state to items (if they are YWidgets) try: for it in list(getattr(self, "_items", []) or []): try: @@ -2331,4 +2413,4 @@ def _set_backend_enabled(self, enabled): def get_backend_widget(self): if self._backend_widget is None: self._create_backend_widget() - return self._backend_widget \ No newline at end of file + return self._backend_widget From 09e2be1e41bc82bb50ca86e8fc37de3e43ebab4a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 15:35:06 +0100 Subject: [PATCH 087/523] manage stretching --- manatools/aui/yui_gtk.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index b69e313..3780322 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -1931,8 +1931,8 @@ def _create_backend_widget(self): mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE listbox.set_selection_mode(mode) # Let listbox expand in available area - listbox.set_vexpand(True) - listbox.set_hexpand(True) + listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) except Exception: pass @@ -1947,10 +1947,10 @@ def _create_backend_widget(self): # Make scrolled window expand to fill container (so tree respects parent stretching) try: - sw.set_vexpand(True) - sw.set_hexpand(True) - vbox.set_vexpand(True) - vbox.set_hexpand(True) + sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) except Exception: pass From 9fe3c2b4852a0a6220f1ae12b6e4c2b740f0be93 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 15:58:11 +0100 Subject: [PATCH 088/523] expanding view --- manatools/aui/yui_gtk.py | 42 ++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 3780322..6421e6d 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -2010,7 +2010,15 @@ def _make_row(self, item, depth): btn.set_relief(Gtk.ReliefStyle.NONE) except Exception: pass - btn.set_focus_on_click(False) + # prevent the toggle button from taking focus / causing selection side-effects + try: + btn.set_focus_on_click(False) + except Exception: + pass + try: + btn.set_can_focus(False) + except Exception: + pass btn.connect("clicked", lambda b, it=item: self._on_toggle_clicked(it)) hbox.append(btn) except Exception: @@ -2061,14 +2069,11 @@ def _make_row(self, item, depth): def _on_toggle_clicked(self, item): """Toggle _is_open and rebuild, preserving selection.""" try: - cur = bool(getattr(item, "_is_open", False)) + cur = item.isOpen() try: - item._is_open = not cur + item.setOpen(not cur) except Exception: - try: - item.setOpen(not cur) - except Exception: - pass + pass # preserve selection ids try: self._last_selected_ids = set(id(i) for i in self._selected_items) @@ -2102,18 +2107,31 @@ def rebuildTree(self): if self._backend_widget is None or self._listbox is None: self._create_backend_widget() try: - # clear listbox rows + # clear listbox rows robustly: repeatedly remove first child until none remain try: - for r in list(self._listbox.get_children()): + while True: + first = None try: - self._listbox.remove(r) + first = self._listbox.get_first_child() except Exception: + # some bindings may return None / raise; try children() try: + chs = self._listbox.get_children() + first = chs[0] if chs else None + except Exception: + first = None + if not first: + break + try: + self._listbox.remove(first) + except Exception: + try: + # fallback API self._listbox.unbind_model() + break except Exception: - pass + break except Exception: - # fallback ignore pass self._rows = [] From 1a96e3c13c32c19e8c855c517581f2f0677e1411 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 16:05:28 +0100 Subject: [PATCH 089/523] Better implementation on button to expand --- manatools/aui/yui_gtk.py | 99 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 6421e6d..1a8b8ec 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -2019,7 +2019,72 @@ def _make_row(self, item, depth): btn.set_can_focus(False) except Exception: pass - btn.connect("clicked", lambda b, it=item: self._on_toggle_clicked(it)) + # make button visually flat (no border/background) so it looks like a tree expander + try: + btn.add_css_class("flat") + except Exception: + # fallback: try another common class name + try: + btn.add_css_class("link") + except Exception: + pass + + # Use a GestureClick on the button to reliably receive a single-click action + # and avoid the occasional need for double clicks caused by focus/selection interplay. + try: + gesture = Gtk.GestureClick() + # accept any button; if set_button exists restrict to primary + try: + gesture.set_button(0) + except Exception: + pass + # pressed handler will toggle immediately + def _on_pressed(gesture_obj, n_press, x, y, target_item=item): + # run toggle synchronously and suppress selection handler while rebuilding + try: + self._suppress_selection_handler = True + except Exception: + pass + try: + # toggle using public API if available + try: + cur = target_item.isOpen() + target_item.setOpen(not cur) + except Exception: + try: + cur = bool(getattr(target_item, "_is_open", False)) + target_item._is_open = not cur + except Exception: + pass + # preserve selection and rebuild + try: + self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) + except Exception: + self._last_selected_ids = set() + try: + self.rebuildTree() + except Exception: + pass + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass + + gesture.connect("pressed", _on_pressed) + try: + btn.add_controller(gesture) + except Exception: + try: + btn.add_controller(gesture) + except Exception: + pass + except Exception: + # Fallback to clicked if GestureClick not available + try: + btn.connect("clicked", lambda b, it=item: self._on_toggle_clicked(it)) + except Exception: + pass hbox.append(btn) except Exception: # fallback spacer @@ -2069,17 +2134,35 @@ def _make_row(self, item, depth): def _on_toggle_clicked(self, item): """Toggle _is_open and rebuild, preserving selection.""" try: - cur = item.isOpen() + # Ensure a single-click toggle: suppress selection events during the operation try: - item.setOpen(not cur) + self._suppress_selection_handler = True except Exception: pass - # preserve selection ids try: - self._last_selected_ids = set(id(i) for i in self._selected_items) - except Exception: - self._last_selected_ids = set() - self.rebuildTree() + try: + cur = item.isOpen() + item.setOpen(not cur) + except Exception: + try: + cur = bool(getattr(item, "_is_open", False)) + item._is_open = not cur + except Exception: + pass + # preserve selection ids and rebuild the visible rows + try: + self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) + except Exception: + self._last_selected_ids = set() + try: + self.rebuildTree() + except Exception: + pass + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass except Exception: pass From 59067227350d9cb6107b7cbdc350a00aa7a6d1a9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 16:38:41 +0100 Subject: [PATCH 090/523] managed items selection --- manatools/aui/yui_gtk.py | 242 +++++++++++++++++++++------------------ 1 file changed, 130 insertions(+), 112 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 1a8b8ec..5cf15c2 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -2296,121 +2296,150 @@ def _visit(nodes, depth=0): except Exception: pass + def _row_is_selected(self, r): + """Robust helper to detect whether a ListBoxRow is selected.""" + try: + # preferred API + sel = getattr(r, "get_selected", None) + if callable(sel): + return bool(sel()) + except Exception: + pass + try: + props = getattr(r, "props", None) + if props and hasattr(props, "selected"): + return bool(getattr(props, "selected")) + except Exception: + pass + # fallback: check whether the listbox reports this row as selected (some bindings) + try: + if self._listbox is not None and hasattr(self._listbox, "get_selected_rows"): + rows = self._listbox.get_selected_rows() or [] + for rr in rows: + if rr is r: + return True + except Exception: + pass + # last-resort flag + return bool(getattr(r, "_selected_flag", False)) + def _on_row_selected(self, listbox, row): - """Handle selection change; implement recursive selection propagation.""" + """Handle selection change; update logical selected items reliably.""" if self._suppress_selection_handler: return try: - # collect currently selected items (by id) - cur_selected_ids = set() + # collect currently selected rows using cached rows list (stable) selected_rows = [] try: - # prefer scanning rows API - for r in self._rows: + for r in list(self._rows or []): try: - sel = False - if getattr(r, "get_selected", None): - sel = r.get_selected() - else: - sel = bool(getattr(r, "_selected_flag", False)) - if sel: + if self._row_is_selected(r): selected_rows.append(r) - it = self._row_to_item.get(r, None) - if it is not None: - cur_selected_ids.add(id(it)) except Exception: pass except Exception: - pass + # fallback: iterate children of the listbox if available + try: + for r in list(self._listbox.get_children() or []): + try: + if self._row_is_selected(r): + selected_rows.append(r) + except Exception: + pass + except Exception: + selected_rows = [] - added = cur_selected_ids - self._last_selected_ids - removed = self._last_selected_ids - cur_selected_ids + # map rows -> items + cur_selected_items = [] + for r in selected_rows: + try: + it = self._row_to_item.get(r, None) + if it is not None: + cur_selected_items.append(it) + except Exception: + pass - # Recursive + multi: selecting parent selects all descendants; deselecting parent deselects descendants - if self._recursive and self._multi: - desired_ids = set(cur_selected_ids) - # handle added -> add descendants - for r in list(selected_rows): - try: - it = self._row_to_item.get(r, None) - if it is None: - continue - if id(it) in added: - for d in self._collect_all_descendants(it): - desired_ids.add(id(d)) - except Exception: - pass - # handle removed -> remove descendants - for rid in list(removed): - try: - # find item object by id among previous items (we can search self._items tree) - def _find_by_id(target_id, nodes): - for n in nodes: - if id(n) == target_id: - return n - try: - chs = [] - if callable(getattr(n, "children", None)): - chs = n.children() or [] - else: - chs = getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - res = _find_by_id(target_id, chs) - if res: - return res - return None - obj = _find_by_id(rid, list(getattr(self, "_items", []) or [])) - if obj is not None: - for d in self._collect_all_descendants(obj): - if id(d) in desired_ids: - desired_ids.discard(id(d)) - except Exception: - pass + # compute added/removed if recursive behavior needs delta handling + prev_ids = set(self._last_selected_ids or []) + cur_ids = set(id(i) for i in cur_selected_items) + added = cur_ids - prev_ids + removed = prev_ids - cur_ids - # apply desired_ids to visible rows - if desired_ids != cur_selected_ids: - try: - self._suppress_selection_handler = True + # Recursive + multi: expand logical selection to include descendants for added, + # and remove descendants for removed. + if self._recursive and self._multi and (added or removed): + try: + desired_ids = set(cur_ids) + # add descendants of newly added items + for it in list(cur_selected_items): try: - self._listbox.unselect_all() + if id(it) in added: + for d in self._collect_all_descendants(it): + desired_ids.add(id(d)) except Exception: pass - for row, it in list(self._row_to_item.items()): - try: - if id(it) in desired_ids: + # remove descendants of removed items + for rid in list(removed): + try: + # find object by id in whole tree + def _find_by_id(target_id, nodes): + for n in nodes: + if id(n) == target_id: + return n try: - row.set_selected(True) + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] except Exception: - pass + chs = getattr(n, "_children", []) or [] + res = _find_by_id(target_id, chs) + if res: + return res + return None + obj = _find_by_id(rid, list(getattr(self, "_items", []) or [])) + if obj is not None: + for d in self._collect_all_descendants(obj): + if id(d) in desired_ids: + desired_ids.discard(id(d)) + except Exception: + pass + + # apply desired_ids to visible rows (only visible rows can be selected) + if desired_ids != cur_ids: + try: + self._suppress_selection_handler = True + try: + self._listbox.unselect_all() except Exception: pass - finally: - self._suppress_selection_handler = False + for rr, it in list(self._row_to_item.items()): + try: + if id(it) in desired_ids: + try: + rr.set_selected(True) + except Exception: + try: + setattr(rr, "_selected_flag", True) + except Exception: + pass + except Exception: + pass + finally: + self._suppress_selection_handler = False - # recompute selection sets after applying - cur_selected_ids = set() - try: - for r in self._rows: + # rebuild cur_selected_items from applied selection + cur_selected_items = [] + for r in list(self._rows or []): try: - sel = False - if getattr(r, "get_selected", None): - sel = r.get_selected() - else: - sel = bool(getattr(r, "_selected_flag", False)) - if sel: + if self._row_is_selected(r): it = self._row_to_item.get(r, None) if it is not None: - cur_selected_ids.add(id(it)) + cur_selected_items.append(it) except Exception: pass - except Exception: - pass + except Exception: + pass - # Build logical selection list and set YItem selected flags - new_selected = [] + # update YTreeItem selected flags: clear all then set for current try: - # clear previous selection flags on all items def _clear_flags(nodes): for n in nodes: try: @@ -2418,44 +2447,33 @@ def _clear_flags(nodes): except Exception: pass try: - childs = [] - if callable(getattr(n, "children", None)): - childs = n.children() or [] - else: - childs = getattr(n, "_children", []) or [] + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] except Exception: - childs = getattr(n, "_children", []) or [] - if childs: - _clear_flags(childs) + chs = getattr(n, "_children", []) or [] + if chs: + _clear_flags(chs) _clear_flags(list(getattr(self, "_items", []) or [])) except Exception: pass - for r in self._rows: + for it in cur_selected_items: try: - sel = False - if getattr(r, "get_selected", None): - sel = r.get_selected() - else: - sel = bool(getattr(r, "_selected_flag", False)) - if sel: - it = self._row_to_item.get(r, None) - if it is not None: - try: - it.setSelected(True) - except Exception: - pass - new_selected.append(it) + it.setSelected(True) except Exception: pass - self._selected_items = new_selected + # store logical selection + self._selected_items = list(cur_selected_items) self._last_selected_ids = set(id(i) for i in self._selected_items) + # notify immediate mode if self._immediate and self.notify(): - dlg = self.findDialog() - if dlg: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + try: + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass except Exception: pass From f939038c2337912d32fa15950cceb8de5171bb87 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 19:11:40 +0100 Subject: [PATCH 091/523] improving recursive selection --- manatools/aui/yui_gtk.py | 150 ++++++++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 40 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 5cf15c2..57c3732 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -2324,7 +2324,11 @@ def _row_is_selected(self, r): return bool(getattr(r, "_selected_flag", False)) def _on_row_selected(self, listbox, row): - """Handle selection change; update logical selected items reliably.""" + """Handle selection change; update logical selected items reliably. + + When recursive selection is enabled and multi-selection is on, + selecting/deselecting a parent will also select/deselect all its descendants. + """ if self._suppress_selection_handler: return try: @@ -2365,51 +2369,82 @@ def _on_row_selected(self, listbox, row): added = cur_ids - prev_ids removed = prev_ids - cur_ids - # Recursive + multi: expand logical selection to include descendants for added, - # and remove descendants for removed. + # If recursive+multi, propagate selection/deselection to descendants immediately if self._recursive and self._multi and (added or removed): try: + # helper: find item objects from ids (search the current visible list first) + id_to_item = { id(it): it for it in (list(getattr(self, "_items", []) or []) + cur_selected_items) } + # ensure we also include items referenced by row_to_item (visible items) + for it in self._row_to_item.values(): + id_to_item[id(it)] = it + + # Collect items added/removed as objects + added_items = [id_to_item.get(a) for a in added if id_to_item.get(a) is not None] + # For removed items we may not have them in id_to_item; search tree + def _find_by_id(target_id, nodes): + for n in nodes: + if id(n) == target_id: + return n + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + res = _find_by_id(target_id, chs) + if res: + return res + return None + removed_items = [] + for rid in removed: + obj = id_to_item.get(rid) + if obj is None: + obj = _find_by_id(rid, list(getattr(self, "_items", []) or [])) + if obj is not None: + removed_items.append(obj) + + # Start with desired_ids = currently selected desired_ids = set(cur_ids) - # add descendants of newly added items - for it in list(cur_selected_items): + + # For each newly added item, select all descendants (and their rows if visible) + for ait in added_items: try: - if id(it) in added: - for d in self._collect_all_descendants(it): - desired_ids.add(id(d)) + for d in self._collect_all_descendants(ait): + desired_ids.add(id(d)) + # select visible row if present + r = self._item_to_row.get(d) + if r is not None: + try: + r.set_selected(True) + except Exception: + try: + setattr(r, "_selected_flag", True) + except Exception: + pass except Exception: pass - # remove descendants of removed items - for rid in list(removed): + + # For each removed item, deselect all descendants + for rit in removed_items: try: - # find object by id in whole tree - def _find_by_id(target_id, nodes): - for n in nodes: - if id(n) == target_id: - return n + for d in self._collect_all_descendants(rit): + if id(d) in desired_ids: + desired_ids.discard(id(d)) + r = self._item_to_row.get(d) + if r is not None: try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + r.set_selected(False) except Exception: - chs = getattr(n, "_children", []) or [] - res = _find_by_id(target_id, chs) - if res: - return res - return None - obj = _find_by_id(rid, list(getattr(self, "_items", []) or [])) - if obj is not None: - for d in self._collect_all_descendants(obj): - if id(d) in desired_ids: - desired_ids.discard(id(d)) + try: + setattr(r, "_selected_flag", False) + except Exception: + pass except Exception: pass - # apply desired_ids to visible rows (only visible rows can be selected) - if desired_ids != cur_ids: + # Ensure desired selection applied on visible rows (some descendants may be visible) + try: + self._suppress_selection_handler = True try: - self._suppress_selection_handler = True - try: - self._listbox.unselect_all() - except Exception: - pass + # Also ensure the rows corresponding to currently selected items remain selected for rr, it in list(self._row_to_item.items()): try: if id(it) in desired_ids: @@ -2420,21 +2455,56 @@ def _find_by_id(target_id, nodes): setattr(rr, "_selected_flag", True) except Exception: pass + else: + try: + rr.set_selected(False) + except Exception: + try: + setattr(rr, "_selected_flag", False) + except Exception: + pass except Exception: pass finally: self._suppress_selection_handler = False + except Exception: + pass - # rebuild cur_selected_items from applied selection - cur_selected_items = [] - for r in list(self._rows or []): + # Recompute cur_selected_items from final visible selection + new_selected = [] + for r in list(self._rows or []): + try: + if self._row_is_selected(r): + it = self._row_to_item.get(r) + if it is not None: + new_selected.append(it) + except Exception: + pass + # Also include non-visible descendants selected logically (those without rows) + # by scanning desired_ids and adding corresponding items that may be non-visible + for root in list(getattr(self, "_items", []) or []): + def _collect_all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_all_nodes(chs)) + return out + all_nodes = _collect_all_nodes([root]) + for n in all_nodes: try: - if self._row_is_selected(r): - it = self._row_to_item.get(r, None) - if it is not None: - cur_selected_items.append(it) + if id(n) in desired_ids and n not in new_selected: + # add non-visible descendant + if n not in new_selected: + new_selected.append(n) except Exception: pass + + cur_selected_items = new_selected except Exception: pass From 430f749cee8a55559868e5427b49e27cb8edb61e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 29 Nov 2025 19:23:35 +0100 Subject: [PATCH 092/523] recursive selection, but select only one YTreeItem --- manatools/aui/yui_gtk.py | 266 ++++++++++++++++++++------------------- 1 file changed, 136 insertions(+), 130 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 57c3732..b3742d4 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -2323,35 +2323,80 @@ def _row_is_selected(self, r): # last-resort flag return bool(getattr(r, "_selected_flag", False)) - def _on_row_selected(self, listbox, row): - """Handle selection change; update logical selected items reliably. + def _gather_selected_rows(self): + """Return list of selected ListBoxRow objects (visible rows).""" + rows = [] + try: + # prefer listbox API if available + if self._listbox is not None and hasattr(self._listbox, "get_selected_rows"): + try: + sel = self._listbox.get_selected_rows() or [] + # If API returns Gtk.ListBoxRow-like objects, include them; otherwise fallback + for s in sel: + if s is None: + continue + # if path-like, ignore (we rely on visible rows) + if isinstance(s, type(self._rows[0])) if self._rows else False: + rows.append(s) + if rows: + return rows + except Exception: + pass + # fallback: scan our cached rows + for r in list(self._rows or []): + try: + if self._row_is_selected(r): + rows.append(r) + except Exception: + pass + except Exception: + pass + return rows - When recursive selection is enabled and multi-selection is on, - selecting/deselecting a parent will also select/deselect all its descendants. - """ - if self._suppress_selection_handler: + def _apply_desired_ids_to_rows(self, desired_ids): + """Set visible rows selected state to match desired_ids (ids of items).""" + if self._listbox is None: return try: - # collect currently selected rows using cached rows list (stable) - selected_rows = [] + self._suppress_selection_handler = True + except Exception: + pass + try: try: - for r in list(self._rows or []): - try: - if self._row_is_selected(r): - selected_rows.append(r) - except Exception: - pass + self._listbox.unselect_all() except Exception: - # fallback: iterate children of the listbox if available + # continue even if unsupported + pass + for row, it in list(self._row_to_item.items()): try: - for r in list(self._listbox.get_children() or []): + target = id(it) in desired_ids + try: + row.set_selected(bool(target)) + except Exception: try: - if self._row_is_selected(r): - selected_rows.append(r) + setattr(row, "_selected_flag", bool(target)) except Exception: pass except Exception: - selected_rows = [] + pass + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass + + def _on_row_selected(self, listbox, row): + """Handle selection change; update logical selected items reliably. + + When recursive selection is enabled and multi-selection is on, + selecting/deselecting a parent will also select/deselect all its descendants. + """ + # ignore if programmatic change in progress + if self._suppress_selection_handler: + return + + try: + selected_rows = self._gather_selected_rows() # map rows -> items cur_selected_items = [] @@ -2363,115 +2408,80 @@ def _on_row_selected(self, listbox, row): except Exception: pass - # compute added/removed if recursive behavior needs delta handling prev_ids = set(self._last_selected_ids or []) cur_ids = set(id(i) for i in cur_selected_items) added = cur_ids - prev_ids removed = prev_ids - cur_ids - # If recursive+multi, propagate selection/deselection to descendants immediately + # If recursive+multi, compute desired ids by adding descendants of added and removing descendants of removed. + desired_ids = set(cur_ids) if self._recursive and self._multi and (added or removed): try: - # helper: find item objects from ids (search the current visible list first) - id_to_item = { id(it): it for it in (list(getattr(self, "_items", []) or []) + cur_selected_items) } - # ensure we also include items referenced by row_to_item (visible items) - for it in self._row_to_item.values(): - id_to_item[id(it)] = it - - # Collect items added/removed as objects - added_items = [id_to_item.get(a) for a in added if id_to_item.get(a) is not None] - # For removed items we may not have them in id_to_item; search tree - def _find_by_id(target_id, nodes): - for n in nodes: - if id(n) == target_id: - return n - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - res = _find_by_id(target_id, chs) - if res: - return res - return None - removed_items = [] - for rid in removed: - obj = id_to_item.get(rid) + # add descendants of newly added items + for a in list(added): + # find object + obj = None + for it in cur_selected_items: + if id(it) == a: + obj = it + break if obj is None: - obj = _find_by_id(rid, list(getattr(self, "_items", []) or [])) - if obj is not None: - removed_items.append(obj) - - # Start with desired_ids = currently selected - desired_ids = set(cur_ids) - - # For each newly added item, select all descendants (and their rows if visible) - for ait in added_items: - try: - for d in self._collect_all_descendants(ait): - desired_ids.add(id(d)) - # select visible row if present - r = self._item_to_row.get(d) - if r is not None: + # try to find in whole tree + def _find_by_id(tid, nodes): + for n in nodes: + if id(n) == tid: + return n try: - r.set_selected(True) + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] except Exception: - try: - setattr(r, "_selected_flag", True) - except Exception: - pass - except Exception: - pass + chs = getattr(n, "_children", []) or [] + r = _find_by_id(tid, chs) + if r: + return r + return None + obj = _find_by_id(a, list(getattr(self, "_items", []) or [])) + if obj is not None: + for d in self._collect_all_descendants(obj): + desired_ids.add(id(d)) - # For each removed item, deselect all descendants - for rit in removed_items: + # remove descendants of removed items + for r_id in list(removed): try: - for d in self._collect_all_descendants(rit): - if id(d) in desired_ids: - desired_ids.discard(id(d)) - r = self._item_to_row.get(d) - if r is not None: + obj = None + # try find in tree + def _find_by_id2(tid, nodes): + for n in nodes: + if id(n) == tid: + return n try: - r.set_selected(False) + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] except Exception: - try: - setattr(r, "_selected_flag", False) - except Exception: - pass + chs = getattr(n, "_children", []) or [] + rr = _find_by_id2(tid, chs) + if rr: + return rr + return None + obj = _find_by_id2(r_id, list(getattr(self, "_items", []) or [])) + if obj is not None: + for d in self._collect_all_descendants(obj): + if id(d) in desired_ids: + desired_ids.discard(id(d)) except Exception: pass - # Ensure desired selection applied on visible rows (some descendants may be visible) - try: - self._suppress_selection_handler = True - try: - # Also ensure the rows corresponding to currently selected items remain selected - for rr, it in list(self._row_to_item.items()): - try: - if id(it) in desired_ids: - try: - rr.set_selected(True) - except Exception: - try: - setattr(rr, "_selected_flag", True) - except Exception: - pass - else: - try: - rr.set_selected(False) - except Exception: - try: - setattr(rr, "_selected_flag", False) - except Exception: - pass - except Exception: - pass - finally: - self._suppress_selection_handler = False - except Exception: - pass + except Exception: + pass + + # Apply desired selection to visible rows + try: + self._apply_desired_ids_to_rows(desired_ids) + except Exception: + pass - # Recompute cur_selected_items from final visible selection - new_selected = [] + # Recompute cur_selected_items including non-visible descendants + new_selected = [] + try: + # visible rows for r in list(self._rows or []): try: if self._row_is_selected(r): @@ -2480,35 +2490,31 @@ def _find_by_id(target_id, nodes): new_selected.append(it) except Exception: pass - # Also include non-visible descendants selected logically (those without rows) - # by scanning desired_ids and adding corresponding items that may be non-visible + # include non-visible nodes that are requested by desired_ids + def _collect_all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_all_nodes(chs)) + return out for root in list(getattr(self, "_items", []) or []): - def _collect_all_nodes(nodes): - out = [] - for n in nodes: - out.append(n) - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - out.extend(_collect_all_nodes(chs)) - return out - all_nodes = _collect_all_nodes([root]) - for n in all_nodes: + for n in _collect_all_nodes([root]): try: if id(n) in desired_ids and n not in new_selected: - # add non-visible descendant - if n not in new_selected: - new_selected.append(n) + new_selected.append(n) except Exception: pass - cur_selected_items = new_selected + cur_ids = set(id(i) for i in cur_selected_items) except Exception: pass - # update YTreeItem selected flags: clear all then set for current + # Update logical selection flags try: def _clear_flags(nodes): for n in nodes: From 73d13f3aaf500cc3760d911dd09fc34cbf932510 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 15:42:26 +0100 Subject: [PATCH 093/523] First YTree curses BE implementation --- manatools/aui/yui_curses.py | 512 ++++++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index a7311d5..e733c53 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -193,6 +193,10 @@ def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: Y """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" return YAlignmentCurses(parent, horAlign=horAlignment, vertAlign=vertAlignment) + def createTree(self, parent, label, multiselection=False, recursiveselection = False): + """Create a Tree widget.""" + return YTreeCurses(parent, label, multiselection, recursiveselection) + # Curses Widget Implementations class YDialogCurses(YSingleChildContainerWidget): _open_dialogs = [] @@ -1689,3 +1693,511 @@ def _draw(self, window, y, x, width, height): self._child._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, getattr(self._child, "_height", 1))) except Exception: pass + +class YTreeCurses(YSelectionWidget): + """ + NCurses implementation of a tree widget. + - Flattens visible nodes according to YTreeItem._is_open + - Supports single/multi selection and recursive selection propagation + - Preserves per-item selected() / setSelected() semantics and restores selections on rebuild + - Keyboard: Up/Down/PageUp/PageDown/Home/End, SPACE = expand/collapse, ENTER = select/deselect + """ + def __init__(self, parent=None, label="", multiselection=False, recursiveselection=False): + super().__init__(parent) + self._label = label + self._multi = bool(multiselection) + self._recursive = bool(recursiveselection) + if self._recursive: + self._multi = True + self._height = 6 # curses backend widgets are drawn; keep minimal height + self._can_focus = True + self._focused = False + self._hover_index = 0 # index in visible_items + self._scroll_offset = 0 + self._visible_items = [] # list of (item, depth) + self._last_selected_ids = set() + self._suppress_selection_handler = False + # honor stretchability so VBoxes/HBoxes can allocate space + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YTree" + + def _create_backend_widget(self): + # ensure visible flatten computed + self.rebuildTree() + + def addItem(self, item): + """Ensure base storage gets the item and rebuild visible list immediately.""" + try: + # prefer base implementation if present + try: + super().addItem(item) + except Exception: + # fallback: append to _items list used by this backend + if not hasattr(self, "_items") or self._items is None: + self._items = [] + self._items.append(item) + finally: + try: + # mark rebuild so new items are visible without waiting for external trigger + self.rebuildTree() + except Exception: + pass + + def removeItem(self, item): + """Remove item from internal list and rebuild.""" + try: + try: + super().removeItem(item) + except Exception: + if hasattr(self, "_items") and item in self._items: + try: + self._items.remove(item) + except Exception: + pass + finally: + try: + self.rebuildTree() + except Exception: + pass + + def clearItems(self): + """Clear items and rebuild.""" + try: + try: + super().clearItems() + except Exception: + self._items = [] + finally: + try: + self.rebuildTree() + except Exception: + pass + + def _collect_all_descendants(self, item): + out = [] + stack = [] + try: + for c in getattr(item, "_children", []) or []: + stack.append(c) + except Exception: + pass + while stack: + cur = stack.pop() + out.append(cur) + try: + for ch in getattr(cur, "_children", []) or []: + stack.append(ch) + except Exception: + pass + return out + + def _flatten_visible(self): + """Produce self._visible_items = [(item, depth), ...] following _is_open flags.""" + self._visible_items = [] + def _visit(nodes, depth=0): + for n in nodes: + self._visible_items.append((n, depth)) + try: + is_open = bool(getattr(n, "_is_open", False)) + except Exception: + is_open = False + if is_open: + try: + childs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + childs = getattr(n, "_children", []) or [] + if childs: + _visit(childs, depth + 1) + roots = list(getattr(self, "_items", []) or []) + _visit(roots, 0) + + def rebuildTree(self): + """Recompute visible items and restore selection from item.selected() or last_selected_ids.""" + # preserve items selection if any + try: + self._flatten_visible() + # if there are previously saved last_selected_ids, prefer them + selected_ids = set(self._last_selected_ids) if self._last_selected_ids else set() + # if none, collect from items' selected() property + if not selected_ids: + try: + def _collect_selected(nodes): + out = [] + for n in nodes: + try: + sel = False + if hasattr(n, "selected") and callable(getattr(n, "selected")): + sel = n.selected() + else: + sel = bool(getattr(n, "_selected", False)) + if sel: + out.append(n) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_selected(chs)) + return out + pre_selected = _collect_selected(list(getattr(self, "_items", []) or [])) + for p in pre_selected: + selected_ids.add(id(p)) + except Exception: + pass + # build logical selected list and last_selected_ids + sel_items = [] + for itm, _d in self._visible_items: + try: + if id(itm) in selected_ids: + sel_items.append(itm) + except Exception: + pass + # also include non-visible selected nodes (descendants) if recursive selection used + if selected_ids: + try: + # scan full tree + def _all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_all_nodes(chs)) + return out + all_nodes = _all_nodes(list(getattr(self, "_items", []) or [])) + for n in all_nodes: + if id(n) in selected_ids and n not in sel_items: + sel_items.append(n) + except Exception: + pass + # apply selected flags to items consistently + try: + # clear all first + def _clear(nodes): + for n in nodes: + try: + n.setSelected(False) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + _clear(chs) + _clear(list(getattr(self, "_items", []) or [])) + except Exception: + pass + for it in sel_items: + try: + it.setSelected(True) + except Exception: + pass + self._selected_items = list(sel_items) + self._last_selected_ids = set(id(i) for i in self._selected_items) + # ensure hover_index valid + if self._hover_index >= len(self._visible_items): + self._hover_index = max(0, len(self._visible_items) - 1) + self._ensure_hover_visible() + except Exception: + pass + + def _ensure_hover_visible(self, height=None): + """Adjust scroll offset so hover visible in given height area (if None use last draw height).""" + try: + # height param is number of rows available for items display (excluding label) + if height is None: + height = max(1, getattr(self, "_height", 1)) + visible = max(1, height) + if self._hover_index < self._scroll_offset: + self._scroll_offset = self._hover_index + elif self._hover_index >= self._scroll_offset + visible: + self._scroll_offset = self._hover_index - visible + 1 + except Exception: + pass + + def _toggle_expand(self, item): + try: + self._suppress_selection_handler = True + except Exception: + pass + try: + try: + cur = item.isOpen() + item.setOpen(not cur) + except Exception: + try: + cur = bool(getattr(item, "_is_open", False)) + item._is_open = not cur + except Exception: + pass + # preserve selected ids and rebuild + try: + self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) + except Exception: + self._last_selected_ids = set() + self.rebuildTree() + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass + + def _handle_selection_action(self, item): + """Toggle selection (ENTER) respecting multi/single & recursive semantics.""" + if item is None: + return + try: + if self._multi: + # toggle membership + if item in self._selected_items: + # deselect item and (if recursive) descendants + if self._recursive: + to_remove = {item} | set(self._collect_all_descendants(item)) + self._selected_items = [it for it in self._selected_items if it not in to_remove] + for it in to_remove: + try: + it.setSelected(False) + except Exception: + pass + else: + try: + self._selected_items.remove(item) + except Exception: + pass + try: + item.setSelected(False) + except Exception: + pass + else: + # select item and possibly descendants + if self._recursive: + to_add = [item] + self._collect_all_descendants(item) + for it in to_add: + if it not in self._selected_items: + self._selected_items.append(it) + try: + it.setSelected(True) + except Exception: + pass + else: + self._selected_items.append(item) + try: + item.setSelected(True) + except Exception: + pass + else: + # single selection: clear all others and set this one + try: + for it in list(getattr(self, "_items", []) or []): + try: + it.setSelected(False) + except Exception: + pass + except Exception: + pass + self._selected_items = [item] + try: + item.setSelected(True) + except Exception: + pass + except Exception: + pass + + # update last_selected_ids and notify + try: + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + self._last_selected_ids = set() + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def _draw(self, window, y, x, width, height): + """Draw tree in provided rectangle. Expects height rows available.""" + try: + # compute drawing area for items (first row may be label) + line = y + start_line = line + if self._label: + try: + window.addstr(line, x, self._label[:width], curses.A_BOLD) + except curses.error: + pass + line += 1 + available = max(self._height, height - (1 if self._label else 0)) + # record last draw height for _ensure_hover_visible + self._height = available + window.addstr(2, 80, f"(available {available}, height {self._height}, wh {height})", curses.A_DIM) + # rebuild visible items (safe cheap operation) + self._flatten_visible() + total = len(self._visible_items) + if total == 0: + try: + window.addstr(line, x, "(empty)", curses.A_DIM) + except curses.error: + pass + return + # clamp scroll offset + if self._scroll_offset < 0: + self._scroll_offset = 0 + if self._hover_index < 0: + self._hover_index = 0 + if self._hover_index >= total: + self._hover_index = total - 1 + if self._scroll_offset > self._hover_index: + self._scroll_offset = self._hover_index + if self._scroll_offset + available <= self._hover_index: + self._scroll_offset = max(0, self._hover_index - available + 1) + # draw visible slice + for i in range(available): + idx = self._scroll_offset + i + if idx >= total: + break + itm, depth = self._visible_items[idx] + is_selected = itm in self._selected_items + # prepare expander + try: + has_children = bool(getattr(itm, "_children", []) or (callable(getattr(itm, "children", None)) and (itm.children() or []))) + except Exception: + has_children = False + try: + is_open = bool(getattr(itm, "_is_open", False)) + except Exception: + is_open = False + exp = "▾" if (has_children and is_open) else ("▸" if has_children else " ") + checkbox = "*" if is_selected else " " + indent = " " * (depth * 2) + text = f"{indent}{exp} [{checkbox}] {itm.label()}" + if len(text) > width: + text = text[:max(0, width - 3)] + "..." + attr = curses.A_REVERSE if (self._focused and idx == self._hover_index and self.isEnabled()) else curses.A_NORMAL + if not self.isEnabled(): + attr |= curses.A_DIM + try: + window.addstr(line + i, x, text.ljust(width), attr) + except curses.error: + pass + # scroll indicators + try: + if self._scroll_offset > 0: + window.addch(start_line + (1 if self._label else 0), x + max(0, width - 1), '^') + if (self._scroll_offset + available) < total: + window.addch(start_line + (1 if self._label else 0) + min(available - 1, total - 1), x + max(0, width - 1), 'v') + except curses.error: + pass + except Exception: + pass + + def _handle_key(self, key): + """Keyboard handling: navigation, expand (SPACE), select (ENTER).""" + if not self._focused or not self.isEnabled(): + return False + handled = True + total = len(self._visible_items) + if key == curses.KEY_UP: + if self._hover_index > 0: + self._hover_index -= 1 + self._ensure_hover_visible(self._height) + elif key == curses.KEY_DOWN: + if self._hover_index < max(0, total - 1): + self._hover_index += 1 + self._ensure_hover_visible(self._height) + elif key == curses.KEY_PPAGE: + step = max(1, self._height) + self._hover_index = max(0, self._hover_index - step) + self._ensure_hover_visible(self._height) + elif key == curses.KEY_NPAGE: + step = max(1, self._height) + self._hover_index = min(max(0, total - 1), self._hover_index + step) + self._ensure_hover_visible(self._height) + elif key == curses.KEY_HOME: + self._hover_index = 0 + self._ensure_hover_visible(self._height) + elif key == curses.KEY_END: + self._hover_index = max(0, total - 1) + self._ensure_hover_visible(self._height) + elif key in (ord(' '),): # SPACE toggles expansion per dialog footer convention + if 0 <= self._hover_index < total: + itm, _ = self._visible_items[self._hover_index] + # Toggle expand/collapse without changing selection + self._toggle_expand(itm) + elif key in (ord('\n'),): # ENTER toggles selection + if 0 <= self._hover_index < total: + itm, _ = self._visible_items[self._hover_index] + self._handle_selection_action(itm) + else: + handled = False + return handled + + def currentItem(self): + try: + return self._selected_items[0] if self._selected_items else None + except Exception: + return None + + def getSelectedItems(self): + return list(self._selected_items) + + def selectItem(self, item, selected=True): + """Programmatic select/deselect that respects recursive flag.""" + if item is None: + return + try: + if selected: + if not self._multi: + # clear others + try: + for it in list(getattr(self, "_items", []) or []): + try: + it.setSelected(False) + except Exception: + pass + item.setSelected(True) + self._selected_items = [item] + except Exception: + pass + else: + if item not in self._selected_items: + item.setSelected(True) + self._selected_items.append(item) + if self._recursive: + for d in self._collect_all_descendants(item): + if d not in self._selected_items: + try: + d.setSelected(True) + except Exception: + pass + self._selected_items.append(d) + else: + # deselect + if item in self._selected_items: + self._selected_items.remove(item) + try: + item.setSelected(False) + except Exception: + pass + if self._recursive: + for d in self._collect_all_descendants(item): + if d in self._selected_items: + self._selected_items.remove(d) + try: + d.setSelected(False) + except Exception: + pass + # update last ids + try: + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + self._last_selected_ids = set() + except Exception: + pass From 1e8739daf7592a2bdf63aba44e2d76274905e9cb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 17:45:42 +0100 Subject: [PATCH 094/523] improved tree drawing --- manatools/aui/yui_curses.py | 113 +++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index e733c53..c5e2666 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -583,37 +583,78 @@ def _set_backend_enabled(self, enabled): def _draw(self, window, y, x, width, height): # Calculate total fixed height and number of stretchable children fixed_height = 0 - stretchable_count = 0 - child_heights = [] - for child in self._children: + stretchable_indices = [] + child_min_heights = [] + for i, child in enumerate(self._children): min_height = getattr(child, '_height', 1) + child_min_heights.append(min_height) if child.stretchable(YUIDimension.YD_VERT): - stretchable_count += 1 - child_heights.append(None) # placeholder for stretchable + stretchable_indices.append(i) else: fixed_height += min_height - child_heights.append(min_height) - - # Calculate available height for stretchable children - spacing = len(self._children) - 1 + + spacing = max(0, len(self._children) - 1) + # Available height to distribute among stretchable children available_height = max(0, height - fixed_height - spacing) - stretch_height = available_height // stretchable_count if stretchable_count else 0 - # Assign heights - for idx, child in enumerate(self._children): - if child_heights[idx] is None: - # Stretchable child - child_heights[idx] = max(1, stretch_height) + # Compute minimal total required by stretchables (each may declare a minimal _height) + total_min_for_stretchables = sum(child_min_heights[i] for i in stretchable_indices) if stretchable_indices else 0 + + child_heights = [0] * len(self._children) + + if stretchable_indices: + if available_height >= total_min_for_stretchables: + # Give each stretchable its min first, then distribute remaining equally + remaining = available_height - total_min_for_stretchables + per_extra = remaining // len(stretchable_indices) + extra_rem = remaining % len(stretchable_indices) + for k, idx in enumerate(stretchable_indices): + base = child_min_heights[idx] + child_heights[idx] = base + per_extra + (1 if k < extra_rem else 0) + else: + # Not enough space to honor all mins: distribute proportionally but at least 1 + per = available_height // len(stretchable_indices) if len(stretchable_indices) else 0 + rem = available_height % len(stretchable_indices) if len(stretchable_indices) else 0 + for k, idx in enumerate(stretchable_indices): + child_heights[idx] = max(1, per + (1 if k < rem else 0)) + # assign fixed heights + for i, child in enumerate(self._children): + if not child.stretchable(YUIDimension.YD_VERT): + child_heights[i] = child_min_heights[i] + + # If due rounding the sum of allocated heights + spacing differs from available height, + # adjust the last stretchable (or last child) to fit exactly. + total_alloc = sum(child_heights) + spacing + if total_alloc < height: + extra = height - total_alloc + # give extra to last stretchable if any, else to last child + if stretchable_indices: + child_heights[stretchable_indices[-1]] += extra + elif child_heights: + child_heights[-1] += extra + elif total_alloc > height: + diff = total_alloc - height + # try to reduce last stretchable first + if stretchable_indices: + idx = stretchable_indices[-1] + child_heights[idx] = max(1, child_heights[idx] - diff) + else: + # reduce last child + child_heights[-1] = max(1, child_heights[-1] - diff) # Draw children current_y = y for idx, child in enumerate(self._children): if not hasattr(child, '_draw'): continue - ch = child_heights[idx] + ch = child_heights[idx] if idx < len(child_heights) else getattr(child, "_height", 1) if current_y + ch > y + height: + # no more space break - child._draw(window, current_y, x, width, ch) + try: + child._draw(window, current_y, x, width, ch) + except Exception: + pass current_y += ch if idx < len(self._children) - 1: current_y += 1 # spacing @@ -1709,7 +1750,11 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti self._recursive = bool(recursiveselection) if self._recursive: self._multi = True - self._height = 6 # curses backend widgets are drawn; keep minimal height + # Minimal height requested by this widget (layout should try to honor this). + # Must be at least 6 as requested. + self._min_height = 6 + # Current viewport height used during last draw (updated in _draw) + self._height = self._min_height self._can_focus = True self._focused = False self._hover_index = 0 # index in visible_items @@ -1725,6 +1770,7 @@ def widgetClass(self): return "YTree" def _create_backend_widget(self): + self._height = max(self._height, self._min_height) # ensure visible flatten computed self.rebuildTree() @@ -2034,10 +2080,17 @@ def _draw(self, window, y, x, width, height): except curses.error: pass line += 1 - available = max(self._height, height - (1 if self._label else 0)) - # record last draw height for _ensure_hover_visible - self._height = available - window.addstr(2, 80, f"(available {available}, height {self._height}, wh {height})", curses.A_DIM) + + # Use the actual area allocated to this widget by the parent. + # Respect the requested minimum height, but allow using all available rows. + # Note: height is passed as the available area by the parent layout available more + # than the minimum height of this widget. + available_area = self._min_height + height + available = max(self._min_height, available_area) + + # record last draw height for navigation/ensure logic + self._height = available_area + # rebuild visible items (safe cheap operation) self._flatten_visible() total = len(self._visible_items) @@ -2047,7 +2100,8 @@ def _draw(self, window, y, x, width, height): except curses.error: pass return - # clamp scroll offset + + # clamp scroll offset and hover index into real total if self._scroll_offset < 0: self._scroll_offset = 0 if self._hover_index < 0: @@ -2058,8 +2112,10 @@ def _draw(self, window, y, x, width, height): self._scroll_offset = self._hover_index if self._scroll_offset + available <= self._hover_index: self._scroll_offset = max(0, self._hover_index - available + 1) - # draw visible slice - for i in range(available): + + # draw visible slice using 'available_area' rows (the rows actually provided by parent) + draw_rows = min(available_area, total - self._scroll_offset) + for i in range(draw_rows): idx = self._scroll_offset + i if idx >= total: break @@ -2087,12 +2143,13 @@ def _draw(self, window, y, x, width, height): window.addstr(line + i, x, text.ljust(width), attr) except curses.error: pass - # scroll indicators + + # scroll indicators (use available_area) try: if self._scroll_offset > 0: window.addch(start_line + (1 if self._label else 0), x + max(0, width - 1), '^') - if (self._scroll_offset + available) < total: - window.addch(start_line + (1 if self._label else 0) + min(available - 1, total - 1), x + max(0, width - 1), 'v') + if (self._scroll_offset + available_area) < total: + window.addch(start_line + (1 if self._label else 0) + min(available_area - 1, total - 1), x + max(0, width - 1), 'v') except curses.error: pass except Exception: From da19411fc003459bccd1b65b9cb1c6637dd7fa97 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 18:58:19 +0100 Subject: [PATCH 095/523] fixing min layout --- manatools/aui/yui_curses.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index c5e2666..3db769e 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -2085,12 +2085,14 @@ def _draw(self, window, y, x, width, height): # Respect the requested minimum height, but allow using all available rows. # Note: height is passed as the available area by the parent layout available more # than the minimum height of this widget. - available_area = self._min_height + height + available_area = max(self._height, height - (1 if self._label else 0)) available = max(self._min_height, available_area) +# available_area = self._min_height + height +# available = max(self._min_height, available_area) # record last draw height for navigation/ensure logic self._height = available_area - + window.addstr(2, 80, f"(available {available_area}, height {self._height}, wh {height})", curses.A_DIM) # rebuild visible items (safe cheap operation) self._flatten_visible() total = len(self._visible_items) From 3d0f9833c30405a0559877c7e01e371b1726a3cb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 19:12:36 +0100 Subject: [PATCH 096/523] Typo --- manatools/aui/yui_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 8971321..f3834af 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -1432,7 +1432,7 @@ def immediateMode(self): def setImmediateMode(self, on:bool=True): self._immediate = on - self.steNotify(on) + self.setNotify(on) def _collect_descendant_qitems(self, qitem): """Return a list of qitem and all descendant QTreeWidgetItem objects.""" From 0fe54dc1bb760ecf0b4123585a96778a4af3948b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 19:13:14 +0100 Subject: [PATCH 097/523] Set Notify on if immediate mode is set --- manatools/aui/yui_gtk.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index b3742d4..299d616 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -2577,14 +2577,16 @@ def activate(self): except Exception: return False + def hasMultiSelection(self): + """Return True if the tree allows selecting multiple items at once.""" + return bool(self._multi) + def immediateMode(self): return bool(self._immediate) - def setImmediateMode(self, on=True): - self._immediate = bool(on) - - def hasMultiSelection(self): - return bool(self._multi) + def setImmediateMode(self, on:bool=True): + self._immediate = on + self.setNotify(on) def _set_backend_enabled(self, enabled): try: From 730c703d3057a4030ad142ccce3ed59380e52688 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 19:14:11 +0100 Subject: [PATCH 098/523] fixed selection and notification --- manatools/aui/yui_curses.py | 95 ++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 16 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 3db769e..1dff797 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -1750,6 +1750,7 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti self._recursive = bool(recursiveselection) if self._recursive: self._multi = True + self._immediate = self.notify() # Minimal height requested by this widget (layout should try to honor this). # Must be at least 6 as requested. self._min_height = 6 @@ -1769,6 +1770,17 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti def widgetClass(self): return "YTree" + def hasMultiSelection(self): + """Return True if the tree allows selecting multiple items at once.""" + return bool(self._multi) + + def immediateMode(self): + return bool(self._immediate) + + def setImmediateMode(self, on:bool=True): + self._immediate = on + self.setNotify(on) + def _create_backend_widget(self): self._height = max(self._height, self._min_height) # ensure visible flatten computed @@ -2013,7 +2025,10 @@ def _handle_selection_action(self, item): try: it.setSelected(False) except Exception: - pass + try: + setattr(it, "_selected", False) + except Exception: + pass else: try: self._selected_items.remove(item) @@ -2022,7 +2037,10 @@ def _handle_selection_action(self, item): try: item.setSelected(False) except Exception: - pass + try: + setattr(item, "_selected", False) + except Exception: + pass else: # select item and possibly descendants if self._recursive: @@ -2033,13 +2051,19 @@ def _handle_selection_action(self, item): try: it.setSelected(True) except Exception: - pass + try: + setattr(it, "_selected", True) + except Exception: + pass else: self._selected_items.append(item) try: item.setSelected(True) except Exception: - pass + try: + setattr(item, "_selected", True) + except Exception: + pass else: # single selection: clear all others and set this one try: @@ -2047,14 +2071,20 @@ def _handle_selection_action(self, item): try: it.setSelected(False) except Exception: - pass + try: + setattr(it, "_selected", False) + except Exception: + pass except Exception: pass self._selected_items = [item] try: item.setSelected(True) except Exception: - pass + try: + setattr(item, "_selected", True) + except Exception: + pass except Exception: pass @@ -2200,7 +2230,13 @@ def _handle_key(self, key): def currentItem(self): try: - return self._selected_items[0] if self._selected_items else None + # Prefer explicit selected_items; if empty return hovered visible item (useful after selection) + if self._selected_items: + return self._selected_items[0] + # fallback: return hovered visible item if any + if 0 <= self._hover_index < len(getattr(self, "_visible_items", [])): + return self._visible_items[self._hover_index][0] + return None except Exception: return None @@ -2220,14 +2256,26 @@ def selectItem(self, item, selected=True): try: it.setSelected(False) except Exception: - pass + try: + setattr(it, "_selected", False) + except Exception: + pass item.setSelected(True) - self._selected_items = [item] except Exception: - pass + try: + setattr(item, "_selected", True) + except Exception: + pass + self._selected_items = [item] else: if item not in self._selected_items: - item.setSelected(True) + try: + item.setSelected(True) + except Exception: + try: + setattr(item, "_selected", True) + except Exception: + pass self._selected_items.append(item) if self._recursive: for d in self._collect_all_descendants(item): @@ -2235,24 +2283,39 @@ def selectItem(self, item, selected=True): try: d.setSelected(True) except Exception: - pass + try: + setattr(d, "_selected", True) + except Exception: + pass self._selected_items.append(d) else: # deselect if item in self._selected_items: - self._selected_items.remove(item) + try: + self._selected_items.remove(item) + except Exception: + pass try: item.setSelected(False) except Exception: - pass + try: + setattr(item, "_selected", False) + except Exception: + pass if self._recursive: for d in self._collect_all_descendants(item): if d in self._selected_items: - self._selected_items.remove(d) + try: + self._selected_items.remove(d) + except Exception: + pass try: d.setSelected(False) except Exception: - pass + try: + setattr(d, "_selected", False) + except Exception: + pass # update last ids try: self._last_selected_ids = set(id(i) for i in self._selected_items) From 9a0c134614f053ad134d7026c51bcf598fda97db Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 19:37:26 +0100 Subject: [PATCH 099/523] added some resizing refresh --- manatools/aui/yui_curses.py | 69 ++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 1dff797..4f8ba09 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -212,6 +212,9 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._last_draw_time = 0 self._draw_interval = 0.1 # seconds self._event_result = None + # Debounce for resize handling (avoid flicker) + self._resize_pending_until = 0.0 + self._last_term_size = (0, 0) # (h, w) YDialogCurses._open_dialogs.append(self) def widgetClass(self): @@ -473,70 +476,88 @@ def waitForEvent(self, timeout_millisec=0): if timeout_millisec and timeout_millisec > 0: deadline = time.time() + (timeout_millisec / 1000.0) - # Main nested loop: iterate until event posted or timeout while self._is_open and self._event_result is None: try: - # Draw only if needed (throttle redraws) - current_time = time.time() - if current_time - self._last_draw_time >= self._draw_interval: + now = time.time() + + # Apply pending resize once debounce expires + if self._resize_pending_until and now >= self._resize_pending_until: + try: + ui._stdscr.clear() + ui._stdscr.refresh() + new_h, new_w = ui._stdscr.getmaxyx() + try: + curses.resizeterm(new_h, new_w) + except Exception: + pass + # Recreate backend window (full-screen) + self._backend_widget = curses.newwin(new_h, new_w, 0, 0) + self._last_term_size = (new_h, new_w) + except Exception: + pass + # Clear pending flag and force immediate redraw + self._resize_pending_until = 0.0 + self._last_draw_time = 0 + + # Draw at most every _draw_interval; forced redraw uses last_draw_time = 0 + if (now - self._last_draw_time) >= self._draw_interval: self._draw_dialog() - self._last_draw_time = current_time + self._last_draw_time = now # Non-blocking input ui._stdscr.nodelay(True) key = ui._stdscr.getch() if key == -1: - # no input; check timeout if deadline and time.time() >= deadline: self._event_result = YTimeoutEvent() break time.sleep(0.01) continue - # Handle global keys + # Global keys if key == curses.KEY_F10 or key == ord('q') or key == ord('Q'): - # Post cancel event self._post_event(YCancelEvent()) break elif key == curses.KEY_RESIZE: - # Handle terminal resize - force redraw - self._last_draw_time = 0 + # Debounce resize; do not redraw immediately to avoid flicker + try: + new_h, new_w = ui._stdscr.getmaxyx() + self._last_term_size = (new_h, new_w) + except Exception: + pass + # Wait 150ms after the last resize event before applying + self._resize_pending_until = time.time() + 0.15 continue - # Handle tab navigation + # Focus navigation if key == ord('\t'): self._cycle_focus(forward=True) - self._last_draw_time = 0 # Force redraw + self._last_draw_time = 0 continue - elif key == curses.KEY_BTAB: # Shift+Tab + elif key == curses.KEY_BTAB: self._cycle_focus(forward=False) - self._last_draw_time = 0 # Force redraw + self._last_draw_time = 0 continue - # Send key event to focused widget + # Dispatch key to focused widget if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): handled = self._focused_widget._handle_key(key) if handled: - self._last_draw_time = 0 # Force redraw + self._last_draw_time = 0 except KeyboardInterrupt: - # treat as cancel self._post_event(YCancelEvent()) break - except Exception as e: - # Don't crash on curses errors - # keep running unless fatal - time.sleep(0.1) + except Exception: + time.sleep(0.05) - # If dialog was closed without explicit event, produce CancelEvent if self._event_result is None: if not self._is_open: self._event_result = YCancelEvent() elif deadline and time.time() >= deadline: self._event_result = YTimeoutEvent() - # cleanup if dialog closed if not self._is_open: try: self.destroy() @@ -2122,7 +2143,7 @@ def _draw(self, window, y, x, width, height): # record last draw height for navigation/ensure logic self._height = available_area - window.addstr(2, 80, f"(available {available_area}, height {self._height}, wh {height})", curses.A_DIM) +# window.addstr(2, 80, f"(available {available_area}, height {self._height}, wh {height})", curses.A_DIM) # rebuild visible items (safe cheap operation) self._flatten_visible() total = len(self._visible_items) From 03546f70912885bbb910d0436c9b0e3477ae24b4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 19:49:11 +0100 Subject: [PATCH 100/523] improved widget drawing --- manatools/aui/yui_curses.py | 136 ++++++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 52 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 4f8ba09..4a2f2b9 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -602,83 +602,115 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): - # Calculate total fixed height and number of stretchable children - fixed_height = 0 - stretchable_indices = [] + # Compute per-child minimum heights and identify stretchable children and their weights + num_children = len(self._children) + if num_children == 0 or height <= 0 or width <= 0: + return + + # One-line spacing between children + spacing = max(0, num_children - 1) + child_min_heights = [] + stretchable_indices = [] + stretchable_weights = [] + fixed_height_total = 0 + for i, child in enumerate(self._children): - min_height = getattr(child, '_height', 1) - child_min_heights.append(min_height) - if child.stretchable(YUIDimension.YD_VERT): + # Use child's _height as its minimal required rows + child_min = max(1, getattr(child, "_height", 1)) + child_min_heights.append(child_min) + + is_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) + if is_stretch: stretchable_indices.append(i) + # Get vertical weight if any; default 1 for stretchables + try: + w = child.weight(YUIDimension.YD_VERT) + w = int(w) if w is not None else 1 + except Exception: + w = 1 + if w <= 0: + w = 1 + stretchable_weights.append(w) else: - fixed_height += min_height + fixed_height_total += child_min - spacing = max(0, len(self._children) - 1) - # Available height to distribute among stretchable children - available_height = max(0, height - fixed_height - spacing) + # Height available beyond fixed-height children and spacing + available_for_stretch = max(0, height - fixed_height_total - spacing) - # Compute minimal total required by stretchables (each may declare a minimal _height) - total_min_for_stretchables = sum(child_min_heights[i] for i in stretchable_indices) if stretchable_indices else 0 - - child_heights = [0] * len(self._children) + # Initialize all child allocated heights to their minima + allocated_heights = list(child_min_heights) if stretchable_indices: - if available_height >= total_min_for_stretchables: - # Give each stretchable its min first, then distribute remaining equally - remaining = available_height - total_min_for_stretchables - per_extra = remaining // len(stretchable_indices) - extra_rem = remaining % len(stretchable_indices) + total_weight = sum(stretchable_weights) + if total_weight <= 0: + total_weight = len(stretchable_indices) + + # Distribute available height among stretchables proportionally to their weights + # Start from each child's minimum height, then add proportional extra rows + remaining = available_for_stretch + if remaining > 0: + # First pass: proportional integer division + extras = [0] * len(stretchable_indices) + base_units = 0 for k, idx in enumerate(stretchable_indices): - base = child_min_heights[idx] - child_heights[idx] = base + per_extra + (1 if k < extra_rem else 0) - else: - # Not enough space to honor all mins: distribute proportionally but at least 1 - per = available_height // len(stretchable_indices) if len(stretchable_indices) else 0 - rem = available_height % len(stretchable_indices) if len(stretchable_indices) else 0 + # Proportional extra for this child + extra = (remaining * stretchable_weights[k]) // total_weight + extras[k] = extra + base_units += extra + # Second pass: distribute leftover rows due to integer division + leftover = remaining - base_units + for k in range(len(stretchable_indices)): + if leftover <= 0: + break + extras[k] += 1 + leftover -= 1 + # Apply extras to allocated heights for k, idx in enumerate(stretchable_indices): - child_heights[idx] = max(1, per + (1 if k < rem else 0)) - # assign fixed heights - for i, child in enumerate(self._children): - if not child.stretchable(YUIDimension.YD_VERT): - child_heights[i] = child_min_heights[i] + allocated_heights[idx] = child_min_heights[idx] + extras[k] - # If due rounding the sum of allocated heights + spacing differs from available height, - # adjust the last stretchable (or last child) to fit exactly. - total_alloc = sum(child_heights) + spacing + # After allocation, ensure the sum fits exactly into the container height including spacing + total_alloc = sum(allocated_heights) + spacing if total_alloc < height: + # Add missing rows to the last stretchable child (or the last child if none) extra = height - total_alloc - # give extra to last stretchable if any, else to last child if stretchable_indices: - child_heights[stretchable_indices[-1]] += extra - elif child_heights: - child_heights[-1] += extra + last_idx = stretchable_indices[-1] + else: + last_idx = num_children - 1 + allocated_heights[last_idx] += extra elif total_alloc > height: + # Reduce rows from the last stretchable child first; else reduce last child diff = total_alloc - height - # try to reduce last stretchable first if stretchable_indices: - idx = stretchable_indices[-1] - child_heights[idx] = max(1, child_heights[idx] - diff) + last_idx = stretchable_indices[-1] else: - # reduce last child - child_heights[-1] = max(1, child_heights[-1] - diff) + last_idx = num_children - 1 + allocated_heights[last_idx] = max(1, allocated_heights[last_idx] - diff) - # Draw children + # Draw children using their allocated heights, respecting the container rectangle current_y = y - for idx, child in enumerate(self._children): - if not hasattr(child, '_draw'): + for i, child in enumerate(self._children): + # Stop if no vertical space left + ch = allocated_heights[i] + if ch <= 0: continue - ch = child_heights[idx] if idx < len(child_heights) else getattr(child, "_height", 1) if current_y + ch > y + height: - # no more space + ch = max(0, (y + height) - current_y) + if ch <= 0: break + try: - child._draw(window, current_y, x, width, ch) + if hasattr(child, "_draw"): + child._draw(window, current_y, x, width, ch) except Exception: pass + current_y += ch - if idx < len(self._children) - 1: - current_y += 1 # spacing + + # Add one-line spacing between children if space remains + if i < num_children - 1 and current_y < (y + height): + current_y += 1 class YHBoxCurses(YWidget): def __init__(self, parent=None): @@ -2163,8 +2195,8 @@ def _draw(self, window, y, x, width, height): self._hover_index = total - 1 if self._scroll_offset > self._hover_index: self._scroll_offset = self._hover_index - if self._scroll_offset + available <= self._hover_index: - self._scroll_offset = max(0, self._hover_index - available + 1) + if self._scroll_offset + available_area <= self._hover_index: + self._scroll_offset = max(0, self._hover_index - available_area + 1) # draw visible slice using 'available_area' rows (the rows actually provided by parent) draw_rows = min(available_area, total - self._scroll_offset) From 2ae039e0f0da090f44c259a0328aa815065f97e5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 20:09:34 +0100 Subject: [PATCH 101/523] Renamed for error in naming --- ...st_selctionbox.py => test_selectionbox.py} | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) rename test/{test_selctionbox.py => test_selectionbox.py} (73%) diff --git a/test/test_selctionbox.py b/test/test_selectionbox.py similarity index 73% rename from test/test_selctionbox.py rename to test/test_selectionbox.py index d553d9a..ccaf345 100644 --- a/test/test_selctionbox.py +++ b/test/test_selectionbox.py @@ -27,9 +27,10 @@ def test_selectionbox(backend_name=None): ui = YUI_ui() factory = ui.widgetFactory() - + ui.application().setIconBasePath("/home/angelo/src/manatools/dnfdragora/share/images/") ############### + ui.application().setApplicationIcon("dnfdragora.png") dialog = factory.createPopupDialog() mainVbox = factory.createVBox( dialog ) hbox = factory.createHBox( mainVbox ) @@ -43,13 +44,20 @@ def test_selectionbox(backend_name=None): selBox.addItem( "Calzone" ) vbox = factory.createVBox( hbox ) - notifyCheckBox = factory.createCheckBox( vbox, "Notify on change", selBox.notify() ) + align = factory.createTop(vbox) + notifyCheckBox = factory.createCheckBox( align, "Notify on change", selBox.notify() ) notifyCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) multiSelectionCheckBox = factory.createCheckBox( vbox, "Multi-selection", selBox.multiSelection() ) multiSelectionCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + align = factory.createBottom( vbox ) + disableSelectionBox = factory.createCheckBox( align, "disable selection box", not selBox.isEnabled() ) + disableSelectionBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + disableValue = factory.createCheckBox( vbox, "disable value button", False ) + disableValue.setStretchable( yui.YUIDimension.YD_HORIZ, True ) hbox = factory.createHBox( mainVbox ) valueButton = factory.createPushButton( hbox, "Value" ) + disableValue.setValue(not valueButton.isEnabled()) label = factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) label.setStretchable( yui.YUIDimension.YD_HORIZ, True ) valueField = factory.createLabel(hbox, "") @@ -57,10 +65,12 @@ def test_selectionbox(backend_name=None): #factory.createVSpacing( vbox, 0.3 ) - #rightAlignment = factory.createRight( vbox ) TODO hbox = factory.createHBox( mainVbox ) - closeButton = factory.createPushButton( hbox, "Close" ) - factory.createLabel(hbox, " ") # spacer + #factory.createLabel(hbox, " ") # spacer + leftAlignment = factory.createLeft( hbox ) + left = factory.createPushButton( leftAlignment, "Left" ) + rightAlignment = factory.createRight( hbox ) + closeButton = factory.createPushButton( rightAlignment, "Close" ) # # Event loop @@ -80,6 +90,8 @@ def test_selectionbox(backend_name=None): if wdg == closeButton: dialog.destroy() break + elif wdg == left: + valueField.setText(left.label()) elif (wdg == valueButton): if selBox.multiSelection(): labels = [item.label() for item in selBox.selectedItems()] @@ -87,10 +99,15 @@ def test_selectionbox(backend_name=None): else: item = selBox.selectedItem() valueField.setText( item.label() if item else "" ) + ui.application().setApplicationTitle("Test App") elif (wdg == notifyCheckBox): selBox.setNotify( notifyCheckBox.value() ) elif (wdg == multiSelectionCheckBox): selBox.setMultiSelection( multiSelectionCheckBox.value() ) + elif (wdg == disableSelectionBox): + selBox.setEnabled( not disableSelectionBox.value() ) + elif (wdg == disableValue): + valueButton.setEnabled( not disableValue.value() ) elif (wdg == selBox): # selBox will only send events with setNotify() TODO if selBox.multiSelection(): labels = [item.label() for item in selBox.selectedItems()] From 70edc86530c5c9ced9fc2a24244bdca3ad99d36e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 20:10:00 +0100 Subject: [PATCH 102/523] honour space for YTree widget --- manatools/aui/yui_curses.py | 183 ++++++++++++++---------------------- 1 file changed, 73 insertions(+), 110 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 4a2f2b9..503aa0f 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -602,12 +602,11 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): - # Compute per-child minimum heights and identify stretchable children and their weights + # Vertical layout with spacing; give stretchable children more than their minimum num_children = len(self._children) if num_children == 0 or height <= 0 or width <= 0: return - # One-line spacing between children spacing = max(0, num_children - 1) child_min_heights = [] @@ -616,14 +615,14 @@ def _draw(self, window, y, x, width, height): fixed_height_total = 0 for i, child in enumerate(self._children): - # Use child's _height as its minimal required rows + # child._height is the preferred minimum (may include its own label rows) child_min = max(1, getattr(child, "_height", 1)) child_min_heights.append(child_min) is_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) if is_stretch: stretchable_indices.append(i) - # Get vertical weight if any; default 1 for stretchables + # default vertical weight = 1 try: w = child.weight(YUIDimension.YD_VERT) w = int(w) if w is not None else 1 @@ -635,82 +634,59 @@ def _draw(self, window, y, x, width, height): else: fixed_height_total += child_min - # Height available beyond fixed-height children and spacing available_for_stretch = max(0, height - fixed_height_total - spacing) - # Initialize all child allocated heights to their minima - allocated_heights = list(child_min_heights) + allocated = list(child_min_heights) if stretchable_indices: - total_weight = sum(stretchable_weights) - if total_weight <= 0: - total_weight = len(stretchable_indices) - - # Distribute available height among stretchables proportionally to their weights - # Start from each child's minimum height, then add proportional extra rows - remaining = available_for_stretch - if remaining > 0: - # First pass: proportional integer division - extras = [0] * len(stretchable_indices) - base_units = 0 - for k, idx in enumerate(stretchable_indices): - # Proportional extra for this child - extra = (remaining * stretchable_weights[k]) // total_weight - extras[k] = extra - base_units += extra - # Second pass: distribute leftover rows due to integer division - leftover = remaining - base_units - for k in range(len(stretchable_indices)): - if leftover <= 0: - break - extras[k] += 1 - leftover -= 1 - # Apply extras to allocated heights - for k, idx in enumerate(stretchable_indices): - allocated_heights[idx] = child_min_heights[idx] + extras[k] - - # After allocation, ensure the sum fits exactly into the container height including spacing - total_alloc = sum(allocated_heights) + spacing + total_weight = sum(stretchable_weights) or len(stretchable_indices) + # Proportional distribution of extra rows + extras = [0] * len(stretchable_indices) + base = 0 + for k, idx in enumerate(stretchable_indices): + extra = (available_for_stretch * stretchable_weights[k]) // total_weight + extras[k] = extra + base += extra + # Distribute leftover rows due to integer division + leftover = available_for_stretch - base + for k in range(len(stretchable_indices)): + if leftover <= 0: + break + extras[k] += 1 + leftover -= 1 + for k, idx in enumerate(stretchable_indices): + allocated[idx] = child_min_heights[idx] + extras[k] + + total_alloc = sum(allocated) + spacing if total_alloc < height: - # Add missing rows to the last stretchable child (or the last child if none) + # Give remainder to the last stretchable (or last child) extra = height - total_alloc - if stretchable_indices: - last_idx = stretchable_indices[-1] - else: - last_idx = num_children - 1 - allocated_heights[last_idx] += extra + target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) + allocated[target] += extra elif total_alloc > height: - # Reduce rows from the last stretchable child first; else reduce last child + # Reduce overflow from the last stretchable (or last child) diff = total_alloc - height - if stretchable_indices: - last_idx = stretchable_indices[-1] - else: - last_idx = num_children - 1 - allocated_heights[last_idx] = max(1, allocated_heights[last_idx] - diff) + target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) + allocated[target] = max(1, allocated[target] - diff) - # Draw children using their allocated heights, respecting the container rectangle - current_y = y + # Draw children with allocated heights + cy = y for i, child in enumerate(self._children): - # Stop if no vertical space left - ch = allocated_heights[i] + ch = allocated[i] if ch <= 0: continue - if current_y + ch > y + height: - ch = max(0, (y + height) - current_y) + if cy + ch > y + height: + ch = max(0, (y + height) - cy) if ch <= 0: break - try: if hasattr(child, "_draw"): - child._draw(window, current_y, x, width, ch) + child._draw(window, cy, x, width, ch) except Exception: pass - - current_y += ch - - # Add one-line spacing between children if space remains - if i < num_children - 1 and current_y < (y + height): - current_y += 1 + cy += ch + if i < num_children - 1 and cy < (y + height): + cy += 1 # one-line spacing class YHBoxCurses(YWidget): def __init__(self, parent=None): @@ -776,7 +752,6 @@ def _draw(self, window, y, x, width, height): spacing = max(0, num_children - 1) available = max(0, width - spacing) - # Allocate fixed width for non-stretchable children widths = [0] * num_children stretchables = [] fixed_total = 0 @@ -788,7 +763,6 @@ def _draw(self, window, y, x, width, height): widths[i] = w fixed_total += w - # Remaining width goes to stretchable children remaining = max(0, available - fixed_total) if stretchables: per = remaining // len(stretchables) @@ -796,7 +770,6 @@ def _draw(self, window, y, x, width, height): for k, idx in enumerate(stretchables): widths[idx] = max(1, per + (1 if k < extra else 0)) else: - # No stretchables: distribute leftover evenly if fixed_total < available: leftover = available - fixed_total per = leftover // num_children @@ -804,22 +777,23 @@ def _draw(self, window, y, x, width, height): for i in range(num_children): base = widths[i] if widths[i] else 1 widths[i] = base + per + (1 if i < extra else 0) - else: - # If even fixed widths overflow, clamp proportionally - pass # widths already reflect minimal values - # Draw children + # Draw children and pass full container height to stretchable children cx = x for i, child in enumerate(self._children): w = widths[i] if w <= 0: continue - if hasattr(child, "_draw"): + # If child is vertically stretchable, give full height; else give its minimum + if child.stretchable(YUIDimension.YD_VERT): + ch = height + else: ch = min(height, getattr(child, "_height", height)) + if hasattr(child, "_draw"): child._draw(window, y, cx, w, ch) cx += w if i < num_children - 1: - cx += 1 # one-column spacing + cx += 1 class YLabelCurses(YWidget): def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): @@ -1804,19 +1778,18 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti if self._recursive: self._multi = True self._immediate = self.notify() - # Minimal height requested by this widget (layout should try to honor this). - # Must be at least 6 as requested. + # Minimal height (items area) requested by this widget self._min_height = 6 - # Current viewport height used during last draw (updated in _draw) - self._height = self._min_height + # Preferred height exposed to layout should include label line if any + self._height = self._min_height + (1 if self._label else 0) self._can_focus = True self._focused = False - self._hover_index = 0 # index in visible_items + self._hover_index = 0 self._scroll_offset = 0 - self._visible_items = [] # list of (item, depth) + self._visible_items = [] + self._selected_items = [] self._last_selected_ids = set() self._suppress_selection_handler = False - # honor stretchability so VBoxes/HBoxes can allocate space self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) @@ -1835,8 +1808,8 @@ def setImmediateMode(self, on:bool=True): self.setNotify(on) def _create_backend_widget(self): - self._height = max(self._height, self._min_height) - # ensure visible flatten computed + # Keep preferred minimum for the layout (items + optional label) + self._height = max(self._height, self._min_height + (1 if self._label else 0)) self.rebuildTree() def addItem(self, item): @@ -2157,6 +2130,9 @@ def _draw(self, window, y, x, width, height): # compute drawing area for items (first row may be label) line = y start_line = line + label_rows = 1 if self._label else 0 + + # Draw label if self._label: try: window.addstr(line, x, self._label[:width], curses.A_BOLD) @@ -2164,49 +2140,36 @@ def _draw(self, window, y, x, width, height): pass line += 1 - # Use the actual area allocated to this widget by the parent. - # Respect the requested minimum height, but allow using all available rows. - # Note: height is passed as the available area by the parent layout available more - # than the minimum height of this widget. - available_area = max(self._height, height - (1 if self._label else 0)) - available = max(self._min_height, available_area) -# available_area = self._min_height + height -# available = max(self._min_height, available_area) + # Actual rows given by parent for items + available_rows = max(0, height - label_rows) + # Keep _height as the current viewport rows (items area), not the preferred minimum + self._height = max(1, available_rows) # record last draw height for navigation/ensure logic - self._height = available_area -# window.addstr(2, 80, f"(available {available_area}, height {self._height}, wh {height})", curses.A_DIM) + self._height = available_rows # rebuild visible items (safe cheap operation) self._flatten_visible() total = len(self._visible_items) if total == 0: try: - window.addstr(line, x, "(empty)", curses.A_DIM) + if available_rows > 0: + window.addstr(line, x, "(empty)", curses.A_DIM) except curses.error: pass return - # clamp scroll offset and hover index into real total - if self._scroll_offset < 0: - self._scroll_offset = 0 - if self._hover_index < 0: - self._hover_index = 0 - if self._hover_index >= total: - self._hover_index = total - 1 - if self._scroll_offset > self._hover_index: - self._scroll_offset = self._hover_index - if self._scroll_offset + available_area <= self._hover_index: - self._scroll_offset = max(0, self._hover_index - available_area + 1) + # Clamp scroll/hover to the viewport + self._ensure_hover_visible(height=self._height) - # draw visible slice using 'available_area' rows (the rows actually provided by parent) - draw_rows = min(available_area, total - self._scroll_offset) + # Draw only inside the allocated rectangle + draw_rows = min(available_rows, max(0, total - self._scroll_offset)) for i in range(draw_rows): idx = self._scroll_offset + i if idx >= total: break itm, depth = self._visible_items[idx] is_selected = itm in self._selected_items - # prepare expander + # expander, text, attrs... try: has_children = bool(getattr(itm, "_children", []) or (callable(getattr(itm, "children", None)) and (itm.children() or []))) except Exception: @@ -2229,12 +2192,12 @@ def _draw(self, window, y, x, width, height): except curses.error: pass - # scroll indicators (use available_area) + # Scroll indicators based on actual viewport rows try: - if self._scroll_offset > 0: - window.addch(start_line + (1 if self._label else 0), x + max(0, width - 1), '^') - if (self._scroll_offset + available_area) < total: - window.addch(start_line + (1 if self._label else 0) + min(available_area - 1, total - 1), x + max(0, width - 1), 'v') + if self._scroll_offset > 0 and available_rows > 0: + window.addch(y + label_rows, x + max(0, width - 1), '^') + if (self._scroll_offset + available_rows) < total and available_rows > 0: + window.addch(y + label_rows + min(available_rows - 1, total - 1), x + max(0, width - 1), 'v') except curses.error: pass except Exception: From 1027829abc37beac610e4d31ce4a465cc3b24985 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 20:11:23 +0100 Subject: [PATCH 103/523] Show label and application name --- test/test_tree.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/test_tree.py b/test/test_tree.py index e2a5329..0a2c9f9 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -27,6 +27,8 @@ def test_tree(backend_name=None): ui = YUI_ui() factory = ui.widgetFactory() + title = ui.application().applicationTitle() + ui.application().setApplicationTitle("Tree Widget Application") # Create dialog focused on ComboBox testing dialog = factory.createMainDialog() @@ -51,7 +53,7 @@ def test_tree(backend_name=None): tree.addItem(item) - selected = factory.createLabel(vbox, "") + selected = factory.createLabel(vbox, "Selected:") hbox = factory.createHBox(vbox) ok_button = factory.createPushButton(hbox, "OK") cancel_button = factory.createPushButton(hbox, "Cancel") @@ -72,19 +74,28 @@ def test_tree(backend_name=None): break elif wdg == tree: if reason == yui.YEventReason.SelectionChanged: - selected.setText(f"Selected: '{tree.selectedItem().label()}'") + if tree.selectedItem() is not None: + selected.setText(f"Selected: '{tree.selectedItem().label()}'") + else: + selected.setText(f"Selected: None") elif reason == yui.YEventReason.Activated: - selected.setText(f"Activated: '{tree.selectedItem().label()}'") + if tree.selectedItem() is not None: + selected.setText(f"Activated: '{tree.selectedItem().label()}'") + else: + selected.setText(f"Activated: None") elif wdg == ok_button: selected.setText(f"OK clicked.") # Show final result - print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") + if tree.selectedItem() is not None: + print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") except Exception as e: print(f"Error testing Tree with backend {backend_name}: {e}") import traceback traceback.print_exc() + finally: + ui.application().setApplicationTitle(title) if __name__ == "__main__": if len(sys.argv) > 1: From 65115f5d58f6b1aeee02ddc4986c86e186451573 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 30 Nov 2025 23:50:37 +0100 Subject: [PATCH 104/523] fixed title --- test/test_multiselection_tree.py | 16 ++++++++++++---- test/test_recursiveselection_tree.py | 15 +++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/test/test_multiselection_tree.py b/test/test_multiselection_tree.py index 45d8cb1..07c964c 100644 --- a/test/test_multiselection_tree.py +++ b/test/test_multiselection_tree.py @@ -27,6 +27,8 @@ def test_tree(backend_name=None): ui = YUI_ui() factory = ui.widgetFactory() + title = ui.application().applicationTitle() + ui.application().setApplicationTitle("Tree Widget (multi selection) Application") # Create dialog focused on ComboBox testing dialog = factory.createMainDialog() @@ -51,7 +53,7 @@ def test_tree(backend_name=None): tree.addItem(item) - selected = factory.createLabel(vbox, "") + selected = factory.createLabel(vbox, "Selected:") hbox = factory.createHBox(vbox) ok_button = factory.createPushButton(hbox, "OK") cancel_button = factory.createPushButton(hbox, "Cancel") @@ -78,19 +80,25 @@ def test_tree(backend_name=None): elif tree.selectedItem() is not None: selected.setText(f"Selected: '{tree.selectedItem().label()}'") else: - selected.setText("Selected: None") + selected.setText(f"Selected: None") elif reason == yui.YEventReason.Activated: - selected.setText(f"Activated: '{tree.selectedItem().label()}'") + if tree.selectedItem() is not None: + selected.setText(f"Activated: '{tree.selectedItem().label()}'") + else: + selected.setText(f"Activated: None") elif wdg == ok_button: selected.setText(f"OK clicked.") # Show final result - print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") + if tree.selectedItem() is not None: + print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") except Exception as e: print(f"Error testing Tree with backend {backend_name}: {e}") import traceback traceback.print_exc() + finally: + ui.application().setApplicationTitle(title) if __name__ == "__main__": if len(sys.argv) > 1: diff --git a/test/test_recursiveselection_tree.py b/test/test_recursiveselection_tree.py index 198a873..67d33d1 100644 --- a/test/test_recursiveselection_tree.py +++ b/test/test_recursiveselection_tree.py @@ -27,6 +27,8 @@ def test_tree(backend_name=None): ui = YUI_ui() factory = ui.widgetFactory() + title = ui.application().applicationTitle() + ui.application().setApplicationTitle("Tree Widget (recursive selection) Application") # Create dialog focused on ComboBox testing dialog = factory.createMainDialog() @@ -39,7 +41,7 @@ def test_tree(backend_name=None): # Test ComboBox with initial selection factory.createLabel(vbox, "") hbox = factory.createHBox(vbox) - tree = factory.createTree(hbox, "Select:", multiselection=False, recursiveselection=True) + tree = factory.createTree(hbox, "Select:", recursiveselection=True) for i in range(5): item = yui.YTreeItem(f"Item {i+1}", is_open=(i==0)) @@ -51,7 +53,7 @@ def test_tree(backend_name=None): tree.addItem(item) - selected = factory.createLabel(vbox, "") + selected = factory.createLabel(vbox, "Selected:") hbox = factory.createHBox(vbox) ok_button = factory.createPushButton(hbox, "OK") cancel_button = factory.createPushButton(hbox, "Cancel") @@ -78,20 +80,25 @@ def test_tree(backend_name=None): elif tree.selectedItem() is not None: selected.setText(f"Selected: '{tree.selectedItem().label()}'") else: - selected.setText("Selected: None") + selected.setText(f"Selected: None") elif reason == yui.YEventReason.Activated: if tree.selectedItem() is not None: selected.setText(f"Activated: '{tree.selectedItem().label()}'") + else: + selected.setText(f"Activated: None") elif wdg == ok_button: selected.setText(f"OK clicked.") # Show final result - print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") + if tree.selectedItem() is not None: + print(f"\nFinal Tree value: '{tree.selectedItem().label()}'") except Exception as e: print(f"Error testing Tree with backend {backend_name}: {e}") import traceback traceback.print_exc() + finally: + ui.application().setApplicationTitle(title) if __name__ == "__main__": if len(sys.argv) > 1: From 8f93505b1b047228ba8684611ad4847891752931 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 4 Dec 2025 18:58:38 +0100 Subject: [PATCH 105/523] Added YFrame --- manatools/aui/yui_qt.py | 210 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 204 insertions(+), 6 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index f3834af..8789406 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -206,26 +206,25 @@ def createProgressBar(self, parent, label, max_value=100): def createComboBox(self, parent, label, editable=False): return YComboBoxQt(parent, label, editable) - - # Alignment helpers + # Alignment helpers def createLeft(self, parent): - return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignBegin, vertAlign=YAlignmentType.YAlignUnchanged) + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignBegin, vertAlign=YAlignmentType.YAlignUnchanged) def createRight(self, parent): return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignEnd, vertAlign=YAlignmentType.YAlignUnchanged) def createTop(self, parent): - return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignBegin) + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignBegin) def createBottom(self, parent): - return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignEnd) + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignEnd) def createHCenter(self, parent): return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignUnchanged) def createVCenter(self, parent): - return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignCenter) + return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignCenter) def createHVCenter(self, parent): return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignCenter, vertAlign=YAlignmentType.YAlignCenter) @@ -237,6 +236,10 @@ def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: Y def createTree(self, parent, label, multiselection=False, recursiveselection = False): """Create a Tree widget.""" return YTreeQt(parent, label, multiselection, recursiveselection) + + def createFrame(self, parent, label: str=""): + """Create a Frame widget.""" + return YFrameQt(parent, label) # Qt Widget Implementations @@ -1617,3 +1620,198 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + +class YFrameQt(YSingleChildContainerWidget): + """ + Qt backend implementation of YFrame. + - Uses QGroupBox to present a labeled framed container. + - Single child is placed inside the group's layout. + - Exposes simple property support for 'label'. + """ + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._backend_widget = None + self._group_layout = None + + def widgetClass(self): + return "YFrame" + + def stretchable(self, dim: YUIDimension): + """Return True if the frame should stretch in given dimension. + The frame is stretchable when its child is stretchable or has a layout weight. + """ + try: + # prefer explicit single child + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return False + try: + if bool(child.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(child.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def label(self): + return self._label + + def setLabel(self, newLabel): + """Set the frame label and update the Qt widget if created.""" + try: + self._label = newLabel + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setTitle(self._label) + except Exception: + pass + except Exception: + pass + + def _attach_child_backend(self): + """Attach existing child backend widget to the groupbox layout.""" + if not (self._backend_widget and self._group_layout and getattr(self, "_child", None)): + return + try: + w = self._child.get_backend_widget() + if w: + # clear any existing widgets in layout (defensive) + try: + while self._group_layout.count(): + it = self._group_layout.takeAt(0) + if it and it.widget(): + it.widget().setParent(None) + except Exception: + pass + self._group_layout.addWidget(w) + except Exception: + pass + + def addChild(self, child): + """Override to attach backend child when available.""" + try: + super().addChild(child) + except Exception: + # best-effort fallback + self._child = child + child._parent = self + # if backend exists, attach new child's backend + if getattr(self, "_backend_widget", None): + try: + self._attach_child_backend() + except Exception: + pass + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + child._parent = self + if getattr(self, "_backend_widget", None): + try: + self._attach_child_backend() + except Exception: + pass + + def _create_backend_widget(self): + """Create the QGroupBox + layout and attach child if present.""" + try: + grp = QtWidgets.QGroupBox(self._label) + layout = QtWidgets.QVBoxLayout(grp) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(4) + self._backend_widget = grp + self._group_layout = layout + + # attach child widget if already set + if getattr(self, "_child", None): + try: + w = self._child.get_backend_widget() + if w: + layout.addWidget(w) + except Exception: + pass + except Exception: + # fallback to a plain QWidget container if QGroupBox creation fails + try: + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(4) + self._backend_widget = container + self._group_layout = layout + if getattr(self, "_child", None): + try: + w = self._child.get_backend_widget() + if w: + layout.addWidget(w) + except Exception: + pass + except Exception: + self._backend_widget = None + self._group_layout = None + + def _set_backend_enabled(self, enabled): + """Enable/disable the frame and propagate state to the child.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate to logical child + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def setProperty(self, propertyName, val): + """Handle simple properties; returns True if handled.""" + try: + if propertyName == "label": + self.setLabel(str(val)) + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "label": + return self.label() + except Exception: + pass + return None + + def propertySet(self): + """Return a minimal property set description (used by some backends).""" + try: + props = YPropertySet() + try: + props.add(YProperty("label", YPropertyType.YStringProperty)) + except Exception: + pass + return props + except Exception: + return None \ No newline at end of file From 1818ad05540d8c800ca4c9bbc66195120bc4fc91 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 4 Dec 2025 19:11:04 +0100 Subject: [PATCH 106/523] Added YFrameGtk --- manatools/aui/yui_gtk.py | 306 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 299d616..7cd048d 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -250,6 +250,10 @@ def createTree(self, parent, label, multiselection=False, recursiveselection = F """Create a Tree widget.""" return YTreeGtk(parent, label, multiselection, recursiveselection) + def createFrame(self, parent, label: str=""): + """Create a Frame widget.""" + return YFrameGtk(parent, label) + # GTK4 Widget Implementations class YDialogGtk(YSingleChildContainerWidget): _open_dialogs = [] @@ -2611,3 +2615,305 @@ def get_backend_widget(self): if self._backend_widget is None: self._create_backend_widget() return self._backend_widget + +class YFrameGtk(YSingleChildContainerWidget): + """ + GTK backend implementation of YFrame. + + - Uses Gtk.Frame (when available) to present a labeled framed container. + - Internally places a Gtk.Box inside the frame to host the single child. + - Honors child's stretchability: the frame reports stretchable when its child is stretchable + so parent layouts can allocate extra space. + - Provides simple property support for 'label'. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._label = label or "" + self._backend_widget = None + self._content_box = None + + def widgetClass(self): + return "YFrame" + + def label(self): + return self._label + + def setLabel(self, new_label: str): + """Set the frame label and update backend if created.""" + try: + self._label = new_label or "" + if getattr(self, "_backend_widget", None) is not None: + try: + # Gtk.Frame in GTK4 supports set_label() in some bindings, else use a child label + if hasattr(self._backend_widget, "set_label"): + self._backend_widget.set_label(self._label) + else: + # fallback: if we created a dedicated label child, update it + if getattr(self, "_label_widget", None) is not None: + try: + self._label_widget.set_text(self._label) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def stretchable(self, dim: YUIDimension): + """ + Report stretchability in a dimension. + + The frame is stretchable when its child is stretchable or has a layout weight. + """ + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return False + try: + if bool(child.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(child.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def _attach_child_backend(self): + """Attach the child's backend widget into the frame's content box.""" + try: + if self._backend_widget is None: + return + if self._content_box is None: + return + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return + try: + cw = child.get_backend_widget() + except Exception: + cw = None + if cw is None: + return + + # Remove existing content children (defensive) + try: + while True: + first = self._content_box.get_first_child() + if first is None: + break + try: + self._content_box.remove(first) + except Exception: + break + except Exception: + pass + + # Append child widget into content box + try: + self._content_box.append(cw) + except Exception: + try: + self._content_box.add(cw) + except Exception: + pass + + # Ensure expansion hints propagate from child + try: + if child.stretchable(YUIDimension.YD_VERT): + if hasattr(cw, "set_vexpand"): + cw.set_vexpand(True) + if hasattr(cw, "set_valign"): + cw.set_valign(Gtk.Align.FILL) + else: + if hasattr(cw, "set_vexpand"): + cw.set_vexpand(False) + if hasattr(cw, "set_valign"): + cw.set_valign(Gtk.Align.START) + if child.stretchable(YUIDimension.YD_HORIZ): + if hasattr(cw, "set_hexpand"): + cw.set_hexpand(True) + if hasattr(cw, "set_halign"): + cw.set_halign(Gtk.Align.FILL) + else: + if hasattr(cw, "set_hexpand"): + cw.set_hexpand(False) + if hasattr(cw, "set_halign"): + cw.set_halign(Gtk.Align.START) + except Exception: + pass + except Exception: + pass + + def addChild(self, child): + """Add logical child and attach backend if possible.""" + try: + super().addChild(child) + except Exception: + # best-effort fallback + try: + self._child = child + child._parent = self + except Exception: + pass + # attach to backend if ready + try: + if getattr(self, "_backend_widget", None) is not None: + self._attach_child_backend() + except Exception: + pass + + def setChild(self, child): + """Set single logical child and attach backend if possible.""" + try: + super().setChild(child) + except Exception: + try: + self._child = child + child._parent = self + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + self._attach_child_backend() + except Exception: + pass + + def _create_backend_widget(self): + """ + Create a Gtk.Frame + inner box to host the single child. + Fall back to a bordered Gtk.Box when Gtk.Frame or set_label is not available. + """ + try: + # Try to create a Gtk.Frame with a label if supported + try: + frame = Gtk.Frame() + # set label if API supports it + if hasattr(frame, "set_label"): + frame.set_label(self._label) + self._label_widget = None + else: + # create a label widget and set as label using set_label_widget if supported + lbl = Gtk.Label(label=self._label) + self._label_widget = lbl + if hasattr(frame, "set_label_widget"): + frame.set_label_widget(lbl) + # Create inner content box + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + content.set_hexpand(True) + content.set_vexpand(True) + # Append content inside frame. In GTK4 a Frame can have a single child. + try: + frame.set_child(content) + except Exception: + try: + # fallback: some bindings use add() + frame.add(content) + except Exception: + pass + self._backend_widget = frame + self._content_box = content + # attach existing child if any + try: + if getattr(self, "_child", None): + self._attach_child_backend() + except Exception: + pass + return + except Exception: + # fallback to a boxed container with a visible border using CSS if Frame creation fails + pass + + # Fallback container: vertical box with a top label and a framed-like border (best-effort) + container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + try: + lbl = Gtk.Label(label=self._label) + lbl.set_xalign(0.0) + container.append(lbl) + # content area + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + content.set_hexpand(True) + content.set_vexpand(True) + container.append(content) + self._label_widget = lbl + self._backend_widget = container + self._content_box = content + if getattr(self, "_child", None): + try: + self._attach_child_backend() + except Exception: + pass + except Exception: + # ultimate fallback: empty widget reference + self._backend_widget = None + self._content_box = None + except Exception: + self._backend_widget = None + self._content_box = None + + def _set_backend_enabled(self, enabled): + """Enable/disable the frame and propagate to child.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate to logical child + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def setProperty(self, propertyName, val): + """Handle simple properties; returns True if property handled here.""" + try: + if propertyName == "label": + try: + self.setLabel(str(val)) + except Exception: + pass + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "label": + return self.label() + except Exception: + pass + return None + + def propertySet(self): + """Return a minimal property set description for introspection.""" + try: + props = YPropertySet() + try: + props.add(YProperty("label", YPropertyType.YStringProperty)) + except Exception: + pass + return props + except Exception: + return None From 1aecf73fef15c995f577e1a798660f17fa6f2a88 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 4 Dec 2025 19:24:35 +0100 Subject: [PATCH 107/523] First attempt to add YFrameCurses --- manatools/aui/yui_curses.py | 187 ++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 503aa0f..39ad6d5 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -196,6 +196,10 @@ def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: Y def createTree(self, parent, label, multiselection=False, recursiveselection = False): """Create a Tree widget.""" return YTreeCurses(parent, label, multiselection, recursiveselection) + + def createFrame(self, parent, label: str=""): + """Create a Frame widget.""" + return YFrameCurses(parent, label) # Curses Widget Implementations class YDialogCurses(YSingleChildContainerWidget): @@ -2339,3 +2343,186 @@ def selectItem(self, item, selected=True): self._last_selected_ids = set() except Exception: pass + +class YFrameCurses(YSingleChildContainerWidget): + """ + NCurses implementation of YFrame. + - Draws a framed box with a title. + - Hosts a single child inside the frame with inner margins so the child's + own label does not overlap the frame title. + - Reports stretchability based on its child. + """ + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label or "" + self._backend_widget = None + # preferred minimal height: include one extra row for frame border + self._height = max(1, getattr(self, "_height", 1)) + # inner top padding to separate frame title from child's label + self._inner_top_padding = 1 + + def widgetClass(self): + return "YFrame" + + def label(self): + return self._label + + def setLabel(self, new_label): + try: + self._label = str(new_label) + except Exception: + self._label = new_label + + def stretchable(self, dim): + """Frame is stretchable if its child is stretchable or has a weight.""" + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return False + try: + if bool(child.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(child.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def _create_backend_widget(self): + # curses backend does not create a separate widget object for frames; + # drawing is performed in _draw by the parent container. + self._backend_widget = None + + def _set_backend_enabled(self, enabled): + """Propagate enabled state to the child.""" + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None and hasattr(child, "setEnabled"): + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + # ensure traversal lists contain the child + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + if child not in self._children: + self._children.append(child) + try: + child._parent = self + except Exception: + pass + except Exception: + pass + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + try: + self._children = [child] + try: + child._parent = self + except Exception: + pass + except Exception: + pass + + def _draw(self, window, y, x, width, height): + """Draw frame border and title, then draw child inside inner area with margins.""" + try: + if width <= 0 or height <= 0: + return + + # Choose box characters (prefer ACS if available) + try: + hline = curses.ACS_HLINE + vline = curses.ACS_VLINE + tl = curses.ACS_ULCORNER + tr = curses.ACS_URCORNER + bl = curses.ACS_LLCORNER + br = curses.ACS_LRCORNER + except Exception: + hline = ord('-') + vline = ord('|') + tl = ord('+') + tr = ord('+') + bl = ord('+') + br = ord('+') + + # Draw corners and edges + try: + window.addch(y, x, tl) + window.addch(y, x + width - 1, tr) + window.addch(y + height - 1, x, bl) + window.addch(y + height - 1, x + width - 1, br) + for cx in range(x + 1, x + width - 1): + window.addch(y, cx, hline) + window.addch(y + height - 1, cx, hline) + for cy in range(y + 1, y + height - 1): + window.addch(cy, x, vline) + window.addch(cy, x + width - 1, vline) + except curses.error: + # best-effort: ignore drawing errors when area is too small + pass + + # Draw title centered on top border (leave at least one space from corners) + if self._label: + try: + title = f" {self._label} " + max_title_len = max(0, width - 4) + if len(title) > max_title_len: + title = title[:max(0, max_title_len - 3)] + "..." + start_x = x + max(1, (width - len(title)) // 2) + # overwrite part of top border with title text + window.addstr(y, start_x, title, curses.A_BOLD) + except curses.error: + pass + + # Compute inner content rectangle (leave 1-char border), apply top padding + inner_x = x + 1 + inner_y = y + 1 + inner_w = max(0, width - 2) + inner_h = max(0, height - 2) + + # Apply inner top padding so child's label doesn't touch the frame title + pad_top = min(self._inner_top_padding, max(0, inner_h)) + content_y = inner_y + pad_top + content_h = max(0, inner_h - pad_top) + + # If there's no child or no space, nothing else to draw + child = getattr(self, "_child", None) + if child is None: + return + if content_w := inner_w: + # Ensure we don't pass negative sizes + try: + if content_h <= 0 or content_w <= 0: + return + # Delegate drawing to child using the inner content area + if hasattr(child, "_draw"): + child._draw(window, content_y, inner_x, content_w, content_h) + except Exception: + pass + except Exception: + pass From 52632d050fe418abee108b54091b2ebc36057f55 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 4 Dec 2025 19:39:57 +0100 Subject: [PATCH 108/523] Improving layout with more frames --- manatools/aui/yui_curses.py | 81 ++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 39ad6d5..6084d85 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -695,7 +695,8 @@ def _draw(self, window, y, x, width, height): class YHBoxCurses(YWidget): def __init__(self, parent=None): super().__init__(parent) - self._height = 1 # HBox always takes one line + # Minimum height will be computed from children + self._height = 1 def widgetClass(self): return "YHBox" @@ -703,6 +704,45 @@ def widgetClass(self): def _create_backend_widget(self): self._backend_widget = None + def _recompute_min_height(self): + """Compute minimal height for this horizontal box as the tallest child's minimum.""" + try: + if not self._children: + self._height = 1 + return + child_mins = [] + for c in self._children: + child_mins.append(max(1, getattr(c, "_height", 1))) + self._height = max(1, max(child_mins)) + except Exception: + self._height = 1 + + def addChild(self, child): + """Ensure internal children list and recompute minimal height.""" + try: + super().addChild(child) + except Exception: + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + self._children.append(child) + child._parent = self + except Exception: + pass + self._recompute_min_height() + + def setChild(self, child): + """Not typical for HBox, but keep parity with containers.""" + try: + super().setChild(child) + except Exception: + try: + self._children = [child] + child._parent = self + except Exception: + pass + self._recompute_min_height() + def _set_backend_enabled(self, enabled): """Enable/disable HBox and propagate to logical children.""" try: @@ -749,6 +789,8 @@ def _child_min_width(self, child, max_width): return max(1, min(10, max_width)) # safe default def _draw(self, window, y, x, width, height): + # Ensure minimal height reflects children so the parent allocated enough rows + self._recompute_min_height() num_children = len(self._children) if num_children == 0 or width <= 0 or height <= 0: return @@ -792,7 +834,7 @@ def _draw(self, window, y, x, width, height): if child.stretchable(YUIDimension.YD_VERT): ch = height else: - ch = min(height, getattr(child, "_height", height)) + ch = min(height, max(1, getattr(child, "_height", 1))) if hasattr(child, "_draw"): child._draw(window, y, cx, w, ch) cx += w @@ -2356,14 +2398,27 @@ def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label or "" self._backend_widget = None - # preferred minimal height: include one extra row for frame border - self._height = max(1, getattr(self, "_height", 1)) + # Preferred minimal height is computed from child (see _update_min_height) + self._height = 3 # inner top padding to separate frame title from child's label self._inner_top_padding = 1 def widgetClass(self): return "YFrame" + def _update_min_height(self): + """Recompute minimal height: at least 3 rows or child_min + borders + padding.""" + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + child_min = max(1, getattr(child, "_height", 1)) if child is not None else 1 + # 2 borders + inner top padding + at least 1 inner row + self._height = max(3, 2 + self._inner_top_padding + child_min) + except Exception: + self._height = max(self._height, 3) + def label(self): return self._label @@ -2400,6 +2455,8 @@ def _create_backend_widget(self): # curses backend does not create a separate widget object for frames; # drawing is performed in _draw by the parent container. self._backend_widget = None + # Update minimal height based on the child + self._update_min_height() def _set_backend_enabled(self, enabled): """Propagate enabled state to the child.""" @@ -2433,6 +2490,8 @@ def addChild(self, child): pass except Exception: pass + # Update minimal height based on the child + self._update_min_height() def setChild(self, child): try: @@ -2447,13 +2506,25 @@ def setChild(self, child): pass except Exception: pass + # Update minimal height based on the child + self._update_min_height() def _draw(self, window, y, x, width, height): """Draw frame border and title, then draw child inside inner area with margins.""" try: if width <= 0 or height <= 0: return - + # If height is too small to render a bordered frame, fallback gracefully + if height < 3 or width < 4: + # Draw a truncated title line if possible and return + try: + if self._label and height >= 1 and width > 2: + title = f" {self._label} " + title = title[:max(0, width - 2)] + window.addstr(y, x, title, curses.A_BOLD) + except curses.error: + pass + return # Choose box characters (prefer ACS if available) try: hline = curses.ACS_HLINE From 424f873d6343a9840b67a0c7931cb487747643af Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 4 Dec 2025 19:51:21 +0100 Subject: [PATCH 109/523] Fixed YFrame Layout --- manatools/aui/yui_curses.py | 86 ++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 6084d85..71b504f 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -201,6 +201,42 @@ def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameCurses(parent, label) + +def _curses_recursive_min_height(widget): + """Compute minimal height for a widget, recursively considering container children.""" + if widget is None: + return 1 + try: + cls = widget.widgetClass() if hasattr(widget, "widgetClass") else "" + except Exception: + cls = "" + try: + if cls == "YVBox": + chs = list(getattr(widget, "_children", []) or []) + spacing = max(0, len(chs) - 1) + total = 0 + for c in chs: + total += _curses_recursive_min_height(c) + return max(1, total + spacing) + elif cls == "YHBox": + chs = list(getattr(widget, "_children", []) or []) + tallest = 1 + for c in chs: + tallest = max(tallest, _curses_recursive_min_height(c)) + return max(1, tallest) + elif cls == "YAlignment": + child = getattr(widget, "_child", None) + return max(1, _curses_recursive_min_height(child)) + elif cls == "YFrame": + child = getattr(widget, "_child", None) + inner_top = max(0, getattr(widget, "_inner_top_padding", 1)) + inner_min = _curses_recursive_min_height(child) + return max(3, 2 + inner_top + inner_min) # borders(2) + padding + inner + else: + return max(1, getattr(widget, "_height", 1)) + except Exception: + return max(1, getattr(widget, "_height", 1)) + # Curses Widget Implementations class YDialogCurses(YSingleChildContainerWidget): _open_dialogs = [] @@ -619,14 +655,13 @@ def _draw(self, window, y, x, width, height): fixed_height_total = 0 for i, child in enumerate(self._children): - # child._height is the preferred minimum (may include its own label rows) - child_min = max(1, getattr(child, "_height", 1)) + # Use recursive min height for containers and frames + child_min = max(1, _curses_recursive_min_height(child)) child_min_heights.append(child_min) is_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) if is_stretch: stretchable_indices.append(i) - # default vertical weight = 1 try: w = child.weight(YUIDimension.YD_VERT) w = int(w) if w is not None else 1 @@ -710,10 +745,7 @@ def _recompute_min_height(self): if not self._children: self._height = 1 return - child_mins = [] - for c in self._children: - child_mins.append(max(1, getattr(c, "_height", 1))) - self._height = max(1, max(child_mins)) + self._height = max(1, max(_curses_recursive_min_height(c) for c in self._children)) except Exception: self._height = 1 @@ -2407,15 +2439,11 @@ def widgetClass(self): return "YFrame" def _update_min_height(self): - """Recompute minimal height: at least 3 rows or child_min + borders + padding.""" + """Recompute minimal height: at least 3 rows or child layout min + borders + padding.""" try: child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - child_min = max(1, getattr(child, "_height", 1)) if child is not None else 1 - # 2 borders + inner top padding + at least 1 inner row - self._height = max(3, 2 + self._inner_top_padding + child_min) + inner_min = _curses_recursive_min_height(child) if child is not None else 1 + self._height = max(3, 2 + self._inner_top_padding + inner_min) except Exception: self._height = max(self._height, 3) @@ -2514,9 +2542,10 @@ def _draw(self, window, y, x, width, height): try: if width <= 0 or height <= 0: return - # If height is too small to render a bordered frame, fallback gracefully + # Ensure minimal height based on child layout before drawing + self._update_min_height() + # Graceful fallback for very small areas if height < 3 or width < 4: - # Draw a truncated title line if possible and return try: if self._label and height >= 1 and width > 2: title = f" {self._label} " @@ -2570,30 +2599,29 @@ def _draw(self, window, y, x, width, height): except curses.error: pass - # Compute inner content rectangle (leave 1-char border), apply top padding + # Compute inner content rectangle inner_x = x + 1 inner_y = y + 1 inner_w = max(0, width - 2) inner_h = max(0, height - 2) - # Apply inner top padding so child's label doesn't touch the frame title pad_top = min(self._inner_top_padding, max(0, inner_h)) content_y = inner_y + pad_top content_h = max(0, inner_h - pad_top) - # If there's no child or no space, nothing else to draw child = getattr(self, "_child", None) if child is None: return - if content_w := inner_w: - # Ensure we don't pass negative sizes - try: - if content_h <= 0 or content_w <= 0: - return - # Delegate drawing to child using the inner content area - if hasattr(child, "_draw"): - child._draw(window, content_y, inner_x, content_w, content_h) - except Exception: - pass + + # Clamp content height to at least the child layout minimal height + needed = _curses_recursive_min_height(child) + # Do not exceed available area; this only influences the draw area passed down + content_h = min(max(content_h, needed), inner_h) + + if content_h <= 0 or inner_w <= 0: + return + if hasattr(child, "_draw"): + child._draw(window, content_y, inner_x, inner_w, content_h) except Exception: pass + From 18fd49acead69a92a013014952edcaff0932e2e3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 4 Dec 2025 19:53:19 +0100 Subject: [PATCH 110/523] updated --- sow/TODO.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 9acb55a..26bab0c 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -11,7 +11,8 @@ Missing Widgets comparing libyui: [X] YComboBox [X] YSelectionBox [X] YMultiSelectionBox - [ ] YTree + [X] YTree + [X] YFrame [ ] YTable [ ] YProgressBar [ ] YRichText @@ -24,7 +25,7 @@ Missing Widgets comparing libyui: [ ] YReplacePoint [ ] YRadioButton, YRadioButtonGroup -To check how to manage YEvents [X] and YItems [ ]. +To check how to manage YEvents [X] and YItems [ ] (verify selection attirbute). Nice to have: improvements outside YUI API [ ] window title From 48993dde57a6b536cf2078ec9236a1b4b991df30 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 4 Dec 2025 19:53:42 +0100 Subject: [PATCH 111/523] Added test frame --- test/test_frame.py | 132 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/test_frame.py diff --git a/test/test_frame.py b/test/test_frame.py new file mode 100644 index 0000000..ecbee78 --- /dev/null +++ b/test/test_frame.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_selectionbox(backend_name=None): + """Test Frame widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + +############### + ui.application().setApplicationTitle("Test Frame") + dialog = factory.createPopupDialog() + mainVbox = factory.createVBox( dialog ) + hbox = factory.createHBox( mainVbox ) + frame = factory.createFrame( hbox , "Pasta Menu") + selBox = factory.createSelectionBox( frame, "Choose your pasta" ) + + selBox.addItem( "Spaghetti Carbonara" ) + selBox.addItem( "Penne Arrabbiata" ) + selBox.addItem( "Fettuccine" ) + selBox.addItem( "Lasagna" ) + selBox.addItem( "Ravioli" ) + selBox.addItem( "Trofie al pesto" ) # Ligurian specialty + + frame1 = factory.createFrame( hbox , "SelectionBox Options") + vbox = factory.createVBox( frame1 ) + align = factory.createTop(vbox) + notifyCheckBox = factory.createCheckBox( align, "Notify on change", selBox.notify() ) + notifyCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + multiSelectionCheckBox = factory.createCheckBox( vbox, "Multi-selection", selBox.multiSelection() ) + multiSelectionCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + align = factory.createBottom( vbox ) + disableSelectionBox = factory.createCheckBox( align, "disable selection box", not selBox.isEnabled() ) + disableSelectionBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + disableValue = factory.createCheckBox( vbox, "disable value button", False ) + disableValue.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + + hbox = factory.createHBox( mainVbox ) + valueButton = factory.createPushButton( hbox, "Value" ) + disableValue.setValue(not valueButton.isEnabled()) + label = factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) + label.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + valueField = factory.createLabel(hbox, "") + valueField.setStretchable( yui.YUIDimension.YD_HORIZ, True ) # // allow stretching over entire dialog width + + #factory.createVSpacing( vbox, 0.3 ) + + hbox = factory.createHBox( mainVbox ) + #factory.createLabel(hbox, " ") # spacer + leftAlignment = factory.createLeft( hbox ) + left = factory.createPushButton( leftAlignment, "Left" ) + rightAlignment = factory.createRight( hbox ) + closeButton = factory.createPushButton( rightAlignment, "Close" ) + + # + # Event loop + # + #valueField.setText( "???" ) + while True: + event = dialog.waitForEvent() + if not event: + print("Empty") + next + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == closeButton: + dialog.destroy() + break + elif wdg == left: + valueField.setText(left.label()) + elif (wdg == valueButton): + if selBox.multiSelection(): + labels = [item.label() for item in selBox.selectedItems()] + valueField.setText( ", ".join(labels) ) + else: + item = selBox.selectedItem() + valueField.setText( item.label() if item else "" ) + ui.application().setApplicationTitle("Test App") + elif (wdg == notifyCheckBox): + selBox.setNotify( notifyCheckBox.value() ) + elif (wdg == multiSelectionCheckBox): + selBox.setMultiSelection( multiSelectionCheckBox.value() ) + elif (wdg == disableSelectionBox): + selBox.setEnabled( not disableSelectionBox.value() ) + elif (wdg == disableValue): + valueButton.setEnabled( not disableValue.value() ) + elif (wdg == selBox): # selBox will only send events with setNotify() TODO + if selBox.multiSelection(): + labels = [item.label() for item in selBox.selectedItems()] + valueField.setText( ", ".join(labels) ) + else: + valueField.setText(selBox.value()) + + except Exception as e: + print(f"Error testing ComboBox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_selectionbox(sys.argv[1]) + else: + test_selectionbox() + + + + From 3e1bf50d550fd3881e39f7af5e99450981b0613d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 7 Dec 2025 18:10:30 +0100 Subject: [PATCH 112/523] updated --- test/test_aligment.py | 6 ++++++ test/test_selectionbox.py | 21 ++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/test/test_aligment.py b/test/test_aligment.py index 6de2197..d4450eb 100644 --- a/test/test_aligment.py +++ b/test/test_aligment.py @@ -15,6 +15,7 @@ def test_Alignment(backend_name=None): try: from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui # Force re-detection YUI._instance = None @@ -35,6 +36,11 @@ def test_Alignment(backend_name=None): rightAlignment = factory.createRight( hbox ) factory.createPushButton( rightAlignment, "Right" ) + hbox = factory.createHBox( vbox ) + rightAlignment = factory.createRight( hbox ) + btn = factory.createPushButton( rightAlignment, ">Right<" ) + btn.setStretchable( yui.YUIDimension.YD_HORIZ, True ) + factory.createLabel(vbox, "Testing aligment Top and Bottom into HBox") hbox = factory.createHBox( vbox ) topAlignment = factory.createTop( hbox ) diff --git a/test/test_selectionbox.py b/test/test_selectionbox.py index ccaf345..7fa9ffa 100644 --- a/test/test_selectionbox.py +++ b/test/test_selectionbox.py @@ -7,7 +7,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) def test_selectionbox(backend_name=None): - """Test ComboBox widget specifically""" + """Test Selection Box widget specifically""" if backend_name: print(f"Setting backend to: {backend_name}") os.environ['YUI_BACKEND'] = backend_name @@ -27,21 +27,20 @@ def test_selectionbox(backend_name=None): ui = YUI_ui() factory = ui.widgetFactory() - ui.application().setIconBasePath("/home/angelo/src/manatools/dnfdragora/share/images/") + #ui.application().setIconBasePath("PATH_TO_TEST") ############### - ui.application().setApplicationIcon("dnfdragora.png") + ui.application().setApplicationIcon("dnfdragora") dialog = factory.createPopupDialog() mainVbox = factory.createVBox( dialog ) hbox = factory.createHBox( mainVbox ) - selBox = factory.createSelectionBox( hbox, "Choose your pizza" ) - - selBox.addItem( "Pizza Margherita" ) - selBox.addItem( "Pizza Capricciosa" ) - selBox.addItem( "Pizza Funghi" ) - selBox.addItem( "Pizza Prosciutto" ) - selBox.addItem( "Pizza Quattro Stagioni" ) - selBox.addItem( "Calzone" ) + selBox = factory.createSelectionBox( hbox, "Choose your pasta" ) + selBox.addItem( "Spaghetti Carbonara" ) + selBox.addItem( "Penne Arrabbiata" ) + selBox.addItem( "Fettuccine" ) + selBox.addItem( "Lasagna" ) + selBox.addItem( "Ravioli" ) + selBox.addItem( "Trofie al pesto" ) # Ligurian specialty vbox = factory.createVBox( hbox ) align = factory.createTop(vbox) From 0717c84bb102a84dfbe739712c187c9cd5858e12 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 7 Dec 2025 21:27:53 +0100 Subject: [PATCH 113/523] moved to single file for any Qt widget class definition --- manatools/aui/backends/qt/__init__.py | 29 + manatools/aui/backends/qt/alignmentqt.py | 213 +++ manatools/aui/backends/qt/checkboxqt.py | 70 + manatools/aui/backends/qt/comboboxqt.py | 108 ++ manatools/aui/backends/qt/dialogqt.py | 252 +++ manatools/aui/backends/qt/frameqt.py | 201 +++ manatools/aui/backends/qt/hboxqt.py | 80 + manatools/aui/backends/qt/inputfieldqt.py | 78 + manatools/aui/backends/qt/labelqt.py | 50 + manatools/aui/backends/qt/pushbuttonqt.py | 75 + manatools/aui/backends/qt/selectionboxqt.py | 153 ++ manatools/aui/backends/qt/treeqt.py | 360 +++++ manatools/aui/backends/qt/vboxqt.py | 83 + manatools/aui/yui_qt.py | 1604 +------------------ setup.py | 58 +- 15 files changed, 1817 insertions(+), 1597 deletions(-) create mode 100644 manatools/aui/backends/qt/__init__.py create mode 100644 manatools/aui/backends/qt/alignmentqt.py create mode 100644 manatools/aui/backends/qt/checkboxqt.py create mode 100644 manatools/aui/backends/qt/comboboxqt.py create mode 100644 manatools/aui/backends/qt/dialogqt.py create mode 100644 manatools/aui/backends/qt/frameqt.py create mode 100644 manatools/aui/backends/qt/hboxqt.py create mode 100644 manatools/aui/backends/qt/inputfieldqt.py create mode 100644 manatools/aui/backends/qt/labelqt.py create mode 100644 manatools/aui/backends/qt/pushbuttonqt.py create mode 100644 manatools/aui/backends/qt/selectionboxqt.py create mode 100644 manatools/aui/backends/qt/treeqt.py create mode 100644 manatools/aui/backends/qt/vboxqt.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py new file mode 100644 index 0000000..1e35ce0 --- /dev/null +++ b/manatools/aui/backends/qt/__init__.py @@ -0,0 +1,29 @@ +from .dialogqt import YDialogQt +from .checkboxqt import YCheckBoxQt +from .hboxqt import YHBoxQt +from .vboxqt import YVBoxQt +from .labelqt import YLabelQt +from .pushbuttonqt import YPushButtonQt +from .treeqt import YTreeQt +from .alignmentqt import YAlignmentQt +from .comboboxqt import YComboBoxQt +from .frameqt import YFrameQt +from .inputfieldqt import YInputFieldQt +from .selectionboxqt import YSelectionBoxQt + + +__all__ = [ + "YDialogQt", + "YFrameQt", + "YVBoxQt", + "YHBoxQt", + "YTreeQt", + "YSelectionBoxQt", + "YLabelQt", + "YPushButtonQt", + "YInputFieldQt", + "YCheckBoxQt", + "YComboBoxQt", + "YAlignmentQt", + # ... +] diff --git a/manatools/aui/backends/qt/alignmentqt.py b/manatools/aui/backends/qt/alignmentqt.py new file mode 100644 index 0000000..3a25aef --- /dev/null +++ b/manatools/aui/backends/qt/alignmentqt.py @@ -0,0 +1,213 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore +from ...yui_common import * + +class YAlignmentQt(YSingleChildContainerWidget): + """ + Single-child alignment container for Qt6. Uses a QWidget + QGridLayout, + applying Qt.Alignment flags to the child. The container expands along + axes needed by Right/HCenter/VCenter/HVCenter to allow alignment. + """ + def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): + super().__init__(parent) + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._backend_widget = None + self._layout = None + + def widgetClass(self): + return "YAlignment" + + def _to_qt_halign(self): + """Convert Horizontal YAlignmentType to QtCore.Qt.AlignmentFlag or None.""" + if self._halign_spec: + if self._halign_spec == YAlignmentType.YAlignBegin: + return QtCore.Qt.AlignmentFlag.AlignLeft + if self._halign_spec == YAlignmentType.YAlignCenter: + return QtCore.Qt.AlignmentFlag.AlignHCenter + if self._halign_spec == YAlignmentType.YAlignEnd: + return QtCore.Qt.AlignmentFlag.AlignRight + return None + + def _to_qt_valign(self): + """Convert Vertical YAlignmentType to QtCore.Qt.AlignmentFlag or None.""" + if self._valign_spec: + if self._valign_spec == YAlignmentType.YAlignBegin: + return QtCore.Qt.AlignmentFlag.AlignTop + if self._valign_spec == YAlignmentType.YAlignCenter: + return QtCore.Qt.AlignmentFlag.AlignVCenter + if self._valign_spec == YAlignmentType.YAlignEnd: + return QtCore.Qt.AlignmentFlag.AlignBottom + return None + + + def stretchable(self, dim: YUIDimension): + ''' Returns the stretchability of the layout box: + * The layout box is stretchable if the alignment spec requests expansion + * (Right/HCenter/HVCenter for horizontal, VCenter/HVCenter for vertical) + * OR if the child itself requests stretchability or has a layout weight. + ''' + # Expand if alignment spec requests it + try: + if dim == YUIDimension.YD_HORIZ: + if self._halign_spec in (YAlignmentType.YAlignEnd, YAlignmentType.YAlignCenter): + return True + if dim == YUIDimension.YD_VERT: + if self._valign_spec in (YAlignmentType.YAlignCenter,): + return True + except Exception: + pass + + # Otherwise honor child's own stretchability/weight + try: + if self._child: + expand = bool(self._child.stretchable(dim)) + weight = bool(self._child.weight(dim)) + if expand or weight: + return True + except Exception: + pass + return False + + def setAlignment(self, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._reapply_alignment() + + def _reapply_alignment(self): + if not (self._layout and self._child): + return + try: + w = self._child.get_backend_widget() + if w: + self._layout.removeWidget(w) + flags = QtCore.Qt.AlignmentFlag(0) + ha = self._to_qt_halign() + va = self._to_qt_valign() + if ha: + flags |= ha + if va: + flags |= va + self._layout.addWidget(w, 0, 0, flags) + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + if self._backend_widget: + self._attach_child_backend() + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + if self._backend_widget: + self._attach_child_backend() + + def _attach_child_backend(self): + if not (self._backend_widget and self._layout and self._child): + return + try: + w = self._child.get_backend_widget() + if w: + # clear previous + try: + self._layout.removeWidget(w) + except Exception: + pass + flags = QtCore.Qt.AlignmentFlag(0) + ha = self._to_qt_halign() + va = self._to_qt_valign() + if ha: + flags |= ha + if va: + flags |= va + # If the child requests horizontal stretch, set its QSizePolicy to Expanding + try: + if self._child and self._child.stretchable(YUIDimension.YD_HORIZ): + sp = w.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + w.setSizePolicy(sp) + # If child requests vertical stretch, set vertical policy + if self._child and self._child.stretchable(YUIDimension.YD_VERT): + sp = w.sizePolicy() + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + w.setSizePolicy(sp) + except Exception: + pass + self._layout.addWidget(w, 0, 0, flags) + except Exception: + pass + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + grid = QtWidgets.QGridLayout(container) + grid.setContentsMargins(0, 0, 0, 0) + grid.setSpacing(0) + + # Size policy: expand along axes needed for alignment to work + sp = container.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) + else QtWidgets.QSizePolicy.Policy.Fixed) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) + else QtWidgets.QSizePolicy.Policy.Fixed) + except Exception: + pass + container.setSizePolicy(sp) + + self._backend_widget = container + self._layout = grid + + if getattr(self, "_child", None): + self._attach_child_backend() + + def _set_backend_enabled(self, enabled): + """Enable/disable the alignment container and propagate to its logical child.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate to logical child + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/qt/checkboxqt.py b/manatools/aui/backends/qt/checkboxqt.py new file mode 100644 index 0000000..6a0dd85 --- /dev/null +++ b/manatools/aui/backends/qt/checkboxqt.py @@ -0,0 +1,70 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore +from ...yui_common import * + +class YCheckBoxQt(YWidget): + def __init__(self, parent=None, label="", is_checked=False): + super().__init__(parent) + self._label = label + self._is_checked = is_checked + + def widgetClass(self): + return "YCheckBox" + + def value(self): + return self._is_checked + + def setValue(self, checked): + self._is_checked = checked + if self._backend_widget: + try: + # avoid emitting signals while programmatically changing state + self._backend_widget.blockSignals(True) + self._backend_widget.setChecked(checked) + finally: + try: + self._backend_widget.blockSignals(False) + except Exception: + pass + + def label(self): + return self._label + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QCheckBox(self._label) + self._backend_widget.setChecked(self._is_checked) + self._backend_widget.stateChanged.connect(self._on_state_changed) + + def _set_backend_enabled(self, enabled): + """Enable/disable the QCheckBox backend.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_state_changed(self, state): + # Update internal state + # state is QtCore.Qt.CheckState (Unchecked=0, PartiallyChecked=1, Checked=2) + self._is_checked = (QtCore.Qt.CheckState(state) == QtCore.Qt.CheckState.Checked) + + if self.notify(): + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"CheckBox state changed (no dialog found): {self._label} = {self._is_checked}") diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py new file mode 100644 index 0000000..3cdd8e6 --- /dev/null +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -0,0 +1,108 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YComboBoxQt(YSelectionWidget): + def __init__(self, parent=None, label="", editable=False): + super().__init__(parent) + self._label = label + self._editable = editable + self._value = "" + self._selected_items = [] + + def widgetClass(self): + return "YComboBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_combo_widget') and self._combo_widget: + index = self._combo_widget.findText(text) + if index >= 0: + self._combo_widget.setCurrentIndex(index) + elif self._editable: + self._combo_widget.setEditText(text) + # update selected_items to keep internal state consistent + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break + + def editable(self): + return self._editable + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + + if self._label: + label = QtWidgets.QLabel(self._label) + layout.addWidget(label) + + if self._editable: + combo = QtWidgets.QComboBox() + combo.setEditable(True) + else: + combo = QtWidgets.QComboBox() + + # Add items to combo box + for item in self._items: + combo.addItem(item.label()) + + combo.currentTextChanged.connect(self._on_text_changed) + # also handle index change (safer for some input methods) + combo.currentIndexChanged.connect(lambda idx: self._on_text_changed(combo.currentText())) + layout.addWidget(combo) + + self._backend_widget = container + self._combo_widget = combo + + def _set_backend_enabled(self, enabled): + """Enable/disable the combobox and its container.""" + try: + if getattr(self, "_combo_widget", None) is not None: + try: + self._combo_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_text_changed(self, text): + self._value = text + # Update selected items + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break + if self.notify(): + # Post selection-changed event to containing dialog + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py new file mode 100644 index 0000000..0bc9803 --- /dev/null +++ b/manatools/aui/backends/qt/dialogqt.py @@ -0,0 +1,252 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' + +from PySide6 import QtWidgets, QtCore, QtGui +from ...yui_common import YSingleChildContainerWidget, YUIDimension, YPropertySet, YProperty, YPropertyType, YUINoDialogException, YDialogType, YDialogColorMode, YEvent, YCancelEvent, YTimeoutEvent + +class YDialogQt(YSingleChildContainerWidget): + _open_dialogs = [] + + def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): + super().__init__() + self._dialog_type = dialog_type + self._color_mode = color_mode + self._is_open = False + self._qwidget = None + self._event_result = None + self._qt_event_loop = None + YDialogQt._open_dialogs.append(self) + + def widgetClass(self): + return "YDialog" + + @staticmethod + def currentDialog(doThrow=True): + '''Return the currently open dialog (topmost), or raise if none.''' + open_dialog = YDialogQt._open_dialogs[-1] if YDialogQt._open_dialogs else None + if not open_dialog and doThrow: + raise YUINoDialogException("No dialog is currently open") + return open_dialog + + @staticmethod + def topmostDialog(doThrow=True): + ''' same as currentDialog ''' + return YDialogQt.currentDialog(doThrow=doThrow) + + def isTopmostDialog(self): + '''Return whether this dialog is the topmost open dialog.''' + return YDialogQt._open_dialogs[-1] == self if YDialogQt._open_dialogs else False + + def open(self): + """ + Finalize and show the dialog in a non-blocking way. + + Matches libyui semantics: open() should only finalize and make visible. + If the application expects blocking behavior it should call waitForEvent() + which will start a nested event loop as required. + """ + if not self._is_open: + if not self._qwidget: + self._create_backend_widget() + + self._qwidget.show() + self._is_open = True + + def isOpen(self): + return self._is_open + + def destroy(self, doThrow=True): + if self._qwidget: + self._qwidget.close() + self._qwidget = None + self._is_open = False + if self in YDialogQt._open_dialogs: + YDialogQt._open_dialogs.remove(self) + return True + + @classmethod + def deleteTopmostDialog(cls, doThrow=True): + if cls._open_dialogs: + dialog = cls._open_dialogs[-1] + return dialog.destroy(doThrow) + return False + + @classmethod + def currentDialog(cls, doThrow=True): + if not cls._open_dialogs: + if doThrow: + raise YUINoDialogException("No dialog open") + return None + return cls._open_dialogs[-1] + + def _create_backend_widget(self): + self._qwidget = QtWidgets.QMainWindow() + # Determine window title:from YApplicationQt instance stored on the YUI backend + title = "Manatools YUI Qt Dialog" + + try: + from . import yui as yui_mod + appobj = None + # YUI._backend may hold the backend instance (YUIQt) + backend = getattr(yui_mod.YUI, "_backend", None) + if backend: + if hasattr(backend, "application"): + appobj = backend.application() + # fallback: YUI._instance might be set and expose application/yApp + if not appobj: + inst = getattr(yui_mod.YUI, "_instance", None) + if inst: + if hasattr(inst, "application"): + appobj = inst.application() + if appobj and hasattr(appobj, "applicationTitle"): + atitle = appobj.applicationTitle() + if atitle: + title = atitle + # try to obtain a resolved QIcon from the application backend if available + app_qicon = None + if appobj: + # prefer cached Qt icon if set by setApplicationIcon + app_qicon = getattr(appobj, "_qt_icon", None) + # otherwise try to resolve applicationIcon string on the fly + if not app_qicon: + try: + icon_spec = appobj.applicationIcon() + if icon_spec: + # use the application's iconBasePath if present + base = getattr(appobj, "_icon_base_path", None) + if base and not os.path.isabs(icon_spec): + p = os.path.join(base, icon_spec) + if os.path.exists(p): + app_qicon = QtGui.QIcon(p) + if not app_qicon: + q = QtGui.QIcon.fromTheme(icon_spec) + if not q.isNull(): + app_qicon = q + except Exception: + pass + # if we have a qicon, set it on the QApplication and the new window + if app_qicon: + try: + qapp = QtWidgets.QApplication.instance() + if qapp: + qapp.setWindowIcon(app_qicon) + except Exception: + pass + # store resolved qicon locally to apply to this window + _resolved_qicon = app_qicon + except Exception: + # ignore and keep default + _resolved_qicon = None + + self._qwidget.setWindowTitle(title) + try: + if _resolved_qicon: + self._qwidget.setWindowIcon(_resolved_qicon) + except Exception: + pass + self._qwidget.resize(600, 400) + + central_widget = QtWidgets.QWidget() + self._qwidget.setCentralWidget(central_widget) + + if self._child: + layout = QtWidgets.QVBoxLayout(central_widget) + layout.addWidget(self._child.get_backend_widget()) + + self._backend_widget = self._qwidget + self._qwidget.closeEvent = self._on_close_event + + def _set_backend_enabled(self, enabled): + """Enable/disable the dialog window and propagate to logical child widgets.""" + try: + if getattr(self, "_qwidget", None) is not None: + try: + self._qwidget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate logical enabled state to contained YWidget(s) + try: + if getattr(self, "_child", None): + try: + self._child.setEnabled(enabled) + except Exception: + pass + else: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def _on_close_event(self, event): + # Post a cancel event so waitForEvent returns a YCancelEvent when the user + # closes the window with the window manager 'X' button. + try: + self._post_event(YCancelEvent()) + except Exception: + pass + # Ensure dialog is destroyed and accept the close + self.destroy() + event.accept() + + def _post_event(self, event): + """Internal: post an event to this dialog and quit local event loop if running.""" + self._event_result = event + if self._qt_event_loop is not None and self._qt_event_loop.isRunning(): + self._qt_event_loop.quit() + + def waitForEvent(self, timeout_millisec=0): + """ + Ensure dialog is finalized/open, then run a nested Qt QEventLoop until an + event is posted or timeout occurs. Returns a YEvent (YWidgetEvent, YTimeoutEvent, ...). + + If the application called open() previously this will just block until an event. + If open() was not called, it will finalize and show the dialog here (so creation + followed by immediate waitForEvent behaves like libyui). + """ + # Ensure dialog is created and visible (finalize if needed) + if not self._qwidget: + self.open() + + # give Qt a chance to process pending show/layout events + app = QtWidgets.QApplication.instance() + if app: + app.processEvents() + + self._event_result = None + loop = QtCore.QEventLoop() + self._qt_event_loop = loop + + timer = None + if timeout_millisec and timeout_millisec > 0: + timer = QtCore.QTimer() + timer.setSingleShot(True) + def on_timeout(): + # post timeout event and quit + self._event_result = YTimeoutEvent() + if loop.isRunning(): + loop.quit() + timer.timeout.connect(on_timeout) + timer.start(timeout_millisec) + + # PySide6 / Qt6 uses exec() + loop.exec() + + # cleanup + if timer and timer.isActive(): + timer.stop() + self._qt_event_loop = None + return self._event_result if self._event_result is not None else YEvent() diff --git a/manatools/aui/backends/qt/frameqt.py b/manatools/aui/backends/qt/frameqt.py new file mode 100644 index 0000000..e28d3e7 --- /dev/null +++ b/manatools/aui/backends/qt/frameqt.py @@ -0,0 +1,201 @@ +""" +Qt backend implementation for YUI +""" + +from PySide6 import QtWidgets +from ...yui_common import * + +class YFrameQt(YSingleChildContainerWidget): + """ + Qt backend implementation of YFrame. + - Uses QGroupBox to present a labeled framed container. + - Single child is placed inside the group's layout. + - Exposes simple property support for 'label'. + """ + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._backend_widget = None + self._group_layout = None + + def widgetClass(self): + return "YFrame" + + def stretchable(self, dim: YUIDimension): + """Return True if the frame should stretch in given dimension. + The frame is stretchable when its child is stretchable or has a layout weight. + """ + try: + # prefer explicit single child + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return False + try: + if bool(child.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(child.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def label(self): + return self._label + + def setLabel(self, newLabel): + """Set the frame label and update the Qt widget if created.""" + try: + self._label = newLabel + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setTitle(self._label) + except Exception: + pass + except Exception: + pass + + def _attach_child_backend(self): + """Attach existing child backend widget to the groupbox layout.""" + if not (self._backend_widget and self._group_layout and getattr(self, "_child", None)): + return + try: + w = self._child.get_backend_widget() + if w: + # clear any existing widgets in layout (defensive) + try: + while self._group_layout.count(): + it = self._group_layout.takeAt(0) + if it and it.widget(): + it.widget().setParent(None) + except Exception: + pass + self._group_layout.addWidget(w) + except Exception: + pass + + def addChild(self, child): + """Override to attach backend child when available.""" + try: + super().addChild(child) + except Exception: + # best-effort fallback + self._child = child + child._parent = self + # if backend exists, attach new child's backend + if getattr(self, "_backend_widget", None): + try: + self._attach_child_backend() + except Exception: + pass + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + child._parent = self + if getattr(self, "_backend_widget", None): + try: + self._attach_child_backend() + except Exception: + pass + + def _create_backend_widget(self): + """Create the QGroupBox + layout and attach child if present.""" + try: + grp = QtWidgets.QGroupBox(self._label) + layout = QtWidgets.QVBoxLayout(grp) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(4) + self._backend_widget = grp + self._group_layout = layout + + # attach child widget if already set + if getattr(self, "_child", None): + try: + w = self._child.get_backend_widget() + if w: + layout.addWidget(w) + except Exception: + pass + except Exception: + # fallback to a plain QWidget container if QGroupBox creation fails + try: + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(4) + self._backend_widget = container + self._group_layout = layout + if getattr(self, "_child", None): + try: + w = self._child.get_backend_widget() + if w: + layout.addWidget(w) + except Exception: + pass + except Exception: + self._backend_widget = None + self._group_layout = None + + def _set_backend_enabled(self, enabled): + """Enable/disable the frame and propagate state to the child.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate to logical child + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def setProperty(self, propertyName, val): + """Handle simple properties; returns True if handled.""" + try: + if propertyName == "label": + self.setLabel(str(val)) + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "label": + return self.label() + except Exception: + pass + return None + + def propertySet(self): + """Return a minimal property set description (used by some backends).""" + try: + props = YPropertySet() + try: + props.add(YProperty("label", YPropertyType.YStringProperty)) + except Exception: + pass + return props + except Exception: + return None \ No newline at end of file diff --git a/manatools/aui/backends/qt/hboxqt.py b/manatools/aui/backends/qt/hboxqt.py new file mode 100644 index 0000000..ecee483 --- /dev/null +++ b/manatools/aui/backends/qt/hboxqt.py @@ -0,0 +1,80 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YHBoxQt(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YHBox" + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(self._backend_widget) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) + + for child in self._children: + widget = child.get_backend_widget() + expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 + + # If the child requests horizontal stretch, set its QSizePolicy to Expanding + try: + if expand == 1: + sp = widget.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + widget.setSizePolicy(sp) + except Exception: + pass + print( f"YHBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug + layout.addWidget(widget, stretch=expand) + + def _set_backend_enabled(self, enabled): + """Enable/disable the HBox container and propagate to children.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/qt/inputfieldqt.py b/manatools/aui/backends/qt/inputfieldqt.py new file mode 100644 index 0000000..d583025 --- /dev/null +++ b/manatools/aui/backends/qt/inputfieldqt.py @@ -0,0 +1,78 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YInputFieldQt(YWidget): + def __init__(self, parent=None, label="", password_mode=False): + super().__init__(parent) + self._label = label + self._value = "" + self._password_mode = password_mode + + def widgetClass(self): + return "YInputField" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_entry_widget') and self._entry_widget: + self._entry_widget.setText(text) + + def label(self): + return self._label + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + + if self._label: + label = QtWidgets.QLabel(self._label) + layout.addWidget(label) + + if self._password_mode: + entry = QtWidgets.QLineEdit() + entry.setEchoMode(QtWidgets.QLineEdit.Password) + else: + entry = QtWidgets.QLineEdit() + + entry.setText(self._value) + entry.textChanged.connect(self._on_text_changed) + layout.addWidget(entry) + + self._backend_widget = container + self._entry_widget = entry + + def _set_backend_enabled(self, enabled): + """Enable/disable the input field: entry and container.""" + try: + if getattr(self, "_entry_widget", None) is not None: + try: + self._entry_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_text_changed(self, text): + self._value = text diff --git a/manatools/aui/backends/qt/labelqt.py b/manatools/aui/backends/qt/labelqt.py new file mode 100644 index 0000000..bc39818 --- /dev/null +++ b/manatools/aui/backends/qt/labelqt.py @@ -0,0 +1,50 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YLabelQt(YWidget): + def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): + super().__init__(parent) + self._text = text + self._is_heading = isHeading + self._is_output_field = isOutputField + + def widgetClass(self): + return "YLabel" + + def text(self): + return self._text + + def setText(self, new_text): + self._text = new_text + if self._backend_widget: + self._backend_widget.setText(new_text) + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QLabel(self._text) + if self._is_heading: + font = self._backend_widget.font() + font.setBold(True) + font.setPointSize(font.pointSize() + 2) + self._backend_widget.setFont(font) + + def _set_backend_enabled(self, enabled): + """Enable/disable the QLabel backend.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py new file mode 100644 index 0000000..5d1e3be --- /dev/null +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -0,0 +1,75 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + + +class YPushButtonQt(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + + def widgetClass(self): + return "YPushButton" + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + if self._backend_widget: + self._backend_widget.setText(label) + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QPushButton(self._label) + # Set size policy to prevent unwanted expansion + try: + try: + sp = self._backend_widget.sizePolicy() + # PySide6 may expect enum class; try both styles defensively + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Minimum) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) + except Exception: + pass + self._backend_widget.setSizePolicy(sp) + except Exception: + try: + # fallback: set using convenience form (two args) + self._backend_widget.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + except Exception: + pass + except Exception: + pass + self._backend_widget.clicked.connect(self._on_clicked) + + def _set_backend_enabled(self, enabled): + """Enable/disable the QPushButton backend.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_clicked(self): + # Post a YWidgetEvent to the containing dialog (walk parents) + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + else: + # fallback logging for now + print(f"Button clicked (no dialog found): {self._label}") diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py new file mode 100644 index 0000000..c6af7d9 --- /dev/null +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -0,0 +1,153 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YSelectionBoxQt(YSelectionWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + self._selected_items = [] + self._multi_selection = False + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YSelectionBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_list_widget') and self._list_widget: + # Find and select the item with matching text + for i in range(self._list_widget.count()): + item = self._list_widget.item(i) + if item.text() == text: + self._list_widget.setCurrentItem(item) + break + # Update selected_items to keep internal state consistent + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break + + def label(self): + return self._label + + def selectedItems(self): + """Get list of selected items""" + return self._selected_items + + def selectItem(self, item, selected=True): + """Select or deselect a specific item""" + if hasattr(self, '_list_widget') and self._list_widget: + for i in range(self._list_widget.count()): + list_item = self._list_widget.item(i) + if list_item.text() == item.label(): + if selected: + self._list_widget.setCurrentItem(list_item) + if item not in self._selected_items: + self._selected_items.append(item) + else: + if item in self._selected_items: + self._selected_items.remove(item) + break + + def setMultiSelection(self, enabled): + """Enable or disable multi-selection.""" + self._multi_selection = bool(enabled) + if hasattr(self, '_list_widget') and self._list_widget: + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi_selection else QtWidgets.QAbstractItemView.SingleSelection + self._list_widget.setSelectionMode(mode) + # if disabling multi-selection, collapse to the first selected item + if not self._multi_selection: + selected = self._list_widget.selectedItems() + if len(selected) > 1: + first = selected[0] + self._list_widget.clearSelection() + first.setSelected(True) + # update internal state to reflect change + self._on_selection_changed() + + def multiSelection(self): + """Return whether multi-selection is enabled.""" + return bool(self._multi_selection) + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + + if self._label: + label = QtWidgets.QLabel(self._label) + layout.addWidget(label) + + list_widget = QtWidgets.QListWidget() + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi_selection else QtWidgets.QAbstractItemView.SingleSelection + list_widget.setSelectionMode(mode) + + # Add items to list widget + for item in self._items: + list_widget.addItem(item.label()) + + list_widget.itemSelectionChanged.connect(self._on_selection_changed) + layout.addWidget(list_widget) + + self._backend_widget = container + self._list_widget = list_widget + + def _set_backend_enabled(self, enabled): + """Enable/disable the selection box and its list widget; propagate where applicable.""" + try: + if getattr(self, "_list_widget", None) is not None: + try: + self._list_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_selection_changed(self): + """Handle selection change in the list widget""" + if hasattr(self, '_list_widget') and self._list_widget: + # Update selected items + self._selected_items = [] + selected_indices = [index.row() for index in self._list_widget.selectedIndexes()] + + for idx in selected_indices: + if idx < len(self._items): + self._selected_items.append(self._items[idx]) + + # Update value to first selected item + if self._selected_items: + self._value = self._selected_items[0].label() + + # Post selection-changed event to containing dialog + try: + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass diff --git a/manatools/aui/backends/qt/treeqt.py b/manatools/aui/backends/qt/treeqt.py new file mode 100644 index 0000000..843a827 --- /dev/null +++ b/manatools/aui/backends/qt/treeqt.py @@ -0,0 +1,360 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YTreeQt(YSelectionWidget): + """ + Qt backend for YTree (based on YTree.h semantics). + - Supports multiSelection and immediateMode. + - Rebuild tree from internal items with rebuildTree(). + - currentItem() returns the YTreeItem wrapper for the focused/selected QTreeWidgetItem. + - activate() simulates user activation of the current item (posts an Activated event). + - recursiveSelection if it should select children recursively + """ + def __init__(self, parent=None, label="", multiSelection=False, recursiveSelection=False): + super().__init__(parent) + self._label = label + self._multi = bool(multiSelection) + self._recursive = bool(recursiveSelection) + if self._recursive: + self._multi = True # recursive selection implies multi-selection + self._immediate = self.notify() + self._backend_widget = None + self._tree_widget = None + # mappings between QTreeWidgetItem and logical YTreeItem (python objects in self._items) + self._qitem_to_item = {} + self._item_to_qitem = {} + # guard to avoid recursion when programmatically changing selection + self._suppress_selection_handler = False + # remember last selected QTreeWidgetItem set to detect added/removed selections + self._last_selected_qitems = set() + + def widgetClass(self): + return "YTree" + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + if self._label: + lbl = QtWidgets.QLabel(self._label) + layout.addWidget(lbl) + + tree = QtWidgets.QTreeWidget() + tree.setHeaderHidden(True) + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi else QtWidgets.QAbstractItemView.SingleSelection + tree.setSelectionMode(mode) + tree.itemSelectionChanged.connect(self._on_selection_changed) + tree.itemActivated.connect(self._on_item_activated) + + layout.addWidget(tree) + self._backend_widget = container + self._tree_widget = tree + + # populate if items already present + try: + self.rebuildTree() + except Exception: + pass + + def rebuildTree(self): + """Rebuild the QTreeWidget from self._items (calls helper recursively).""" + if self._tree_widget is None: + # ensure backend exists + self._create_backend_widget() + # clear existing + self._qitem_to_item.clear() + self._item_to_qitem.clear() + self._tree_widget.clear() + + def _add_recursive(parent_qitem, item): + # item expected to provide label() and possibly children() iterable + text = "" + try: + text = item.label() + except Exception: + try: + text = str(item) + except Exception: + text = "" + qitem = QtWidgets.QTreeWidgetItem([text]) + # preserve mapping + self._qitem_to_item[qitem] = item + self._item_to_qitem[item] = qitem + # attach to parent or top-level + if parent_qitem is None: + self._tree_widget.addTopLevelItem(qitem) + else: + parent_qitem.addChild(qitem) + + # set expanded state according to the logical item's _is_open flag + try: + is_open = bool(getattr(item, "_is_open", False)) + # setExpanded ensures the node shows as expanded/collapsed + qitem.setExpanded(is_open) + except Exception: + pass + + # recurse on children if available + try: + children = getattr(item, "children", None) + if callable(children): + childs = children() + else: + childs = children or [] + except Exception: + childs = [] + # many YTreeItem implementations may expose _children or similar; try common patterns + if not childs: + try: + childs = getattr(item, "_children", []) or [] + except Exception: + childs = [] + + for c in childs: + _add_recursive(qitem, c) + + return qitem + + for it in list(getattr(self, "_items", []) or []): + try: + _add_recursive(None, it) + except Exception: + pass + # do not call expandAll(); expansion is controlled per-item by _is_open + + def currentItem(self): + """Return the logical YTreeItem corresponding to the current/focused QTreeWidgetItem.""" + if not self._tree_widget: + return None + try: + qcur = self._tree_widget.currentItem() + if qcur is None: + # fallback to first selected item if current not set + sel = self._tree_widget.selectedItems() + qcur = sel[0] if sel else None + if qcur is None: + return None + return self._qitem_to_item.get(qcur, None) + except Exception: + return None + + def activate(self): + """Simulate activation of the current item (post Activated event).""" + item = self.currentItem() + if item is None: + return False + try: + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + return True + except Exception: + return False + + def hasMultiSelection(self): + """Return True if the tree allows selecting multiple items at once.""" + return bool(self._multi) + + def immediateMode(self): + return bool(self._immediate) + + def setImmediateMode(self, on:bool=True): + self._immediate = on + self.setNotify(on) + + def _collect_descendant_qitems(self, qitem): + """Return a list of qitem and all descendant QTreeWidgetItem objects.""" + out = [] + if qitem is None: + return out + stack = [qitem] + while stack: + cur = stack.pop() + out.append(cur) + try: + for i in range(cur.childCount()): + stack.append(cur.child(i)) + except Exception: + pass + return out + + # selection change handler + def _on_selection_changed(self): + """Update logical selection list and emit selection-changed event when needed.""" + # Defensive guard: when we change selection programmatically we don't want to re-enter here. + if self._suppress_selection_handler: + return + + try: + if not self._tree_widget: + return + + sel_qitems = list(self._tree_widget.selectedItems()) + current_set = set(sel_qitems) + + # If recursive selection is enabled and multi-selection is allowed, + # adjust selection so that selecting a parent selects all descendants + # and deselecting a parent deselects all descendants. + if self._recursive and self._multi: + added = current_set - self._last_selected_qitems + removed = self._last_selected_qitems - current_set + + # start desired_set from current_set + desired_set = set(current_set) + + # For every newly added item, ensure its descendants are selected + for q in list(added): + for dq in self._collect_descendant_qitems(q): + desired_set.add(dq) + + # For every removed item, ensure its descendants are deselected + for q in list(removed): + for dq in self._collect_descendant_qitems(q): + if dq in desired_set: + desired_set.discard(dq) + + # If desired_set differs from what's currently selected in the widget, + # apply the change programmatically. + if desired_set != current_set: + try: + self._suppress_selection_handler = True + self._tree_widget.clearSelection() + for q in desired_set: + try: + q.setSelected(True) + except Exception: + pass + finally: + self._suppress_selection_handler = False + # refresh sel_qitems and current_set after modification + sel_qitems = list(self._tree_widget.selectedItems()) + current_set = set(sel_qitems) + + # Build logical_qitems: if recursive + single select, include descendants in logical list; + # otherwise logical_qitems mirrors current UI selection. + logical_qitems = [] + if self._recursive and not self._multi: + for q in sel_qitems: + logical_qitems.append(q) + for dq in self._collect_descendant_qitems(q): + if dq is not q: + logical_qitems.append(dq) + else: + logical_qitems = sel_qitems + + # Map qitems -> logical YTreeItem objects + new_selected = [] + for qitem in logical_qitems: + itm = self._qitem_to_item.get(qitem, None) + if itm is not None: + new_selected.append(itm) + + # Update internal selected items list (logical selection used by base class) + try: + # clear previous selection flags for all known items + for it in list(getattr(self, "_items", []) or []): + try: + it.setSelected(False) + except Exception: + pass + # set selection flag for newly selected items + for it in new_selected: + try: + it.setSelected(True) + except Exception: + pass + except Exception: + pass + + self._selected_items = new_selected + + # remember last selected QTreeWidgetItem set for next invocation + try: + self._last_selected_qitems = set(self._tree_widget.selectedItems()) + except Exception: + self._last_selected_qitems = set() + + # immediate mode: notify container/dialog + try: + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + except Exception: + pass + + # item activated (double click / Enter) + def _on_item_activated(self, qitem, column): + try: + # map to logical item + item = self._qitem_to_item.get(qitem, None) + if item is None: + return + # post activated event + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + pass + + def addItem(self, item): + '''Add a YItem redefinition from YSelectionWidget to manage YTreeItems.''' + if isinstance(item, str): + item = YTreeItem(item) + self._items.append(item) + else: + super().addItem(item) + + # property API hooks (minimal implementation) + def setProperty(self, propertyName, val): + try: + if propertyName == "immediateMode": + self.setImmediateMode(bool(val)) + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "immediateMode": + return self.immediateMode() + except Exception: + pass + return None + + def _set_backend_enabled(self, enabled): + """Enable/disable the tree widget and propagate to logical items/widgets.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # logical propagation to child YWidgets (if any) + try: + for c in list(getattr(self, "_items", []) or []): + try: + if hasattr(c, "setEnabled"): + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/qt/vboxqt.py b/manatools/aui/backends/qt/vboxqt.py new file mode 100644 index 0000000..4d8ab15 --- /dev/null +++ b/manatools/aui/backends/qt/vboxqt.py @@ -0,0 +1,83 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YVBoxQt(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YVBox" + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + + def _create_backend_widget(self): + self._backend_widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(self._backend_widget) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) + + for child in self._children: + widget = child.get_backend_widget() + expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 + + # If the child requests horizontal stretch, set its QSizePolicy to Expanding + try: + if expand == 1: + sp = widget.sizePolicy() + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + widget.setSizePolicy(sp) + except Exception: + pass + + + + print( f"YVBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug + layout.addWidget(widget, stretch=expand) + + def _set_backend_enabled(self, enabled): + """Enable/disable the VBox container and propagate to children.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 8789406..7d0b128 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -3,10 +3,40 @@ """ import sys +import importlib from PySide6 import QtWidgets, QtCore, QtGui import os from .yui_common import * +# Import backend symbols only into this shim module. +def _import_qt_backend_symbols(): + mod = None + try: + # Package-relative import + mod = importlib.import_module(".backends.qt", __package__) + except Exception: + try: + # Absolute fallback + mod = importlib.import_module("manatools.aui.backends.qt") + except Exception: + mod = None + if not mod: + return + names = getattr(mod, "__all__", None) + if names: + for name in names: + try: + globals()[name] = getattr(mod, name) + except Exception: + pass + else: + # Fallback: import non-private names + for name, obj in mod.__dict__.items(): + if not name.startswith("_"): + globals()[name] = obj + +_import_qt_backend_symbols() + class YUIQt: def __init__(self): self._widget_factory = YWidgetFactoryQt() @@ -241,1577 +271,3 @@ def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameQt(parent, label) - -# Qt Widget Implementations -class YDialogQt(YSingleChildContainerWidget): - _open_dialogs = [] - - def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): - super().__init__() - self._dialog_type = dialog_type - self._color_mode = color_mode - self._is_open = False - self._qwidget = None - self._event_result = None - self._qt_event_loop = None - YDialogQt._open_dialogs.append(self) - - def widgetClass(self): - return "YDialog" - - @staticmethod - def currentDialog(doThrow=True): - '''Return the currently open dialog (topmost), or raise if none.''' - open_dialog = YDialogQt._open_dialogs[-1] if YDialogQt._open_dialogs else None - if not open_dialog and doThrow: - raise YUINoDialogException("No dialog is currently open") - return open_dialog - - @staticmethod - def topmostDialog(doThrow=True): - ''' same as currentDialog ''' - return YDialogQt.currentDialog(doThrow=doThrow) - - def isTopmostDialog(self): - '''Return whether this dialog is the topmost open dialog.''' - return YDialogQt._open_dialogs[-1] == self if YDialogQt._open_dialogs else False - - def open(self): - """ - Finalize and show the dialog in a non-blocking way. - - Matches libyui semantics: open() should only finalize and make visible. - If the application expects blocking behavior it should call waitForEvent() - which will start a nested event loop as required. - """ - if not self._is_open: - if not self._qwidget: - self._create_backend_widget() - - self._qwidget.show() - self._is_open = True - - def isOpen(self): - return self._is_open - - def destroy(self, doThrow=True): - if self._qwidget: - self._qwidget.close() - self._qwidget = None - self._is_open = False - if self in YDialogQt._open_dialogs: - YDialogQt._open_dialogs.remove(self) - return True - - @classmethod - def deleteTopmostDialog(cls, doThrow=True): - if cls._open_dialogs: - dialog = cls._open_dialogs[-1] - return dialog.destroy(doThrow) - return False - - @classmethod - def currentDialog(cls, doThrow=True): - if not cls._open_dialogs: - if doThrow: - raise YUINoDialogException("No dialog open") - return None - return cls._open_dialogs[-1] - - def _create_backend_widget(self): - self._qwidget = QtWidgets.QMainWindow() - # Determine window title:from YApplicationQt instance stored on the YUI backend - title = "Manatools YUI Qt Dialog" - - try: - from . import yui as yui_mod - appobj = None - # YUI._backend may hold the backend instance (YUIQt) - backend = getattr(yui_mod.YUI, "_backend", None) - if backend: - if hasattr(backend, "application"): - appobj = backend.application() - # fallback: YUI._instance might be set and expose application/yApp - if not appobj: - inst = getattr(yui_mod.YUI, "_instance", None) - if inst: - if hasattr(inst, "application"): - appobj = inst.application() - if appobj and hasattr(appobj, "applicationTitle"): - atitle = appobj.applicationTitle() - if atitle: - title = atitle - # try to obtain a resolved QIcon from the application backend if available - app_qicon = None - if appobj: - # prefer cached Qt icon if set by setApplicationIcon - app_qicon = getattr(appobj, "_qt_icon", None) - # otherwise try to resolve applicationIcon string on the fly - if not app_qicon: - try: - icon_spec = appobj.applicationIcon() - if icon_spec: - # use the application's iconBasePath if present - base = getattr(appobj, "_icon_base_path", None) - if base and not os.path.isabs(icon_spec): - p = os.path.join(base, icon_spec) - if os.path.exists(p): - app_qicon = QtGui.QIcon(p) - if not app_qicon: - q = QtGui.QIcon.fromTheme(icon_spec) - if not q.isNull(): - app_qicon = q - except Exception: - pass - # if we have a qicon, set it on the QApplication and the new window - if app_qicon: - try: - qapp = QtWidgets.QApplication.instance() - if qapp: - qapp.setWindowIcon(app_qicon) - except Exception: - pass - # store resolved qicon locally to apply to this window - _resolved_qicon = app_qicon - except Exception: - # ignore and keep default - _resolved_qicon = None - - self._qwidget.setWindowTitle(title) - try: - if _resolved_qicon: - self._qwidget.setWindowIcon(_resolved_qicon) - except Exception: - pass - self._qwidget.resize(600, 400) - - central_widget = QtWidgets.QWidget() - self._qwidget.setCentralWidget(central_widget) - - if self._child: - layout = QtWidgets.QVBoxLayout(central_widget) - layout.addWidget(self._child.get_backend_widget()) - - self._backend_widget = self._qwidget - self._qwidget.closeEvent = self._on_close_event - - def _set_backend_enabled(self, enabled): - """Enable/disable the dialog window and propagate to logical child widgets.""" - try: - if getattr(self, "_qwidget", None) is not None: - try: - self._qwidget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - # propagate logical enabled state to contained YWidget(s) - try: - if getattr(self, "_child", None): - try: - self._child.setEnabled(enabled) - except Exception: - pass - else: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - - def _on_close_event(self, event): - # Post a cancel event so waitForEvent returns a YCancelEvent when the user - # closes the window with the window manager 'X' button. - try: - self._post_event(YCancelEvent()) - except Exception: - pass - # Ensure dialog is destroyed and accept the close - self.destroy() - event.accept() - - def _post_event(self, event): - """Internal: post an event to this dialog and quit local event loop if running.""" - self._event_result = event - if self._qt_event_loop is not None and self._qt_event_loop.isRunning(): - self._qt_event_loop.quit() - - def waitForEvent(self, timeout_millisec=0): - """ - Ensure dialog is finalized/open, then run a nested Qt QEventLoop until an - event is posted or timeout occurs. Returns a YEvent (YWidgetEvent, YTimeoutEvent, ...). - - If the application called open() previously this will just block until an event. - If open() was not called, it will finalize and show the dialog here (so creation - followed by immediate waitForEvent behaves like libyui). - """ - # Ensure dialog is created and visible (finalize if needed) - if not self._qwidget: - self.open() - - # give Qt a chance to process pending show/layout events - app = QtWidgets.QApplication.instance() - if app: - app.processEvents() - - self._event_result = None - loop = QtCore.QEventLoop() - self._qt_event_loop = loop - - timer = None - if timeout_millisec and timeout_millisec > 0: - timer = QtCore.QTimer() - timer.setSingleShot(True) - def on_timeout(): - # post timeout event and quit - self._event_result = YTimeoutEvent() - if loop.isRunning(): - loop.quit() - timer.timeout.connect(on_timeout) - timer.start(timeout_millisec) - - # PySide6 / Qt6 uses exec() - loop.exec() - - # cleanup - if timer and timer.isActive(): - timer.stop() - self._qt_event_loop = None - return self._event_result if self._event_result is not None else YEvent() - - -class YVBoxQt(YWidget): - def __init__(self, parent=None): - super().__init__(parent) - - def widgetClass(self): - return "YVBox" - - # Returns the stretchability of the layout box: - # * The layout box is stretchable if one of the children is stretchable in - # * this dimension or if one of the child widgets has a layout weight in - # * this dimension. - def stretchable(self, dim): - for child in self._children: - widget = child.get_backend_widget() - expand = bool(child.stretchable(dim)) - weight = bool(child.weight(dim)) - if expand or weight: - return True - # No child is stretchable in this dimension - return False - - def _create_backend_widget(self): - self._backend_widget = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout(self._backend_widget) - layout.setContentsMargins(10, 10, 10, 10) - layout.setSpacing(5) - - for child in self._children: - widget = child.get_backend_widget() - expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 - - # If the child requests horizontal stretch, set its QSizePolicy to Expanding - try: - if expand == 1: - sp = widget.sizePolicy() - try: - sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) - except Exception: - try: - sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) - except Exception: - pass - widget.setSizePolicy(sp) - except Exception: - pass - - - - print( f"YVBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug - layout.addWidget(widget, stretch=expand) - - def _set_backend_enabled(self, enabled): - """Enable/disable the VBox container and propagate to children.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - try: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - -class YHBoxQt(YWidget): - def __init__(self, parent=None): - super().__init__(parent) - - def widgetClass(self): - return "YHBox" - - # Returns the stretchability of the layout box: - # * The layout box is stretchable if one of the children is stretchable in - # * this dimension or if one of the child widgets has a layout weight in - # * this dimension. - def stretchable(self, dim): - for child in self._children: - widget = child.get_backend_widget() - expand = bool(child.stretchable(dim)) - weight = bool(child.weight(dim)) - if expand or weight: - return True - # No child is stretchable in this dimension - return False - - def _create_backend_widget(self): - self._backend_widget = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(self._backend_widget) - layout.setContentsMargins(10, 10, 10, 10) - layout.setSpacing(5) - - for child in self._children: - widget = child.get_backend_widget() - expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 - - # If the child requests horizontal stretch, set its QSizePolicy to Expanding - try: - if expand == 1: - sp = widget.sizePolicy() - try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) - except Exception: - try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) - except Exception: - pass - widget.setSizePolicy(sp) - except Exception: - pass - print( f"YHBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug - layout.addWidget(widget, stretch=expand) - - def _set_backend_enabled(self, enabled): - """Enable/disable the HBox container and propagate to children.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - try: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - -class YLabelQt(YWidget): - def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): - super().__init__(parent) - self._text = text - self._is_heading = isHeading - self._is_output_field = isOutputField - - def widgetClass(self): - return "YLabel" - - def text(self): - return self._text - - def setText(self, new_text): - self._text = new_text - if self._backend_widget: - self._backend_widget.setText(new_text) - - def _create_backend_widget(self): - self._backend_widget = QtWidgets.QLabel(self._text) - if self._is_heading: - font = self._backend_widget.font() - font.setBold(True) - font.setPointSize(font.pointSize() + 2) - self._backend_widget.setFont(font) - - def _set_backend_enabled(self, enabled): - """Enable/disable the QLabel backend.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - -class YInputFieldQt(YWidget): - def __init__(self, parent=None, label="", password_mode=False): - super().__init__(parent) - self._label = label - self._value = "" - self._password_mode = password_mode - - def widgetClass(self): - return "YInputField" - - def value(self): - return self._value - - def setValue(self, text): - self._value = text - if hasattr(self, '_entry_widget') and self._entry_widget: - self._entry_widget.setText(text) - - def label(self): - return self._label - - def _create_backend_widget(self): - container = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(container) - layout.setContentsMargins(0, 0, 0, 0) - - if self._label: - label = QtWidgets.QLabel(self._label) - layout.addWidget(label) - - if self._password_mode: - entry = QtWidgets.QLineEdit() - entry.setEchoMode(QtWidgets.QLineEdit.Password) - else: - entry = QtWidgets.QLineEdit() - - entry.setText(self._value) - entry.textChanged.connect(self._on_text_changed) - layout.addWidget(entry) - - self._backend_widget = container - self._entry_widget = entry - - def _set_backend_enabled(self, enabled): - """Enable/disable the input field: entry and container.""" - try: - if getattr(self, "_entry_widget", None) is not None: - try: - self._entry_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - - def _on_text_changed(self, text): - self._value = text - -class YPushButtonQt(YWidget): - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label - - def widgetClass(self): - return "YPushButton" - - def label(self): - return self._label - - def setLabel(self, label): - self._label = label - if self._backend_widget: - self._backend_widget.setText(label) - - def _create_backend_widget(self): - self._backend_widget = QtWidgets.QPushButton(self._label) - # Set size policy to prevent unwanted expansion - try: - try: - sp = self._backend_widget.sizePolicy() - # PySide6 may expect enum class; try both styles defensively - try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Minimum) - except Exception: - try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) - except Exception: - pass - self._backend_widget.setSizePolicy(sp) - except Exception: - try: - # fallback: set using convenience form (two args) - self._backend_widget.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - except Exception: - pass - except Exception: - pass - self._backend_widget.clicked.connect(self._on_clicked) - - def _set_backend_enabled(self, enabled): - """Enable/disable the QPushButton backend.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - - def _on_clicked(self): - # Post a YWidgetEvent to the containing dialog (walk parents) - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) - else: - # fallback logging for now - print(f"Button clicked (no dialog found): {self._label}") - -class YCheckBoxQt(YWidget): - def __init__(self, parent=None, label="", is_checked=False): - super().__init__(parent) - self._label = label - self._is_checked = is_checked - - def widgetClass(self): - return "YCheckBox" - - def value(self): - return self._is_checked - - def setValue(self, checked): - self._is_checked = checked - if self._backend_widget: - try: - # avoid emitting signals while programmatically changing state - self._backend_widget.blockSignals(True) - self._backend_widget.setChecked(checked) - finally: - try: - self._backend_widget.blockSignals(False) - except Exception: - pass - - def label(self): - return self._label - - def _create_backend_widget(self): - self._backend_widget = QtWidgets.QCheckBox(self._label) - self._backend_widget.setChecked(self._is_checked) - self._backend_widget.stateChanged.connect(self._on_state_changed) - - def _set_backend_enabled(self, enabled): - """Enable/disable the QCheckBox backend.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - - def _on_state_changed(self, state): - # Update internal state - # state is QtCore.Qt.CheckState (Unchecked=0, PartiallyChecked=1, Checked=2) - self._is_checked = (QtCore.Qt.CheckState(state) == QtCore.Qt.CheckState.Checked) - - if self.notify(): - # Post a YWidgetEvent to the containing dialog - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - else: - print(f"CheckBox state changed (no dialog found): {self._label} = {self._is_checked}") - -class YComboBoxQt(YSelectionWidget): - def __init__(self, parent=None, label="", editable=False): - super().__init__(parent) - self._label = label - self._editable = editable - self._value = "" - self._selected_items = [] - - def widgetClass(self): - return "YComboBox" - - def value(self): - return self._value - - def setValue(self, text): - self._value = text - if hasattr(self, '_combo_widget') and self._combo_widget: - index = self._combo_widget.findText(text) - if index >= 0: - self._combo_widget.setCurrentIndex(index) - elif self._editable: - self._combo_widget.setEditText(text) - # update selected_items to keep internal state consistent - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break - - def editable(self): - return self._editable - - def _create_backend_widget(self): - container = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(container) - layout.setContentsMargins(0, 0, 0, 0) - - if self._label: - label = QtWidgets.QLabel(self._label) - layout.addWidget(label) - - if self._editable: - combo = QtWidgets.QComboBox() - combo.setEditable(True) - else: - combo = QtWidgets.QComboBox() - - # Add items to combo box - for item in self._items: - combo.addItem(item.label()) - - combo.currentTextChanged.connect(self._on_text_changed) - # also handle index change (safer for some input methods) - combo.currentIndexChanged.connect(lambda idx: self._on_text_changed(combo.currentText())) - layout.addWidget(combo) - - self._backend_widget = container - self._combo_widget = combo - - def _set_backend_enabled(self, enabled): - """Enable/disable the combobox and its container.""" - try: - if getattr(self, "_combo_widget", None) is not None: - try: - self._combo_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - - def _on_text_changed(self, text): - self._value = text - # Update selected items - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break - if self.notify(): - # Post selection-changed event to containing dialog - try: - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - -class YSelectionBoxQt(YSelectionWidget): - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label - self._value = "" - self._selected_items = [] - self._multi_selection = False - self.setStretchable(YUIDimension.YD_HORIZ, True) - self.setStretchable(YUIDimension.YD_VERT, True) - - def widgetClass(self): - return "YSelectionBox" - - def value(self): - return self._value - - def setValue(self, text): - self._value = text - if hasattr(self, '_list_widget') and self._list_widget: - # Find and select the item with matching text - for i in range(self._list_widget.count()): - item = self._list_widget.item(i) - if item.text() == text: - self._list_widget.setCurrentItem(item) - break - # Update selected_items to keep internal state consistent - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break - - def label(self): - return self._label - - def selectedItems(self): - """Get list of selected items""" - return self._selected_items - - def selectItem(self, item, selected=True): - """Select or deselect a specific item""" - if hasattr(self, '_list_widget') and self._list_widget: - for i in range(self._list_widget.count()): - list_item = self._list_widget.item(i) - if list_item.text() == item.label(): - if selected: - self._list_widget.setCurrentItem(list_item) - if item not in self._selected_items: - self._selected_items.append(item) - else: - if item in self._selected_items: - self._selected_items.remove(item) - break - - def setMultiSelection(self, enabled): - """Enable or disable multi-selection.""" - self._multi_selection = bool(enabled) - if hasattr(self, '_list_widget') and self._list_widget: - mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi_selection else QtWidgets.QAbstractItemView.SingleSelection - self._list_widget.setSelectionMode(mode) - # if disabling multi-selection, collapse to the first selected item - if not self._multi_selection: - selected = self._list_widget.selectedItems() - if len(selected) > 1: - first = selected[0] - self._list_widget.clearSelection() - first.setSelected(True) - # update internal state to reflect change - self._on_selection_changed() - - def multiSelection(self): - """Return whether multi-selection is enabled.""" - return bool(self._multi_selection) - - def _create_backend_widget(self): - container = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout(container) - layout.setContentsMargins(0, 0, 0, 0) - - if self._label: - label = QtWidgets.QLabel(self._label) - layout.addWidget(label) - - list_widget = QtWidgets.QListWidget() - mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi_selection else QtWidgets.QAbstractItemView.SingleSelection - list_widget.setSelectionMode(mode) - - # Add items to list widget - for item in self._items: - list_widget.addItem(item.label()) - - list_widget.itemSelectionChanged.connect(self._on_selection_changed) - layout.addWidget(list_widget) - - self._backend_widget = container - self._list_widget = list_widget - - def _set_backend_enabled(self, enabled): - """Enable/disable the selection box and its list widget; propagate where applicable.""" - try: - if getattr(self, "_list_widget", None) is not None: - try: - self._list_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - - def _on_selection_changed(self): - """Handle selection change in the list widget""" - if hasattr(self, '_list_widget') and self._list_widget: - # Update selected items - self._selected_items = [] - selected_indices = [index.row() for index in self._list_widget.selectedIndexes()] - - for idx in selected_indices: - if idx < len(self._items): - self._selected_items.append(self._items[idx]) - - # Update value to first selected item - if self._selected_items: - self._value = self._selected_items[0].label() - - # Post selection-changed event to containing dialog - try: - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - -class YAlignmentQt(YSingleChildContainerWidget): - """ - Single-child alignment container for Qt6. Uses a QWidget + QGridLayout, - applying Qt.Alignment flags to the child. The container expands along - axes needed by Right/HCenter/VCenter/HVCenter to allow alignment. - """ - def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): - super().__init__(parent) - self._halign_spec = horAlign - self._valign_spec = vertAlign - self._backend_widget = None - self._layout = None - - def widgetClass(self): - return "YAlignment" - - def _to_qt_halign(self): - """Convert Horizontal YAlignmentType to QtCore.Qt.AlignmentFlag or None.""" - if self._halign_spec: - if self._halign_spec == YAlignmentType.YAlignBegin: - return QtCore.Qt.AlignmentFlag.AlignLeft - if self._halign_spec == YAlignmentType.YAlignCenter: - return QtCore.Qt.AlignmentFlag.AlignHCenter - if self._halign_spec == YAlignmentType.YAlignEnd: - return QtCore.Qt.AlignmentFlag.AlignRight - return None - - def _to_qt_valign(self): - """Convert Vertical YAlignmentType to QtCore.Qt.AlignmentFlag or None.""" - if self._valign_spec: - if self._valign_spec == YAlignmentType.YAlignBegin: - return QtCore.Qt.AlignmentFlag.AlignTop - if self._valign_spec == YAlignmentType.YAlignCenter: - return QtCore.Qt.AlignmentFlag.AlignVCenter - if self._valign_spec == YAlignmentType.YAlignEnd: - return QtCore.Qt.AlignmentFlag.AlignBottom - return None - - - def stretchable(self, dim: YUIDimension): - ''' Returns the stretchability of the layout box: - * The layout box is stretchable if the alignment spec requests expansion - * (Right/HCenter/HVCenter for horizontal, VCenter/HVCenter for vertical) - * OR if the child itself requests stretchability or has a layout weight. - ''' - # Expand if alignment spec requests it - try: - if dim == YUIDimension.YD_HORIZ: - if self._halign_spec in (YAlignmentType.YAlignEnd, YAlignmentType.YAlignCenter): - return True - if dim == YUIDimension.YD_VERT: - if self._valign_spec in (YAlignmentType.YAlignCenter,): - return True - except Exception: - pass - - # Otherwise honor child's own stretchability/weight - try: - if self._child: - expand = bool(self._child.stretchable(dim)) - weight = bool(self._child.weight(dim)) - if expand or weight: - return True - except Exception: - pass - return False - - def setAlignment(self, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): - self._halign_spec = horAlign - self._valign_spec = vertAlign - self._reapply_alignment() - - def _reapply_alignment(self): - if not (self._layout and self._child): - return - try: - w = self._child.get_backend_widget() - if w: - self._layout.removeWidget(w) - flags = QtCore.Qt.AlignmentFlag(0) - ha = self._to_qt_halign() - va = self._to_qt_valign() - if ha: - flags |= ha - if va: - flags |= va - self._layout.addWidget(w, 0, 0, flags) - except Exception: - pass - - def addChild(self, child): - try: - super().addChild(child) - except Exception: - self._child = child - if self._backend_widget: - self._attach_child_backend() - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - if self._backend_widget: - self._attach_child_backend() - - def _attach_child_backend(self): - if not (self._backend_widget and self._layout and self._child): - return - try: - w = self._child.get_backend_widget() - if w: - # clear previous - try: - self._layout.removeWidget(w) - except Exception: - pass - flags = QtCore.Qt.AlignmentFlag(0) - ha = self._to_qt_halign() - va = self._to_qt_valign() - if ha: - flags |= ha - if va: - flags |= va - # If the child requests horizontal stretch, set its QSizePolicy to Expanding - try: - if self._child and self._child.stretchable(YUIDimension.YD_HORIZ): - sp = w.sizePolicy() - try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) - except Exception: - try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) - except Exception: - pass - w.setSizePolicy(sp) - # If child requests vertical stretch, set vertical policy - if self._child and self._child.stretchable(YUIDimension.YD_VERT): - sp = w.sizePolicy() - try: - sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) - except Exception: - try: - sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) - except Exception: - pass - w.setSizePolicy(sp) - except Exception: - pass - self._layout.addWidget(w, 0, 0, flags) - except Exception: - pass - - def _create_backend_widget(self): - container = QtWidgets.QWidget() - grid = QtWidgets.QGridLayout(container) - grid.setContentsMargins(0, 0, 0, 0) - grid.setSpacing(0) - - # Size policy: expand along axes needed for alignment to work - sp = container.sizePolicy() - try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) - else QtWidgets.QSizePolicy.Policy.Fixed) - sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) - else QtWidgets.QSizePolicy.Policy.Fixed) - except Exception: - pass - container.setSizePolicy(sp) - - self._backend_widget = container - self._layout = grid - - if getattr(self, "_child", None): - self._attach_child_backend() - - def _set_backend_enabled(self, enabled): - """Enable/disable the alignment container and propagate to its logical child.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - # propagate to logical child - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is not None: - try: - child.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - -class YTreeQt(YSelectionWidget): - """ - Qt backend for YTree (based on YTree.h semantics). - - Supports multiSelection and immediateMode. - - Rebuild tree from internal items with rebuildTree(). - - currentItem() returns the YTreeItem wrapper for the focused/selected QTreeWidgetItem. - - activate() simulates user activation of the current item (posts an Activated event). - - recursiveSelection if it should select children recursively - """ - def __init__(self, parent=None, label="", multiSelection=False, recursiveSelection=False): - super().__init__(parent) - self._label = label - self._multi = bool(multiSelection) - self._recursive = bool(recursiveSelection) - if self._recursive: - self._multi = True # recursive selection implies multi-selection - self._immediate = self.notify() - self._backend_widget = None - self._tree_widget = None - # mappings between QTreeWidgetItem and logical YTreeItem (python objects in self._items) - self._qitem_to_item = {} - self._item_to_qitem = {} - # guard to avoid recursion when programmatically changing selection - self._suppress_selection_handler = False - # remember last selected QTreeWidgetItem set to detect added/removed selections - self._last_selected_qitems = set() - - def widgetClass(self): - return "YTree" - - def _create_backend_widget(self): - container = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout(container) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - if self._label: - lbl = QtWidgets.QLabel(self._label) - layout.addWidget(lbl) - - tree = QtWidgets.QTreeWidget() - tree.setHeaderHidden(True) - mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi else QtWidgets.QAbstractItemView.SingleSelection - tree.setSelectionMode(mode) - tree.itemSelectionChanged.connect(self._on_selection_changed) - tree.itemActivated.connect(self._on_item_activated) - - layout.addWidget(tree) - self._backend_widget = container - self._tree_widget = tree - - # populate if items already present - try: - self.rebuildTree() - except Exception: - pass - - def rebuildTree(self): - """Rebuild the QTreeWidget from self._items (calls helper recursively).""" - if self._tree_widget is None: - # ensure backend exists - self._create_backend_widget() - # clear existing - self._qitem_to_item.clear() - self._item_to_qitem.clear() - self._tree_widget.clear() - - def _add_recursive(parent_qitem, item): - # item expected to provide label() and possibly children() iterable - text = "" - try: - text = item.label() - except Exception: - try: - text = str(item) - except Exception: - text = "" - qitem = QtWidgets.QTreeWidgetItem([text]) - # preserve mapping - self._qitem_to_item[qitem] = item - self._item_to_qitem[item] = qitem - # attach to parent or top-level - if parent_qitem is None: - self._tree_widget.addTopLevelItem(qitem) - else: - parent_qitem.addChild(qitem) - - # set expanded state according to the logical item's _is_open flag - try: - is_open = bool(getattr(item, "_is_open", False)) - # setExpanded ensures the node shows as expanded/collapsed - qitem.setExpanded(is_open) - except Exception: - pass - - # recurse on children if available - try: - children = getattr(item, "children", None) - if callable(children): - childs = children() - else: - childs = children or [] - except Exception: - childs = [] - # many YTreeItem implementations may expose _children or similar; try common patterns - if not childs: - try: - childs = getattr(item, "_children", []) or [] - except Exception: - childs = [] - - for c in childs: - _add_recursive(qitem, c) - - return qitem - - for it in list(getattr(self, "_items", []) or []): - try: - _add_recursive(None, it) - except Exception: - pass - # do not call expandAll(); expansion is controlled per-item by _is_open - - def currentItem(self): - """Return the logical YTreeItem corresponding to the current/focused QTreeWidgetItem.""" - if not self._tree_widget: - return None - try: - qcur = self._tree_widget.currentItem() - if qcur is None: - # fallback to first selected item if current not set - sel = self._tree_widget.selectedItems() - qcur = sel[0] if sel else None - if qcur is None: - return None - return self._qitem_to_item.get(qcur, None) - except Exception: - return None - - def activate(self): - """Simulate activation of the current item (post Activated event).""" - item = self.currentItem() - if item is None: - return False - try: - dlg = self.findDialog() - if dlg is not None and self.notify(): - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) - return True - except Exception: - return False - - def hasMultiSelection(self): - """Return True if the tree allows selecting multiple items at once.""" - return bool(self._multi) - - def immediateMode(self): - return bool(self._immediate) - - def setImmediateMode(self, on:bool=True): - self._immediate = on - self.setNotify(on) - - def _collect_descendant_qitems(self, qitem): - """Return a list of qitem and all descendant QTreeWidgetItem objects.""" - out = [] - if qitem is None: - return out - stack = [qitem] - while stack: - cur = stack.pop() - out.append(cur) - try: - for i in range(cur.childCount()): - stack.append(cur.child(i)) - except Exception: - pass - return out - - # selection change handler - def _on_selection_changed(self): - """Update logical selection list and emit selection-changed event when needed.""" - # Defensive guard: when we change selection programmatically we don't want to re-enter here. - if self._suppress_selection_handler: - return - - try: - if not self._tree_widget: - return - - sel_qitems = list(self._tree_widget.selectedItems()) - current_set = set(sel_qitems) - - # If recursive selection is enabled and multi-selection is allowed, - # adjust selection so that selecting a parent selects all descendants - # and deselecting a parent deselects all descendants. - if self._recursive and self._multi: - added = current_set - self._last_selected_qitems - removed = self._last_selected_qitems - current_set - - # start desired_set from current_set - desired_set = set(current_set) - - # For every newly added item, ensure its descendants are selected - for q in list(added): - for dq in self._collect_descendant_qitems(q): - desired_set.add(dq) - - # For every removed item, ensure its descendants are deselected - for q in list(removed): - for dq in self._collect_descendant_qitems(q): - if dq in desired_set: - desired_set.discard(dq) - - # If desired_set differs from what's currently selected in the widget, - # apply the change programmatically. - if desired_set != current_set: - try: - self._suppress_selection_handler = True - self._tree_widget.clearSelection() - for q in desired_set: - try: - q.setSelected(True) - except Exception: - pass - finally: - self._suppress_selection_handler = False - # refresh sel_qitems and current_set after modification - sel_qitems = list(self._tree_widget.selectedItems()) - current_set = set(sel_qitems) - - # Build logical_qitems: if recursive + single select, include descendants in logical list; - # otherwise logical_qitems mirrors current UI selection. - logical_qitems = [] - if self._recursive and not self._multi: - for q in sel_qitems: - logical_qitems.append(q) - for dq in self._collect_descendant_qitems(q): - if dq is not q: - logical_qitems.append(dq) - else: - logical_qitems = sel_qitems - - # Map qitems -> logical YTreeItem objects - new_selected = [] - for qitem in logical_qitems: - itm = self._qitem_to_item.get(qitem, None) - if itm is not None: - new_selected.append(itm) - - # Update internal selected items list (logical selection used by base class) - try: - # clear previous selection flags for all known items - for it in list(getattr(self, "_items", []) or []): - try: - it.setSelected(False) - except Exception: - pass - # set selection flag for newly selected items - for it in new_selected: - try: - it.setSelected(True) - except Exception: - pass - except Exception: - pass - - self._selected_items = new_selected - - # remember last selected QTreeWidgetItem set for next invocation - try: - self._last_selected_qitems = set(self._tree_widget.selectedItems()) - except Exception: - self._last_selected_qitems = set() - - # immediate mode: notify container/dialog - try: - if self._immediate and self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - except Exception: - pass - - # item activated (double click / Enter) - def _on_item_activated(self, qitem, column): - try: - # map to logical item - item = self._qitem_to_item.get(qitem, None) - if item is None: - return - # post activated event - dlg = self.findDialog() - if dlg is not None and self.notify(): - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) - except Exception: - pass - - def addItem(self, item): - '''Add a YItem redefinition from YSelectionWidget to manage YTreeItems.''' - if isinstance(item, str): - item = YTreeItem(item) - self._items.append(item) - else: - super().addItem(item) - - # property API hooks (minimal implementation) - def setProperty(self, propertyName, val): - try: - if propertyName == "immediateMode": - self.setImmediateMode(bool(val)) - return True - except Exception: - pass - return False - - def getProperty(self, propertyName): - try: - if propertyName == "immediateMode": - return self.immediateMode() - except Exception: - pass - return None - - def _set_backend_enabled(self, enabled): - """Enable/disable the tree widget and propagate to logical items/widgets.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - # logical propagation to child YWidgets (if any) - try: - for c in list(getattr(self, "_items", []) or []): - try: - if hasattr(c, "setEnabled"): - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - -class YFrameQt(YSingleChildContainerWidget): - """ - Qt backend implementation of YFrame. - - Uses QGroupBox to present a labeled framed container. - - Single child is placed inside the group's layout. - - Exposes simple property support for 'label'. - """ - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label - self._backend_widget = None - self._group_layout = None - - def widgetClass(self): - return "YFrame" - - def stretchable(self, dim: YUIDimension): - """Return True if the frame should stretch in given dimension. - The frame is stretchable when its child is stretchable or has a layout weight. - """ - try: - # prefer explicit single child - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is None: - return False - try: - if bool(child.stretchable(dim)): - return True - except Exception: - pass - try: - if bool(child.weight(dim)): - return True - except Exception: - pass - except Exception: - pass - return False - - def label(self): - return self._label - - def setLabel(self, newLabel): - """Set the frame label and update the Qt widget if created.""" - try: - self._label = newLabel - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setTitle(self._label) - except Exception: - pass - except Exception: - pass - - def _attach_child_backend(self): - """Attach existing child backend widget to the groupbox layout.""" - if not (self._backend_widget and self._group_layout and getattr(self, "_child", None)): - return - try: - w = self._child.get_backend_widget() - if w: - # clear any existing widgets in layout (defensive) - try: - while self._group_layout.count(): - it = self._group_layout.takeAt(0) - if it and it.widget(): - it.widget().setParent(None) - except Exception: - pass - self._group_layout.addWidget(w) - except Exception: - pass - - def addChild(self, child): - """Override to attach backend child when available.""" - try: - super().addChild(child) - except Exception: - # best-effort fallback - self._child = child - child._parent = self - # if backend exists, attach new child's backend - if getattr(self, "_backend_widget", None): - try: - self._attach_child_backend() - except Exception: - pass - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - child._parent = self - if getattr(self, "_backend_widget", None): - try: - self._attach_child_backend() - except Exception: - pass - - def _create_backend_widget(self): - """Create the QGroupBox + layout and attach child if present.""" - try: - grp = QtWidgets.QGroupBox(self._label) - layout = QtWidgets.QVBoxLayout(grp) - layout.setContentsMargins(6, 6, 6, 6) - layout.setSpacing(4) - self._backend_widget = grp - self._group_layout = layout - - # attach child widget if already set - if getattr(self, "_child", None): - try: - w = self._child.get_backend_widget() - if w: - layout.addWidget(w) - except Exception: - pass - except Exception: - # fallback to a plain QWidget container if QGroupBox creation fails - try: - container = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout(container) - layout.setContentsMargins(6, 6, 6, 6) - layout.setSpacing(4) - self._backend_widget = container - self._group_layout = layout - if getattr(self, "_child", None): - try: - w = self._child.get_backend_widget() - if w: - layout.addWidget(w) - except Exception: - pass - except Exception: - self._backend_widget = None - self._group_layout = None - - def _set_backend_enabled(self, enabled): - """Enable/disable the frame and propagate state to the child.""" - try: - if getattr(self, "_backend_widget", None) is not None: - try: - self._backend_widget.setEnabled(bool(enabled)) - except Exception: - pass - except Exception: - pass - # propagate to logical child - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is not None: - try: - child.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - - def setProperty(self, propertyName, val): - """Handle simple properties; returns True if handled.""" - try: - if propertyName == "label": - self.setLabel(str(val)) - return True - except Exception: - pass - return False - - def getProperty(self, propertyName): - try: - if propertyName == "label": - return self.label() - except Exception: - pass - return None - - def propertySet(self): - """Return a minimal property set description (used by some backends).""" - try: - props = YPropertySet() - try: - props.add(YProperty("label", YPropertyType.YStringProperty)) - except Exception: - pass - return props - except Exception: - return None \ No newline at end of file diff --git a/setup.py b/setup.py index e10a481..098363f 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,43 @@ #!/usr/bin/env python3 from setuptools import setup -exec(open('manatools/version.py').read()) +import sys +import io +import os -try: - import yui -except ImportError: - import sys - print('Please install python3-yui in order to install this package', - file=sys.stderr) - sys.exit(1) +# load version variables from manatools/version.py +_version_file = os.path.join(os.path.dirname(__file__), "manatools", "version.py") +with io.open(_version_file, "r", encoding="utf-8") as vf: + exec(vf.read()) +# Read long description if README.md exists +_long_description = "" +_readme_path = os.path.join(os.path.dirname(__file__), "README.md") +if os.path.exists(_readme_path): + with io.open(_readme_path, "r", encoding="utf-8") as rf: + _long_description = rf.read() setup( - name=__project_name__, - version=__project_version__, - author='Angelo Naselli', - author_email='anaselli@linux.it', - packages=['manatools', 'manatools.aui', 'manatools.ui'], - #scripts=['scripts/'], - license='LGPLv2+', - description='Python ManaTools framework.', - long_description=open('README.md').read(), - #data_files=[('conf/manatools', ['XXX.yy',]), ], - install_requires=[ - "dbus-python", - "python-gettext", - "PyYAML", - ], + name=__project_name__, + version=__project_version__, + author='Angelo Naselli', + author_email='anaselli@linux.it', + packages=[ + 'manatools', + 'manatools.aui', + 'manatools.aui.backends.qt', + 'manatools.aui.backends.gtk', + 'manatools.aui.backends.curses', + 'manatools.ui' + ], + include_package_data=True, + license='LGPLv2+', + description='Python ManaTools framework.', + long_description=_long_description, + long_description_content_type="text/markdown", + install_requires=[ + "dbus-python", + "python-gettext", + "PyYAML", + ], ) From 9d549c4956a4a763a7b77d7430a048edcd6313fa Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 7 Dec 2025 21:45:14 +0100 Subject: [PATCH 114/523] moved to single file for any Gtk widget class definition --- manatools/aui/backends/gtk/__init__.py | 29 + manatools/aui/backends/gtk/alignmentgtk.py | 361 +++ manatools/aui/backends/gtk/checkboxgtk.py | 74 + manatools/aui/backends/gtk/comboboxgtk.py | 237 ++ manatools/aui/backends/gtk/dialoggtk.py | 311 ++ manatools/aui/backends/gtk/framegtk.py | 323 ++ manatools/aui/backends/gtk/hboxgtk.py | 87 + manatools/aui/backends/gtk/inputfieldgtk.py | 108 + manatools/aui/backends/gtk/labelgtk.py | 69 + manatools/aui/backends/gtk/pushbuttongtk.py | 76 + manatools/aui/backends/gtk/selectionboxgtk.py | 418 +++ manatools/aui/backends/gtk/treegtk.py | 752 +++++ manatools/aui/backends/gtk/vboxgtk.py | 96 + manatools/aui/yui_gtk.py | 2696 +---------------- 14 files changed, 2972 insertions(+), 2665 deletions(-) create mode 100644 manatools/aui/backends/gtk/__init__.py create mode 100644 manatools/aui/backends/gtk/alignmentgtk.py create mode 100644 manatools/aui/backends/gtk/checkboxgtk.py create mode 100644 manatools/aui/backends/gtk/comboboxgtk.py create mode 100644 manatools/aui/backends/gtk/dialoggtk.py create mode 100644 manatools/aui/backends/gtk/framegtk.py create mode 100644 manatools/aui/backends/gtk/hboxgtk.py create mode 100644 manatools/aui/backends/gtk/inputfieldgtk.py create mode 100644 manatools/aui/backends/gtk/labelgtk.py create mode 100644 manatools/aui/backends/gtk/pushbuttongtk.py create mode 100644 manatools/aui/backends/gtk/selectionboxgtk.py create mode 100644 manatools/aui/backends/gtk/treegtk.py create mode 100644 manatools/aui/backends/gtk/vboxgtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py new file mode 100644 index 0000000..81e5906 --- /dev/null +++ b/manatools/aui/backends/gtk/__init__.py @@ -0,0 +1,29 @@ +from .dialoggtk import YDialogGtk +from .checkboxgtk import YCheckBoxGtk +from .hboxgtk import YHBoxGtk +from .vboxgtk import YVBoxGtk +from .labelgtk import YLabelGtk +from .pushbuttongtk import YPushButtonGtk +from .treegtk import YTreeGtk +from .alignmentgtk import YAlignmentGtk +from .comboboxgtk import YComboBoxGtk +from .framegtk import YFrameGtk +from .inputfieldgtk import YInputFieldGtk +from .selectionboxgtk import YSelectionBoxGtk + + +__all__ = [ + "YDialogGtk", + "YFrameGtk", + "YVBoxGtk", + "YHBoxGtk", + "YTreeGtk", + "YSelectionBoxGtk", + "YLabelGtk", + "YPushButtonGtk", + "YInputFieldGtk", + "YCheckBoxGtk", + "YComboBoxGtk", + "YAlignmentGtk", + # ... +] diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py new file mode 100644 index 0000000..ac18f9b --- /dev/null +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -0,0 +1,361 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YAlignmentGtk(YSingleChildContainerWidget): + """ + GTK4 implementation of YAlignment. + + - Uses a Gtk.Box as a lightweight container that requests expansion when + needed so child halign/valign can take effect (matches the small GTK sample). + - Applies halign/valign hints to the child's backend widget. + - Defers attaching the child if its backend is not yet created (GLib.idle_add). + - Supports an optional repeating background pixbuf painted in the draw signal. + """ + def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): + super().__init__(parent) + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._background_pixbuf = None + self._signal_id = None + self._backend_widget = None + # schedule guard for deferred attach + self._attach_scheduled = False + # Track if we've already attached a child + self._child_attached = False + + def widgetClass(self): + return "YAlignment" + + def _to_gtk_halign(self): + """Convert Horizontal YAlignmentType to Gtk.Align or None.""" + if self._halign_spec: + if self._halign_spec == YAlignmentType.YAlignBegin: + return Gtk.Align.START + if self._halign_spec == YAlignmentType.YAlignCenter: + return Gtk.Align.CENTER + if self._halign_spec == YAlignmentType.YAlignEnd: + return Gtk.Align.END + return None + + def _to_gtk_valign(self): + """Convert Vertical YAlignmentType to Gtk.Align or None.""" + if self._valign_spec: + if self._valign_spec == YAlignmentType.YAlignBegin: + return Gtk.Align.START + if self._valign_spec == YAlignmentType.YAlignCenter: + return Gtk.Align.CENTER + if self._valign_spec == YAlignmentType.YAlignEnd: + return Gtk.Align.END + return None + + #def stretchable(self, dim): + # """Report whether this alignment should expand in given dimension. + # + # Parents (HBox/VBox) use this to distribute space. + # """ + # try: + # if dim == YUIDimension.YD_HORIZ: + # align = self._to_gtk_halign() + # return align in (Gtk.Align.CENTER, Gtk.Align.END) #TODO: verify + # if dim == YUIDimension.YD_VERT: + # align = self._to_gtk_valign() + # return align == Gtk.Align.CENTER #TODO: verify + # except Exception: + # pass + # return False + + def stretchable(self, dim: YUIDimension): + ''' Returns the stretchability of the layout box: + * The layout box is stretchable if the child is stretchable in + * this dimension or if the child widget has a layout weight in + * this dimension. + ''' + if self._child: + expand = bool(self._child.stretchable(dim)) + weight = bool(self._child.weight(dim)) + if expand or weight: + return True + return False + + def setBackgroundPixmap(self, filename): + """Set a repeating background pixbuf and connect draw handler.""" + # disconnect previous handler + if self._signal_id and self._backend_widget: + try: + self._backend_widget.disconnect(self._signal_id) + except Exception: + pass + self._signal_id = None + + # release previous pixbuf if present + self._background_pixbuf = None + + if filename: + try: + self._background_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + if self._backend_widget: + self._signal_id = self._backend_widget.connect("draw", self._on_draw) + self._backend_widget.queue_draw() # Trigger redraw + except Exception as e: + print(f"Failed to load background image: {e}") + self._background_pixbuf = None + + def _on_draw(self, widget, cr): + """Draw callback that tiles the background pixbuf.""" + if not self._background_pixbuf: + return False + try: + # Get actual allocation + width = widget.get_allocated_width() + height = widget.get_allocated_height() + + Gdk.cairo_set_source_pixbuf(cr, self._background_pixbuf, 0, 0) + # set repeat + pat = cr.get_source() + pat.set_extend(cairo.Extend.REPEAT) + cr.rectangle(0, 0, width, height) + cr.fill() + except Exception as e: + print(f"Error drawing background: {e}") + return False + + def addChild(self, child): + """Keep base behavior and ensure we attempt to attach child's backend.""" + try: + super().addChild(child) + except Exception: + self._child = child + self._child_attached = False + self._schedule_attach_child() + + def setChild(self, child): + """Keep base behavior and ensure we attempt to attach child's backend.""" + try: + super().setChild(child) + except Exception: + self._child = child + self._child_attached = False + self._schedule_attach_child() + + def _schedule_attach_child(self): + """Schedule a single idle callback to attach child backend later.""" + if self._attach_scheduled or self._child_attached: + return + self._attach_scheduled = True + + def _idle_cb(): + self._attach_scheduled = False + try: + self._ensure_child_attached() + except Exception as e: + print(f"Error attaching child: {e}") + return False + + try: + GLib.idle_add(_idle_cb) + except Exception: + # fallback: call synchronously if idle_add not available + _idle_cb() + + def _ensure_child_attached(self): + """Attach child's backend to our container, apply alignment hints.""" + if self._backend_widget is None: + self._create_backend_widget() + return + + # choose child reference (support _child or _children storage) + child = getattr(self, "_child", None) + if child is None: + try: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + except Exception: + child = None + if child is None: + return + + # get child's backend widget + try: + cw = child.get_backend_widget() + except Exception: + cw = None + + if cw is None: + # child backend not yet ready; schedule again + if not self._child_attached: + self._schedule_attach_child() + return + + # convert specs -> Gtk.Align + hal = self._to_gtk_halign() + val = self._to_gtk_valign() + + # Apply alignment and expansion hints to child + try: + # Set horizontal alignment and expansion + if hasattr(cw, "set_halign"): + if hal is not None: + cw.set_halign(hal) + else: + cw.set_halign(Gtk.Align.FILL) + + # Request expansion for alignment to work properly + cw.set_hexpand(True) + + # Set vertical alignment and expansion + if hasattr(cw, "set_valign"): + if val is not None: + cw.set_valign(val) + else: + cw.set_valign(Gtk.Align.FILL) + + # Request expansion for alignment to work properly + cw.set_vexpand(True) + + except Exception as e: + print(f"Error setting alignment properties: {e}") + + # If the child widget is already parented to us, nothing to do + parent_of_cw = None + try: + if hasattr(cw, 'get_parent'): + parent_of_cw = cw.get_parent() + except Exception: + parent_of_cw = None + + if parent_of_cw == self._backend_widget: + self._child_attached = True + return + + # Remove any existing children from our container + try: + # In GTK4, we need to remove all existing children + while True: + child_widget = self._backend_widget.get_first_child() + if child_widget is None: + break + self._backend_widget.remove(child_widget) + except Exception as e: + print(f"Error removing existing children: {e}") + + # Append child to our box - this is the critical fix for GTK4 + try: + self._backend_widget.append(cw) + self._child_attached = True + print(f"Successfully attached child {child.widgetClass()} {child.debugLabel()} to alignment container") + except Exception as e: + print(f"Error appending child: {e}") + # Try alternative method for GTK4 + try: + self._backend_widget.set_child(cw) + self._child_attached = True + print(f"Successfully set child {child.widgetClass()} {child.debugLabel()} using set_child()") + except Exception as e2: + print(f"Error setting child: {e2}") + + def _create_backend_widget(self): + """Create a Box container oriented to allow alignment to work. + + In GTK4, we use a simple Box that expands in both directions + to provide space for the child widget to align within. + """ + try: + # Use a box that can expand in both directions + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + + # Make the box expand to fill available space + box.set_hexpand(True) + box.set_vexpand(True) + + # Set the box to fill its allocation so child has space to align + box.set_halign(Gtk.Align.FILL) + box.set_valign(Gtk.Align.FILL) + + except Exception as e: + print(f"Error creating backend widget: {e}") + box = Gtk.Box() + + self._backend_widget = box + + # Connect draw handler if we have a background pixbuf + if self._background_pixbuf and not self._signal_id: + try: + self._signal_id = box.connect("draw", self._on_draw) + except Exception as e: + print(f"Error connecting draw signal: {e}") + self._signal_id = None + + # Mark that backend is ready and attempt to attach child + self._ensure_child_attached() + + def get_backend_widget(self): + """Return the backend GTK widget.""" + if self._backend_widget is None: + self._create_backend_widget() + return self._backend_widget + + def setSize(self, width, height): + """Set size of the alignment widget.""" + if self._backend_widget: + if width > 0 and height > 0: + self._backend_widget.set_size_request(width, height) + else: + self._backend_widget.set_size_request(-1, -1) + + def setEnabled(self, enabled): + """Set widget enabled state.""" + if self._backend_widget: + self._backend_widget.set_sensitive(enabled) + super().setEnabled(enabled) + + def setVisible(self, visible): + """Set widget visibility.""" + if self._backend_widget: + try: + self._backend_widget.set_visible(visible) + except Exception: + pass + super().setVisible(visible) + + def _set_backend_enabled(self, enabled): + """Enable/disable the alignment container and its child (if any).""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate to logical child so child's backend updates too + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/gtk/checkboxgtk.py b/manatools/aui/backends/gtk/checkboxgtk.py new file mode 100644 index 0000000..4124014 --- /dev/null +++ b/manatools/aui/backends/gtk/checkboxgtk.py @@ -0,0 +1,74 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YCheckBoxGtk(YWidget): + def __init__(self, parent=None, label="", is_checked=False): + super().__init__(parent) + self._label = label + self._is_checked = is_checked + + def widgetClass(self): + return "YCheckBox" + + def value(self): + return self._is_checked + + def setValue(self, checked): + self._is_checked = checked + if self._backend_widget: + try: + self._backend_widget.set_active(checked) + except Exception: + pass + + def label(self): + return self._label + + def _create_backend_widget(self): + self._backend_widget = Gtk.CheckButton(label=self._label) + try: + self._backend_widget.set_active(self._is_checked) + self._backend_widget.connect("toggled", self._on_toggled) + except Exception: + pass + + def _on_toggled(self, button): + try: + self._is_checked = button.get_active() + except Exception: + self._is_checked = bool(self._is_checked) + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + + def _set_backend_enabled(self, enabled): + """Enable/disable the check button backend.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py new file mode 100644 index 0000000..52a1444 --- /dev/null +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -0,0 +1,237 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YComboBoxGtk(YSelectionWidget): + def __init__(self, parent=None, label="", editable=False): + super().__init__(parent) + self._label = label + self._editable = editable + self._value = "" + self._selected_items = [] + self._combo_widget = None + + def widgetClass(self): + return "YComboBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if self._combo_widget: + try: + # try entry child for editable combos + child = None + if self._editable: + child = self._combo_widget.get_child() + if child and hasattr(child, "set_text"): + child.set_text(text) + else: + # attempt to set active by matching text if API available + if hasattr(self._combo_widget, "set_active_id"): + # Gtk.DropDown uses ids in models; we keep simple and try to match by text + # fallback: rebuild model and select programmatically below + pass + # update selected_items + self._selected_items = [it for it in self._items if it.label() == text][:1] + except Exception: + pass + + def editable(self): + return self._editable + + def _create_backend_widget(self): + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + + if self._label: + label = Gtk.Label(label=self._label) + try: + if hasattr(label, "set_xalign"): + label.set_xalign(0.0) + except Exception: + pass + try: + hbox.append(label) + except Exception: + hbox.add(label) + + # For Gtk4 there is no ComboBoxText; try DropDown for non-editable, + # and Entry for editable combos (simple fallback). + if self._editable: + entry = Gtk.Entry() + entry.set_text(self._value) + entry.connect("changed", self._on_text_changed) + self._combo_widget = entry + try: + hbox.append(entry) + except Exception: + hbox.add(entry) + else: + # Build a simple Gtk.DropDown backed by a Gtk.StringList (if available) + try: + if hasattr(Gtk, "StringList") and hasattr(Gtk, "DropDown"): + model = Gtk.StringList() + for it in self._items: + model.append(it.label()) + dropdown = Gtk.DropDown.new(model, None) + # select initial value + if self._value: + for idx, it in enumerate(self._items): + if it.label() == self._value: + dropdown.set_selected(idx) + break + dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) + self._combo_widget = dropdown + hbox.append(dropdown) + else: + # fallback: simple Gtk.Button that cycles items on click (very simple) + btn = Gtk.Button(label=self._value or (self._items[0].label() if self._items else "")) + btn.connect("clicked", self._on_fallback_button_clicked) + self._combo_widget = btn + hbox.append(btn) + except Exception: + # final fallback: entry + entry = Gtk.Entry() + entry.set_text(self._value) + entry.connect("changed", self._on_text_changed) + self._combo_widget = entry + hbox.append(entry) + + self._backend_widget = hbox + + def _set_backend_enabled(self, enabled): + """Enable/disable the combobox/backing widget and its entry/dropdown.""" + try: + # prefer to enable the primary control if present + ctl = getattr(self, "_combo_widget", None) + if ctl is not None: + try: + ctl.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + + def _on_fallback_button_clicked(self, btn): + # naive cycle through items + if not self._items: + return + current = btn.get_label() + labels = [it.label() for it in self._items] + try: + idx = labels.index(current) + idx = (idx + 1) % len(labels) + except Exception: + idx = 0 + new = labels[idx] + btn.set_label(new) + self.setValue(new) + if self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def _on_text_changed(self, entry): + try: + text = entry.get_text() + except Exception: + text = "" + self._value = text + self._selected_items = [it for it in self._items if it.label() == self._value][:1] + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def _on_changed_dropdown(self, dropdown): + try: + # Prefer using the selected index to get a reliable label + idx = None + try: + idx = dropdown.get_selected() + except Exception: + idx = None + + if isinstance(idx, int) and 0 <= idx < len(self._items): + self._value = self._items[idx].label() + else: + # Fallback: try to extract text from the selected-item object + val = None + try: + val = dropdown.get_selected_item() + except Exception: + val = None + + self._value = "" + if isinstance(val, str): + self._value = val + elif val is not None: + # Try common accessor names that GTK objects may expose + for meth in ("get_string", "get_text", "get_value", "get_label", "get_name", "to_string"): + try: + fn = getattr(val, meth, None) + if callable(fn): + v = fn() + if isinstance(v, str) and v: + self._value = v + break + except Exception: + continue + # Try properties if available + if not self._value: + try: + props = getattr(val, "props", None) + if props: + for attr in ("string", "value", "label", "name", "text"): + try: + pv = getattr(props, attr) + if isinstance(pv, str) and pv: + self._value = pv + break + except Exception: + pass + except Exception: + pass + # final fallback to str() + if not self._value: + try: + self._value = str(val) + except Exception: + self._value = "" + + # update selected_items using reliable labels + self._selected_items = [it for it in self._items if it.label() == self._value][:1] + except Exception: + pass + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py new file mode 100644 index 0000000..347bc68 --- /dev/null +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -0,0 +1,311 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + +class YDialogGtk(YSingleChildContainerWidget): + _open_dialogs = [] + + def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): + super().__init__() + self._dialog_type = dialog_type + self._color_mode = color_mode + self._is_open = False + self._window = None + self._event_result = None + self._glib_loop = None + YDialogGtk._open_dialogs.append(self) + + def widgetClass(self): + return "YDialog" + + @staticmethod + def currentDialog(doThrow=True): + open_dialog = YDialogGtk._open_dialogs[-1] if YDialogGtk._open_dialogs else None + if not open_dialog and doThrow: + raise YUINoDialogException("No dialog is currently open") + return open_dialog + + @staticmethod + def topmostDialog(doThrow=True): + ''' same as currentDialog ''' + return YDialogGtk.currentDialog(doThrow=doThrow) + + def isTopmostDialog(self): + '''Return whether this dialog is the topmost open dialog.''' + return YDialogGtk._open_dialogs[-1] == self if YDialogGtk._open_dialogs else False + + def open(self): + # Finalize and show the dialog in a non-blocking way. + if not self._is_open: + if not self._window: + self._create_backend_widget() + # in Gtk4, show_all is not recommended; use present() or show + try: + self._window.present() + except Exception: + try: + self._window.show() + except Exception: + pass + self._is_open = True + + def isOpen(self): + return self._is_open + + def destroy(self, doThrow=True): + if self._window: + try: + self._window.destroy() + except Exception: + try: + self._window.close() + except Exception: + pass + self._window = None + self._is_open = False + if self in YDialogGtk._open_dialogs: + YDialogGtk._open_dialogs.remove(self) + + # Stop GLib main loop if no dialogs left (nested loops only) + if not YDialogGtk._open_dialogs: + try: + if self._glib_loop and self._glib_loop.is_running(): + self._glib_loop.quit() + except Exception: + pass + return True + + def _post_event(self, event): + """Internal: post an event to this dialog and quit local GLib.MainLoop if running.""" + self._event_result = event + if self._glib_loop is not None and self._glib_loop.is_running(): + try: + self._glib_loop.quit() + except Exception: + pass + + def waitForEvent(self, timeout_millisec=0): + """ + Run a GLib.MainLoop until an event is posted or timeout occurs. + Returns a YEvent (YWidgetEvent, YTimeoutEvent, ...). + """ + # Ensure dialog is finalized/open (finalize if caller didn't call open()). + if not self.isOpen(): + self.open() + + # Let GTK/GLib process pending events (show/layout) before entering nested loop. + # Gtk.events_pending()/Gtk.main_iteration() do not exist in GTK4; use MainContext iteration. + try: + ctx = GLib.MainContext.default() + while ctx.pending(): + ctx.iteration(False) + except Exception: + # be defensive if API differs on some bindings + pass + + self._event_result = None + self._glib_loop = GLib.MainLoop() + + def on_timeout(): + # post timeout event and quit loop + self._event_result = YTimeoutEvent() + try: + if self._glib_loop.is_running(): + self._glib_loop.quit() + except Exception: + pass + return False # don't repeat + + self._timeout_id = None + if timeout_millisec and timeout_millisec > 0: + self._timeout_id = GLib.timeout_add(timeout_millisec, on_timeout) + + # run nested loop + self._glib_loop.run() + + # cleanup + if self._timeout_id: + try: + GLib.source_remove(self._timeout_id) + except Exception: + pass + self._timeout_id = None + self._glib_loop = None + return self._event_result if self._event_result is not None else YEvent() + + @classmethod + def deleteTopmostDialog(cls, doThrow=True): + if cls._open_dialogs: + dialog = cls._open_dialogs[-1] + return dialog.destroy(doThrow) + return False + + @classmethod + def currentDialog(cls, doThrow=True): + if not cls._open_dialogs: + if doThrow: + raise YUINoDialogException("No dialog open") + return None + return cls._open_dialogs[-1] + + def _create_backend_widget(self): + # Determine window title from YApplicationGtk instance stored on the YUI backend + title = "Manatools YUI GTK Dialog" + try: + from . import yui as yui_mod + appobj = None + # YUI._backend may hold the backend instance (YUIGtk) + backend = getattr(yui_mod.YUI, "_backend", None) + if backend and hasattr(backend, "application"): + appobj = backend.application() + # fallback: YUI._instance might be set and expose application/yApp + if not appobj: + inst = getattr(yui_mod.YUI, "_instance", None) + if inst and hasattr(inst, "application"): + appobj = inst.application() + if appobj and hasattr(appobj, "applicationTitle"): + atitle = appobj.applicationTitle() + if atitle: + title = atitle + # try to obtain resolved pixbuf from application and store for window icon + _resolved_pixbuf = None + try: + _resolved_pixbuf = getattr(appobj, "_gtk_icon_pixbuf", None) + except Exception: + _resolved_pixbuf = None + except Exception: + pass + + # Create Gtk4 Window + self._window = Gtk.Window(title=title) + # set window icon if available + try: + if _resolved_pixbuf is not None: + try: + self._window.set_icon(_resolved_pixbuf) + except Exception: + try: + # fallback to name if pixbuf not accepted + icname = getattr(appobj, "applicationIcon", lambda : None)() + if icname: + self._window.set_icon_name(icname) + except Exception: + pass + else: + try: + # try setting icon name if application provided it + icname = getattr(appobj, "applicationIcon", lambda : None)() + if icname: + self._window.set_icon_name(icname) + except Exception: + pass + except Exception: + pass + try: + self._window.set_default_size(600, 400) + except Exception: + pass + + # Content container with margins (window.set_child used in Gtk4) + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + content.set_margin_start(10) + content.set_margin_end(10) + content.set_margin_top(10) + content.set_margin_bottom(10) + + if self._child: + child_widget = self._child.get_backend_widget() + # ensure child is shown properly + try: + content.append(child_widget) + except Exception: + try: + content.add(child_widget) + except Exception: + pass + + try: + self._window.set_child(content) + except Exception: + # fallback for older bindings + try: + self._window.add(content) + except Exception: + pass + + self._backend_widget = self._window + # Connect destroy/close handlers + try: + # Gtk4: use 'close-request' if available, otherwise 'destroy' + if hasattr(self._window, "connect"): + try: + self._window.connect("close-request", self._on_delete_event) + except Exception: + try: + self._window.connect("destroy", self._on_destroy) + except Exception: + pass + except Exception: + pass + + def _on_destroy(self, widget): + try: + self.destroy() + except Exception: + pass + + def _on_delete_event(self, *args): + # close-request handler in Gtk4: post cancel event and destroy + try: + self._post_event(YCancelEvent()) + except Exception: + pass + try: + self.destroy() + except Exception: + pass + # returning False/True not used in this simplified handler + return False + + def setVisible(self, visible): + """Set widget visibility.""" + if self._backend_widget: + try: + self._backend_widget.set_visible(visible) + except Exception: + pass + super().setVisible(visible) + + def _set_backend_enabled(self, enabled): + """Enable/disable the dialog window backend.""" + try: + if self._window is not None: + try: + self._window.set_sensitive(enabled) + except Exception: + # fallback: propagate to child content + try: + child = getattr(self, "_window", None) + if child and hasattr(child, "set_sensitive"): + child.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/gtk/framegtk.py b/manatools/aui/backends/gtk/framegtk.py new file mode 100644 index 0000000..948a5bd --- /dev/null +++ b/manatools/aui/backends/gtk/framegtk.py @@ -0,0 +1,323 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YFrameGtk(YSingleChildContainerWidget): + """ + GTK backend implementation of YFrame. + + - Uses Gtk.Frame (when available) to present a labeled framed container. + - Internally places a Gtk.Box inside the frame to host the single child. + - Honors child's stretchability: the frame reports stretchable when its child is stretchable + so parent layouts can allocate extra space. + - Provides simple property support for 'label'. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._label = label or "" + self._backend_widget = None + self._content_box = None + + def widgetClass(self): + return "YFrame" + + def label(self): + return self._label + + def setLabel(self, new_label: str): + """Set the frame label and update backend if created.""" + try: + self._label = new_label or "" + if getattr(self, "_backend_widget", None) is not None: + try: + # Gtk.Frame in GTK4 supports set_label() in some bindings, else use a child label + if hasattr(self._backend_widget, "set_label"): + self._backend_widget.set_label(self._label) + else: + # fallback: if we created a dedicated label child, update it + if getattr(self, "_label_widget", None) is not None: + try: + self._label_widget.set_text(self._label) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def stretchable(self, dim: YUIDimension): + """ + Report stretchability in a dimension. + + The frame is stretchable when its child is stretchable or has a layout weight. + """ + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return False + try: + if bool(child.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(child.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def _attach_child_backend(self): + """Attach the child's backend widget into the frame's content box.""" + try: + if self._backend_widget is None: + return + if self._content_box is None: + return + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return + try: + cw = child.get_backend_widget() + except Exception: + cw = None + if cw is None: + return + + # Remove existing content children (defensive) + try: + while True: + first = self._content_box.get_first_child() + if first is None: + break + try: + self._content_box.remove(first) + except Exception: + break + except Exception: + pass + + # Append child widget into content box + try: + self._content_box.append(cw) + except Exception: + try: + self._content_box.add(cw) + except Exception: + pass + + # Ensure expansion hints propagate from child + try: + if child.stretchable(YUIDimension.YD_VERT): + if hasattr(cw, "set_vexpand"): + cw.set_vexpand(True) + if hasattr(cw, "set_valign"): + cw.set_valign(Gtk.Align.FILL) + else: + if hasattr(cw, "set_vexpand"): + cw.set_vexpand(False) + if hasattr(cw, "set_valign"): + cw.set_valign(Gtk.Align.START) + if child.stretchable(YUIDimension.YD_HORIZ): + if hasattr(cw, "set_hexpand"): + cw.set_hexpand(True) + if hasattr(cw, "set_halign"): + cw.set_halign(Gtk.Align.FILL) + else: + if hasattr(cw, "set_hexpand"): + cw.set_hexpand(False) + if hasattr(cw, "set_halign"): + cw.set_halign(Gtk.Align.START) + except Exception: + pass + except Exception: + pass + + def addChild(self, child): + """Add logical child and attach backend if possible.""" + try: + super().addChild(child) + except Exception: + # best-effort fallback + try: + self._child = child + child._parent = self + except Exception: + pass + # attach to backend if ready + try: + if getattr(self, "_backend_widget", None) is not None: + self._attach_child_backend() + except Exception: + pass + + def setChild(self, child): + """Set single logical child and attach backend if possible.""" + try: + super().setChild(child) + except Exception: + try: + self._child = child + child._parent = self + except Exception: + pass + try: + if getattr(self, "_backend_widget", None) is not None: + self._attach_child_backend() + except Exception: + pass + + def _create_backend_widget(self): + """ + Create a Gtk.Frame + inner box to host the single child. + Fall back to a bordered Gtk.Box when Gtk.Frame or set_label is not available. + """ + try: + # Try to create a Gtk.Frame with a label if supported + try: + frame = Gtk.Frame() + # set label if API supports it + if hasattr(frame, "set_label"): + frame.set_label(self._label) + self._label_widget = None + else: + # create a label widget and set as label using set_label_widget if supported + lbl = Gtk.Label(label=self._label) + self._label_widget = lbl + if hasattr(frame, "set_label_widget"): + frame.set_label_widget(lbl) + # Create inner content box + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + content.set_hexpand(True) + content.set_vexpand(True) + # Append content inside frame. In GTK4 a Frame can have a single child. + try: + frame.set_child(content) + except Exception: + try: + # fallback: some bindings use add() + frame.add(content) + except Exception: + pass + self._backend_widget = frame + self._content_box = content + # attach existing child if any + try: + if getattr(self, "_child", None): + self._attach_child_backend() + except Exception: + pass + return + except Exception: + # fallback to a boxed container with a visible border using CSS if Frame creation fails + pass + + # Fallback container: vertical box with a top label and a framed-like border (best-effort) + container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + try: + lbl = Gtk.Label(label=self._label) + lbl.set_xalign(0.0) + container.append(lbl) + # content area + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + content.set_hexpand(True) + content.set_vexpand(True) + container.append(content) + self._label_widget = lbl + self._backend_widget = container + self._content_box = content + if getattr(self, "_child", None): + try: + self._attach_child_backend() + except Exception: + pass + except Exception: + # ultimate fallback: empty widget reference + self._backend_widget = None + self._content_box = None + except Exception: + self._backend_widget = None + self._content_box = None + + def _set_backend_enabled(self, enabled): + """Enable/disable the frame and propagate to child.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate to logical child + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None: + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def setProperty(self, propertyName, val): + """Handle simple properties; returns True if property handled here.""" + try: + if propertyName == "label": + try: + self.setLabel(str(val)) + except Exception: + pass + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "label": + return self.label() + except Exception: + pass + return None + + def propertySet(self): + """Return a minimal property set description for introspection.""" + try: + props = YPropertySet() + try: + props.add(YProperty("label", YPropertyType.YStringProperty)) + except Exception: + pass + return props + except Exception: + return None diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py new file mode 100644 index 0000000..82012de --- /dev/null +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -0,0 +1,87 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YHBoxGtk(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YHBox" + + def stretchable(self, dim): + for child in self._children: + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + return False + + def _create_backend_widget(self): + self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + + for child in self._children: + print("HBox child: ", child.widgetClass()) + widget = child.get_backend_widget() + expand = bool(child.stretchable(YUIDimension.YD_HORIZ)) + fill = True + padding = 0 + try: + if expand: + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(True) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.FILL) + else: + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(False) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.START) + except Exception: + pass + + try: + self._backend_widget.append(widget) + except Exception: + try: + self._backend_widget.add(widget) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + """Enable/disable the HBox and propagate to children.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + diff --git a/manatools/aui/backends/gtk/inputfieldgtk.py b/manatools/aui/backends/gtk/inputfieldgtk.py new file mode 100644 index 0000000..e4b555c --- /dev/null +++ b/manatools/aui/backends/gtk/inputfieldgtk.py @@ -0,0 +1,108 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YInputFieldGtk(YWidget): + def __init__(self, parent=None, label="", password_mode=False): + super().__init__(parent) + self._label = label + self._value = "" + self._password_mode = password_mode + + def widgetClass(self): + return "YInputField" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + if hasattr(self, '_entry_widget') and self._entry_widget: + try: + self._entry_widget.set_text(text) + except Exception: + pass + + def label(self): + return self._label + + def _create_backend_widget(self): + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + + if self._label: + label = Gtk.Label(label=self._label) + try: + if hasattr(label, "set_xalign"): + label.set_xalign(0.0) + except Exception: + pass + try: + hbox.append(label) + except Exception: + hbox.add(label) + + if self._password_mode: + entry = Gtk.Entry() + try: + entry.set_visibility(False) + except Exception: + pass + else: + entry = Gtk.Entry() + + try: + entry.set_text(self._value) + entry.connect("changed", self._on_changed) + except Exception: + pass + + try: + hbox.append(entry) + except Exception: + hbox.add(entry) + + self._backend_widget = hbox + self._entry_widget = entry + + def _on_changed(self, entry): + try: + self._value = entry.get_text() + except Exception: + self._value = "" + + def _set_backend_enabled(self, enabled): + """Enable/disable the input field (entry and container).""" + try: + if getattr(self, "_entry_widget", None) is not None: + try: + self._entry_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py new file mode 100644 index 0000000..d06bc42 --- /dev/null +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -0,0 +1,69 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YLabelGtk(YWidget): + def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): + super().__init__(parent) + self._text = text + self._is_heading = isHeading + self._is_output_field = isOutputField + + def widgetClass(self): + return "YLabel" + + def text(self): + return self._text + + def setText(self, new_text): + self._text = new_text + if self._backend_widget: + try: + self._backend_widget.set_text(new_text) + except Exception: + pass + + def _create_backend_widget(self): + self._backend_widget = Gtk.Label(label=self._text) + try: + # alignment API in Gtk4 differs; fall back to setting xalign if available + if hasattr(self._backend_widget, "set_xalign"): + self._backend_widget.set_xalign(0.0) + except Exception: + pass + + if self._is_heading: + try: + markup = f"{self._text}" + self._backend_widget.set_markup(markup) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + """Enable/disable the label widget backend.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py new file mode 100644 index 0000000..de59b2a --- /dev/null +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -0,0 +1,76 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YPushButtonGtk(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + + def widgetClass(self): + return "YPushButton" + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + if self._backend_widget: + try: + self._backend_widget.set_label(label) + except Exception: + pass + + def _create_backend_widget(self): + self._backend_widget = Gtk.Button(label=self._label) + # Prevent button from being stretched horizontally by default. + try: + if hasattr(self._backend_widget, "set_hexpand"): + self._backend_widget.set_hexpand(False) + if hasattr(self._backend_widget, "set_halign"): + self._backend_widget.set_halign(Gtk.Align.START) + except Exception: + pass + try: + self._backend_widget.connect("clicked", self._on_clicked) + except Exception: + pass + + def _on_clicked(self, button): + if self.notify() is False: + return + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + else: + # silent fallback + pass + + def _set_backend_enabled(self, enabled): + """Enable/disable the push button backend.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py new file mode 100644 index 0000000..f647919 --- /dev/null +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -0,0 +1,418 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YSelectionBoxGtk(YSelectionWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + self._selected_items = [] + self._multi_selection = False + self._listbox = None + self._backend_widget = None + # keep a stable list of rows we create so we don't rely on ListBox container APIs + # (GTK4 bindings may not expose get_children()) + self._rows = [] + # Preferred visible rows for layout/paging; parent can give more space when stretchable + self._preferred_rows = 6 + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YSelectionBox" + + def label(self): + return self._label + + def value(self): + return self._value + + def setValue(self, text): + """Select first item matching text.""" + self._value = text + self._selected_items = [it for it in self._items if it.label() == text] + if self._listbox is None: + return + # find and select corresponding row using the cached rows list + for i, row in enumerate(getattr(self, "_rows", [])): + if i >= len(self._items): + continue + try: + if self._items[i].label() == text: + row.set_selectable(True) + row.set_selected(True) + else: + # ensure others are not selected in single-selection mode + if not self._multi_selection: + row.set_selected(False) + except Exception: + pass + # notify + self._on_selection_changed() + + def selectedItems(self): + return list(self._selected_items) + + def selectItem(self, item, selected=True): + if selected: + if not self._multi_selection: + self._selected_items = [item] + self._value = item.label() + else: + if item not in self._selected_items: + self._selected_items.append(item) + else: + if item in self._selected_items: + self._selected_items.remove(item) + self._value = self._selected_items[0].label() if self._selected_items else "" + + if self._listbox is None: + return + + # reflect change in UI + rows = getattr(self, "_rows", []) + for i, it in enumerate(self._items): + if it is item or it.label() == item.label(): + try: + row = rows[i] + row.set_selected(selected) + except Exception: + pass + break + self._on_selection_changed() + + def setMultiSelection(self, enabled): + self._multi_selection = bool(enabled) + # If listbox already created, update its selection mode at runtime. + if self._listbox is None: + return + try: + mode = Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE + self._listbox.set_selection_mode(mode) + except Exception: + pass + # Rewire signals: disconnect previous handlers and connect appropriate one. + try: + # Disconnect any previously stored handlers + try: + for key, hid in list(getattr(self, "_signal_handlers", {}).items()): + if hid and isinstance(hid, int): + try: + self._listbox.disconnect(hid) + except Exception: + pass + self._signal_handlers = {} + except Exception: + self._signal_handlers = {} + + # Connect new handler based on mode + if self._multi_selection: + try: + hid = self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) + self._signal_handlers['selected-rows-changed'] = hid + except Exception: + try: + hid = self._listbox.connect("row-selected", lambda lb, row: self._on_selected_rows_changed(lb)) + self._signal_handlers['row-selected_for_multi'] = hid + except Exception: + pass + else: + try: + hid = self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + self._signal_handlers['row-selected'] = hid + except Exception: + pass + except Exception: + pass + + def multiSelection(self): + return bool(self._multi_selection) + + def _create_backend_widget(self): + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + if self._label: + lbl = Gtk.Label(label=self._label) + try: + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + except Exception: + pass + try: + vbox.append(lbl) + except Exception: + vbox.add(lbl) + + # Use Gtk.ListBox inside a ScrolledWindow for Gtk4 + listbox = Gtk.ListBox() + listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE) + # allow listbox to expand if parent allocates more space + try: + listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + pass + # populate rows + self._rows = [] + for it in self._items: + row = Gtk.ListBoxRow() + lbl = Gtk.Label(label=it.label() or "") + try: + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + except Exception: + pass + try: + row.set_child(lbl) + except Exception: + try: + row.add(lbl) + except Exception: + pass + + # Make every row selectable so users can multi-select if mode allows. + try: + row.set_selectable(True) + except Exception: + pass + + # If this item matches current value, mark selected + try: + if self._value and it.label() == self._value: + row.set_selectable(True) + row.set_selected(True) + except Exception: + pass + self._rows.append(row) + listbox.append(row) + + sw = Gtk.ScrolledWindow() + # allow scrolled window to expand vertically and horizontally + try: + sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + # give a reasonable minimum content height so layout initially shows several rows; + # Gtk4 expects pixels — try a conservative estimate (rows * ~20px) + min_h = int(getattr(self, "_preferred_rows", 6) * 20) + try: + # some Gtk4 bindings expose set_min_content_height + sw.set_min_content_height(min_h) + except Exception: + pass + except Exception: + pass + # policy APIs changed in Gtk4: use set_overlay_scrolling and set_min_content_height if needed + try: + sw.set_child(listbox) + except Exception: + try: + sw.add(listbox) + except Exception: + pass + + # also request vexpand on the outer vbox so parent layout sees it can grow + try: + vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + pass + + try: + vbox.append(sw) + except Exception: + vbox.add(sw) + + # connect selection signal: choose appropriate signal per selection mode + # store handler ids so we can disconnect later if selection mode changes at runtime + self._signal_handlers = {} + try: + # ensure any previous handlers are disconnected (defensive) + try: + for hid in list(self._signal_handlers.values()): + if hid and isinstance(hid, int): + try: + listbox.disconnect(hid) + except Exception: + pass + except Exception: + pass + + # Use row-selected for both single and multi modes; handler will toggle for multi + try: + hid = listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + self._signal_handlers['row-selected'] = hid + except Exception: + pass + except Exception: + pass + + self._backend_widget = vbox + self._listbox = listbox + + def _set_backend_enabled(self, enabled): + """Enable/disable the selection box and its listbox/rows.""" + try: + if getattr(self, "_listbox", None) is not None: + try: + self._listbox.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate logical enabled state to child items/widgets + try: + for c in list(getattr(self, "_rows", []) or []): + try: + c.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + + def _row_is_selected(self, r): + """Robust helper to detect whether a ListBoxRow is selected.""" + try: + return bool(r.get_selected()) + except Exception: + pass + try: + props = getattr(r, "props", None) + if props and hasattr(props, "selected"): + return bool(getattr(props, "selected")) + except Exception: + pass + return bool(getattr(r, "_selected_flag", False)) + + def _on_row_selected(self, listbox, row): + """ + Handler for row selection. In single-selection mode behaves as before + (select provided row and deselect others). In multi-selection mode toggles + the provided row and rebuilds the selected items list. + """ + try: + if row is not None: + if self._multi_selection: + # toggle selection state for this row + try: + cur = self._row_is_selected(row) + try: + row.set_selected(not cur) + except Exception: + # fallback: store a flag when set_selected isn't available + setattr(row, "_selected_flag", not cur) + except Exception: + pass + else: + # single-selection: select provided row and deselect others + for r in getattr(self, "_rows", []): + try: + r.set_selected(r is row) + except Exception: + try: + setattr(r, "_selected_flag", (r is row)) + except Exception: + pass + + # rebuild selected_items scanning cached rows (works for both modes) + self._selected_items = [] + for i, r in enumerate(getattr(self, "_rows", [])): + try: + if self._row_is_selected(r) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + + self._value = self._selected_items[0].label() if self._selected_items else None + except Exception: + # be defensive + self._selected_items = [] + self._value = None + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def _on_selected_rows_changed(self, listbox): + """ + Handler for multi-selection (or bulk selection change). Rebuild selected list + using either ListBox APIs (if available) or by scanning cached rows. + """ + try: + # Try to use any available API that returns selected rows + sel_rows = None + try: + # Some bindings may provide get_selected_rows() + sel_rows = listbox.get_selected_rows() + print(f"Using get_selected_rows() {len(sel_rows)} API") + except Exception: + sel_rows = None + + self._selected_items = [] + if sel_rows: + # sel_rows may be list of Row objects or Paths; try to match by identity + for r in sel_rows: + try: + # if r is a ListBoxRow already + if isinstance(r, type(self._rows[0])) if self._rows else False: + try: + idx = self._rows.index(r) + if idx < len(self._items): + self._selected_items.append(self._items[idx]) + except Exception: + pass + else: + # fallback: scan cached rows to find selected ones + for i, cr in enumerate(getattr(self, "_rows", [])): + try: + if self._row_is_selected(cr) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + except Exception: + pass + else: + # Generic fallback: scan cached rows and collect selected ones + for i, r in enumerate(getattr(self, "_rows", [])): + try: + if self._row_is_selected(r) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + + self._value = self._selected_items[0].label() if self._selected_items else None + except Exception: + self._selected_items = [] + self._value = None + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py new file mode 100644 index 0000000..0d17444 --- /dev/null +++ b/manatools/aui/backends/gtk/treegtk.py @@ -0,0 +1,752 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + + +class YTreeGtk(YSelectionWidget): + """ + Stable Gtk4 implementation of a tree using Gtk.ListBox + ScrolledWindow. + + - Renders visible nodes (respecting YTreeItem._is_open). + - Supports multiselection and recursiveSelection (select/deselect parents -> children). + - Preserves stretching: the ScrolledWindow/ListBox expand to fill container. + """ + def __init__(self, parent=None, label="", multiselection=False, recursiveselection=False): + super().__init__(parent) + self._label = label + self._multi = bool(multiselection) + self._recursive = bool(recursiveselection) + if self._recursive: + # recursive selection implies multi-selection semantics + self._multi = True + self._immediate = self.notify() + self._backend_widget = None + self._listbox = None + # cached rows and mappings + self._rows = [] # ordered list of Gtk.ListBoxRow + self._row_to_item = {} # row -> YTreeItem + self._item_to_row = {} # YTreeItem -> row + self._visible_items = [] # list of (item, depth) + self._suppress_selection_handler = False + self._last_selected_ids = set() + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YTree" + + def _create_backend_widget(self): + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + + if self._label: + try: + lbl = Gtk.Label(label=self._label) + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + vbox.append(lbl) + except Exception: + pass + + # ListBox (flat, shows only visible nodes). Put into ScrolledWindow so it won't grow parent on expand. + listbox = Gtk.ListBox() + try: + mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE + listbox.set_selection_mode(mode) + # Let listbox expand in available area + listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + pass + + sw = Gtk.ScrolledWindow() + try: + sw.set_child(listbox) + except Exception: + try: + sw.add(listbox) + except Exception: + pass + + # Make scrolled window expand to fill container (so tree respects parent stretching) + try: + sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + pass + + # connect selection signal; use defensive handler that scans rows + try: + listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + except Exception: + pass + + self._backend_widget = vbox + self._listbox = listbox + + try: + vbox.append(sw) + except Exception: + try: + vbox.add(sw) + except Exception: + pass + + # populate if items already exist + try: + if getattr(self, "_items", None): + self.rebuildTree() + except Exception: + pass + + def _make_row(self, item, depth): + """Create a ListBoxRow for item with indentation and (optional) toggle button.""" + row = Gtk.ListBoxRow() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + + # indentation spacer + try: + indent = Gtk.Box() + indent.set_size_request(depth * 12, 1) + hbox.append(indent) + except Exception: + pass + + # toggle if item has children + has_children = False + try: + childs = [] + if callable(getattr(item, "children", None)): + childs = item.children() or [] + else: + childs = getattr(item, "_children", []) or [] + has_children = len(childs) > 0 + except Exception: + has_children = False + + if has_children: + try: + btn = Gtk.Button(label="▾" if bool(getattr(item, "_is_open", False)) else "▸") + try: + btn.set_relief(Gtk.ReliefStyle.NONE) + except Exception: + pass + # prevent the toggle button from taking focus / causing selection side-effects + try: + btn.set_focus_on_click(False) + except Exception: + pass + try: + btn.set_can_focus(False) + except Exception: + pass + # make button visually flat (no border/background) so it looks like a tree expander + try: + btn.add_css_class("flat") + except Exception: + # fallback: try another common class name + try: + btn.add_css_class("link") + except Exception: + pass + + # Use a GestureClick on the button to reliably receive a single-click action + # and avoid the occasional need for double clicks caused by focus/selection interplay. + try: + gesture = Gtk.GestureClick() + # accept any button; if set_button exists restrict to primary + try: + gesture.set_button(0) + except Exception: + pass + # pressed handler will toggle immediately + def _on_pressed(gesture_obj, n_press, x, y, target_item=item): + # run toggle synchronously and suppress selection handler while rebuilding + try: + self._suppress_selection_handler = True + except Exception: + pass + try: + # toggle using public API if available + try: + cur = target_item.isOpen() + target_item.setOpen(not cur) + except Exception: + try: + cur = bool(getattr(target_item, "_is_open", False)) + target_item._is_open = not cur + except Exception: + pass + # preserve selection and rebuild + try: + self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) + except Exception: + self._last_selected_ids = set() + try: + self.rebuildTree() + except Exception: + pass + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass + + gesture.connect("pressed", _on_pressed) + try: + btn.add_controller(gesture) + except Exception: + try: + btn.add_controller(gesture) + except Exception: + pass + except Exception: + # Fallback to clicked if GestureClick not available + try: + btn.connect("clicked", lambda b, it=item: self._on_toggle_clicked(it)) + except Exception: + pass + hbox.append(btn) + except Exception: + # fallback spacer + try: + spacer = Gtk.Box() + spacer.set_size_request(14, 1) + hbox.append(spacer) + except Exception: + pass + else: + try: + spacer = Gtk.Box() + spacer.set_size_request(14, 1) + hbox.append(spacer) + except Exception: + pass + + # label + try: + lbl = Gtk.Label(label=item.label() if hasattr(item, "label") else str(item)) + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + # ensure label expands to take remaining space + try: + lbl.set_hexpand(True) + except Exception: + pass + hbox.append(lbl) + except Exception: + pass + + try: + row.set_child(hbox) + except Exception: + try: + row.add(hbox) + except Exception: + pass + + try: + row.set_selectable(True) + except Exception: + pass + + return row + + def _on_toggle_clicked(self, item): + """Toggle _is_open and rebuild, preserving selection.""" + try: + # Ensure a single-click toggle: suppress selection events during the operation + try: + self._suppress_selection_handler = True + except Exception: + pass + try: + try: + cur = item.isOpen() + item.setOpen(not cur) + except Exception: + try: + cur = bool(getattr(item, "_is_open", False)) + item._is_open = not cur + except Exception: + pass + # preserve selection ids and rebuild the visible rows + try: + self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) + except Exception: + self._last_selected_ids = set() + try: + self.rebuildTree() + except Exception: + pass + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass + except Exception: + pass + + def _collect_all_descendants(self, item): + """Return set of all descendant items (recursive).""" + out = set() + stack = [] + try: + for c in getattr(item, "_children", []) or []: + stack.append(c) + except Exception: + pass + while stack: + cur = stack.pop() + out.add(cur) + try: + for ch in getattr(cur, "_children", []) or []: + stack.append(ch) + except Exception: + pass + return out + + def rebuildTree(self): + """Flatten visible items according to _is_open and populate the ListBox.""" + if self._backend_widget is None or self._listbox is None: + self._create_backend_widget() + try: + # clear listbox rows robustly: repeatedly remove first child until none remain + try: + while True: + first = None + try: + first = self._listbox.get_first_child() + except Exception: + # some bindings may return None / raise; try children() + try: + chs = self._listbox.get_children() + first = chs[0] if chs else None + except Exception: + first = None + if not first: + break + try: + self._listbox.remove(first) + except Exception: + try: + # fallback API + self._listbox.unbind_model() + break + except Exception: + break + except Exception: + pass + + self._rows = [] + self._row_to_item.clear() + self._item_to_row.clear() + self._visible_items = [] + + # Depth-first traversal producing visible nodes only when ancestors are open + def _visit(nodes, depth=0): + for n in nodes: + self._visible_items.append((n, depth)) + try: + is_open = bool(getattr(n, "_is_open", False)) + except Exception: + is_open = False + if is_open: + try: + childs = [] + if callable(getattr(n, "children", None)): + childs = n.children() or [] + else: + childs = getattr(n, "_children", []) or [] + except Exception: + childs = getattr(n, "_children", []) or [] + if childs: + _visit(childs, depth + 1) + + roots = list(getattr(self, "_items", []) or []) + _visit(roots, 0) + + # create rows + for item, depth in self._visible_items: + try: + row = self._make_row(item, depth) + self._listbox.append(row) + self._rows.append(row) + self._row_to_item[row] = item + self._item_to_row[item] = row + except Exception: + pass + + # restore previous selection (visible rows only) + try: + if self._last_selected_ids: + self._suppress_selection_handler = True + try: + self._listbox.unselect_all() + except Exception: + pass + for row, item in list(self._row_to_item.items()): + try: + if id(item) in self._last_selected_ids: + try: + row.set_selected(True) + except Exception: + pass + except Exception: + pass + self._suppress_selection_handler = False + except Exception: + self._suppress_selection_handler = False + + # rebuild logical selected items from rows + self._selected_items = [] + for row in self._rows: + try: + if getattr(row, "get_selected", None): + sel = row.get_selected() + else: + sel = bool(getattr(row, "_selected_flag", False)) + if sel: + it = self._row_to_item.get(row, None) + if it is not None: + self._selected_items.append(it) + except Exception: + pass + + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + pass + + def _row_is_selected(self, r): + """Robust helper to detect whether a ListBoxRow is selected.""" + try: + # preferred API + sel = getattr(r, "get_selected", None) + if callable(sel): + return bool(sel()) + except Exception: + pass + try: + props = getattr(r, "props", None) + if props and hasattr(props, "selected"): + return bool(getattr(props, "selected")) + except Exception: + pass + # fallback: check whether the listbox reports this row as selected (some bindings) + try: + if self._listbox is not None and hasattr(self._listbox, "get_selected_rows"): + rows = self._listbox.get_selected_rows() or [] + for rr in rows: + if rr is r: + return True + except Exception: + pass + # last-resort flag + return bool(getattr(r, "_selected_flag", False)) + + def _gather_selected_rows(self): + """Return list of selected ListBoxRow objects (visible rows).""" + rows = [] + try: + # prefer listbox API if available + if self._listbox is not None and hasattr(self._listbox, "get_selected_rows"): + try: + sel = self._listbox.get_selected_rows() or [] + # If API returns Gtk.ListBoxRow-like objects, include them; otherwise fallback + for s in sel: + if s is None: + continue + # if path-like, ignore (we rely on visible rows) + if isinstance(s, type(self._rows[0])) if self._rows else False: + rows.append(s) + if rows: + return rows + except Exception: + pass + # fallback: scan our cached rows + for r in list(self._rows or []): + try: + if self._row_is_selected(r): + rows.append(r) + except Exception: + pass + except Exception: + pass + return rows + + def _apply_desired_ids_to_rows(self, desired_ids): + """Set visible rows selected state to match desired_ids (ids of items).""" + if self._listbox is None: + return + try: + self._suppress_selection_handler = True + except Exception: + pass + try: + try: + self._listbox.unselect_all() + except Exception: + # continue even if unsupported + pass + for row, it in list(self._row_to_item.items()): + try: + target = id(it) in desired_ids + try: + row.set_selected(bool(target)) + except Exception: + try: + setattr(row, "_selected_flag", bool(target)) + except Exception: + pass + except Exception: + pass + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass + + def _on_row_selected(self, listbox, row): + """Handle selection change; update logical selected items reliably. + + When recursive selection is enabled and multi-selection is on, + selecting/deselecting a parent will also select/deselect all its descendants. + """ + # ignore if programmatic change in progress + if self._suppress_selection_handler: + return + + try: + selected_rows = self._gather_selected_rows() + + # map rows -> items + cur_selected_items = [] + for r in selected_rows: + try: + it = self._row_to_item.get(r, None) + if it is not None: + cur_selected_items.append(it) + except Exception: + pass + + prev_ids = set(self._last_selected_ids or []) + cur_ids = set(id(i) for i in cur_selected_items) + added = cur_ids - prev_ids + removed = prev_ids - cur_ids + + # If recursive+multi, compute desired ids by adding descendants of added and removing descendants of removed. + desired_ids = set(cur_ids) + if self._recursive and self._multi and (added or removed): + try: + # add descendants of newly added items + for a in list(added): + # find object + obj = None + for it in cur_selected_items: + if id(it) == a: + obj = it + break + if obj is None: + # try to find in whole tree + def _find_by_id(tid, nodes): + for n in nodes: + if id(n) == tid: + return n + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + r = _find_by_id(tid, chs) + if r: + return r + return None + obj = _find_by_id(a, list(getattr(self, "_items", []) or [])) + if obj is not None: + for d in self._collect_all_descendants(obj): + desired_ids.add(id(d)) + + # remove descendants of removed items + for r_id in list(removed): + try: + obj = None + # try find in tree + def _find_by_id2(tid, nodes): + for n in nodes: + if id(n) == tid: + return n + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + rr = _find_by_id2(tid, chs) + if rr: + return rr + return None + obj = _find_by_id2(r_id, list(getattr(self, "_items", []) or [])) + if obj is not None: + for d in self._collect_all_descendants(obj): + if id(d) in desired_ids: + desired_ids.discard(id(d)) + except Exception: + pass + + except Exception: + pass + + # Apply desired selection to visible rows + try: + self._apply_desired_ids_to_rows(desired_ids) + except Exception: + pass + + # Recompute cur_selected_items including non-visible descendants + new_selected = [] + try: + # visible rows + for r in list(self._rows or []): + try: + if self._row_is_selected(r): + it = self._row_to_item.get(r) + if it is not None: + new_selected.append(it) + except Exception: + pass + # include non-visible nodes that are requested by desired_ids + def _collect_all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_all_nodes(chs)) + return out + for root in list(getattr(self, "_items", []) or []): + for n in _collect_all_nodes([root]): + try: + if id(n) in desired_ids and n not in new_selected: + new_selected.append(n) + except Exception: + pass + cur_selected_items = new_selected + cur_ids = set(id(i) for i in cur_selected_items) + except Exception: + pass + + # Update logical selection flags + try: + def _clear_flags(nodes): + for n in nodes: + try: + n.setSelected(False) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + _clear_flags(chs) + _clear_flags(list(getattr(self, "_items", []) or [])) + except Exception: + pass + + for it in cur_selected_items: + try: + it.setSelected(True) + except Exception: + pass + + # store logical selection + self._selected_items = list(cur_selected_items) + self._last_selected_ids = set(id(i) for i in self._selected_items) + + # notify immediate mode + if self._immediate and self.notify(): + try: + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + except Exception: + pass + + def currentItem(self): + try: + return self._selected_items[0] if self._selected_items else None + except Exception: + return None + + def getSelectedItem(self): + return self.currentItem() + + def getSelectedItems(self): + return list(self._selected_items) + + def activate(self): + try: + itm = self.currentItem() + if itm is None: + return False + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + return True + except Exception: + return False + + def hasMultiSelection(self): + """Return True if the tree allows selecting multiple items at once.""" + return bool(self._multi) + + def immediateMode(self): + return bool(self._immediate) + + def setImmediateMode(self, on:bool=True): + self._immediate = on + self.setNotify(on) + + def _set_backend_enabled(self, enabled): + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + try: + for it in list(getattr(self, "_items", []) or []): + try: + if hasattr(it, "setEnabled"): + it.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def get_backend_widget(self): + if self._backend_widget is None: + self._create_backend_widget() + return self._backend_widget diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py new file mode 100644 index 0000000..d870fea --- /dev/null +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -0,0 +1,96 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + +class YVBoxGtk(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YVBox" + + # Returns the stretchability of the layout box: + def stretchable(self, dim): + for child in self._children: + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + return False + + def _create_backend_widget(self): + self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(YUIDimension.YD_VERT)) + fill = True + padding = 0 + + try: + if expand: + if hasattr(widget, "set_vexpand"): + widget.set_vexpand(True) + if hasattr(widget, "set_valign"): + widget.set_valign(Gtk.Align.FILL) + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(True) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.FILL) + else: + if hasattr(widget, "set_vexpand"): + widget.set_vexpand(False) + if hasattr(widget, "set_valign"): + widget.set_valign(Gtk.Align.START) + if hasattr(widget, "set_hexpand"): + widget.set_hexpand(False) + if hasattr(widget, "set_halign"): + widget.set_halign(Gtk.Align.START) + except Exception: + pass + + # Gtk4: use append instead of pack_start + try: + self._backend_widget.append(widget) + except Exception: + try: + self._backend_widget.add(widget) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + """Enable/disable the VBox and propagate to children.""" + try: + if self._backend_widget is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + # propagate logical enabled state to child widgets so they update their backends + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 7cd048d..9d4d9e8 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -8,8 +8,38 @@ import cairo import threading import os +import importlib from .yui_common import * - + +# Import backend symbols only into this shim module. +def _import_gtk_backend_symbols(): + mod = None + try: + # Package-relative import + mod = importlib.import_module(".backends.gtk", __package__) + except Exception: + try: + # Absolute fallback + mod = importlib.import_module("manatools.aui.backends.gtk") + except Exception: + mod = None + if not mod: + return + names = getattr(mod, "__all__", None) + if names: + for name in names: + try: + globals()[name] = getattr(mod, name) + except Exception: + pass + else: + # Fallback: import non-private names + for name, obj in mod.__dict__.items(): + if not name.startswith("_"): + globals()[name] = obj + +_import_gtk_backend_symbols() + class YUIGtk: def __init__(self): self._widget_factory = YWidgetFactoryGtk() @@ -179,7 +209,6 @@ def setApplicationIcon(self, Icon): def applicationIcon(self): return self._icon - class YWidgetFactoryGtk: def __init__(self): pass @@ -254,2666 +283,3 @@ def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) -# GTK4 Widget Implementations -class YDialogGtk(YSingleChildContainerWidget): - _open_dialogs = [] - - def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): - super().__init__() - self._dialog_type = dialog_type - self._color_mode = color_mode - self._is_open = False - self._window = None - self._event_result = None - self._glib_loop = None - YDialogGtk._open_dialogs.append(self) - - def widgetClass(self): - return "YDialog" - - @staticmethod - def currentDialog(doThrow=True): - open_dialog = YDialogGtk._open_dialogs[-1] if YDialogGtk._open_dialogs else None - if not open_dialog and doThrow: - raise YUINoDialogException("No dialog is currently open") - return open_dialog - - @staticmethod - def topmostDialog(doThrow=True): - ''' same as currentDialog ''' - return YDialogGtk.currentDialog(doThrow=doThrow) - - def isTopmostDialog(self): - '''Return whether this dialog is the topmost open dialog.''' - return YDialogGtk._open_dialogs[-1] == self if YDialogGtk._open_dialogs else False - - def open(self): - # Finalize and show the dialog in a non-blocking way. - if not self._is_open: - if not self._window: - self._create_backend_widget() - # in Gtk4, show_all is not recommended; use present() or show - try: - self._window.present() - except Exception: - try: - self._window.show() - except Exception: - pass - self._is_open = True - - def isOpen(self): - return self._is_open - - def destroy(self, doThrow=True): - if self._window: - try: - self._window.destroy() - except Exception: - try: - self._window.close() - except Exception: - pass - self._window = None - self._is_open = False - if self in YDialogGtk._open_dialogs: - YDialogGtk._open_dialogs.remove(self) - - # Stop GLib main loop if no dialogs left (nested loops only) - if not YDialogGtk._open_dialogs: - try: - if self._glib_loop and self._glib_loop.is_running(): - self._glib_loop.quit() - except Exception: - pass - return True - - def _post_event(self, event): - """Internal: post an event to this dialog and quit local GLib.MainLoop if running.""" - self._event_result = event - if self._glib_loop is not None and self._glib_loop.is_running(): - try: - self._glib_loop.quit() - except Exception: - pass - - def waitForEvent(self, timeout_millisec=0): - """ - Run a GLib.MainLoop until an event is posted or timeout occurs. - Returns a YEvent (YWidgetEvent, YTimeoutEvent, ...). - """ - # Ensure dialog is finalized/open (finalize if caller didn't call open()). - if not self.isOpen(): - self.open() - - # Let GTK/GLib process pending events (show/layout) before entering nested loop. - # Gtk.events_pending()/Gtk.main_iteration() do not exist in GTK4; use MainContext iteration. - try: - ctx = GLib.MainContext.default() - while ctx.pending(): - ctx.iteration(False) - except Exception: - # be defensive if API differs on some bindings - pass - - self._event_result = None - self._glib_loop = GLib.MainLoop() - - def on_timeout(): - # post timeout event and quit loop - self._event_result = YTimeoutEvent() - try: - if self._glib_loop.is_running(): - self._glib_loop.quit() - except Exception: - pass - return False # don't repeat - - self._timeout_id = None - if timeout_millisec and timeout_millisec > 0: - self._timeout_id = GLib.timeout_add(timeout_millisec, on_timeout) - - # run nested loop - self._glib_loop.run() - - # cleanup - if self._timeout_id: - try: - GLib.source_remove(self._timeout_id) - except Exception: - pass - self._timeout_id = None - self._glib_loop = None - return self._event_result if self._event_result is not None else YEvent() - - @classmethod - def deleteTopmostDialog(cls, doThrow=True): - if cls._open_dialogs: - dialog = cls._open_dialogs[-1] - return dialog.destroy(doThrow) - return False - - @classmethod - def currentDialog(cls, doThrow=True): - if not cls._open_dialogs: - if doThrow: - raise YUINoDialogException("No dialog open") - return None - return cls._open_dialogs[-1] - - def _create_backend_widget(self): - # Determine window title from YApplicationGtk instance stored on the YUI backend - title = "Manatools YUI GTK Dialog" - try: - from . import yui as yui_mod - appobj = None - # YUI._backend may hold the backend instance (YUIGtk) - backend = getattr(yui_mod.YUI, "_backend", None) - if backend and hasattr(backend, "application"): - appobj = backend.application() - # fallback: YUI._instance might be set and expose application/yApp - if not appobj: - inst = getattr(yui_mod.YUI, "_instance", None) - if inst and hasattr(inst, "application"): - appobj = inst.application() - if appobj and hasattr(appobj, "applicationTitle"): - atitle = appobj.applicationTitle() - if atitle: - title = atitle - # try to obtain resolved pixbuf from application and store for window icon - _resolved_pixbuf = None - try: - _resolved_pixbuf = getattr(appobj, "_gtk_icon_pixbuf", None) - except Exception: - _resolved_pixbuf = None - except Exception: - pass - - # Create Gtk4 Window - self._window = Gtk.Window(title=title) - # set window icon if available - try: - if _resolved_pixbuf is not None: - try: - self._window.set_icon(_resolved_pixbuf) - except Exception: - try: - # fallback to name if pixbuf not accepted - icname = getattr(appobj, "applicationIcon", lambda : None)() - if icname: - self._window.set_icon_name(icname) - except Exception: - pass - else: - try: - # try setting icon name if application provided it - icname = getattr(appobj, "applicationIcon", lambda : None)() - if icname: - self._window.set_icon_name(icname) - except Exception: - pass - except Exception: - pass - try: - self._window.set_default_size(600, 400) - except Exception: - pass - - # Content container with margins (window.set_child used in Gtk4) - content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) - content.set_margin_start(10) - content.set_margin_end(10) - content.set_margin_top(10) - content.set_margin_bottom(10) - - if self._child: - child_widget = self._child.get_backend_widget() - # ensure child is shown properly - try: - content.append(child_widget) - except Exception: - try: - content.add(child_widget) - except Exception: - pass - - try: - self._window.set_child(content) - except Exception: - # fallback for older bindings - try: - self._window.add(content) - except Exception: - pass - - self._backend_widget = self._window - # Connect destroy/close handlers - try: - # Gtk4: use 'close-request' if available, otherwise 'destroy' - if hasattr(self._window, "connect"): - try: - self._window.connect("close-request", self._on_delete_event) - except Exception: - try: - self._window.connect("destroy", self._on_destroy) - except Exception: - pass - except Exception: - pass - - def _on_destroy(self, widget): - try: - self.destroy() - except Exception: - pass - - def _on_delete_event(self, *args): - # close-request handler in Gtk4: post cancel event and destroy - try: - self._post_event(YCancelEvent()) - except Exception: - pass - try: - self.destroy() - except Exception: - pass - # returning False/True not used in this simplified handler - return False - - def setVisible(self, visible): - """Set widget visibility.""" - if self._backend_widget: - try: - self._backend_widget.set_visible(visible) - except Exception: - pass - super().setVisible(visible) - - def _set_backend_enabled(self, enabled): - """Enable/disable the dialog window backend.""" - try: - if self._window is not None: - try: - self._window.set_sensitive(enabled) - except Exception: - # fallback: propagate to child content - try: - child = getattr(self, "_window", None) - if child and hasattr(child, "set_sensitive"): - child.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - - -class YVBoxGtk(YWidget): - def __init__(self, parent=None): - super().__init__(parent) - - def widgetClass(self): - return "YVBox" - - # Returns the stretchability of the layout box: - def stretchable(self, dim): - for child in self._children: - expand = bool(child.stretchable(dim)) - weight = bool(child.weight(dim)) - if expand or weight: - return True - return False - - def _create_backend_widget(self): - self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) - - for child in self._children: - widget = child.get_backend_widget() - expand = bool(child.stretchable(YUIDimension.YD_VERT)) - fill = True - padding = 0 - - try: - if expand: - if hasattr(widget, "set_vexpand"): - widget.set_vexpand(True) - if hasattr(widget, "set_valign"): - widget.set_valign(Gtk.Align.FILL) - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(True) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.FILL) - else: - if hasattr(widget, "set_vexpand"): - widget.set_vexpand(False) - if hasattr(widget, "set_valign"): - widget.set_valign(Gtk.Align.START) - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(False) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.START) - except Exception: - pass - - # Gtk4: use append instead of pack_start - try: - self._backend_widget.append(widget) - except Exception: - try: - self._backend_widget.add(widget) - except Exception: - pass - - def _set_backend_enabled(self, enabled): - """Enable/disable the VBox and propagate to children.""" - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - # propagate logical enabled state to child widgets so they update their backends - try: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - -class YHBoxGtk(YWidget): - def __init__(self, parent=None): - super().__init__(parent) - - def widgetClass(self): - return "YHBox" - - def stretchable(self, dim): - for child in self._children: - expand = bool(child.stretchable(dim)) - weight = bool(child.weight(dim)) - if expand or weight: - return True - return False - - def _create_backend_widget(self): - self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - - for child in self._children: - print("HBox child: ", child.widgetClass()) - widget = child.get_backend_widget() - expand = bool(child.stretchable(YUIDimension.YD_HORIZ)) - fill = True - padding = 0 - try: - if expand: - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(True) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.FILL) - else: - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(False) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.START) - except Exception: - pass - - try: - self._backend_widget.append(widget) - except Exception: - try: - self._backend_widget.add(widget) - except Exception: - pass - - def _set_backend_enabled(self, enabled): - """Enable/disable the HBox and propagate to children.""" - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - try: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - -class YLabelGtk(YWidget): - def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): - super().__init__(parent) - self._text = text - self._is_heading = isHeading - self._is_output_field = isOutputField - - def widgetClass(self): - return "YLabel" - - def text(self): - return self._text - - def setText(self, new_text): - self._text = new_text - if self._backend_widget: - try: - self._backend_widget.set_text(new_text) - except Exception: - pass - - def _create_backend_widget(self): - self._backend_widget = Gtk.Label(label=self._text) - try: - # alignment API in Gtk4 differs; fall back to setting xalign if available - if hasattr(self._backend_widget, "set_xalign"): - self._backend_widget.set_xalign(0.0) - except Exception: - pass - - if self._is_heading: - try: - markup = f"{self._text}" - self._backend_widget.set_markup(markup) - except Exception: - pass - - def _set_backend_enabled(self, enabled): - """Enable/disable the label widget backend.""" - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - -class YInputFieldGtk(YWidget): - def __init__(self, parent=None, label="", password_mode=False): - super().__init__(parent) - self._label = label - self._value = "" - self._password_mode = password_mode - - def widgetClass(self): - return "YInputField" - - def value(self): - return self._value - - def setValue(self, text): - self._value = text - if hasattr(self, '_entry_widget') and self._entry_widget: - try: - self._entry_widget.set_text(text) - except Exception: - pass - - def label(self): - return self._label - - def _create_backend_widget(self): - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - - if self._label: - label = Gtk.Label(label=self._label) - try: - if hasattr(label, "set_xalign"): - label.set_xalign(0.0) - except Exception: - pass - try: - hbox.append(label) - except Exception: - hbox.add(label) - - if self._password_mode: - entry = Gtk.Entry() - try: - entry.set_visibility(False) - except Exception: - pass - else: - entry = Gtk.Entry() - - try: - entry.set_text(self._value) - entry.connect("changed", self._on_changed) - except Exception: - pass - - try: - hbox.append(entry) - except Exception: - hbox.add(entry) - - self._backend_widget = hbox - self._entry_widget = entry - - def _on_changed(self, entry): - try: - self._value = entry.get_text() - except Exception: - self._value = "" - - def _set_backend_enabled(self, enabled): - """Enable/disable the input field (entry and container).""" - try: - if getattr(self, "_entry_widget", None) is not None: - try: - self._entry_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - -class YPushButtonGtk(YWidget): - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label - - def widgetClass(self): - return "YPushButton" - - def label(self): - return self._label - - def setLabel(self, label): - self._label = label - if self._backend_widget: - try: - self._backend_widget.set_label(label) - except Exception: - pass - - def _create_backend_widget(self): - self._backend_widget = Gtk.Button(label=self._label) - # Prevent button from being stretched horizontally by default. - try: - if hasattr(self._backend_widget, "set_hexpand"): - self._backend_widget.set_hexpand(False) - if hasattr(self._backend_widget, "set_halign"): - self._backend_widget.set_halign(Gtk.Align.START) - except Exception: - pass - try: - self._backend_widget.connect("clicked", self._on_clicked) - except Exception: - pass - - def _on_clicked(self, button): - if self.notify() is False: - return - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) - else: - # silent fallback - pass - - def _set_backend_enabled(self, enabled): - """Enable/disable the push button backend.""" - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - -class YCheckBoxGtk(YWidget): - def __init__(self, parent=None, label="", is_checked=False): - super().__init__(parent) - self._label = label - self._is_checked = is_checked - - def widgetClass(self): - return "YCheckBox" - - def value(self): - return self._is_checked - - def setValue(self, checked): - self._is_checked = checked - if self._backend_widget: - try: - self._backend_widget.set_active(checked) - except Exception: - pass - - def label(self): - return self._label - - def _create_backend_widget(self): - self._backend_widget = Gtk.CheckButton(label=self._label) - try: - self._backend_widget.set_active(self._is_checked) - self._backend_widget.connect("toggled", self._on_toggled) - except Exception: - pass - - def _on_toggled(self, button): - try: - self._is_checked = button.get_active() - except Exception: - self._is_checked = bool(self._is_checked) - - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - - def _set_backend_enabled(self, enabled): - """Enable/disable the check button backend.""" - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - -class YComboBoxGtk(YSelectionWidget): - def __init__(self, parent=None, label="", editable=False): - super().__init__(parent) - self._label = label - self._editable = editable - self._value = "" - self._selected_items = [] - self._combo_widget = None - - def widgetClass(self): - return "YComboBox" - - def value(self): - return self._value - - def setValue(self, text): - self._value = text - if self._combo_widget: - try: - # try entry child for editable combos - child = None - if self._editable: - child = self._combo_widget.get_child() - if child and hasattr(child, "set_text"): - child.set_text(text) - else: - # attempt to set active by matching text if API available - if hasattr(self._combo_widget, "set_active_id"): - # Gtk.DropDown uses ids in models; we keep simple and try to match by text - # fallback: rebuild model and select programmatically below - pass - # update selected_items - self._selected_items = [it for it in self._items if it.label() == text][:1] - except Exception: - pass - - def editable(self): - return self._editable - - def _create_backend_widget(self): - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - - if self._label: - label = Gtk.Label(label=self._label) - try: - if hasattr(label, "set_xalign"): - label.set_xalign(0.0) - except Exception: - pass - try: - hbox.append(label) - except Exception: - hbox.add(label) - - # For Gtk4 there is no ComboBoxText; try DropDown for non-editable, - # and Entry for editable combos (simple fallback). - if self._editable: - entry = Gtk.Entry() - entry.set_text(self._value) - entry.connect("changed", self._on_text_changed) - self._combo_widget = entry - try: - hbox.append(entry) - except Exception: - hbox.add(entry) - else: - # Build a simple Gtk.DropDown backed by a Gtk.StringList (if available) - try: - if hasattr(Gtk, "StringList") and hasattr(Gtk, "DropDown"): - model = Gtk.StringList() - for it in self._items: - model.append(it.label()) - dropdown = Gtk.DropDown.new(model, None) - # select initial value - if self._value: - for idx, it in enumerate(self._items): - if it.label() == self._value: - dropdown.set_selected(idx) - break - dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) - self._combo_widget = dropdown - hbox.append(dropdown) - else: - # fallback: simple Gtk.Button that cycles items on click (very simple) - btn = Gtk.Button(label=self._value or (self._items[0].label() if self._items else "")) - btn.connect("clicked", self._on_fallback_button_clicked) - self._combo_widget = btn - hbox.append(btn) - except Exception: - # final fallback: entry - entry = Gtk.Entry() - entry.set_text(self._value) - entry.connect("changed", self._on_text_changed) - self._combo_widget = entry - hbox.append(entry) - - self._backend_widget = hbox - - def _set_backend_enabled(self, enabled): - """Enable/disable the combobox/backing widget and its entry/dropdown.""" - try: - # prefer to enable the primary control if present - ctl = getattr(self, "_combo_widget", None) - if ctl is not None: - try: - ctl.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - - def _on_fallback_button_clicked(self, btn): - # naive cycle through items - if not self._items: - return - current = btn.get_label() - labels = [it.label() for it in self._items] - try: - idx = labels.index(current) - idx = (idx + 1) % len(labels) - except Exception: - idx = 0 - new = labels[idx] - btn.set_label(new) - self.setValue(new) - if self.notify(): - dlg = self.findDialog() - if dlg: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - - def _on_text_changed(self, entry): - try: - text = entry.get_text() - except Exception: - text = "" - self._value = text - self._selected_items = [it for it in self._items if it.label() == self._value][:1] - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - - def _on_changed_dropdown(self, dropdown): - try: - # Prefer using the selected index to get a reliable label - idx = None - try: - idx = dropdown.get_selected() - except Exception: - idx = None - - if isinstance(idx, int) and 0 <= idx < len(self._items): - self._value = self._items[idx].label() - else: - # Fallback: try to extract text from the selected-item object - val = None - try: - val = dropdown.get_selected_item() - except Exception: - val = None - - self._value = "" - if isinstance(val, str): - self._value = val - elif val is not None: - # Try common accessor names that GTK objects may expose - for meth in ("get_string", "get_text", "get_value", "get_label", "get_name", "to_string"): - try: - fn = getattr(val, meth, None) - if callable(fn): - v = fn() - if isinstance(v, str) and v: - self._value = v - break - except Exception: - continue - # Try properties if available - if not self._value: - try: - props = getattr(val, "props", None) - if props: - for attr in ("string", "value", "label", "name", "text"): - try: - pv = getattr(props, attr) - if isinstance(pv, str) and pv: - self._value = pv - break - except Exception: - pass - except Exception: - pass - # final fallback to str() - if not self._value: - try: - self._value = str(val) - except Exception: - self._value = "" - - # update selected_items using reliable labels - self._selected_items = [it for it in self._items if it.label() == self._value][:1] - except Exception: - pass - - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - - -class YSelectionBoxGtk(YSelectionWidget): - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label - self._value = "" - self._selected_items = [] - self._multi_selection = False - self._listbox = None - self._backend_widget = None - # keep a stable list of rows we create so we don't rely on ListBox container APIs - # (GTK4 bindings may not expose get_children()) - self._rows = [] - # Preferred visible rows for layout/paging; parent can give more space when stretchable - self._preferred_rows = 6 - self.setStretchable(YUIDimension.YD_HORIZ, True) - self.setStretchable(YUIDimension.YD_VERT, True) - - def widgetClass(self): - return "YSelectionBox" - - def label(self): - return self._label - - def value(self): - return self._value - - def setValue(self, text): - """Select first item matching text.""" - self._value = text - self._selected_items = [it for it in self._items if it.label() == text] - if self._listbox is None: - return - # find and select corresponding row using the cached rows list - for i, row in enumerate(getattr(self, "_rows", [])): - if i >= len(self._items): - continue - try: - if self._items[i].label() == text: - row.set_selectable(True) - row.set_selected(True) - else: - # ensure others are not selected in single-selection mode - if not self._multi_selection: - row.set_selected(False) - except Exception: - pass - # notify - self._on_selection_changed() - - def selectedItems(self): - return list(self._selected_items) - - def selectItem(self, item, selected=True): - if selected: - if not self._multi_selection: - self._selected_items = [item] - self._value = item.label() - else: - if item not in self._selected_items: - self._selected_items.append(item) - else: - if item in self._selected_items: - self._selected_items.remove(item) - self._value = self._selected_items[0].label() if self._selected_items else "" - - if self._listbox is None: - return - - # reflect change in UI - rows = getattr(self, "_rows", []) - for i, it in enumerate(self._items): - if it is item or it.label() == item.label(): - try: - row = rows[i] - row.set_selected(selected) - except Exception: - pass - break - self._on_selection_changed() - - def setMultiSelection(self, enabled): - self._multi_selection = bool(enabled) - # If listbox already created, update its selection mode at runtime. - if self._listbox is None: - return - try: - mode = Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE - self._listbox.set_selection_mode(mode) - except Exception: - pass - # Rewire signals: disconnect previous handlers and connect appropriate one. - try: - # Disconnect any previously stored handlers - try: - for key, hid in list(getattr(self, "_signal_handlers", {}).items()): - if hid and isinstance(hid, int): - try: - self._listbox.disconnect(hid) - except Exception: - pass - self._signal_handlers = {} - except Exception: - self._signal_handlers = {} - - # Connect new handler based on mode - if self._multi_selection: - try: - hid = self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) - self._signal_handlers['selected-rows-changed'] = hid - except Exception: - try: - hid = self._listbox.connect("row-selected", lambda lb, row: self._on_selected_rows_changed(lb)) - self._signal_handlers['row-selected_for_multi'] = hid - except Exception: - pass - else: - try: - hid = self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) - self._signal_handlers['row-selected'] = hid - except Exception: - pass - except Exception: - pass - - def multiSelection(self): - return bool(self._multi_selection) - - def _create_backend_widget(self): - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) - if self._label: - lbl = Gtk.Label(label=self._label) - try: - if hasattr(lbl, "set_xalign"): - lbl.set_xalign(0.0) - except Exception: - pass - try: - vbox.append(lbl) - except Exception: - vbox.add(lbl) - - # Use Gtk.ListBox inside a ScrolledWindow for Gtk4 - listbox = Gtk.ListBox() - listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE) - # allow listbox to expand if parent allocates more space - try: - listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) - except Exception: - pass - # populate rows - self._rows = [] - for it in self._items: - row = Gtk.ListBoxRow() - lbl = Gtk.Label(label=it.label() or "") - try: - if hasattr(lbl, "set_xalign"): - lbl.set_xalign(0.0) - except Exception: - pass - try: - row.set_child(lbl) - except Exception: - try: - row.add(lbl) - except Exception: - pass - - # Make every row selectable so users can multi-select if mode allows. - try: - row.set_selectable(True) - except Exception: - pass - - # If this item matches current value, mark selected - try: - if self._value and it.label() == self._value: - row.set_selectable(True) - row.set_selected(True) - except Exception: - pass - self._rows.append(row) - listbox.append(row) - - sw = Gtk.ScrolledWindow() - # allow scrolled window to expand vertically and horizontally - try: - sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) - # give a reasonable minimum content height so layout initially shows several rows; - # Gtk4 expects pixels — try a conservative estimate (rows * ~20px) - min_h = int(getattr(self, "_preferred_rows", 6) * 20) - try: - # some Gtk4 bindings expose set_min_content_height - sw.set_min_content_height(min_h) - except Exception: - pass - except Exception: - pass - # policy APIs changed in Gtk4: use set_overlay_scrolling and set_min_content_height if needed - try: - sw.set_child(listbox) - except Exception: - try: - sw.add(listbox) - except Exception: - pass - - # also request vexpand on the outer vbox so parent layout sees it can grow - try: - vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) - except Exception: - pass - - try: - vbox.append(sw) - except Exception: - vbox.add(sw) - - # connect selection signal: choose appropriate signal per selection mode - # store handler ids so we can disconnect later if selection mode changes at runtime - self._signal_handlers = {} - try: - # ensure any previous handlers are disconnected (defensive) - try: - for hid in list(self._signal_handlers.values()): - if hid and isinstance(hid, int): - try: - listbox.disconnect(hid) - except Exception: - pass - except Exception: - pass - - # Use row-selected for both single and multi modes; handler will toggle for multi - try: - hid = listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) - self._signal_handlers['row-selected'] = hid - except Exception: - pass - except Exception: - pass - - self._backend_widget = vbox - self._listbox = listbox - - def _set_backend_enabled(self, enabled): - """Enable/disable the selection box and its listbox/rows.""" - try: - if getattr(self, "_listbox", None) is not None: - try: - self._listbox.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - # propagate logical enabled state to child items/widgets - try: - for c in list(getattr(self, "_rows", []) or []): - try: - c.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - - def _row_is_selected(self, r): - """Robust helper to detect whether a ListBoxRow is selected.""" - try: - return bool(r.get_selected()) - except Exception: - pass - try: - props = getattr(r, "props", None) - if props and hasattr(props, "selected"): - return bool(getattr(props, "selected")) - except Exception: - pass - return bool(getattr(r, "_selected_flag", False)) - - def _on_row_selected(self, listbox, row): - """ - Handler for row selection. In single-selection mode behaves as before - (select provided row and deselect others). In multi-selection mode toggles - the provided row and rebuilds the selected items list. - """ - try: - if row is not None: - if self._multi_selection: - # toggle selection state for this row - try: - cur = self._row_is_selected(row) - try: - row.set_selected(not cur) - except Exception: - # fallback: store a flag when set_selected isn't available - setattr(row, "_selected_flag", not cur) - except Exception: - pass - else: - # single-selection: select provided row and deselect others - for r in getattr(self, "_rows", []): - try: - r.set_selected(r is row) - except Exception: - try: - setattr(r, "_selected_flag", (r is row)) - except Exception: - pass - - # rebuild selected_items scanning cached rows (works for both modes) - self._selected_items = [] - for i, r in enumerate(getattr(self, "_rows", [])): - try: - if self._row_is_selected(r) and i < len(self._items): - self._selected_items.append(self._items[i]) - except Exception: - pass - - self._value = self._selected_items[0].label() if self._selected_items else None - except Exception: - # be defensive - self._selected_items = [] - self._value = None - - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - - def _on_selected_rows_changed(self, listbox): - """ - Handler for multi-selection (or bulk selection change). Rebuild selected list - using either ListBox APIs (if available) or by scanning cached rows. - """ - try: - # Try to use any available API that returns selected rows - sel_rows = None - try: - # Some bindings may provide get_selected_rows() - sel_rows = listbox.get_selected_rows() - print(f"Using get_selected_rows() {len(sel_rows)} API") - except Exception: - sel_rows = None - - self._selected_items = [] - if sel_rows: - # sel_rows may be list of Row objects or Paths; try to match by identity - for r in sel_rows: - try: - # if r is a ListBoxRow already - if isinstance(r, type(self._rows[0])) if self._rows else False: - try: - idx = self._rows.index(r) - if idx < len(self._items): - self._selected_items.append(self._items[idx]) - except Exception: - pass - else: - # fallback: scan cached rows to find selected ones - for i, cr in enumerate(getattr(self, "_rows", [])): - try: - if self._row_is_selected(cr) and i < len(self._items): - self._selected_items.append(self._items[i]) - except Exception: - pass - except Exception: - pass - else: - # Generic fallback: scan cached rows and collect selected ones - for i, r in enumerate(getattr(self, "_rows", [])): - try: - if self._row_is_selected(r) and i < len(self._items): - self._selected_items.append(self._items[i]) - except Exception: - pass - - self._value = self._selected_items[0].label() if self._selected_items else None - except Exception: - self._selected_items = [] - self._value = None - - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - - -class YAlignmentGtk(YSingleChildContainerWidget): - """ - GTK4 implementation of YAlignment. - - - Uses a Gtk.Box as a lightweight container that requests expansion when - needed so child halign/valign can take effect (matches the small GTK sample). - - Applies halign/valign hints to the child's backend widget. - - Defers attaching the child if its backend is not yet created (GLib.idle_add). - - Supports an optional repeating background pixbuf painted in the draw signal. - """ - def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): - super().__init__(parent) - self._halign_spec = horAlign - self._valign_spec = vertAlign - self._background_pixbuf = None - self._signal_id = None - self._backend_widget = None - # schedule guard for deferred attach - self._attach_scheduled = False - # Track if we've already attached a child - self._child_attached = False - - def widgetClass(self): - return "YAlignment" - - def _to_gtk_halign(self): - """Convert Horizontal YAlignmentType to Gtk.Align or None.""" - if self._halign_spec: - if self._halign_spec == YAlignmentType.YAlignBegin: - return Gtk.Align.START - if self._halign_spec == YAlignmentType.YAlignCenter: - return Gtk.Align.CENTER - if self._halign_spec == YAlignmentType.YAlignEnd: - return Gtk.Align.END - return None - - def _to_gtk_valign(self): - """Convert Vertical YAlignmentType to Gtk.Align or None.""" - if self._valign_spec: - if self._valign_spec == YAlignmentType.YAlignBegin: - return Gtk.Align.START - if self._valign_spec == YAlignmentType.YAlignCenter: - return Gtk.Align.CENTER - if self._valign_spec == YAlignmentType.YAlignEnd: - return Gtk.Align.END - return None - - #def stretchable(self, dim): - # """Report whether this alignment should expand in given dimension. - # - # Parents (HBox/VBox) use this to distribute space. - # """ - # try: - # if dim == YUIDimension.YD_HORIZ: - # align = self._to_gtk_halign() - # return align in (Gtk.Align.CENTER, Gtk.Align.END) #TODO: verify - # if dim == YUIDimension.YD_VERT: - # align = self._to_gtk_valign() - # return align == Gtk.Align.CENTER #TODO: verify - # except Exception: - # pass - # return False - - def stretchable(self, dim: YUIDimension): - ''' Returns the stretchability of the layout box: - * The layout box is stretchable if the child is stretchable in - * this dimension or if the child widget has a layout weight in - * this dimension. - ''' - if self._child: - expand = bool(self._child.stretchable(dim)) - weight = bool(self._child.weight(dim)) - if expand or weight: - return True - return False - - def setBackgroundPixmap(self, filename): - """Set a repeating background pixbuf and connect draw handler.""" - # disconnect previous handler - if self._signal_id and self._backend_widget: - try: - self._backend_widget.disconnect(self._signal_id) - except Exception: - pass - self._signal_id = None - - # release previous pixbuf if present - self._background_pixbuf = None - - if filename: - try: - self._background_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) - if self._backend_widget: - self._signal_id = self._backend_widget.connect("draw", self._on_draw) - self._backend_widget.queue_draw() # Trigger redraw - except Exception as e: - print(f"Failed to load background image: {e}") - self._background_pixbuf = None - - def _on_draw(self, widget, cr): - """Draw callback that tiles the background pixbuf.""" - if not self._background_pixbuf: - return False - try: - # Get actual allocation - width = widget.get_allocated_width() - height = widget.get_allocated_height() - - Gdk.cairo_set_source_pixbuf(cr, self._background_pixbuf, 0, 0) - # set repeat - pat = cr.get_source() - pat.set_extend(cairo.Extend.REPEAT) - cr.rectangle(0, 0, width, height) - cr.fill() - except Exception as e: - print(f"Error drawing background: {e}") - return False - - def addChild(self, child): - """Keep base behavior and ensure we attempt to attach child's backend.""" - try: - super().addChild(child) - except Exception: - self._child = child - self._child_attached = False - self._schedule_attach_child() - - def setChild(self, child): - """Keep base behavior and ensure we attempt to attach child's backend.""" - try: - super().setChild(child) - except Exception: - self._child = child - self._child_attached = False - self._schedule_attach_child() - - def _schedule_attach_child(self): - """Schedule a single idle callback to attach child backend later.""" - if self._attach_scheduled or self._child_attached: - return - self._attach_scheduled = True - - def _idle_cb(): - self._attach_scheduled = False - try: - self._ensure_child_attached() - except Exception as e: - print(f"Error attaching child: {e}") - return False - - try: - GLib.idle_add(_idle_cb) - except Exception: - # fallback: call synchronously if idle_add not available - _idle_cb() - - def _ensure_child_attached(self): - """Attach child's backend to our container, apply alignment hints.""" - if self._backend_widget is None: - self._create_backend_widget() - return - - # choose child reference (support _child or _children storage) - child = getattr(self, "_child", None) - if child is None: - try: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - except Exception: - child = None - if child is None: - return - - # get child's backend widget - try: - cw = child.get_backend_widget() - except Exception: - cw = None - - if cw is None: - # child backend not yet ready; schedule again - if not self._child_attached: - self._schedule_attach_child() - return - - # convert specs -> Gtk.Align - hal = self._to_gtk_halign() - val = self._to_gtk_valign() - - # Apply alignment and expansion hints to child - try: - # Set horizontal alignment and expansion - if hasattr(cw, "set_halign"): - if hal is not None: - cw.set_halign(hal) - else: - cw.set_halign(Gtk.Align.FILL) - - # Request expansion for alignment to work properly - cw.set_hexpand(True) - - # Set vertical alignment and expansion - if hasattr(cw, "set_valign"): - if val is not None: - cw.set_valign(val) - else: - cw.set_valign(Gtk.Align.FILL) - - # Request expansion for alignment to work properly - cw.set_vexpand(True) - - except Exception as e: - print(f"Error setting alignment properties: {e}") - - # If the child widget is already parented to us, nothing to do - parent_of_cw = None - try: - if hasattr(cw, 'get_parent'): - parent_of_cw = cw.get_parent() - except Exception: - parent_of_cw = None - - if parent_of_cw == self._backend_widget: - self._child_attached = True - return - - # Remove any existing children from our container - try: - # In GTK4, we need to remove all existing children - while True: - child_widget = self._backend_widget.get_first_child() - if child_widget is None: - break - self._backend_widget.remove(child_widget) - except Exception as e: - print(f"Error removing existing children: {e}") - - # Append child to our box - this is the critical fix for GTK4 - try: - self._backend_widget.append(cw) - self._child_attached = True - print(f"Successfully attached child {child.widgetClass()} {child.debugLabel()} to alignment container") - except Exception as e: - print(f"Error appending child: {e}") - # Try alternative method for GTK4 - try: - self._backend_widget.set_child(cw) - self._child_attached = True - print(f"Successfully set child {child.widgetClass()} {child.debugLabel()} using set_child()") - except Exception as e2: - print(f"Error setting child: {e2}") - - def _create_backend_widget(self): - """Create a Box container oriented to allow alignment to work. - - In GTK4, we use a simple Box that expands in both directions - to provide space for the child widget to align within. - """ - try: - # Use a box that can expand in both directions - box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) - - # Make the box expand to fill available space - box.set_hexpand(True) - box.set_vexpand(True) - - # Set the box to fill its allocation so child has space to align - box.set_halign(Gtk.Align.FILL) - box.set_valign(Gtk.Align.FILL) - - except Exception as e: - print(f"Error creating backend widget: {e}") - box = Gtk.Box() - - self._backend_widget = box - - # Connect draw handler if we have a background pixbuf - if self._background_pixbuf and not self._signal_id: - try: - self._signal_id = box.connect("draw", self._on_draw) - except Exception as e: - print(f"Error connecting draw signal: {e}") - self._signal_id = None - - # Mark that backend is ready and attempt to attach child - self._ensure_child_attached() - - def get_backend_widget(self): - """Return the backend GTK widget.""" - if self._backend_widget is None: - self._create_backend_widget() - return self._backend_widget - - def setSize(self, width, height): - """Set size of the alignment widget.""" - if self._backend_widget: - if width > 0 and height > 0: - self._backend_widget.set_size_request(width, height) - else: - self._backend_widget.set_size_request(-1, -1) - - def setEnabled(self, enabled): - """Set widget enabled state.""" - if self._backend_widget: - self._backend_widget.set_sensitive(enabled) - super().setEnabled(enabled) - - def setVisible(self, visible): - """Set widget visibility.""" - if self._backend_widget: - try: - self._backend_widget.set_visible(visible) - except Exception: - pass - super().setVisible(visible) - - def _set_backend_enabled(self, enabled): - """Enable/disable the alignment container and its child (if any).""" - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - # propagate to logical child so child's backend updates too - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is not None: - try: - child.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - -class YTreeGtk(YSelectionWidget): - """ - Stable Gtk4 implementation of a tree using Gtk.ListBox + ScrolledWindow. - - - Renders visible nodes (respecting YTreeItem._is_open). - - Supports multiselection and recursiveSelection (select/deselect parents -> children). - - Preserves stretching: the ScrolledWindow/ListBox expand to fill container. - """ - def __init__(self, parent=None, label="", multiselection=False, recursiveselection=False): - super().__init__(parent) - self._label = label - self._multi = bool(multiselection) - self._recursive = bool(recursiveselection) - if self._recursive: - # recursive selection implies multi-selection semantics - self._multi = True - self._immediate = self.notify() - self._backend_widget = None - self._listbox = None - # cached rows and mappings - self._rows = [] # ordered list of Gtk.ListBoxRow - self._row_to_item = {} # row -> YTreeItem - self._item_to_row = {} # YTreeItem -> row - self._visible_items = [] # list of (item, depth) - self._suppress_selection_handler = False - self._last_selected_ids = set() - self.setStretchable(YUIDimension.YD_HORIZ, True) - self.setStretchable(YUIDimension.YD_VERT, True) - - def widgetClass(self): - return "YTree" - - def _create_backend_widget(self): - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - - if self._label: - try: - lbl = Gtk.Label(label=self._label) - if hasattr(lbl, "set_xalign"): - lbl.set_xalign(0.0) - vbox.append(lbl) - except Exception: - pass - - # ListBox (flat, shows only visible nodes). Put into ScrolledWindow so it won't grow parent on expand. - listbox = Gtk.ListBox() - try: - mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE - listbox.set_selection_mode(mode) - # Let listbox expand in available area - listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) - except Exception: - pass - - sw = Gtk.ScrolledWindow() - try: - sw.set_child(listbox) - except Exception: - try: - sw.add(listbox) - except Exception: - pass - - # Make scrolled window expand to fill container (so tree respects parent stretching) - try: - sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) - vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) - except Exception: - pass - - # connect selection signal; use defensive handler that scans rows - try: - listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) - except Exception: - pass - - self._backend_widget = vbox - self._listbox = listbox - - try: - vbox.append(sw) - except Exception: - try: - vbox.add(sw) - except Exception: - pass - - # populate if items already exist - try: - if getattr(self, "_items", None): - self.rebuildTree() - except Exception: - pass - - def _make_row(self, item, depth): - """Create a ListBoxRow for item with indentation and (optional) toggle button.""" - row = Gtk.ListBoxRow() - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - - # indentation spacer - try: - indent = Gtk.Box() - indent.set_size_request(depth * 12, 1) - hbox.append(indent) - except Exception: - pass - - # toggle if item has children - has_children = False - try: - childs = [] - if callable(getattr(item, "children", None)): - childs = item.children() or [] - else: - childs = getattr(item, "_children", []) or [] - has_children = len(childs) > 0 - except Exception: - has_children = False - - if has_children: - try: - btn = Gtk.Button(label="▾" if bool(getattr(item, "_is_open", False)) else "▸") - try: - btn.set_relief(Gtk.ReliefStyle.NONE) - except Exception: - pass - # prevent the toggle button from taking focus / causing selection side-effects - try: - btn.set_focus_on_click(False) - except Exception: - pass - try: - btn.set_can_focus(False) - except Exception: - pass - # make button visually flat (no border/background) so it looks like a tree expander - try: - btn.add_css_class("flat") - except Exception: - # fallback: try another common class name - try: - btn.add_css_class("link") - except Exception: - pass - - # Use a GestureClick on the button to reliably receive a single-click action - # and avoid the occasional need for double clicks caused by focus/selection interplay. - try: - gesture = Gtk.GestureClick() - # accept any button; if set_button exists restrict to primary - try: - gesture.set_button(0) - except Exception: - pass - # pressed handler will toggle immediately - def _on_pressed(gesture_obj, n_press, x, y, target_item=item): - # run toggle synchronously and suppress selection handler while rebuilding - try: - self._suppress_selection_handler = True - except Exception: - pass - try: - # toggle using public API if available - try: - cur = target_item.isOpen() - target_item.setOpen(not cur) - except Exception: - try: - cur = bool(getattr(target_item, "_is_open", False)) - target_item._is_open = not cur - except Exception: - pass - # preserve selection and rebuild - try: - self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) - except Exception: - self._last_selected_ids = set() - try: - self.rebuildTree() - except Exception: - pass - finally: - try: - self._suppress_selection_handler = False - except Exception: - pass - - gesture.connect("pressed", _on_pressed) - try: - btn.add_controller(gesture) - except Exception: - try: - btn.add_controller(gesture) - except Exception: - pass - except Exception: - # Fallback to clicked if GestureClick not available - try: - btn.connect("clicked", lambda b, it=item: self._on_toggle_clicked(it)) - except Exception: - pass - hbox.append(btn) - except Exception: - # fallback spacer - try: - spacer = Gtk.Box() - spacer.set_size_request(14, 1) - hbox.append(spacer) - except Exception: - pass - else: - try: - spacer = Gtk.Box() - spacer.set_size_request(14, 1) - hbox.append(spacer) - except Exception: - pass - - # label - try: - lbl = Gtk.Label(label=item.label() if hasattr(item, "label") else str(item)) - if hasattr(lbl, "set_xalign"): - lbl.set_xalign(0.0) - # ensure label expands to take remaining space - try: - lbl.set_hexpand(True) - except Exception: - pass - hbox.append(lbl) - except Exception: - pass - - try: - row.set_child(hbox) - except Exception: - try: - row.add(hbox) - except Exception: - pass - - try: - row.set_selectable(True) - except Exception: - pass - - return row - - def _on_toggle_clicked(self, item): - """Toggle _is_open and rebuild, preserving selection.""" - try: - # Ensure a single-click toggle: suppress selection events during the operation - try: - self._suppress_selection_handler = True - except Exception: - pass - try: - try: - cur = item.isOpen() - item.setOpen(not cur) - except Exception: - try: - cur = bool(getattr(item, "_is_open", False)) - item._is_open = not cur - except Exception: - pass - # preserve selection ids and rebuild the visible rows - try: - self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) - except Exception: - self._last_selected_ids = set() - try: - self.rebuildTree() - except Exception: - pass - finally: - try: - self._suppress_selection_handler = False - except Exception: - pass - except Exception: - pass - - def _collect_all_descendants(self, item): - """Return set of all descendant items (recursive).""" - out = set() - stack = [] - try: - for c in getattr(item, "_children", []) or []: - stack.append(c) - except Exception: - pass - while stack: - cur = stack.pop() - out.add(cur) - try: - for ch in getattr(cur, "_children", []) or []: - stack.append(ch) - except Exception: - pass - return out - - def rebuildTree(self): - """Flatten visible items according to _is_open and populate the ListBox.""" - if self._backend_widget is None or self._listbox is None: - self._create_backend_widget() - try: - # clear listbox rows robustly: repeatedly remove first child until none remain - try: - while True: - first = None - try: - first = self._listbox.get_first_child() - except Exception: - # some bindings may return None / raise; try children() - try: - chs = self._listbox.get_children() - first = chs[0] if chs else None - except Exception: - first = None - if not first: - break - try: - self._listbox.remove(first) - except Exception: - try: - # fallback API - self._listbox.unbind_model() - break - except Exception: - break - except Exception: - pass - - self._rows = [] - self._row_to_item.clear() - self._item_to_row.clear() - self._visible_items = [] - - # Depth-first traversal producing visible nodes only when ancestors are open - def _visit(nodes, depth=0): - for n in nodes: - self._visible_items.append((n, depth)) - try: - is_open = bool(getattr(n, "_is_open", False)) - except Exception: - is_open = False - if is_open: - try: - childs = [] - if callable(getattr(n, "children", None)): - childs = n.children() or [] - else: - childs = getattr(n, "_children", []) or [] - except Exception: - childs = getattr(n, "_children", []) or [] - if childs: - _visit(childs, depth + 1) - - roots = list(getattr(self, "_items", []) or []) - _visit(roots, 0) - - # create rows - for item, depth in self._visible_items: - try: - row = self._make_row(item, depth) - self._listbox.append(row) - self._rows.append(row) - self._row_to_item[row] = item - self._item_to_row[item] = row - except Exception: - pass - - # restore previous selection (visible rows only) - try: - if self._last_selected_ids: - self._suppress_selection_handler = True - try: - self._listbox.unselect_all() - except Exception: - pass - for row, item in list(self._row_to_item.items()): - try: - if id(item) in self._last_selected_ids: - try: - row.set_selected(True) - except Exception: - pass - except Exception: - pass - self._suppress_selection_handler = False - except Exception: - self._suppress_selection_handler = False - - # rebuild logical selected items from rows - self._selected_items = [] - for row in self._rows: - try: - if getattr(row, "get_selected", None): - sel = row.get_selected() - else: - sel = bool(getattr(row, "_selected_flag", False)) - if sel: - it = self._row_to_item.get(row, None) - if it is not None: - self._selected_items.append(it) - except Exception: - pass - - self._last_selected_ids = set(id(i) for i in self._selected_items) - except Exception: - pass - - def _row_is_selected(self, r): - """Robust helper to detect whether a ListBoxRow is selected.""" - try: - # preferred API - sel = getattr(r, "get_selected", None) - if callable(sel): - return bool(sel()) - except Exception: - pass - try: - props = getattr(r, "props", None) - if props and hasattr(props, "selected"): - return bool(getattr(props, "selected")) - except Exception: - pass - # fallback: check whether the listbox reports this row as selected (some bindings) - try: - if self._listbox is not None and hasattr(self._listbox, "get_selected_rows"): - rows = self._listbox.get_selected_rows() or [] - for rr in rows: - if rr is r: - return True - except Exception: - pass - # last-resort flag - return bool(getattr(r, "_selected_flag", False)) - - def _gather_selected_rows(self): - """Return list of selected ListBoxRow objects (visible rows).""" - rows = [] - try: - # prefer listbox API if available - if self._listbox is not None and hasattr(self._listbox, "get_selected_rows"): - try: - sel = self._listbox.get_selected_rows() or [] - # If API returns Gtk.ListBoxRow-like objects, include them; otherwise fallback - for s in sel: - if s is None: - continue - # if path-like, ignore (we rely on visible rows) - if isinstance(s, type(self._rows[0])) if self._rows else False: - rows.append(s) - if rows: - return rows - except Exception: - pass - # fallback: scan our cached rows - for r in list(self._rows or []): - try: - if self._row_is_selected(r): - rows.append(r) - except Exception: - pass - except Exception: - pass - return rows - - def _apply_desired_ids_to_rows(self, desired_ids): - """Set visible rows selected state to match desired_ids (ids of items).""" - if self._listbox is None: - return - try: - self._suppress_selection_handler = True - except Exception: - pass - try: - try: - self._listbox.unselect_all() - except Exception: - # continue even if unsupported - pass - for row, it in list(self._row_to_item.items()): - try: - target = id(it) in desired_ids - try: - row.set_selected(bool(target)) - except Exception: - try: - setattr(row, "_selected_flag", bool(target)) - except Exception: - pass - except Exception: - pass - finally: - try: - self._suppress_selection_handler = False - except Exception: - pass - - def _on_row_selected(self, listbox, row): - """Handle selection change; update logical selected items reliably. - - When recursive selection is enabled and multi-selection is on, - selecting/deselecting a parent will also select/deselect all its descendants. - """ - # ignore if programmatic change in progress - if self._suppress_selection_handler: - return - - try: - selected_rows = self._gather_selected_rows() - - # map rows -> items - cur_selected_items = [] - for r in selected_rows: - try: - it = self._row_to_item.get(r, None) - if it is not None: - cur_selected_items.append(it) - except Exception: - pass - - prev_ids = set(self._last_selected_ids or []) - cur_ids = set(id(i) for i in cur_selected_items) - added = cur_ids - prev_ids - removed = prev_ids - cur_ids - - # If recursive+multi, compute desired ids by adding descendants of added and removing descendants of removed. - desired_ids = set(cur_ids) - if self._recursive and self._multi and (added or removed): - try: - # add descendants of newly added items - for a in list(added): - # find object - obj = None - for it in cur_selected_items: - if id(it) == a: - obj = it - break - if obj is None: - # try to find in whole tree - def _find_by_id(tid, nodes): - for n in nodes: - if id(n) == tid: - return n - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - r = _find_by_id(tid, chs) - if r: - return r - return None - obj = _find_by_id(a, list(getattr(self, "_items", []) or [])) - if obj is not None: - for d in self._collect_all_descendants(obj): - desired_ids.add(id(d)) - - # remove descendants of removed items - for r_id in list(removed): - try: - obj = None - # try find in tree - def _find_by_id2(tid, nodes): - for n in nodes: - if id(n) == tid: - return n - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - rr = _find_by_id2(tid, chs) - if rr: - return rr - return None - obj = _find_by_id2(r_id, list(getattr(self, "_items", []) or [])) - if obj is not None: - for d in self._collect_all_descendants(obj): - if id(d) in desired_ids: - desired_ids.discard(id(d)) - except Exception: - pass - - except Exception: - pass - - # Apply desired selection to visible rows - try: - self._apply_desired_ids_to_rows(desired_ids) - except Exception: - pass - - # Recompute cur_selected_items including non-visible descendants - new_selected = [] - try: - # visible rows - for r in list(self._rows or []): - try: - if self._row_is_selected(r): - it = self._row_to_item.get(r) - if it is not None: - new_selected.append(it) - except Exception: - pass - # include non-visible nodes that are requested by desired_ids - def _collect_all_nodes(nodes): - out = [] - for n in nodes: - out.append(n) - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - out.extend(_collect_all_nodes(chs)) - return out - for root in list(getattr(self, "_items", []) or []): - for n in _collect_all_nodes([root]): - try: - if id(n) in desired_ids and n not in new_selected: - new_selected.append(n) - except Exception: - pass - cur_selected_items = new_selected - cur_ids = set(id(i) for i in cur_selected_items) - except Exception: - pass - - # Update logical selection flags - try: - def _clear_flags(nodes): - for n in nodes: - try: - n.setSelected(False) - except Exception: - pass - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - _clear_flags(chs) - _clear_flags(list(getattr(self, "_items", []) or [])) - except Exception: - pass - - for it in cur_selected_items: - try: - it.setSelected(True) - except Exception: - pass - - # store logical selection - self._selected_items = list(cur_selected_items) - self._last_selected_ids = set(id(i) for i in self._selected_items) - - # notify immediate mode - if self._immediate and self.notify(): - try: - dlg = self.findDialog() - if dlg: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - except Exception: - pass - - def currentItem(self): - try: - return self._selected_items[0] if self._selected_items else None - except Exception: - return None - - def getSelectedItem(self): - return self.currentItem() - - def getSelectedItems(self): - return list(self._selected_items) - - def activate(self): - try: - itm = self.currentItem() - if itm is None: - return False - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) - return True - except Exception: - return False - - def hasMultiSelection(self): - """Return True if the tree allows selecting multiple items at once.""" - return bool(self._multi) - - def immediateMode(self): - return bool(self._immediate) - - def setImmediateMode(self, on:bool=True): - self._immediate = on - self.setNotify(on) - - def _set_backend_enabled(self, enabled): - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - try: - for it in list(getattr(self, "_items", []) or []): - try: - if hasattr(it, "setEnabled"): - it.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - - def get_backend_widget(self): - if self._backend_widget is None: - self._create_backend_widget() - return self._backend_widget - -class YFrameGtk(YSingleChildContainerWidget): - """ - GTK backend implementation of YFrame. - - - Uses Gtk.Frame (when available) to present a labeled framed container. - - Internally places a Gtk.Box inside the frame to host the single child. - - Honors child's stretchability: the frame reports stretchable when its child is stretchable - so parent layouts can allocate extra space. - - Provides simple property support for 'label'. - """ - def __init__(self, parent=None, label: str = ""): - super().__init__(parent) - self._label = label or "" - self._backend_widget = None - self._content_box = None - - def widgetClass(self): - return "YFrame" - - def label(self): - return self._label - - def setLabel(self, new_label: str): - """Set the frame label and update backend if created.""" - try: - self._label = new_label or "" - if getattr(self, "_backend_widget", None) is not None: - try: - # Gtk.Frame in GTK4 supports set_label() in some bindings, else use a child label - if hasattr(self._backend_widget, "set_label"): - self._backend_widget.set_label(self._label) - else: - # fallback: if we created a dedicated label child, update it - if getattr(self, "_label_widget", None) is not None: - try: - self._label_widget.set_text(self._label) - except Exception: - pass - except Exception: - pass - except Exception: - pass - - def stretchable(self, dim: YUIDimension): - """ - Report stretchability in a dimension. - - The frame is stretchable when its child is stretchable or has a layout weight. - """ - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is None: - return False - try: - if bool(child.stretchable(dim)): - return True - except Exception: - pass - try: - if bool(child.weight(dim)): - return True - except Exception: - pass - except Exception: - pass - return False - - def _attach_child_backend(self): - """Attach the child's backend widget into the frame's content box.""" - try: - if self._backend_widget is None: - return - if self._content_box is None: - return - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is None: - return - try: - cw = child.get_backend_widget() - except Exception: - cw = None - if cw is None: - return - - # Remove existing content children (defensive) - try: - while True: - first = self._content_box.get_first_child() - if first is None: - break - try: - self._content_box.remove(first) - except Exception: - break - except Exception: - pass - - # Append child widget into content box - try: - self._content_box.append(cw) - except Exception: - try: - self._content_box.add(cw) - except Exception: - pass - - # Ensure expansion hints propagate from child - try: - if child.stretchable(YUIDimension.YD_VERT): - if hasattr(cw, "set_vexpand"): - cw.set_vexpand(True) - if hasattr(cw, "set_valign"): - cw.set_valign(Gtk.Align.FILL) - else: - if hasattr(cw, "set_vexpand"): - cw.set_vexpand(False) - if hasattr(cw, "set_valign"): - cw.set_valign(Gtk.Align.START) - if child.stretchable(YUIDimension.YD_HORIZ): - if hasattr(cw, "set_hexpand"): - cw.set_hexpand(True) - if hasattr(cw, "set_halign"): - cw.set_halign(Gtk.Align.FILL) - else: - if hasattr(cw, "set_hexpand"): - cw.set_hexpand(False) - if hasattr(cw, "set_halign"): - cw.set_halign(Gtk.Align.START) - except Exception: - pass - except Exception: - pass - - def addChild(self, child): - """Add logical child and attach backend if possible.""" - try: - super().addChild(child) - except Exception: - # best-effort fallback - try: - self._child = child - child._parent = self - except Exception: - pass - # attach to backend if ready - try: - if getattr(self, "_backend_widget", None) is not None: - self._attach_child_backend() - except Exception: - pass - - def setChild(self, child): - """Set single logical child and attach backend if possible.""" - try: - super().setChild(child) - except Exception: - try: - self._child = child - child._parent = self - except Exception: - pass - try: - if getattr(self, "_backend_widget", None) is not None: - self._attach_child_backend() - except Exception: - pass - - def _create_backend_widget(self): - """ - Create a Gtk.Frame + inner box to host the single child. - Fall back to a bordered Gtk.Box when Gtk.Frame or set_label is not available. - """ - try: - # Try to create a Gtk.Frame with a label if supported - try: - frame = Gtk.Frame() - # set label if API supports it - if hasattr(frame, "set_label"): - frame.set_label(self._label) - self._label_widget = None - else: - # create a label widget and set as label using set_label_widget if supported - lbl = Gtk.Label(label=self._label) - self._label_widget = lbl - if hasattr(frame, "set_label_widget"): - frame.set_label_widget(lbl) - # Create inner content box - content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - content.set_hexpand(True) - content.set_vexpand(True) - # Append content inside frame. In GTK4 a Frame can have a single child. - try: - frame.set_child(content) - except Exception: - try: - # fallback: some bindings use add() - frame.add(content) - except Exception: - pass - self._backend_widget = frame - self._content_box = content - # attach existing child if any - try: - if getattr(self, "_child", None): - self._attach_child_backend() - except Exception: - pass - return - except Exception: - # fallback to a boxed container with a visible border using CSS if Frame creation fails - pass - - # Fallback container: vertical box with a top label and a framed-like border (best-effort) - container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - try: - lbl = Gtk.Label(label=self._label) - lbl.set_xalign(0.0) - container.append(lbl) - # content area - content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - content.set_hexpand(True) - content.set_vexpand(True) - container.append(content) - self._label_widget = lbl - self._backend_widget = container - self._content_box = content - if getattr(self, "_child", None): - try: - self._attach_child_backend() - except Exception: - pass - except Exception: - # ultimate fallback: empty widget reference - self._backend_widget = None - self._content_box = None - except Exception: - self._backend_widget = None - self._content_box = None - - def _set_backend_enabled(self, enabled): - """Enable/disable the frame and propagate to child.""" - try: - if self._backend_widget is not None: - try: - self._backend_widget.set_sensitive(enabled) - except Exception: - pass - except Exception: - pass - # propagate to logical child - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is not None: - try: - child.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - - def setProperty(self, propertyName, val): - """Handle simple properties; returns True if property handled here.""" - try: - if propertyName == "label": - try: - self.setLabel(str(val)) - except Exception: - pass - return True - except Exception: - pass - return False - - def getProperty(self, propertyName): - try: - if propertyName == "label": - return self.label() - except Exception: - pass - return None - - def propertySet(self): - """Return a minimal property set description for introspection.""" - try: - props = YPropertySet() - try: - props.add(YProperty("label", YPropertyType.YStringProperty)) - except Exception: - pass - return props - except Exception: - return None From 502626148e4416baad08b2eba5700fa236990286 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 7 Dec 2025 22:48:40 +0100 Subject: [PATCH 115/523] moved to single file for any curses widget class definition --- manatools/aui/backends/curses/__init__.py | 29 + .../aui/backends/curses/alignmentcurses.py | 143 + .../aui/backends/curses/checkboxcurses.py | 110 + .../aui/backends/curses/comboboxcurses.py | 246 ++ manatools/aui/backends/curses/commoncurses.py | 55 + manatools/aui/backends/curses/dialogcurses.py | 385 +++ manatools/aui/backends/curses/framecurses.py | 226 ++ manatools/aui/backends/curses/hboxcurses.py | 165 ++ .../aui/backends/curses/inputfieldcurses.py | 153 + manatools/aui/backends/curses/labelcurses.py | 64 + .../aui/backends/curses/pushbuttoncurses.py | 93 + .../aui/backends/curses/selectionboxcurses.py | 284 ++ manatools/aui/backends/curses/treecurses.py | 596 ++++ manatools/aui/backends/curses/vboxcurses.py | 140 + manatools/aui/yui_curses.py | 2452 +---------------- 15 files changed, 2719 insertions(+), 2422 deletions(-) create mode 100644 manatools/aui/backends/curses/__init__.py create mode 100644 manatools/aui/backends/curses/alignmentcurses.py create mode 100644 manatools/aui/backends/curses/checkboxcurses.py create mode 100644 manatools/aui/backends/curses/comboboxcurses.py create mode 100644 manatools/aui/backends/curses/commoncurses.py create mode 100644 manatools/aui/backends/curses/dialogcurses.py create mode 100644 manatools/aui/backends/curses/framecurses.py create mode 100644 manatools/aui/backends/curses/hboxcurses.py create mode 100644 manatools/aui/backends/curses/inputfieldcurses.py create mode 100644 manatools/aui/backends/curses/labelcurses.py create mode 100644 manatools/aui/backends/curses/pushbuttoncurses.py create mode 100644 manatools/aui/backends/curses/selectionboxcurses.py create mode 100644 manatools/aui/backends/curses/treecurses.py create mode 100644 manatools/aui/backends/curses/vboxcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py new file mode 100644 index 0000000..a402e63 --- /dev/null +++ b/manatools/aui/backends/curses/__init__.py @@ -0,0 +1,29 @@ +from .dialogcurses import YDialogCurses +from .checkboxcurses import YCheckBoxCurses +from .hboxcurses import YHBoxCurses +from .vboxcurses import YVBoxCurses +from .labelcurses import YLabelCurses +from .pushbuttoncurses import YPushButtonCurses +from .treecurses import YTreeCurses +from .alignmentcurses import YAlignmentCurses +from .comboboxcurses import YComboBoxCurses +from .framecurses import YFrameCurses +from .inputfieldcurses import YInputFieldCurses +from .selectionboxcurses import YSelectionBoxCurses + + +__all__ = [ + "YDialogCurses", + "YFrameCurses", + "YVBoxCurses", + "YHBoxCurses", + "YTreeCurses", + "YSelectionBoxCurses", + "YLabelCurses", + "YPushButtonCurses", + "YInputFieldCurses", + "YCheckBoxCurses", + "YComboBoxCurses", + "YAlignmentCurses", + # ... +] diff --git a/manatools/aui/backends/curses/alignmentcurses.py b/manatools/aui/backends/curses/alignmentcurses.py new file mode 100644 index 0000000..937b947 --- /dev/null +++ b/manatools/aui/backends/curses/alignmentcurses.py @@ -0,0 +1,143 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YAlignmentCurses(YSingleChildContainerWidget): + """ + Single-child alignment container for ncurses. It becomes stretchable on the + requested axes, and positions the child inside its draw area accordingly. + """ + def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): + super().__init__(parent) + self._halign_spec = horAlign + self._valign_spec = vertAlign + self._backend_widget = None # not used by curses + self._height = 1 + + def widgetClass(self): + return "YAlignment" + + def stretchable(self, dim: YUIDimension): + ''' Returns the stretchability of the layout box: + * The layout box is stretchable if the child is stretchable in + * this dimension or if the child widget has a layout weight in + * this dimension. + ''' + if self._child: + expand = bool(self._child.stretchable(dim)) + weight = bool(self._child.weight(dim)) + if expand or weight: + return True + return False + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + # Ensure child is visible to traversal (dialog looks at widget._children) + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + if child not in self._children: + self._children.append(child) + # keep parent pointer consistent + try: + setattr(child, "_parent", self) + except Exception: + pass + except Exception: + pass + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + # Mirror to _children so focus traversal finds it + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + # replace existing children with this single child to avoid stale entries + if self._children != [child]: + self._children = [child] + try: + setattr(child, "_parent", self) + except Exception: + pass + except Exception: + pass + + def _create_backend_widget(self): + self._backend_widget = None + self._height = max(1, getattr(self._child, "_height", 1) if self._child else 1) + + def _set_backend_enabled(self, enabled): + """Enable/disable alignment container and propagate to its logical child.""" + try: + # propagate to logical child so it updates its own focusability/state + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None and hasattr(child, "setEnabled"): + try: + child.setEnabled(enabled) + except Exception: + pass + # nothing else to do for curses backend (no real widget object) + except Exception: + pass + + def _child_min_width(self, child, max_width): + # Heuristic minimal width similar to YHBoxCurses TODO: verify with widget information instead of hardcoded classes + try: + cls = child.widgetClass() if hasattr(child, "widgetClass") else "" + if cls in ("YLabel", "YPushButton", "YCheckBox"): + text = getattr(child, "_text", None) + if text is None: + text = getattr(child, "_label", "") + pad = 4 if cls == "YPushButton" else 0 + return min(max_width, max(1, len(str(text)) + pad)) + except Exception: + pass + return max(1, min(10, max_width)) + + def _draw(self, window, y, x, width, height): + if not self._child or not hasattr(self._child, "_draw"): + return + try: + # width to give to the child: minimal needed (so it can be pushed) + ch_min_w = self._child_min_width(self._child, width) + # Horizontal position + if self._halign_spec == YAlignmentType.YAlignEnd: + cx = x + max(0, width - ch_min_w) + elif self._halign_spec == YAlignmentType.YAlignCenter: + cx = x + max(0, (width - ch_min_w) // 2) + else: + cx = x + # Vertical position (single line widgets mostly) + if self._valign_spec == YAlignmentType.YAlignCenter: + cy = y + max(0, (height - 1) // 2) + elif self._valign_spec == YAlignmentType.YAlignEnd: + cy = y + max(0, height - 1) + else: + cy = y + self._child._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, getattr(self._child, "_height", 1))) + except Exception: + pass diff --git a/manatools/aui/backends/curses/checkboxcurses.py b/manatools/aui/backends/curses/checkboxcurses.py new file mode 100644 index 0000000..1785e22 --- /dev/null +++ b/manatools/aui/backends/curses/checkboxcurses.py @@ -0,0 +1,110 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YCheckBoxCurses(YWidget): + def __init__(self, parent=None, label="", is_checked=False): + super().__init__(parent) + self._label = label + self._is_checked = is_checked + self._focused = False + self._can_focus = True + self._height = 1 + + def widgetClass(self): + return "YCheckBox" + + def value(self): + return self._is_checked + + def setValue(self, checked): + self._is_checked = checked + + def label(self): + return self._label + + def _create_backend_widget(self): + # In curses, there's no actual backend widget, just internal state + pass + + def _set_backend_enabled(self, enabled): + """Enable/disable checkbox: update focusability and collapse focus if disabling.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + checkbox_symbol = "[X]" if self._is_checked else "[ ]" + text = f"{checkbox_symbol} {self._label}" + if len(text) > width: + text = text[:max(0, width - 3)] + "..." + + if self._focused and self.isEnabled(): + window.attron(curses.A_REVERSE) + elif not self.isEnabled(): + # indicate disabled with dim attribute + window.attron(curses.A_DIM) + + window.addstr(y, x, text) + + if self._focused and self.isEnabled(): + window.attroff(curses.A_REVERSE) + elif not self.isEnabled(): + try: + window.attroff(curses.A_DIM) + except Exception: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self.isEnabled(): + return False + # Space or Enter to toggle + if key in (ord(' '), ord('\n'), curses.KEY_ENTER): + self._toggle() + return True + return False + + def _toggle(self): + """Toggle checkbox state and post event""" + self._is_checked = not self._is_checked + + if self.notify(): + # Post a YWidgetEvent to the containing dialog + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + print(f"CheckBox toggled (no dialog found): {self._label} = {self._is_checked}") diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py new file mode 100644 index 0000000..987428d --- /dev/null +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -0,0 +1,246 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YComboBoxCurses(YSelectionWidget): + def __init__(self, parent=None, label="", editable=False): + super().__init__(parent) + self._label = label + self._editable = editable + self._value = "" + self._focused = False + self._can_focus = True + self._height = 1 + self._expanded = False + self._hover_index = 0 + self._combo_x = 0 + self._combo_y = 0 + self._combo_width = 0 + + def widgetClass(self): + return "YComboBox" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + # Update selected items + self._selected_items = [] + for item in self._items: + if item.label() == text: + self._selected_items.append(item) + break + + def editable(self): + return self._editable + + def _create_backend_widget(self): + self._backend_widget = None + + def _set_backend_enabled(self, enabled): + """Enable/disable combobox: affect focusability, expanded state and focused state.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + # collapse expanded dropdown if any + try: + if getattr(self, "_expanded", False): + self._expanded = False + except Exception: + pass + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + + def _draw(self, window, y, x, width, height): + # Store position and dimensions for dropdown drawing + self._combo_y = y + self._combo_x = x + self._combo_width = width + + try: + # Calculate available space for combo box + label_space = len(self._label) + 1 if self._label else 0 + combo_space = width - label_space + + if combo_space <= 3: + return + + # Draw label + if self._label: + label_text = self._label + if len(label_text) > label_space - 1: + label_text = label_text[:label_space - 1] + lbl_attr = curses.A_NORMAL + if not self.isEnabled(): + lbl_attr |= curses.A_DIM + window.addstr(y, x, label_text, lbl_attr) + x += len(label_text) + 1 + + # Prepare display value + display_value = self._value if self._value else "Select..." + max_display_width = combo_space - 3 + if len(display_value) > max_display_width: + display_value = display_value[:max_display_width] + "..." + + # Draw combo box background + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + + combo_bg = " " * combo_space + window.addstr(y, x, combo_bg, attr) + + combo_text = f" {display_value} ▼" + if len(combo_text) > combo_space: + combo_text = combo_text[:combo_space] + + window.addstr(y, x, combo_text, attr) + + # Draw expanded list if active and enabled + if self._expanded and self.isEnabled(): + self._draw_expanded_list(window) + except curses.error: + pass + + + def _draw_expanded_list(self, window): + """Draw the expanded dropdown list at correct position""" + if not self._expanded or not self._items: + return + + try: + # Make sure we don't draw outside screen + screen_height, screen_width = window.getmaxyx() + + list_height = min(len(self._items), screen_height) + + # Calculate dropdown position - right below the combo box + dropdown_y = self._combo_y + 1 + dropdown_x = self._combo_x + (len(self._label) + 1 if self._label else 0) + dropdown_width = self._combo_width - (len(self._label) + 1 if self._label else 0) + + # If not enough space below, draw above + if dropdown_y + list_height >= screen_height: + dropdown_y = max(1, self._combo_y - list_height - 1) + + # Ensure dropdown doesn't go beyond right edge + if dropdown_x + dropdown_width >= screen_width: + dropdown_width = screen_width - dropdown_x - 1 + + if dropdown_width <= 5: # Need reasonable width + return + + # Draw dropdown background for each item + for i in range(list_height): + if i >= len(self._items): + break + + item = self._items[i] + item_text = item.label() + if len(item_text) > dropdown_width - 2: + item_text = item_text[:dropdown_width - 2] + "..." + + # Highlight hovered item + attr = curses.A_REVERSE if i == self._hover_index else curses.A_NORMAL + + # Create background for the item + bg_text = " " + item_text.ljust(dropdown_width - 2) + if len(bg_text) > dropdown_width: + bg_text = bg_text[:dropdown_width] + + # Ensure we don't write beyond screen bounds + if (dropdown_y + i < screen_height and + dropdown_x < screen_width and + dropdown_x + len(bg_text) <= screen_width): + try: + window.addstr(dropdown_y + i, dropdown_x, bg_text, attr) + except curses.error: + pass # Ignore out-of-bounds errors + + except curses.error: + # Ignore drawing errors + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + handled = True + + # If currently expanded, give expanded-list handling priority so Enter + # selects the hovered item instead of simply toggling expansion. + if self._expanded: + # Handle navigation in expanded list + if key == curses.KEY_UP: + if self._hover_index > 0: + self._hover_index -= 1 + elif key == curses.KEY_DOWN: + if self._hover_index < len(self._items) - 1: + self._hover_index += 1 + elif key == ord('\n') or key == ord(' '): + # Select hovered item + if self._items and 0 <= self._hover_index < len(self._items): + selected_item = self._items[self._hover_index] + self.setValue(selected_item.label()) # update internal value/selection + self._expanded = False + if self.notify(): + # force parent dialog redraw if present + dlg = self.findDialog() + if dlg is not None: + try: + # notify dialog to redraw immediately + dlg._last_draw_time = 0 + # post a widget event for selection change + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + # selection made -> handled + elif key == 27: # ESC key + self._expanded = False + else: + handled = False + else: + # Not expanded: Enter/Space expands the list + if key == ord('\n') or key == ord(' '): + self._expanded = not self._expanded + if self._expanded and self._items: + # Set hover index to current value if exists + self._hover_index = 0 + if self._value: + for i, item in enumerate(self._items): + if item.label() == self._value: + self._hover_index = i + break + else: + handled = False + + return handled diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py new file mode 100644 index 0000000..f93cff3 --- /dev/null +++ b/manatools/aui/backends/curses/commoncurses.py @@ -0,0 +1,55 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + +__all__ = ["_curses_recursive_min_height"] + + +def _curses_recursive_min_height(widget): + """Compute minimal height for a widget, recursively considering container children.""" + if widget is None: + return 1 + try: + cls = widget.widgetClass() if hasattr(widget, "widgetClass") else "" + except Exception: + cls = "" + try: + if cls == "YVBox": + chs = list(getattr(widget, "_children", []) or []) + spacing = max(0, len(chs) - 1) + total = 0 + for c in chs: + total += _curses_recursive_min_height(c) + return max(1, total + spacing) + elif cls == "YHBox": + chs = list(getattr(widget, "_children", []) or []) + tallest = 1 + for c in chs: + tallest = max(tallest, _curses_recursive_min_height(c)) + return max(1, tallest) + elif cls == "YAlignment": + child = getattr(widget, "_child", None) + return max(1, _curses_recursive_min_height(child)) + elif cls == "YFrame": + child = getattr(widget, "_child", None) + inner_top = max(0, getattr(widget, "_inner_top_padding", 1)) + inner_min = _curses_recursive_min_height(child) + return max(3, 2 + inner_top + inner_min) # borders(2) + padding + inner + else: + return max(1, getattr(widget, "_height", 1)) + except Exception: + return max(1, getattr(widget, "_height", 1)) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py new file mode 100644 index 0000000..d2aff98 --- /dev/null +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -0,0 +1,385 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + +class YDialogCurses(YSingleChildContainerWidget): + _open_dialogs = [] + _current_dialog = None + + def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): + super().__init__() + self._dialog_type = dialog_type + self._color_mode = color_mode + self._is_open = False + self._window = None + self._focused_widget = None + self._last_draw_time = 0 + self._draw_interval = 0.1 # seconds + self._event_result = None + # Debounce for resize handling (avoid flicker) + self._resize_pending_until = 0.0 + self._last_term_size = (0, 0) # (h, w) + YDialogCurses._open_dialogs.append(self) + + def widgetClass(self): + return "YDialog" + + @staticmethod + def currentDialog(doThrow=True): + open_dialog = YDialogCurses._open_dialogs[-1] if YDialogCurses._open_dialogs else None + if not open_dialog and doThrow: + raise YUINoDialogException("No dialog is currently open") + return open_dialog + + @staticmethod + def topmostDialog(doThrow=True): + ''' same as currentDialog ''' + return YDialogCurses.currentDialog(doThrow=doThrow) + + def isTopmostDialog(self): + '''Return whether this dialog is the topmost open dialog.''' + return YDialogCurses._open_dialogs[-1] == self if YDialogCurses._open_dialogs else False + + def open(self): + if not self._window: + self._create_backend_widget() + + self._is_open = True + YDialogCurses._current_dialog = self + + # Find first focusable widget + focusable = self._find_focusable_widgets() + if focusable: + self._focused_widget = focusable[0] + self._focused_widget._focused = True + + # open() must be non-blocking (finalize and show). Event loop is + # started by waitForEvent() to match libyui semantics. + return True + + def isOpen(self): + return self._is_open + + def destroy(self, doThrow=True): + self._is_open = False + if self in YDialogCurses._open_dialogs: + YDialogCurses._open_dialogs.remove(self) + if YDialogCurses._current_dialog == self: + YDialogCurses._current_dialog = None + return True + + @classmethod + def deleteTopmostDialog(cls, doThrow=True): + if cls._open_dialogs: + dialog = cls._open_dialogs[-1] + return dialog.destroy(doThrow) + return False + + @classmethod + def currentDialog(cls, doThrow=True): + if not cls._open_dialogs: + if doThrow: + raise YUINoDialogException("No dialog open") + return None + return cls._open_dialogs[-1] + + def _create_backend_widget(self): + # Use the main screen + self._backend_widget = curses.newwin(0, 0, 0, 0) + + def _set_backend_enabled(self, enabled): + """Enable/disable the dialog and propagate to contained widgets.""" + try: + # propagate logical enabled state to entire subtree using setEnabled on children + # so each widget's hook executes and updates its state. + if getattr(self, "_child", None): + try: + self._child.setEnabled(enabled) + except Exception: + pass + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + # If disabling and dialog had focused widget, clear focus + if not enabled: + try: + if getattr(self, "_focused_widget", None): + self._focused_widget._focused = False + self._focused_widget = None + except Exception: + pass + # Force a redraw so disabled/enabled visual state appears immediately + try: + self._last_draw_time = 0 + except Exception: + pass + except Exception: + pass + + def _draw_dialog(self): + """Draw the entire dialog (called by event loop)""" + if not hasattr(self, '_backend_widget') or not self._backend_widget: + return + + try: + height, width = self._backend_widget.getmaxyx() + + # Clear screen + self._backend_widget.clear() + + # Draw border + self._backend_widget.border() + + # Draw title + title = " manatools YUI NCurses Dialog " + try: + from . import yui as yui_mod + appobj = None + # YUI._backend may hold the backend instance (YUIQt) + backend = getattr(yui_mod.YUI, "_backend", None) + if backend: + if hasattr(backend, "application"): + appobj = backend.application() + # fallback: YUI._instance might be set and expose application/yApp + if not appobj: + inst = getattr(yui_mod.YUI, "_instance", None) + if inst: + if hasattr(inst, "application"): + appobj = inst.application() + if appobj and hasattr(appobj, "applicationTitle"): + atitle = appobj.applicationTitle() + if atitle: + title = atitle + if appobj: + appobj.setApplicationTitle(title) + except Exception: + # ignore and keep default + pass + title_x = max(0, (width - len(title)) // 2) + self._backend_widget.addstr(0, title_x, title, curses.A_BOLD) + + # Draw content area - fixed coordinates for child + content_height = height - 4 + content_width = width - 4 + content_y = 2 + content_x = 2 + + # Draw child content + if self._child: + self._draw_child_content(content_y, content_x, content_width, content_height) + + # Draw footer with instructions + footer_text = " TAB=Navigate | SPACE=Expand | ENTER=Select | F10/Q=Quit " + footer_x = max(0, (width - len(footer_text)) // 2) + if footer_x + len(footer_text) < width: + self._backend_widget.addstr(height - 1, footer_x, footer_text, curses.A_DIM) + + # Draw focus indicator + if self._focused_widget: + focus_text = f" Focus: {getattr(self._focused_widget, '_label', 'Unknown')} " + if len(focus_text) < width: + self._backend_widget.addstr(height - 1, 2, focus_text, curses.A_REVERSE) + #if the focused widget has an expnded list (menus, combos,...), draw it on top + if hasattr(self._focused_widget, "_draw_expanded_list"): + self._focused_widget._draw_expanded_list(self._backend_widget) + + # Refresh main window first + self._backend_widget.refresh() + + except curses.error as e: + # Ignore curses errors (like writing beyond screen bounds) + pass + + def _draw_child_content(self, start_y, start_x, max_width, max_height): + """Draw the child widget content respecting container hierarchy""" + if not self._child: + return + + # Draw only the root child - it will handle drawing its own children + if hasattr(self._child, '_draw'): + self._child._draw(self._backend_widget, start_y, start_x, max_width, max_height) + + + def _cycle_focus(self, forward=True): + """Cycle focus between focusable widgets""" + focusable = self._find_focusable_widgets() + if not focusable: + return + + current_index = -1 + if self._focused_widget: + for i, widget in enumerate(focusable): + if widget == self._focused_widget: + current_index = i + break + + if current_index == -1: + new_index = 0 + else: + if forward: + new_index = (current_index + 1) % len(focusable) + else: + new_index = (current_index - 1) % len(focusable) + + # If the currently focused widget is an expanded combo, collapse it + # so tabbing away closes the dropdown but does not change selection. + if self._focused_widget: + try: + if getattr(self._focused_widget, "_expanded", False): + self._focused_widget._expanded = False + except Exception: + pass + self._focused_widget._focused = False + + self._focused_widget = focusable[new_index] + self._focused_widget._focused = True + # Force redraw on focus change + self._last_draw_time = 0 + + def _find_focusable_widgets(self): + """Find all widgets that can receive focus""" + focusable = [] + + def find_in_widget(widget): + if hasattr(widget, '_can_focus') and widget._can_focus: + focusable.append(widget) + for child in widget._children: + find_in_widget(child) + + if self._child: + find_in_widget(self._child) + + return focusable + + + def _post_event(self, event): + """Post an event to this dialog; waitForEvent will return it.""" + self._event_result = event + # If dialog is not open anymore, ensure cleanup + if isinstance(event, YCancelEvent): + # Mark closed so loop can clean up + self._is_open = False + + def waitForEvent(self, timeout_millisec=0): + """ + Run the ncurses event loop until an event is posted or timeout occurs. + timeout_millisec == 0 -> block indefinitely until an event (no timeout). + Returns a YEvent (YWidgetEvent, YTimeoutEvent, YCancelEvent, ...). + """ + from manatools.aui.yui import YUI + ui = YUI.ui() + + # Ensure dialog is open/finalized + if not self._is_open: + self.open() + + self._event_result = None + deadline = None + if timeout_millisec and timeout_millisec > 0: + deadline = time.time() + (timeout_millisec / 1000.0) + + while self._is_open and self._event_result is None: + try: + now = time.time() + + # Apply pending resize once debounce expires + if self._resize_pending_until and now >= self._resize_pending_until: + try: + ui._stdscr.clear() + ui._stdscr.refresh() + new_h, new_w = ui._stdscr.getmaxyx() + try: + curses.resizeterm(new_h, new_w) + except Exception: + pass + # Recreate backend window (full-screen) + self._backend_widget = curses.newwin(new_h, new_w, 0, 0) + self._last_term_size = (new_h, new_w) + except Exception: + pass + # Clear pending flag and force immediate redraw + self._resize_pending_until = 0.0 + self._last_draw_time = 0 + + # Draw at most every _draw_interval; forced redraw uses last_draw_time = 0 + if (now - self._last_draw_time) >= self._draw_interval: + self._draw_dialog() + self._last_draw_time = now + + # Non-blocking input + ui._stdscr.nodelay(True) + key = ui._stdscr.getch() + + if key == -1: + if deadline and time.time() >= deadline: + self._event_result = YTimeoutEvent() + break + time.sleep(0.01) + continue + + # Global keys + if key == curses.KEY_F10 or key == ord('q') or key == ord('Q'): + self._post_event(YCancelEvent()) + break + elif key == curses.KEY_RESIZE: + # Debounce resize; do not redraw immediately to avoid flicker + try: + new_h, new_w = ui._stdscr.getmaxyx() + self._last_term_size = (new_h, new_w) + except Exception: + pass + # Wait 150ms after the last resize event before applying + self._resize_pending_until = time.time() + 0.15 + continue + + # Focus navigation + if key == ord('\t'): + self._cycle_focus(forward=True) + self._last_draw_time = 0 + continue + elif key == curses.KEY_BTAB: + self._cycle_focus(forward=False) + self._last_draw_time = 0 + continue + + # Dispatch key to focused widget + if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): + handled = self._focused_widget._handle_key(key) + if handled: + self._last_draw_time = 0 + + except KeyboardInterrupt: + self._post_event(YCancelEvent()) + break + except Exception: + time.sleep(0.05) + + if self._event_result is None: + if not self._is_open: + self._event_result = YCancelEvent() + elif deadline and time.time() >= deadline: + self._event_result = YTimeoutEvent() + + if not self._is_open: + try: + self.destroy() + except Exception: + pass + + return self._event_result if self._event_result is not None else YEvent() diff --git a/manatools/aui/backends/curses/framecurses.py b/manatools/aui/backends/curses/framecurses.py new file mode 100644 index 0000000..9da0dd4 --- /dev/null +++ b/manatools/aui/backends/curses/framecurses.py @@ -0,0 +1,226 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + +from .commoncurses import _curses_recursive_min_height + +class YFrameCurses(YSingleChildContainerWidget): + """ + NCurses implementation of YFrame. + - Draws a framed box with a title. + - Hosts a single child inside the frame with inner margins so the child's + own label does not overlap the frame title. + - Reports stretchability based on its child. + """ + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label or "" + self._backend_widget = None + # Preferred minimal height is computed from child (see _update_min_height) + self._height = 3 + # inner top padding to separate frame title from child's label + self._inner_top_padding = 1 + + def widgetClass(self): + return "YFrame" + + def _update_min_height(self): + """Recompute minimal height: at least 3 rows or child layout min + borders + padding.""" + try: + child = getattr(self, "_child", None) + inner_min = _curses_recursive_min_height(child) if child is not None else 1 + self._height = max(3, 2 + self._inner_top_padding + inner_min) + except Exception: + self._height = max(self._height, 3) + + def label(self): + return self._label + + def setLabel(self, new_label): + try: + self._label = str(new_label) + except Exception: + self._label = new_label + + def stretchable(self, dim): + """Frame is stretchable if its child is stretchable or has a weight.""" + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return False + try: + if bool(child.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(child.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def _create_backend_widget(self): + # curses backend does not create a separate widget object for frames; + # drawing is performed in _draw by the parent container. + self._backend_widget = None + # Update minimal height based on the child + self._update_min_height() + + def _set_backend_enabled(self, enabled): + """Propagate enabled state to the child.""" + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is not None and hasattr(child, "setEnabled"): + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + # ensure traversal lists contain the child + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + if child not in self._children: + self._children.append(child) + try: + child._parent = self + except Exception: + pass + except Exception: + pass + # Update minimal height based on the child + self._update_min_height() + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + try: + self._children = [child] + try: + child._parent = self + except Exception: + pass + except Exception: + pass + # Update minimal height based on the child + self._update_min_height() + + def _draw(self, window, y, x, width, height): + """Draw frame border and title, then draw child inside inner area with margins.""" + try: + if width <= 0 or height <= 0: + return + # Ensure minimal height based on child layout before drawing + self._update_min_height() + # Graceful fallback for very small areas + if height < 3 or width < 4: + try: + if self._label and height >= 1 and width > 2: + title = f" {self._label} " + title = title[:max(0, width - 2)] + window.addstr(y, x, title, curses.A_BOLD) + except curses.error: + pass + return + # Choose box characters (prefer ACS if available) + try: + hline = curses.ACS_HLINE + vline = curses.ACS_VLINE + tl = curses.ACS_ULCORNER + tr = curses.ACS_URCORNER + bl = curses.ACS_LLCORNER + br = curses.ACS_LRCORNER + except Exception: + hline = ord('-') + vline = ord('|') + tl = ord('+') + tr = ord('+') + bl = ord('+') + br = ord('+') + + # Draw corners and edges + try: + window.addch(y, x, tl) + window.addch(y, x + width - 1, tr) + window.addch(y + height - 1, x, bl) + window.addch(y + height - 1, x + width - 1, br) + for cx in range(x + 1, x + width - 1): + window.addch(y, cx, hline) + window.addch(y + height - 1, cx, hline) + for cy in range(y + 1, y + height - 1): + window.addch(cy, x, vline) + window.addch(cy, x + width - 1, vline) + except curses.error: + # best-effort: ignore drawing errors when area is too small + pass + + # Draw title centered on top border (leave at least one space from corners) + if self._label: + try: + title = f" {self._label} " + max_title_len = max(0, width - 4) + if len(title) > max_title_len: + title = title[:max(0, max_title_len - 3)] + "..." + start_x = x + max(1, (width - len(title)) // 2) + # overwrite part of top border with title text + window.addstr(y, start_x, title, curses.A_BOLD) + except curses.error: + pass + + # Compute inner content rectangle + inner_x = x + 1 + inner_y = y + 1 + inner_w = max(0, width - 2) + inner_h = max(0, height - 2) + + pad_top = min(self._inner_top_padding, max(0, inner_h)) + content_y = inner_y + pad_top + content_h = max(0, inner_h - pad_top) + + child = getattr(self, "_child", None) + if child is None: + return + + # Clamp content height to at least the child layout minimal height + needed = _curses_recursive_min_height(child) + # Do not exceed available area; this only influences the draw area passed down + content_h = min(max(content_h, needed), inner_h) + + if content_h <= 0 or inner_w <= 0: + return + if hasattr(child, "_draw"): + child._draw(window, content_y, inner_x, inner_w, content_h) + except Exception: + pass diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py new file mode 100644 index 0000000..83b4ee8 --- /dev/null +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -0,0 +1,165 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + +from .commoncurses import _curses_recursive_min_height + +class YHBoxCurses(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + # Minimum height will be computed from children + self._height = 1 + + def widgetClass(self): + return "YHBox" + + def _create_backend_widget(self): + self._backend_widget = None + + def _recompute_min_height(self): + """Compute minimal height for this horizontal box as the tallest child's minimum.""" + try: + if not self._children: + self._height = 1 + return + self._height = max(1, max(_curses_recursive_min_height(c) for c in self._children)) + except Exception: + self._height = 1 + + def addChild(self, child): + """Ensure internal children list and recompute minimal height.""" + try: + super().addChild(child) + except Exception: + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + self._children.append(child) + child._parent = self + except Exception: + pass + self._recompute_min_height() + + def setChild(self, child): + """Not typical for HBox, but keep parity with containers.""" + try: + super().setChild(child) + except Exception: + try: + self._children = [child] + child._parent = self + except Exception: + pass + self._recompute_min_height() + + def _set_backend_enabled(self, enabled): + """Enable/disable HBox and propagate to logical children.""" + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + + def _child_min_width(self, child, max_width): + # Best-effort minimal width heuristic + try: + if hasattr(child, "minWidth"): + return min(max_width, max(1, int(child.minWidth()))) + except Exception: + pass + # Heuristics based on common attributes + try: + cls = child.widgetClass() if hasattr(child, "widgetClass") else "" + if cls in ("YLabel", "YPushButton", "YCheckBox"): + text = getattr(child, "_text", None) + if text is None: + text = getattr(child, "_label", "") + pad = 4 if cls == "YPushButton" else 0 + return min(max_width, max(1, len(str(text)) + pad)) + except Exception: + pass + return max(1, min(10, max_width)) # safe default + + def _draw(self, window, y, x, width, height): + # Ensure minimal height reflects children so the parent allocated enough rows + self._recompute_min_height() + num_children = len(self._children) + if num_children == 0 or width <= 0 or height <= 0: + return + + spacing = max(0, num_children - 1) + available = max(0, width - spacing) + + widths = [0] * num_children + stretchables = [] + fixed_total = 0 + for i, child in enumerate(self._children): + if child.stretchable(YUIDimension.YD_HORIZ): + stretchables.append(i) + else: + w = self._child_min_width(child, available) + widths[i] = w + fixed_total += w + + remaining = max(0, available - fixed_total) + if stretchables: + per = remaining // len(stretchables) + extra = remaining % len(stretchables) + for k, idx in enumerate(stretchables): + widths[idx] = max(1, per + (1 if k < extra else 0)) + else: + if fixed_total < available: + leftover = available - fixed_total + per = leftover // num_children + extra = leftover % num_children + for i in range(num_children): + base = widths[i] if widths[i] else 1 + widths[i] = base + per + (1 if i < extra else 0) + + # Draw children and pass full container height to stretchable children + cx = x + for i, child in enumerate(self._children): + w = widths[i] + if w <= 0: + continue + # If child is vertically stretchable, give full height; else give its minimum + if child.stretchable(YUIDimension.YD_VERT): + ch = height + else: + ch = min(height, max(1, getattr(child, "_height", 1))) + if hasattr(child, "_draw"): + child._draw(window, y, cx, w, ch) + cx += w + if i < num_children - 1: + cx += 1 diff --git a/manatools/aui/backends/curses/inputfieldcurses.py b/manatools/aui/backends/curses/inputfieldcurses.py new file mode 100644 index 0000000..ac9ec29 --- /dev/null +++ b/manatools/aui/backends/curses/inputfieldcurses.py @@ -0,0 +1,153 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YInputFieldCurses(YWidget): + def __init__(self, parent=None, label="", password_mode=False): + super().__init__(parent) + self._label = label + self._value = "" + self._password_mode = password_mode + self._cursor_pos = 0 + self._focused = False + self._can_focus = True + self._height = 1 + + def widgetClass(self): + return "YInputField" + + def value(self): + return self._value + + def setValue(self, text): + self._value = text + self._cursor_pos = len(text) + + def label(self): + return self._label + + def _create_backend_widget(self): + self._backend_widget = None + + def _set_backend_enabled(self, enabled): + """Enable/disable the input field: affect focusability and focused state.""" + try: + # Save/restore _can_focus when toggling + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + # disable focusable behavior + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = False + self._can_focus = False + # if currently focused, remove focus + if getattr(self, "_focused", False): + self._focused = False + else: + # restore previous focusability + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + # Draw label + if self._label: + label_text = self._label + if len(label_text) > width // 3: + label_text = label_text[:width // 3] + lbl_attr = curses.A_BOLD if self._is_heading else curses.A_NORMAL + if not self.isEnabled(): + lbl_attr |= curses.A_DIM + window.addstr(y, x, label_text, lbl_attr) + x += len(label_text) + 1 + width -= len(label_text) + 1 + + if width <= 0: + return + + # Prepare display value + if self._password_mode and self._value: + display_value = '*' * len(self._value) + else: + display_value = self._value + + # Handle scrolling for long values + if len(display_value) > width: + if self._cursor_pos >= width: + start_pos = self._cursor_pos - width + 1 + display_value = display_value[start_pos:start_pos + width] + else: + display_value = display_value[:width] + + # Draw input field background + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + + field_bg = ' ' * width + window.addstr(y, x, field_bg, attr) + + # Draw text + if display_value: + window.addstr(y, x, display_value, attr) + + # Show cursor if focused and enabled + if self._focused and self.isEnabled(): + cursor_display_pos = min(self._cursor_pos, width - 1) + if cursor_display_pos < len(display_value): + window.chgat(y, x + cursor_display_pos, 1, curses.A_REVERSE | curses.A_BOLD) + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + + handled = True + + if key == curses.KEY_BACKSPACE or key == 127 or key == 8: + if self._cursor_pos > 0: + self._value = self._value[:self._cursor_pos-1] + self._value[self._cursor_pos:] + self._cursor_pos -= 1 + elif key == curses.KEY_DC: # Delete key + if self._cursor_pos < len(self._value): + self._value = self._value[:self._cursor_pos] + self._value[self._cursor_pos+1:] + elif key == curses.KEY_LEFT: + if self._cursor_pos > 0: + self._cursor_pos -= 1 + elif key == curses.KEY_RIGHT: + if self._cursor_pos < len(self._value): + self._cursor_pos += 1 + elif key == curses.KEY_HOME: + self._cursor_pos = 0 + elif key == curses.KEY_END: + self._cursor_pos = len(self._value) + elif 32 <= key <= 126: # Printable characters + self._value = self._value[:self._cursor_pos] + chr(key) + self._value[self._cursor_pos:] + self._cursor_pos += 1 + else: + handled = False + + return handled diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py new file mode 100644 index 0000000..1fd97cf --- /dev/null +++ b/manatools/aui/backends/curses/labelcurses.py @@ -0,0 +1,64 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YLabelCurses(YWidget): + def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): + super().__init__(parent) + self._text = text + self._is_heading = isHeading + self._is_output_field = isOutputField + self._height = 1 + self._focused = False + self._can_focus = False # Labels don't get focus + + def widgetClass(self): + return "YLabel" + + def text(self): + return self._text + + def setText(self, new_text): + self._text = new_text + + def _create_backend_widget(self): + self._backend_widget = None + + def _set_backend_enabled(self, enabled): + """Enable/disable label: labels are not focusable; just keep enabled state for drawing.""" + try: + # labels don't accept focus; nothing to change except state used by draw + # draw() will consult self._enabled from base class + pass + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + attr = 0 + if self._is_heading: + attr |= curses.A_BOLD + # dim if disabled + if not self.isEnabled(): + attr |= curses.A_DIM + + # Truncate text to fit available width + display_text = self._text[:max(0, width-1)] + window.addstr(y, x, display_text, attr) + except curses.error: + pass diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py new file mode 100644 index 0000000..58424dc --- /dev/null +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -0,0 +1,93 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YPushButtonCurses(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._focused = False + self._can_focus = True + self._height = 1 # Fixed height - buttons are always one line + + def widgetClass(self): + return "YPushButton" + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + + def _create_backend_widget(self): + self._backend_widget = None + + def _set_backend_enabled(self, enabled): + """Enable/disable push button: update focusability and collapse focus if disabling.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + # Center the button label within available width + button_text = f"[ {self._label} ]" + text_x = x + max(0, (width - len(button_text)) // 2) + + # Only draw if we have enough space + if text_x + len(button_text) <= x + width: + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + if self._focused: + attr |= curses.A_BOLD + + window.addstr(y, text_x, button_text, attr) + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + + if key == ord('\n') or key == ord(' '): + # Button pressed -> post widget event to containing dialog + dlg = self.findDialog() + if dlg is not None: + try: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + pass + return True + return False diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py new file mode 100644 index 0000000..be97e10 --- /dev/null +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -0,0 +1,284 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YSelectionBoxCurses(YSelectionWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + self._selected_items = [] + self._multi_selection = False + + # UI state for drawing/navigation + # actual minimal height for layout (keep small so parent can expand it) + self._height = 1 + # preferred rows used for paging when no draw happened yet + self._preferred_rows = 6 + + self._scroll_offset = 0 + self._hover_index = 0 # index into self._items (global) + self._can_focus = True + self._focused = False + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + # Track last computed visible rows during last _draw call so + # navigation/ensure logic uses actual available space. + self._current_visible_rows = None + + def widgetClass(self): + return "YSelectionBox" + + def label(self): + return self._label + + def value(self): + return self._value + + def setValue(self, text): + """Select first item matching text.""" + self._value = text + # update selected_items + self._selected_items = [it for it in self._items if it.label() == text][:1] + # update hover to first matching index + for idx, it in enumerate(self._items): + if it.label() == text: + self._hover_index = idx + # adjust scroll offset to make hovered visible + self._ensure_hover_visible() + break + + def selectedItems(self): + return list(self._selected_items) + + def selectItem(self, item, selected=True): + """Programmatically select/deselect an item.""" + # find index + idx = None + for i, it in enumerate(self._items): + if it is item or it.label() == item.label(): + idx = i + break + if idx is None: + return + + if selected: + if not self._multi_selection: + self._selected_items = [self._items[idx]] + self._value = self._items[idx].label() + else: + if self._items[idx] not in self._selected_items: + self._selected_items.append(self._items[idx]) + else: + if self._items[idx] in self._selected_items: + self._selected_items.remove(self._items[idx]) + self._value = self._selected_items[0].label() if self._selected_items else "" + + # ensure hover and scroll reflect this item + self._hover_index = idx + self._ensure_hover_visible() + + if self.notify(): + # notify dialog + try: + if getattr(self, "notify", lambda: True)(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + + def setMultiSelection(self, enabled): + self._multi_selection = bool(enabled) + # if disabling multi-selection, reduce to first selected item + if not self._multi_selection and len(self._selected_items) > 1: + first = self._selected_items[0] + self._selected_items = [first] + self._value = first.label() + + def multiSelection(self): + return bool(self._multi_selection) + + def _ensure_hover_visible(self): + """Adjust scroll offset so that hover_index is visible in the box.""" + # Prefer the visible row count computed during the last _draw call + # (which takes the actual available height into account). Fallback + # to the configured visible row count if no draw happened yet. + visible = self._current_visible_rows if self._current_visible_rows is not None else self._visible_row_count() + if visible <= 0: + return + if self._hover_index < self._scroll_offset: + self._scroll_offset = self._hover_index + elif self._hover_index >= self._scroll_offset + visible: + self._scroll_offset = self._hover_index - visible + 1 + + def _visible_row_count(self): + # Return preferred visible rows for navigation (PageUp/PageDown step). + # Use preferred_rows (default 6) rather than forcing the layout minimum. + return max(1, getattr(self, "_preferred_rows", 6)) + + def _create_backend_widget(self): + # No curses backend widget object; drawing handled in _draw. + # Keep minimal layout height small so parent can give more space. + self._height = len(self._items) + (1 if self._label else 0) + # reset scroll/hover if out of range + if self._hover_index >= len(self._items): + self._hover_index = max(0, len(self._items) - 1) + self._ensure_hover_visible() + # reset the cached visible rows so future navigation uses the next draw's value + self._current_visible_rows = None + + def _set_backend_enabled(self, enabled): + """Enable/disable selection box: affect focusability and propagate to row items.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + # propagate logical enabled state to contained items (if they are YWidget) + try: + for it in list(getattr(self, "_items", []) or []): + if hasattr(it, "setEnabled"): + try: + it.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + line = y + # draw label if present + if self._label: + lbl = self._label + lbl_attr = curses.A_BOLD + if not self.isEnabled(): + lbl_attr |= curses.A_DIM + try: + window.addstr(line, x, lbl[:width], lbl_attr) + except curses.error: + pass + line += 1 + + visible = self._visible_row_count() + available_rows = max(0, height - (1 if self._label else 0)) + if self.stretchable(YUIDimension.YD_VERT): + visible = min(len(self._items), available_rows) + else: + visible = min(len(self._items), self._visible_row_count(), available_rows) + self._current_visible_rows = visible + + for i in range(visible): + item_idx = self._scroll_offset + i + if item_idx >= len(self._items): + break + item = self._items[item_idx] + text = item.label() + checkbox = "*" if item in self._selected_items else " " + display = f"[{checkbox}] {text}" + if len(display) > width: + display = display[:max(0, width - 3)] + "..." + attr = curses.A_NORMAL + if not self.isEnabled(): + attr |= curses.A_DIM + if self._focused and item_idx == self._hover_index and self.isEnabled(): + attr |= curses.A_REVERSE + try: + window.addstr(line + i, x, display.ljust(width), attr) + except curses.error: + pass + + if self._focused and len(self._items) > visible and width > 0 and self.isEnabled(): + try: + if self._scroll_offset > 0: + window.addch(y + (1 if self._label else 0), x + width - 1, '^') + if (self._scroll_offset + visible) < len(self._items): + window.addch(y + (1 if self._label else 0) + visible - 1, x + width - 1, 'v') + except curses.error: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + handled = True + if key == curses.KEY_UP: + if self._hover_index > 0: + self._hover_index -= 1 + self._ensure_hover_visible() + elif key == curses.KEY_DOWN: + if self._hover_index < max(0, len(self._items) - 1): + self._hover_index += 1 + self._ensure_hover_visible() + elif key == curses.KEY_PPAGE: # PageUp + step = self._visible_row_count() or 1 + self._hover_index = max(0, self._hover_index - step) + self._ensure_hover_visible() + elif key == curses.KEY_NPAGE: # PageDown + step = self._visible_row_count() or 1 + self._hover_index = min(max(0, len(self._items) - 1), self._hover_index + step) + self._ensure_hover_visible() + elif key == curses.KEY_HOME: + self._hover_index = 0 + self._ensure_hover_visible() + elif key == curses.KEY_END: + self._hover_index = max(0, len(self._items) - 1) + self._ensure_hover_visible() + elif key in (ord(' '), ord('\n')): # toggle/select + if 0 <= self._hover_index < len(self._items): + item = self._items[self._hover_index] + if self._multi_selection: + # toggle membership + if item in self._selected_items: + self._selected_items.remove(item) + else: + self._selected_items.append(item) + # update primary value to first selected or empty + self._value = self._selected_items[0].label() if self._selected_items else "" + else: + # single selection: set as sole selected + self._selected_items = [item] + self._value = item.label() + # notify dialog of selection change + try: + if getattr(self, "notify", lambda: True)(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + else: + handled = False + + return handled diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py new file mode 100644 index 0000000..eca697c --- /dev/null +++ b/manatools/aui/backends/curses/treecurses.py @@ -0,0 +1,596 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YTreeCurses(YSelectionWidget): + """ + NCurses implementation of a tree widget. + - Flattens visible nodes according to YTreeItem._is_open + - Supports single/multi selection and recursive selection propagation + - Preserves per-item selected() / setSelected() semantics and restores selections on rebuild + - Keyboard: Up/Down/PageUp/PageDown/Home/End, SPACE = expand/collapse, ENTER = select/deselect + """ + def __init__(self, parent=None, label="", multiselection=False, recursiveselection=False): + super().__init__(parent) + self._label = label + self._multi = bool(multiselection) + self._recursive = bool(recursiveselection) + if self._recursive: + self._multi = True + self._immediate = self.notify() + # Minimal height (items area) requested by this widget + self._min_height = 6 + # Preferred height exposed to layout should include label line if any + self._height = self._min_height + (1 if self._label else 0) + self._can_focus = True + self._focused = False + self._hover_index = 0 + self._scroll_offset = 0 + self._visible_items = [] + self._selected_items = [] + self._last_selected_ids = set() + self._suppress_selection_handler = False + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YTree" + + def hasMultiSelection(self): + """Return True if the tree allows selecting multiple items at once.""" + return bool(self._multi) + + def immediateMode(self): + return bool(self._immediate) + + def setImmediateMode(self, on:bool=True): + self._immediate = on + self.setNotify(on) + + def _create_backend_widget(self): + # Keep preferred minimum for the layout (items + optional label) + self._height = max(self._height, self._min_height + (1 if self._label else 0)) + self.rebuildTree() + + def addItem(self, item): + """Ensure base storage gets the item and rebuild visible list immediately.""" + try: + # prefer base implementation if present + try: + super().addItem(item) + except Exception: + # fallback: append to _items list used by this backend + if not hasattr(self, "_items") or self._items is None: + self._items = [] + self._items.append(item) + finally: + try: + # mark rebuild so new items are visible without waiting for external trigger + self.rebuildTree() + except Exception: + pass + + def removeItem(self, item): + """Remove item from internal list and rebuild.""" + try: + try: + super().removeItem(item) + except Exception: + if hasattr(self, "_items") and item in self._items: + try: + self._items.remove(item) + except Exception: + pass + finally: + try: + self.rebuildTree() + except Exception: + pass + + def clearItems(self): + """Clear items and rebuild.""" + try: + try: + super().clearItems() + except Exception: + self._items = [] + finally: + try: + self.rebuildTree() + except Exception: + pass + + def _collect_all_descendants(self, item): + out = [] + stack = [] + try: + for c in getattr(item, "_children", []) or []: + stack.append(c) + except Exception: + pass + while stack: + cur = stack.pop() + out.append(cur) + try: + for ch in getattr(cur, "_children", []) or []: + stack.append(ch) + except Exception: + pass + return out + + def _flatten_visible(self): + """Produce self._visible_items = [(item, depth), ...] following _is_open flags.""" + self._visible_items = [] + def _visit(nodes, depth=0): + for n in nodes: + self._visible_items.append((n, depth)) + try: + is_open = bool(getattr(n, "_is_open", False)) + except Exception: + is_open = False + if is_open: + try: + childs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + childs = getattr(n, "_children", []) or [] + if childs: + _visit(childs, depth + 1) + roots = list(getattr(self, "_items", []) or []) + _visit(roots, 0) + + def rebuildTree(self): + """Recompute visible items and restore selection from item.selected() or last_selected_ids.""" + # preserve items selection if any + try: + self._flatten_visible() + # if there are previously saved last_selected_ids, prefer them + selected_ids = set(self._last_selected_ids) if self._last_selected_ids else set() + # if none, collect from items' selected() property + if not selected_ids: + try: + def _collect_selected(nodes): + out = [] + for n in nodes: + try: + sel = False + if hasattr(n, "selected") and callable(getattr(n, "selected")): + sel = n.selected() + else: + sel = bool(getattr(n, "_selected", False)) + if sel: + out.append(n) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_selected(chs)) + return out + pre_selected = _collect_selected(list(getattr(self, "_items", []) or [])) + for p in pre_selected: + selected_ids.add(id(p)) + except Exception: + pass + # build logical selected list and last_selected_ids + sel_items = [] + for itm, _d in self._visible_items: + try: + if id(itm) in selected_ids: + sel_items.append(itm) + except Exception: + pass + # also include non-visible selected nodes (descendants) if recursive selection used + if selected_ids: + try: + # scan full tree + def _all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_all_nodes(chs)) + return out + all_nodes = _all_nodes(list(getattr(self, "_items", []) or [])) + for n in all_nodes: + if id(n) in selected_ids and n not in sel_items: + sel_items.append(n) + except Exception: + pass + # apply selected flags to items consistently + try: + # clear all first + def _clear(nodes): + for n in nodes: + try: + n.setSelected(False) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + _clear(chs) + _clear(list(getattr(self, "_items", []) or [])) + except Exception: + pass + for it in sel_items: + try: + it.setSelected(True) + except Exception: + pass + self._selected_items = list(sel_items) + self._last_selected_ids = set(id(i) for i in self._selected_items) + # ensure hover_index valid + if self._hover_index >= len(self._visible_items): + self._hover_index = max(0, len(self._visible_items) - 1) + self._ensure_hover_visible() + except Exception: + pass + + def _ensure_hover_visible(self, height=None): + """Adjust scroll offset so hover visible in given height area (if None use last draw height).""" + try: + # height param is number of rows available for items display (excluding label) + if height is None: + height = max(1, getattr(self, "_height", 1)) + visible = max(1, height) + if self._hover_index < self._scroll_offset: + self._scroll_offset = self._hover_index + elif self._hover_index >= self._scroll_offset + visible: + self._scroll_offset = self._hover_index - visible + 1 + except Exception: + pass + + def _toggle_expand(self, item): + try: + self._suppress_selection_handler = True + except Exception: + pass + try: + try: + cur = item.isOpen() + item.setOpen(not cur) + except Exception: + try: + cur = bool(getattr(item, "_is_open", False)) + item._is_open = not cur + except Exception: + pass + # preserve selected ids and rebuild + try: + self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) + except Exception: + self._last_selected_ids = set() + self.rebuildTree() + finally: + try: + self._suppress_selection_handler = False + except Exception: + pass + + def _handle_selection_action(self, item): + """Toggle selection (ENTER) respecting multi/single & recursive semantics.""" + if item is None: + return + try: + if self._multi: + # toggle membership + if item in self._selected_items: + # deselect item and (if recursive) descendants + if self._recursive: + to_remove = {item} | set(self._collect_all_descendants(item)) + self._selected_items = [it for it in self._selected_items if it not in to_remove] + for it in to_remove: + try: + it.setSelected(False) + except Exception: + try: + setattr(it, "_selected", False) + except Exception: + pass + else: + try: + self._selected_items.remove(item) + except Exception: + pass + try: + item.setSelected(False) + except Exception: + try: + setattr(item, "_selected", False) + except Exception: + pass + else: + # select item and possibly descendants + if self._recursive: + to_add = [item] + self._collect_all_descendants(item) + for it in to_add: + if it not in self._selected_items: + self._selected_items.append(it) + try: + it.setSelected(True) + except Exception: + try: + setattr(it, "_selected", True) + except Exception: + pass + else: + self._selected_items.append(item) + try: + item.setSelected(True) + except Exception: + try: + setattr(item, "_selected", True) + except Exception: + pass + else: + # single selection: clear all others and set this one + try: + for it in list(getattr(self, "_items", []) or []): + try: + it.setSelected(False) + except Exception: + try: + setattr(it, "_selected", False) + except Exception: + pass + except Exception: + pass + self._selected_items = [item] + try: + item.setSelected(True) + except Exception: + try: + setattr(item, "_selected", True) + except Exception: + pass + except Exception: + pass + + # update last_selected_ids and notify + try: + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + self._last_selected_ids = set() + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def _draw(self, window, y, x, width, height): + """Draw tree in provided rectangle. Expects height rows available.""" + try: + # compute drawing area for items (first row may be label) + line = y + start_line = line + label_rows = 1 if self._label else 0 + + # Draw label + if self._label: + try: + window.addstr(line, x, self._label[:width], curses.A_BOLD) + except curses.error: + pass + line += 1 + + # Actual rows given by parent for items + available_rows = max(0, height - label_rows) + # Keep _height as the current viewport rows (items area), not the preferred minimum + self._height = max(1, available_rows) + + # record last draw height for navigation/ensure logic + self._height = available_rows + # rebuild visible items (safe cheap operation) + self._flatten_visible() + total = len(self._visible_items) + if total == 0: + try: + if available_rows > 0: + window.addstr(line, x, "(empty)", curses.A_DIM) + except curses.error: + pass + return + + # Clamp scroll/hover to the viewport + self._ensure_hover_visible(height=self._height) + + # Draw only inside the allocated rectangle + draw_rows = min(available_rows, max(0, total - self._scroll_offset)) + for i in range(draw_rows): + idx = self._scroll_offset + i + if idx >= total: + break + itm, depth = self._visible_items[idx] + is_selected = itm in self._selected_items + # expander, text, attrs... + try: + has_children = bool(getattr(itm, "_children", []) or (callable(getattr(itm, "children", None)) and (itm.children() or []))) + except Exception: + has_children = False + try: + is_open = bool(getattr(itm, "_is_open", False)) + except Exception: + is_open = False + exp = "▾" if (has_children and is_open) else ("▸" if has_children else " ") + checkbox = "*" if is_selected else " " + indent = " " * (depth * 2) + text = f"{indent}{exp} [{checkbox}] {itm.label()}" + if len(text) > width: + text = text[:max(0, width - 3)] + "..." + attr = curses.A_REVERSE if (self._focused and idx == self._hover_index and self.isEnabled()) else curses.A_NORMAL + if not self.isEnabled(): + attr |= curses.A_DIM + try: + window.addstr(line + i, x, text.ljust(width), attr) + except curses.error: + pass + + # Scroll indicators based on actual viewport rows + try: + if self._scroll_offset > 0 and available_rows > 0: + window.addch(y + label_rows, x + max(0, width - 1), '^') + if (self._scroll_offset + available_rows) < total and available_rows > 0: + window.addch(y + label_rows + min(available_rows - 1, total - 1), x + max(0, width - 1), 'v') + except curses.error: + pass + except Exception: + pass + + def _handle_key(self, key): + """Keyboard handling: navigation, expand (SPACE), select (ENTER).""" + if not self._focused or not self.isEnabled(): + return False + handled = True + total = len(self._visible_items) + if key == curses.KEY_UP: + if self._hover_index > 0: + self._hover_index -= 1 + self._ensure_hover_visible(self._height) + elif key == curses.KEY_DOWN: + if self._hover_index < max(0, total - 1): + self._hover_index += 1 + self._ensure_hover_visible(self._height) + elif key == curses.KEY_PPAGE: + step = max(1, self._height) + self._hover_index = max(0, self._hover_index - step) + self._ensure_hover_visible(self._height) + elif key == curses.KEY_NPAGE: + step = max(1, self._height) + self._hover_index = min(max(0, total - 1), self._hover_index + step) + self._ensure_hover_visible(self._height) + elif key == curses.KEY_HOME: + self._hover_index = 0 + self._ensure_hover_visible(self._height) + elif key == curses.KEY_END: + self._hover_index = max(0, total - 1) + self._ensure_hover_visible(self._height) + elif key in (ord(' '),): # SPACE toggles expansion per dialog footer convention + if 0 <= self._hover_index < total: + itm, _ = self._visible_items[self._hover_index] + # Toggle expand/collapse without changing selection + self._toggle_expand(itm) + elif key in (ord('\n'),): # ENTER toggles selection + if 0 <= self._hover_index < total: + itm, _ = self._visible_items[self._hover_index] + self._handle_selection_action(itm) + else: + handled = False + return handled + + def currentItem(self): + try: + # Prefer explicit selected_items; if empty return hovered visible item (useful after selection) + if self._selected_items: + return self._selected_items[0] + # fallback: return hovered visible item if any + if 0 <= self._hover_index < len(getattr(self, "_visible_items", [])): + return self._visible_items[self._hover_index][0] + return None + except Exception: + return None + + def getSelectedItems(self): + return list(self._selected_items) + + def selectItem(self, item, selected=True): + """Programmatic select/deselect that respects recursive flag.""" + if item is None: + return + try: + if selected: + if not self._multi: + # clear others + try: + for it in list(getattr(self, "_items", []) or []): + try: + it.setSelected(False) + except Exception: + try: + setattr(it, "_selected", False) + except Exception: + pass + item.setSelected(True) + except Exception: + try: + setattr(item, "_selected", True) + except Exception: + pass + self._selected_items = [item] + else: + if item not in self._selected_items: + try: + item.setSelected(True) + except Exception: + try: + setattr(item, "_selected", True) + except Exception: + pass + self._selected_items.append(item) + if self._recursive: + for d in self._collect_all_descendants(item): + if d not in self._selected_items: + try: + d.setSelected(True) + except Exception: + try: + setattr(d, "_selected", True) + except Exception: + pass + self._selected_items.append(d) + else: + # deselect + if item in self._selected_items: + try: + self._selected_items.remove(item) + except Exception: + pass + try: + item.setSelected(False) + except Exception: + try: + setattr(item, "_selected", False) + except Exception: + pass + if self._recursive: + for d in self._collect_all_descendants(item): + if d in self._selected_items: + try: + self._selected_items.remove(d) + except Exception: + pass + try: + d.setSelected(False) + except Exception: + try: + setattr(d, "_selected", False) + except Exception: + pass + # update last ids + try: + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + self._last_selected_ids = set() + except Exception: + pass diff --git a/manatools/aui/backends/curses/vboxcurses.py b/manatools/aui/backends/curses/vboxcurses.py new file mode 100644 index 0000000..2cf717d --- /dev/null +++ b/manatools/aui/backends/curses/vboxcurses.py @@ -0,0 +1,140 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + +from .commoncurses import _curses_recursive_min_height + +class YVBoxCurses(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + + def widgetClass(self): + return "YVBox" + + # Returns the stretchability of the layout box: + # * The layout box is stretchable if one of the children is stretchable in + # * this dimension or if one of the child widgets has a layout weight in + # * this dimension. + def stretchable(self, dim): + for child in self._children: + widget = child.get_backend_widget() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) + if expand or weight: + return True + # No child is stretchable in this dimension + return False + + def _create_backend_widget(self): + self._backend_widget = None + + def _set_backend_enabled(self, enabled): + """Enable/disable VBox and propagate to logical children.""" + try: + for c in list(getattr(self, "_children", []) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def _draw(self, window, y, x, width, height): + # Vertical layout with spacing; give stretchable children more than their minimum + num_children = len(self._children) + if num_children == 0 or height <= 0 or width <= 0: + return + + spacing = max(0, num_children - 1) + + child_min_heights = [] + stretchable_indices = [] + stretchable_weights = [] + fixed_height_total = 0 + + for i, child in enumerate(self._children): + # Use recursive min height for containers and frames + child_min = max(1, _curses_recursive_min_height(child)) + child_min_heights.append(child_min) + + is_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) + if is_stretch: + stretchable_indices.append(i) + try: + w = child.weight(YUIDimension.YD_VERT) + w = int(w) if w is not None else 1 + except Exception: + w = 1 + if w <= 0: + w = 1 + stretchable_weights.append(w) + else: + fixed_height_total += child_min + + available_for_stretch = max(0, height - fixed_height_total - spacing) + + allocated = list(child_min_heights) + + if stretchable_indices: + total_weight = sum(stretchable_weights) or len(stretchable_indices) + # Proportional distribution of extra rows + extras = [0] * len(stretchable_indices) + base = 0 + for k, idx in enumerate(stretchable_indices): + extra = (available_for_stretch * stretchable_weights[k]) // total_weight + extras[k] = extra + base += extra + # Distribute leftover rows due to integer division + leftover = available_for_stretch - base + for k in range(len(stretchable_indices)): + if leftover <= 0: + break + extras[k] += 1 + leftover -= 1 + for k, idx in enumerate(stretchable_indices): + allocated[idx] = child_min_heights[idx] + extras[k] + + total_alloc = sum(allocated) + spacing + if total_alloc < height: + # Give remainder to the last stretchable (or last child) + extra = height - total_alloc + target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) + allocated[target] += extra + elif total_alloc > height: + # Reduce overflow from the last stretchable (or last child) + diff = total_alloc - height + target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) + allocated[target] = max(1, allocated[target] - diff) + + # Draw children with allocated heights + cy = y + for i, child in enumerate(self._children): + ch = allocated[i] + if ch <= 0: + continue + if cy + ch > y + height: + ch = max(0, (y + height) - cy) + if ch <= 0: + break + try: + if hasattr(child, "_draw"): + child._draw(window, cy, x, width, ch) + except Exception: + pass + cy += ch + if i < num_children - 1 and cy < (y + height): + cy += 1 # one-line spacing diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 71b504f..e369ca0 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -7,8 +7,38 @@ import sys import os import time +import importlib from .yui_common import * +# Import backend symbols only into this shim module. +def _import_curses_backend_symbols(): + mod = None + try: + # Package-relative import + mod = importlib.import_module(".backends.curses", __package__) + except Exception: + try: + # Absolute fallback + mod = importlib.import_module("manatools.aui.backends.curses") + except Exception: + mod = None + if not mod: + return + names = getattr(mod, "__all__", None) + if names: + for name in names: + try: + globals()[name] = getattr(mod, name) + except Exception: + pass + else: + # Fallback: import non-private names + for name, obj in mod.__dict__.items(): + if not name.startswith("_"): + globals()[name] = obj + +_import_curses_backend_symbols() + class YUICurses: def __init__(self): self._widget_factory = YWidgetFactoryCurses() @@ -202,2426 +232,4 @@ def createFrame(self, parent, label: str=""): return YFrameCurses(parent, label) -def _curses_recursive_min_height(widget): - """Compute minimal height for a widget, recursively considering container children.""" - if widget is None: - return 1 - try: - cls = widget.widgetClass() if hasattr(widget, "widgetClass") else "" - except Exception: - cls = "" - try: - if cls == "YVBox": - chs = list(getattr(widget, "_children", []) or []) - spacing = max(0, len(chs) - 1) - total = 0 - for c in chs: - total += _curses_recursive_min_height(c) - return max(1, total + spacing) - elif cls == "YHBox": - chs = list(getattr(widget, "_children", []) or []) - tallest = 1 - for c in chs: - tallest = max(tallest, _curses_recursive_min_height(c)) - return max(1, tallest) - elif cls == "YAlignment": - child = getattr(widget, "_child", None) - return max(1, _curses_recursive_min_height(child)) - elif cls == "YFrame": - child = getattr(widget, "_child", None) - inner_top = max(0, getattr(widget, "_inner_top_padding", 1)) - inner_min = _curses_recursive_min_height(child) - return max(3, 2 + inner_top + inner_min) # borders(2) + padding + inner - else: - return max(1, getattr(widget, "_height", 1)) - except Exception: - return max(1, getattr(widget, "_height", 1)) - -# Curses Widget Implementations -class YDialogCurses(YSingleChildContainerWidget): - _open_dialogs = [] - _current_dialog = None - - def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): - super().__init__() - self._dialog_type = dialog_type - self._color_mode = color_mode - self._is_open = False - self._window = None - self._focused_widget = None - self._last_draw_time = 0 - self._draw_interval = 0.1 # seconds - self._event_result = None - # Debounce for resize handling (avoid flicker) - self._resize_pending_until = 0.0 - self._last_term_size = (0, 0) # (h, w) - YDialogCurses._open_dialogs.append(self) - - def widgetClass(self): - return "YDialog" - - @staticmethod - def currentDialog(doThrow=True): - open_dialog = YDialogCurses._open_dialogs[-1] if YDialogCurses._open_dialogs else None - if not open_dialog and doThrow: - raise YUINoDialogException("No dialog is currently open") - return open_dialog - - @staticmethod - def topmostDialog(doThrow=True): - ''' same as currentDialog ''' - return YDialogCurses.currentDialog(doThrow=doThrow) - - def isTopmostDialog(self): - '''Return whether this dialog is the topmost open dialog.''' - return YDialogCurses._open_dialogs[-1] == self if YDialogCurses._open_dialogs else False - - def open(self): - if not self._window: - self._create_backend_widget() - - self._is_open = True - YDialogCurses._current_dialog = self - - # Find first focusable widget - focusable = self._find_focusable_widgets() - if focusable: - self._focused_widget = focusable[0] - self._focused_widget._focused = True - - # open() must be non-blocking (finalize and show). Event loop is - # started by waitForEvent() to match libyui semantics. - return True - - def isOpen(self): - return self._is_open - - def destroy(self, doThrow=True): - self._is_open = False - if self in YDialogCurses._open_dialogs: - YDialogCurses._open_dialogs.remove(self) - if YDialogCurses._current_dialog == self: - YDialogCurses._current_dialog = None - return True - - @classmethod - def deleteTopmostDialog(cls, doThrow=True): - if cls._open_dialogs: - dialog = cls._open_dialogs[-1] - return dialog.destroy(doThrow) - return False - - @classmethod - def currentDialog(cls, doThrow=True): - if not cls._open_dialogs: - if doThrow: - raise YUINoDialogException("No dialog open") - return None - return cls._open_dialogs[-1] - - def _create_backend_widget(self): - # Use the main screen - self._backend_widget = curses.newwin(0, 0, 0, 0) - - def _set_backend_enabled(self, enabled): - """Enable/disable the dialog and propagate to contained widgets.""" - try: - # propagate logical enabled state to entire subtree using setEnabled on children - # so each widget's hook executes and updates its state. - if getattr(self, "_child", None): - try: - self._child.setEnabled(enabled) - except Exception: - pass - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - # If disabling and dialog had focused widget, clear focus - if not enabled: - try: - if getattr(self, "_focused_widget", None): - self._focused_widget._focused = False - self._focused_widget = None - except Exception: - pass - # Force a redraw so disabled/enabled visual state appears immediately - try: - self._last_draw_time = 0 - except Exception: - pass - except Exception: - pass - - def _draw_dialog(self): - """Draw the entire dialog (called by event loop)""" - if not hasattr(self, '_backend_widget') or not self._backend_widget: - return - - try: - height, width = self._backend_widget.getmaxyx() - - # Clear screen - self._backend_widget.clear() - - # Draw border - self._backend_widget.border() - - # Draw title - title = " manatools YUI NCurses Dialog " - try: - from . import yui as yui_mod - appobj = None - # YUI._backend may hold the backend instance (YUIQt) - backend = getattr(yui_mod.YUI, "_backend", None) - if backend: - if hasattr(backend, "application"): - appobj = backend.application() - # fallback: YUI._instance might be set and expose application/yApp - if not appobj: - inst = getattr(yui_mod.YUI, "_instance", None) - if inst: - if hasattr(inst, "application"): - appobj = inst.application() - if appobj and hasattr(appobj, "applicationTitle"): - atitle = appobj.applicationTitle() - if atitle: - title = atitle - if appobj: - appobj.setApplicationTitle(title) - except Exception: - # ignore and keep default - pass - title_x = max(0, (width - len(title)) // 2) - self._backend_widget.addstr(0, title_x, title, curses.A_BOLD) - - # Draw content area - fixed coordinates for child - content_height = height - 4 - content_width = width - 4 - content_y = 2 - content_x = 2 - - # Draw child content - if self._child: - self._draw_child_content(content_y, content_x, content_width, content_height) - - # Draw footer with instructions - footer_text = " TAB=Navigate | SPACE=Expand | ENTER=Select | F10/Q=Quit " - footer_x = max(0, (width - len(footer_text)) // 2) - if footer_x + len(footer_text) < width: - self._backend_widget.addstr(height - 1, footer_x, footer_text, curses.A_DIM) - - # Draw focus indicator - if self._focused_widget: - focus_text = f" Focus: {getattr(self._focused_widget, '_label', 'Unknown')} " - if len(focus_text) < width: - self._backend_widget.addstr(height - 1, 2, focus_text, curses.A_REVERSE) - #if the focused widget has an expnded list (menus, combos,...), draw it on top - if hasattr(self._focused_widget, "_draw_expanded_list"): - self._focused_widget._draw_expanded_list(self._backend_widget) - - # Refresh main window first - self._backend_widget.refresh() - - except curses.error as e: - # Ignore curses errors (like writing beyond screen bounds) - pass - - def _draw_child_content(self, start_y, start_x, max_width, max_height): - """Draw the child widget content respecting container hierarchy""" - if not self._child: - return - - # Draw only the root child - it will handle drawing its own children - if hasattr(self._child, '_draw'): - self._child._draw(self._backend_widget, start_y, start_x, max_width, max_height) - - - def _cycle_focus(self, forward=True): - """Cycle focus between focusable widgets""" - focusable = self._find_focusable_widgets() - if not focusable: - return - - current_index = -1 - if self._focused_widget: - for i, widget in enumerate(focusable): - if widget == self._focused_widget: - current_index = i - break - - if current_index == -1: - new_index = 0 - else: - if forward: - new_index = (current_index + 1) % len(focusable) - else: - new_index = (current_index - 1) % len(focusable) - - # If the currently focused widget is an expanded combo, collapse it - # so tabbing away closes the dropdown but does not change selection. - if self._focused_widget: - try: - if getattr(self._focused_widget, "_expanded", False): - self._focused_widget._expanded = False - except Exception: - pass - self._focused_widget._focused = False - - self._focused_widget = focusable[new_index] - self._focused_widget._focused = True - # Force redraw on focus change - self._last_draw_time = 0 - - def _find_focusable_widgets(self): - """Find all widgets that can receive focus""" - focusable = [] - - def find_in_widget(widget): - if hasattr(widget, '_can_focus') and widget._can_focus: - focusable.append(widget) - for child in widget._children: - find_in_widget(child) - - if self._child: - find_in_widget(self._child) - - return focusable - - - def _post_event(self, event): - """Post an event to this dialog; waitForEvent will return it.""" - self._event_result = event - # If dialog is not open anymore, ensure cleanup - if isinstance(event, YCancelEvent): - # Mark closed so loop can clean up - self._is_open = False - - def waitForEvent(self, timeout_millisec=0): - """ - Run the ncurses event loop until an event is posted or timeout occurs. - timeout_millisec == 0 -> block indefinitely until an event (no timeout). - Returns a YEvent (YWidgetEvent, YTimeoutEvent, YCancelEvent, ...). - """ - from manatools.aui.yui import YUI - ui = YUI.ui() - - # Ensure dialog is open/finalized - if not self._is_open: - self.open() - - self._event_result = None - deadline = None - if timeout_millisec and timeout_millisec > 0: - deadline = time.time() + (timeout_millisec / 1000.0) - - while self._is_open and self._event_result is None: - try: - now = time.time() - - # Apply pending resize once debounce expires - if self._resize_pending_until and now >= self._resize_pending_until: - try: - ui._stdscr.clear() - ui._stdscr.refresh() - new_h, new_w = ui._stdscr.getmaxyx() - try: - curses.resizeterm(new_h, new_w) - except Exception: - pass - # Recreate backend window (full-screen) - self._backend_widget = curses.newwin(new_h, new_w, 0, 0) - self._last_term_size = (new_h, new_w) - except Exception: - pass - # Clear pending flag and force immediate redraw - self._resize_pending_until = 0.0 - self._last_draw_time = 0 - - # Draw at most every _draw_interval; forced redraw uses last_draw_time = 0 - if (now - self._last_draw_time) >= self._draw_interval: - self._draw_dialog() - self._last_draw_time = now - - # Non-blocking input - ui._stdscr.nodelay(True) - key = ui._stdscr.getch() - - if key == -1: - if deadline and time.time() >= deadline: - self._event_result = YTimeoutEvent() - break - time.sleep(0.01) - continue - - # Global keys - if key == curses.KEY_F10 or key == ord('q') or key == ord('Q'): - self._post_event(YCancelEvent()) - break - elif key == curses.KEY_RESIZE: - # Debounce resize; do not redraw immediately to avoid flicker - try: - new_h, new_w = ui._stdscr.getmaxyx() - self._last_term_size = (new_h, new_w) - except Exception: - pass - # Wait 150ms after the last resize event before applying - self._resize_pending_until = time.time() + 0.15 - continue - - # Focus navigation - if key == ord('\t'): - self._cycle_focus(forward=True) - self._last_draw_time = 0 - continue - elif key == curses.KEY_BTAB: - self._cycle_focus(forward=False) - self._last_draw_time = 0 - continue - - # Dispatch key to focused widget - if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): - handled = self._focused_widget._handle_key(key) - if handled: - self._last_draw_time = 0 - - except KeyboardInterrupt: - self._post_event(YCancelEvent()) - break - except Exception: - time.sleep(0.05) - - if self._event_result is None: - if not self._is_open: - self._event_result = YCancelEvent() - elif deadline and time.time() >= deadline: - self._event_result = YTimeoutEvent() - - if not self._is_open: - try: - self.destroy() - except Exception: - pass - - return self._event_result if self._event_result is not None else YEvent() - -class YVBoxCurses(YWidget): - def __init__(self, parent=None): - super().__init__(parent) - - def widgetClass(self): - return "YVBox" - - # Returns the stretchability of the layout box: - # * The layout box is stretchable if one of the children is stretchable in - # * this dimension or if one of the child widgets has a layout weight in - # * this dimension. - def stretchable(self, dim): - for child in self._children: - widget = child.get_backend_widget() - expand = bool(child.stretchable(dim)) - weight = bool(child.weight(dim)) - if expand or weight: - return True - # No child is stretchable in this dimension - return False - - def _create_backend_widget(self): - self._backend_widget = None - - def _set_backend_enabled(self, enabled): - """Enable/disable VBox and propagate to logical children.""" - try: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - - def _draw(self, window, y, x, width, height): - # Vertical layout with spacing; give stretchable children more than their minimum - num_children = len(self._children) - if num_children == 0 or height <= 0 or width <= 0: - return - - spacing = max(0, num_children - 1) - - child_min_heights = [] - stretchable_indices = [] - stretchable_weights = [] - fixed_height_total = 0 - - for i, child in enumerate(self._children): - # Use recursive min height for containers and frames - child_min = max(1, _curses_recursive_min_height(child)) - child_min_heights.append(child_min) - - is_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) - if is_stretch: - stretchable_indices.append(i) - try: - w = child.weight(YUIDimension.YD_VERT) - w = int(w) if w is not None else 1 - except Exception: - w = 1 - if w <= 0: - w = 1 - stretchable_weights.append(w) - else: - fixed_height_total += child_min - - available_for_stretch = max(0, height - fixed_height_total - spacing) - - allocated = list(child_min_heights) - - if stretchable_indices: - total_weight = sum(stretchable_weights) or len(stretchable_indices) - # Proportional distribution of extra rows - extras = [0] * len(stretchable_indices) - base = 0 - for k, idx in enumerate(stretchable_indices): - extra = (available_for_stretch * stretchable_weights[k]) // total_weight - extras[k] = extra - base += extra - # Distribute leftover rows due to integer division - leftover = available_for_stretch - base - for k in range(len(stretchable_indices)): - if leftover <= 0: - break - extras[k] += 1 - leftover -= 1 - for k, idx in enumerate(stretchable_indices): - allocated[idx] = child_min_heights[idx] + extras[k] - - total_alloc = sum(allocated) + spacing - if total_alloc < height: - # Give remainder to the last stretchable (or last child) - extra = height - total_alloc - target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) - allocated[target] += extra - elif total_alloc > height: - # Reduce overflow from the last stretchable (or last child) - diff = total_alloc - height - target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) - allocated[target] = max(1, allocated[target] - diff) - - # Draw children with allocated heights - cy = y - for i, child in enumerate(self._children): - ch = allocated[i] - if ch <= 0: - continue - if cy + ch > y + height: - ch = max(0, (y + height) - cy) - if ch <= 0: - break - try: - if hasattr(child, "_draw"): - child._draw(window, cy, x, width, ch) - except Exception: - pass - cy += ch - if i < num_children - 1 and cy < (y + height): - cy += 1 # one-line spacing - -class YHBoxCurses(YWidget): - def __init__(self, parent=None): - super().__init__(parent) - # Minimum height will be computed from children - self._height = 1 - - def widgetClass(self): - return "YHBox" - - def _create_backend_widget(self): - self._backend_widget = None - - def _recompute_min_height(self): - """Compute minimal height for this horizontal box as the tallest child's minimum.""" - try: - if not self._children: - self._height = 1 - return - self._height = max(1, max(_curses_recursive_min_height(c) for c in self._children)) - except Exception: - self._height = 1 - - def addChild(self, child): - """Ensure internal children list and recompute minimal height.""" - try: - super().addChild(child) - except Exception: - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - self._children.append(child) - child._parent = self - except Exception: - pass - self._recompute_min_height() - - def setChild(self, child): - """Not typical for HBox, but keep parity with containers.""" - try: - super().setChild(child) - except Exception: - try: - self._children = [child] - child._parent = self - except Exception: - pass - self._recompute_min_height() - - def _set_backend_enabled(self, enabled): - """Enable/disable HBox and propagate to logical children.""" - try: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - - # Returns the stretchability of the layout box: - # * The layout box is stretchable if one of the children is stretchable in - # * this dimension or if one of the child widgets has a layout weight in - # * this dimension. - def stretchable(self, dim): - for child in self._children: - widget = child.get_backend_widget() - expand = bool(child.stretchable(dim)) - weight = bool(child.weight(dim)) - if expand or weight: - return True - # No child is stretchable in this dimension - return False - - def _child_min_width(self, child, max_width): - # Best-effort minimal width heuristic - try: - if hasattr(child, "minWidth"): - return min(max_width, max(1, int(child.minWidth()))) - except Exception: - pass - # Heuristics based on common attributes - try: - cls = child.widgetClass() if hasattr(child, "widgetClass") else "" - if cls in ("YLabel", "YPushButton", "YCheckBox"): - text = getattr(child, "_text", None) - if text is None: - text = getattr(child, "_label", "") - pad = 4 if cls == "YPushButton" else 0 - return min(max_width, max(1, len(str(text)) + pad)) - except Exception: - pass - return max(1, min(10, max_width)) # safe default - - def _draw(self, window, y, x, width, height): - # Ensure minimal height reflects children so the parent allocated enough rows - self._recompute_min_height() - num_children = len(self._children) - if num_children == 0 or width <= 0 or height <= 0: - return - - spacing = max(0, num_children - 1) - available = max(0, width - spacing) - - widths = [0] * num_children - stretchables = [] - fixed_total = 0 - for i, child in enumerate(self._children): - if child.stretchable(YUIDimension.YD_HORIZ): - stretchables.append(i) - else: - w = self._child_min_width(child, available) - widths[i] = w - fixed_total += w - - remaining = max(0, available - fixed_total) - if stretchables: - per = remaining // len(stretchables) - extra = remaining % len(stretchables) - for k, idx in enumerate(stretchables): - widths[idx] = max(1, per + (1 if k < extra else 0)) - else: - if fixed_total < available: - leftover = available - fixed_total - per = leftover // num_children - extra = leftover % num_children - for i in range(num_children): - base = widths[i] if widths[i] else 1 - widths[i] = base + per + (1 if i < extra else 0) - - # Draw children and pass full container height to stretchable children - cx = x - for i, child in enumerate(self._children): - w = widths[i] - if w <= 0: - continue - # If child is vertically stretchable, give full height; else give its minimum - if child.stretchable(YUIDimension.YD_VERT): - ch = height - else: - ch = min(height, max(1, getattr(child, "_height", 1))) - if hasattr(child, "_draw"): - child._draw(window, y, cx, w, ch) - cx += w - if i < num_children - 1: - cx += 1 - -class YLabelCurses(YWidget): - def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): - super().__init__(parent) - self._text = text - self._is_heading = isHeading - self._is_output_field = isOutputField - self._height = 1 - self._focused = False - self._can_focus = False # Labels don't get focus - - def widgetClass(self): - return "YLabel" - - def text(self): - return self._text - - def setText(self, new_text): - self._text = new_text - - def _create_backend_widget(self): - self._backend_widget = None - - def _set_backend_enabled(self, enabled): - """Enable/disable label: labels are not focusable; just keep enabled state for drawing.""" - try: - # labels don't accept focus; nothing to change except state used by draw - # draw() will consult self._enabled from base class - pass - except Exception: - pass - - def _draw(self, window, y, x, width, height): - try: - attr = 0 - if self._is_heading: - attr |= curses.A_BOLD - # dim if disabled - if not self.isEnabled(): - attr |= curses.A_DIM - - # Truncate text to fit available width - display_text = self._text[:max(0, width-1)] - window.addstr(y, x, display_text, attr) - except curses.error: - pass - -class YInputFieldCurses(YWidget): - def __init__(self, parent=None, label="", password_mode=False): - super().__init__(parent) - self._label = label - self._value = "" - self._password_mode = password_mode - self._cursor_pos = 0 - self._focused = False - self._can_focus = True - self._height = 1 - - def widgetClass(self): - return "YInputField" - - def value(self): - return self._value - - def setValue(self, text): - self._value = text - self._cursor_pos = len(text) - - def label(self): - return self._label - - def _create_backend_widget(self): - self._backend_widget = None - - def _set_backend_enabled(self, enabled): - """Enable/disable the input field: affect focusability and focused state.""" - try: - # Save/restore _can_focus when toggling - if not hasattr(self, "_saved_can_focus"): - self._saved_can_focus = getattr(self, "_can_focus", True) - if not enabled: - # disable focusable behavior - try: - self._saved_can_focus = self._can_focus - except Exception: - self._saved_can_focus = False - self._can_focus = False - # if currently focused, remove focus - if getattr(self, "_focused", False): - self._focused = False - else: - # restore previous focusability - try: - self._can_focus = bool(getattr(self, "_saved_can_focus", True)) - except Exception: - self._can_focus = True - except Exception: - pass - - def _draw(self, window, y, x, width, height): - try: - # Draw label - if self._label: - label_text = self._label - if len(label_text) > width // 3: - label_text = label_text[:width // 3] - lbl_attr = curses.A_BOLD if self._is_heading else curses.A_NORMAL - if not self.isEnabled(): - lbl_attr |= curses.A_DIM - window.addstr(y, x, label_text, lbl_attr) - x += len(label_text) + 1 - width -= len(label_text) + 1 - - if width <= 0: - return - - # Prepare display value - if self._password_mode and self._value: - display_value = '*' * len(self._value) - else: - display_value = self._value - - # Handle scrolling for long values - if len(display_value) > width: - if self._cursor_pos >= width: - start_pos = self._cursor_pos - width + 1 - display_value = display_value[start_pos:start_pos + width] - else: - display_value = display_value[:width] - - # Draw input field background - if not self.isEnabled(): - attr = curses.A_DIM - else: - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - - field_bg = ' ' * width - window.addstr(y, x, field_bg, attr) - - # Draw text - if display_value: - window.addstr(y, x, display_value, attr) - - # Show cursor if focused and enabled - if self._focused and self.isEnabled(): - cursor_display_pos = min(self._cursor_pos, width - 1) - if cursor_display_pos < len(display_value): - window.chgat(y, x + cursor_display_pos, 1, curses.A_REVERSE | curses.A_BOLD) - except curses.error: - pass - - def _handle_key(self, key): - if not self._focused or not self.isEnabled(): - return False - - handled = True - - if key == curses.KEY_BACKSPACE or key == 127 or key == 8: - if self._cursor_pos > 0: - self._value = self._value[:self._cursor_pos-1] + self._value[self._cursor_pos:] - self._cursor_pos -= 1 - elif key == curses.KEY_DC: # Delete key - if self._cursor_pos < len(self._value): - self._value = self._value[:self._cursor_pos] + self._value[self._cursor_pos+1:] - elif key == curses.KEY_LEFT: - if self._cursor_pos > 0: - self._cursor_pos -= 1 - elif key == curses.KEY_RIGHT: - if self._cursor_pos < len(self._value): - self._cursor_pos += 1 - elif key == curses.KEY_HOME: - self._cursor_pos = 0 - elif key == curses.KEY_END: - self._cursor_pos = len(self._value) - elif 32 <= key <= 126: # Printable characters - self._value = self._value[:self._cursor_pos] + chr(key) + self._value[self._cursor_pos:] - self._cursor_pos += 1 - else: - handled = False - - return handled - -class YPushButtonCurses(YWidget): - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label - self._focused = False - self._can_focus = True - self._height = 1 # Fixed height - buttons are always one line - - def widgetClass(self): - return "YPushButton" - - def label(self): - return self._label - - def setLabel(self, label): - self._label = label - - def _create_backend_widget(self): - self._backend_widget = None - - def _set_backend_enabled(self, enabled): - """Enable/disable push button: update focusability and collapse focus if disabling.""" - try: - if not hasattr(self, "_saved_can_focus"): - self._saved_can_focus = getattr(self, "_can_focus", True) - if not enabled: - try: - self._saved_can_focus = self._can_focus - except Exception: - self._saved_can_focus = True - self._can_focus = False - if getattr(self, "_focused", False): - self._focused = False - else: - try: - self._can_focus = bool(getattr(self, "_saved_can_focus", True)) - except Exception: - self._can_focus = True - except Exception: - pass - - def _draw(self, window, y, x, width, height): - try: - # Center the button label within available width - button_text = f"[ {self._label} ]" - text_x = x + max(0, (width - len(button_text)) // 2) - - # Only draw if we have enough space - if text_x + len(button_text) <= x + width: - if not self.isEnabled(): - attr = curses.A_DIM - else: - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - if self._focused: - attr |= curses.A_BOLD - - window.addstr(y, text_x, button_text, attr) - except curses.error: - pass - - def _handle_key(self, key): - if not self._focused or not self.isEnabled(): - return False - - if key == ord('\n') or key == ord(' '): - # Button pressed -> post widget event to containing dialog - dlg = self.findDialog() - if dlg is not None: - try: - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) - except Exception: - pass - return True - return False - -class YCheckBoxCurses(YWidget): - def __init__(self, parent=None, label="", is_checked=False): - super().__init__(parent) - self._label = label - self._is_checked = is_checked - self._focused = False - self._can_focus = True - self._height = 1 - - def widgetClass(self): - return "YCheckBox" - - def value(self): - return self._is_checked - - def setValue(self, checked): - self._is_checked = checked - - def label(self): - return self._label - - def _create_backend_widget(self): - # In curses, there's no actual backend widget, just internal state - pass - - def _set_backend_enabled(self, enabled): - """Enable/disable checkbox: update focusability and collapse focus if disabling.""" - try: - if not hasattr(self, "_saved_can_focus"): - self._saved_can_focus = getattr(self, "_can_focus", True) - if not enabled: - try: - self._saved_can_focus = self._can_focus - except Exception: - self._saved_can_focus = True - self._can_focus = False - if getattr(self, "_focused", False): - self._focused = False - else: - try: - self._can_focus = bool(getattr(self, "_saved_can_focus", True)) - except Exception: - self._can_focus = True - except Exception: - pass - - def _draw(self, window, y, x, width, height): - try: - checkbox_symbol = "[X]" if self._is_checked else "[ ]" - text = f"{checkbox_symbol} {self._label}" - if len(text) > width: - text = text[:max(0, width - 3)] + "..." - - if self._focused and self.isEnabled(): - window.attron(curses.A_REVERSE) - elif not self.isEnabled(): - # indicate disabled with dim attribute - window.attron(curses.A_DIM) - - window.addstr(y, x, text) - - if self._focused and self.isEnabled(): - window.attroff(curses.A_REVERSE) - elif not self.isEnabled(): - try: - window.attroff(curses.A_DIM) - except Exception: - pass - except curses.error: - pass - - def _handle_key(self, key): - if not self.isEnabled(): - return False - # Space or Enter to toggle - if key in (ord(' '), ord('\n'), curses.KEY_ENTER): - self._toggle() - return True - return False - - def _toggle(self): - """Toggle checkbox state and post event""" - self._is_checked = not self._is_checked - - if self.notify(): - # Post a YWidgetEvent to the containing dialog - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - else: - print(f"CheckBox toggled (no dialog found): {self._label} = {self._is_checked}") - -class YComboBoxCurses(YSelectionWidget): - def __init__(self, parent=None, label="", editable=False): - super().__init__(parent) - self._label = label - self._editable = editable - self._value = "" - self._focused = False - self._can_focus = True - self._height = 1 - self._expanded = False - self._hover_index = 0 - self._combo_x = 0 - self._combo_y = 0 - self._combo_width = 0 - - def widgetClass(self): - return "YComboBox" - - def value(self): - return self._value - - def setValue(self, text): - self._value = text - # Update selected items - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break - - def editable(self): - return self._editable - - def _create_backend_widget(self): - self._backend_widget = None - - def _set_backend_enabled(self, enabled): - """Enable/disable combobox: affect focusability, expanded state and focused state.""" - try: - if not hasattr(self, "_saved_can_focus"): - self._saved_can_focus = getattr(self, "_can_focus", True) - if not enabled: - try: - self._saved_can_focus = self._can_focus - except Exception: - self._saved_can_focus = True - self._can_focus = False - # collapse expanded dropdown if any - try: - if getattr(self, "_expanded", False): - self._expanded = False - except Exception: - pass - if getattr(self, "_focused", False): - self._focused = False - else: - try: - self._can_focus = bool(getattr(self, "_saved_can_focus", True)) - except Exception: - self._can_focus = True - except Exception: - pass - - def _draw(self, window, y, x, width, height): - # Store position and dimensions for dropdown drawing - self._combo_y = y - self._combo_x = x - self._combo_width = width - - try: - # Calculate available space for combo box - label_space = len(self._label) + 1 if self._label else 0 - combo_space = width - label_space - - if combo_space <= 3: - return - - # Draw label - if self._label: - label_text = self._label - if len(label_text) > label_space - 1: - label_text = label_text[:label_space - 1] - lbl_attr = curses.A_NORMAL - if not self.isEnabled(): - lbl_attr |= curses.A_DIM - window.addstr(y, x, label_text, lbl_attr) - x += len(label_text) + 1 - - # Prepare display value - display_value = self._value if self._value else "Select..." - max_display_width = combo_space - 3 - if len(display_value) > max_display_width: - display_value = display_value[:max_display_width] + "..." - - # Draw combo box background - if not self.isEnabled(): - attr = curses.A_DIM - else: - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - - combo_bg = " " * combo_space - window.addstr(y, x, combo_bg, attr) - - combo_text = f" {display_value} ▼" - if len(combo_text) > combo_space: - combo_text = combo_text[:combo_space] - - window.addstr(y, x, combo_text, attr) - - # Draw expanded list if active and enabled - if self._expanded and self.isEnabled(): - self._draw_expanded_list(window) - except curses.error: - pass - - - def _draw_expanded_list(self, window): - """Draw the expanded dropdown list at correct position""" - if not self._expanded or not self._items: - return - - try: - # Make sure we don't draw outside screen - screen_height, screen_width = window.getmaxyx() - - list_height = min(len(self._items), screen_height) - - # Calculate dropdown position - right below the combo box - dropdown_y = self._combo_y + 1 - dropdown_x = self._combo_x + (len(self._label) + 1 if self._label else 0) - dropdown_width = self._combo_width - (len(self._label) + 1 if self._label else 0) - - # If not enough space below, draw above - if dropdown_y + list_height >= screen_height: - dropdown_y = max(1, self._combo_y - list_height - 1) - - # Ensure dropdown doesn't go beyond right edge - if dropdown_x + dropdown_width >= screen_width: - dropdown_width = screen_width - dropdown_x - 1 - - if dropdown_width <= 5: # Need reasonable width - return - - # Draw dropdown background for each item - for i in range(list_height): - if i >= len(self._items): - break - - item = self._items[i] - item_text = item.label() - if len(item_text) > dropdown_width - 2: - item_text = item_text[:dropdown_width - 2] + "..." - - # Highlight hovered item - attr = curses.A_REVERSE if i == self._hover_index else curses.A_NORMAL - - # Create background for the item - bg_text = " " + item_text.ljust(dropdown_width - 2) - if len(bg_text) > dropdown_width: - bg_text = bg_text[:dropdown_width] - - # Ensure we don't write beyond screen bounds - if (dropdown_y + i < screen_height and - dropdown_x < screen_width and - dropdown_x + len(bg_text) <= screen_width): - try: - window.addstr(dropdown_y + i, dropdown_x, bg_text, attr) - except curses.error: - pass # Ignore out-of-bounds errors - - except curses.error: - # Ignore drawing errors - pass - - def _handle_key(self, key): - if not self._focused or not self.isEnabled(): - return False - handled = True - - # If currently expanded, give expanded-list handling priority so Enter - # selects the hovered item instead of simply toggling expansion. - if self._expanded: - # Handle navigation in expanded list - if key == curses.KEY_UP: - if self._hover_index > 0: - self._hover_index -= 1 - elif key == curses.KEY_DOWN: - if self._hover_index < len(self._items) - 1: - self._hover_index += 1 - elif key == ord('\n') or key == ord(' '): - # Select hovered item - if self._items and 0 <= self._hover_index < len(self._items): - selected_item = self._items[self._hover_index] - self.setValue(selected_item.label()) # update internal value/selection - self._expanded = False - if self.notify(): - # force parent dialog redraw if present - dlg = self.findDialog() - if dlg is not None: - try: - # notify dialog to redraw immediately - dlg._last_draw_time = 0 - # post a widget event for selection change - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - # selection made -> handled - elif key == 27: # ESC key - self._expanded = False - else: - handled = False - else: - # Not expanded: Enter/Space expands the list - if key == ord('\n') or key == ord(' '): - self._expanded = not self._expanded - if self._expanded and self._items: - # Set hover index to current value if exists - self._hover_index = 0 - if self._value: - for i, item in enumerate(self._items): - if item.label() == self._value: - self._hover_index = i - break - else: - handled = False - - return handled - -class YSelectionBoxCurses(YSelectionWidget): - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label - self._value = "" - self._selected_items = [] - self._multi_selection = False - - # UI state for drawing/navigation - # actual minimal height for layout (keep small so parent can expand it) - self._height = 1 - # preferred rows used for paging when no draw happened yet - self._preferred_rows = 6 - - self._scroll_offset = 0 - self._hover_index = 0 # index into self._items (global) - self._can_focus = True - self._focused = False - self.setStretchable(YUIDimension.YD_HORIZ, True) - self.setStretchable(YUIDimension.YD_VERT, True) - - # Track last computed visible rows during last _draw call so - # navigation/ensure logic uses actual available space. - self._current_visible_rows = None - - def widgetClass(self): - return "YSelectionBox" - - def label(self): - return self._label - - def value(self): - return self._value - - def setValue(self, text): - """Select first item matching text.""" - self._value = text - # update selected_items - self._selected_items = [it for it in self._items if it.label() == text][:1] - # update hover to first matching index - for idx, it in enumerate(self._items): - if it.label() == text: - self._hover_index = idx - # adjust scroll offset to make hovered visible - self._ensure_hover_visible() - break - - def selectedItems(self): - return list(self._selected_items) - - def selectItem(self, item, selected=True): - """Programmatically select/deselect an item.""" - # find index - idx = None - for i, it in enumerate(self._items): - if it is item or it.label() == item.label(): - idx = i - break - if idx is None: - return - - if selected: - if not self._multi_selection: - self._selected_items = [self._items[idx]] - self._value = self._items[idx].label() - else: - if self._items[idx] not in self._selected_items: - self._selected_items.append(self._items[idx]) - else: - if self._items[idx] in self._selected_items: - self._selected_items.remove(self._items[idx]) - self._value = self._selected_items[0].label() if self._selected_items else "" - - # ensure hover and scroll reflect this item - self._hover_index = idx - self._ensure_hover_visible() - - if self.notify(): - # notify dialog - try: - if getattr(self, "notify", lambda: True)(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - - def setMultiSelection(self, enabled): - self._multi_selection = bool(enabled) - # if disabling multi-selection, reduce to first selected item - if not self._multi_selection and len(self._selected_items) > 1: - first = self._selected_items[0] - self._selected_items = [first] - self._value = first.label() - - def multiSelection(self): - return bool(self._multi_selection) - - def _ensure_hover_visible(self): - """Adjust scroll offset so that hover_index is visible in the box.""" - # Prefer the visible row count computed during the last _draw call - # (which takes the actual available height into account). Fallback - # to the configured visible row count if no draw happened yet. - visible = self._current_visible_rows if self._current_visible_rows is not None else self._visible_row_count() - if visible <= 0: - return - if self._hover_index < self._scroll_offset: - self._scroll_offset = self._hover_index - elif self._hover_index >= self._scroll_offset + visible: - self._scroll_offset = self._hover_index - visible + 1 - - def _visible_row_count(self): - # Return preferred visible rows for navigation (PageUp/PageDown step). - # Use preferred_rows (default 6) rather than forcing the layout minimum. - return max(1, getattr(self, "_preferred_rows", 6)) - - def _create_backend_widget(self): - # No curses backend widget object; drawing handled in _draw. - # Keep minimal layout height small so parent can give more space. - self._height = len(self._items) + (1 if self._label else 0) - # reset scroll/hover if out of range - if self._hover_index >= len(self._items): - self._hover_index = max(0, len(self._items) - 1) - self._ensure_hover_visible() - # reset the cached visible rows so future navigation uses the next draw's value - self._current_visible_rows = None - - def _set_backend_enabled(self, enabled): - """Enable/disable selection box: affect focusability and propagate to row items.""" - try: - if not hasattr(self, "_saved_can_focus"): - self._saved_can_focus = getattr(self, "_can_focus", True) - if not enabled: - try: - self._saved_can_focus = self._can_focus - except Exception: - self._saved_can_focus = True - self._can_focus = False - if getattr(self, "_focused", False): - self._focused = False - else: - try: - self._can_focus = bool(getattr(self, "_saved_can_focus", True)) - except Exception: - self._can_focus = True - # propagate logical enabled state to contained items (if they are YWidget) - try: - for it in list(getattr(self, "_items", []) or []): - if hasattr(it, "setEnabled"): - try: - it.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - except Exception: - pass - - def _draw(self, window, y, x, width, height): - try: - line = y - # draw label if present - if self._label: - lbl = self._label - lbl_attr = curses.A_BOLD - if not self.isEnabled(): - lbl_attr |= curses.A_DIM - try: - window.addstr(line, x, lbl[:width], lbl_attr) - except curses.error: - pass - line += 1 - - visible = self._visible_row_count() - available_rows = max(0, height - (1 if self._label else 0)) - if self.stretchable(YUIDimension.YD_VERT): - visible = min(len(self._items), available_rows) - else: - visible = min(len(self._items), self._visible_row_count(), available_rows) - self._current_visible_rows = visible - - for i in range(visible): - item_idx = self._scroll_offset + i - if item_idx >= len(self._items): - break - item = self._items[item_idx] - text = item.label() - checkbox = "*" if item in self._selected_items else " " - display = f"[{checkbox}] {text}" - if len(display) > width: - display = display[:max(0, width - 3)] + "..." - attr = curses.A_NORMAL - if not self.isEnabled(): - attr |= curses.A_DIM - if self._focused and item_idx == self._hover_index and self.isEnabled(): - attr |= curses.A_REVERSE - try: - window.addstr(line + i, x, display.ljust(width), attr) - except curses.error: - pass - - if self._focused and len(self._items) > visible and width > 0 and self.isEnabled(): - try: - if self._scroll_offset > 0: - window.addch(y + (1 if self._label else 0), x + width - 1, '^') - if (self._scroll_offset + visible) < len(self._items): - window.addch(y + (1 if self._label else 0) + visible - 1, x + width - 1, 'v') - except curses.error: - pass - except curses.error: - pass - - def _handle_key(self, key): - if not self._focused or not self.isEnabled(): - return False - handled = True - if key == curses.KEY_UP: - if self._hover_index > 0: - self._hover_index -= 1 - self._ensure_hover_visible() - elif key == curses.KEY_DOWN: - if self._hover_index < max(0, len(self._items) - 1): - self._hover_index += 1 - self._ensure_hover_visible() - elif key == curses.KEY_PPAGE: # PageUp - step = self._visible_row_count() or 1 - self._hover_index = max(0, self._hover_index - step) - self._ensure_hover_visible() - elif key == curses.KEY_NPAGE: # PageDown - step = self._visible_row_count() or 1 - self._hover_index = min(max(0, len(self._items) - 1), self._hover_index + step) - self._ensure_hover_visible() - elif key == curses.KEY_HOME: - self._hover_index = 0 - self._ensure_hover_visible() - elif key == curses.KEY_END: - self._hover_index = max(0, len(self._items) - 1) - self._ensure_hover_visible() - elif key in (ord(' '), ord('\n')): # toggle/select - if 0 <= self._hover_index < len(self._items): - item = self._items[self._hover_index] - if self._multi_selection: - # toggle membership - if item in self._selected_items: - self._selected_items.remove(item) - else: - self._selected_items.append(item) - # update primary value to first selected or empty - self._value = self._selected_items[0].label() if self._selected_items else "" - else: - # single selection: set as sole selected - self._selected_items = [item] - self._value = item.label() - # notify dialog of selection change - try: - if getattr(self, "notify", lambda: True)(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - else: - handled = False - - return handled - -class YAlignmentCurses(YSingleChildContainerWidget): - """ - Single-child alignment container for ncurses. It becomes stretchable on the - requested axes, and positions the child inside its draw area accordingly. - """ - def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): - super().__init__(parent) - self._halign_spec = horAlign - self._valign_spec = vertAlign - self._backend_widget = None # not used by curses - self._height = 1 - - def widgetClass(self): - return "YAlignment" - - def stretchable(self, dim: YUIDimension): - ''' Returns the stretchability of the layout box: - * The layout box is stretchable if the child is stretchable in - * this dimension or if the child widget has a layout weight in - * this dimension. - ''' - if self._child: - expand = bool(self._child.stretchable(dim)) - weight = bool(self._child.weight(dim)) - if expand or weight: - return True - return False - - def addChild(self, child): - try: - super().addChild(child) - except Exception: - self._child = child - # Ensure child is visible to traversal (dialog looks at widget._children) - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - if child not in self._children: - self._children.append(child) - # keep parent pointer consistent - try: - setattr(child, "_parent", self) - except Exception: - pass - except Exception: - pass - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - # Mirror to _children so focus traversal finds it - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - # replace existing children with this single child to avoid stale entries - if self._children != [child]: - self._children = [child] - try: - setattr(child, "_parent", self) - except Exception: - pass - except Exception: - pass - - def _create_backend_widget(self): - self._backend_widget = None - self._height = max(1, getattr(self._child, "_height", 1) if self._child else 1) - - def _set_backend_enabled(self, enabled): - """Enable/disable alignment container and propagate to its logical child.""" - try: - # propagate to logical child so it updates its own focusability/state - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is not None and hasattr(child, "setEnabled"): - try: - child.setEnabled(enabled) - except Exception: - pass - # nothing else to do for curses backend (no real widget object) - except Exception: - pass - - def _child_min_width(self, child, max_width): - # Heuristic minimal width similar to YHBoxCurses TODO: verify with widget information instead of hardcoded classes - try: - cls = child.widgetClass() if hasattr(child, "widgetClass") else "" - if cls in ("YLabel", "YPushButton", "YCheckBox"): - text = getattr(child, "_text", None) - if text is None: - text = getattr(child, "_label", "") - pad = 4 if cls == "YPushButton" else 0 - return min(max_width, max(1, len(str(text)) + pad)) - except Exception: - pass - return max(1, min(10, max_width)) - - def _draw(self, window, y, x, width, height): - if not self._child or not hasattr(self._child, "_draw"): - return - try: - # width to give to the child: minimal needed (so it can be pushed) - ch_min_w = self._child_min_width(self._child, width) - # Horizontal position - if self._halign_spec == YAlignmentType.YAlignEnd: - cx = x + max(0, width - ch_min_w) - elif self._halign_spec == YAlignmentType.YAlignCenter: - cx = x + max(0, (width - ch_min_w) // 2) - else: - cx = x - # Vertical position (single line widgets mostly) - if self._valign_spec == YAlignmentType.YAlignCenter: - cy = y + max(0, (height - 1) // 2) - elif self._valign_spec == YAlignmentType.YAlignEnd: - cy = y + max(0, height - 1) - else: - cy = y - self._child._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, getattr(self._child, "_height", 1))) - except Exception: - pass - -class YTreeCurses(YSelectionWidget): - """ - NCurses implementation of a tree widget. - - Flattens visible nodes according to YTreeItem._is_open - - Supports single/multi selection and recursive selection propagation - - Preserves per-item selected() / setSelected() semantics and restores selections on rebuild - - Keyboard: Up/Down/PageUp/PageDown/Home/End, SPACE = expand/collapse, ENTER = select/deselect - """ - def __init__(self, parent=None, label="", multiselection=False, recursiveselection=False): - super().__init__(parent) - self._label = label - self._multi = bool(multiselection) - self._recursive = bool(recursiveselection) - if self._recursive: - self._multi = True - self._immediate = self.notify() - # Minimal height (items area) requested by this widget - self._min_height = 6 - # Preferred height exposed to layout should include label line if any - self._height = self._min_height + (1 if self._label else 0) - self._can_focus = True - self._focused = False - self._hover_index = 0 - self._scroll_offset = 0 - self._visible_items = [] - self._selected_items = [] - self._last_selected_ids = set() - self._suppress_selection_handler = False - self.setStretchable(YUIDimension.YD_HORIZ, True) - self.setStretchable(YUIDimension.YD_VERT, True) - - def widgetClass(self): - return "YTree" - - def hasMultiSelection(self): - """Return True if the tree allows selecting multiple items at once.""" - return bool(self._multi) - - def immediateMode(self): - return bool(self._immediate) - - def setImmediateMode(self, on:bool=True): - self._immediate = on - self.setNotify(on) - - def _create_backend_widget(self): - # Keep preferred minimum for the layout (items + optional label) - self._height = max(self._height, self._min_height + (1 if self._label else 0)) - self.rebuildTree() - - def addItem(self, item): - """Ensure base storage gets the item and rebuild visible list immediately.""" - try: - # prefer base implementation if present - try: - super().addItem(item) - except Exception: - # fallback: append to _items list used by this backend - if not hasattr(self, "_items") or self._items is None: - self._items = [] - self._items.append(item) - finally: - try: - # mark rebuild so new items are visible without waiting for external trigger - self.rebuildTree() - except Exception: - pass - - def removeItem(self, item): - """Remove item from internal list and rebuild.""" - try: - try: - super().removeItem(item) - except Exception: - if hasattr(self, "_items") and item in self._items: - try: - self._items.remove(item) - except Exception: - pass - finally: - try: - self.rebuildTree() - except Exception: - pass - - def clearItems(self): - """Clear items and rebuild.""" - try: - try: - super().clearItems() - except Exception: - self._items = [] - finally: - try: - self.rebuildTree() - except Exception: - pass - - def _collect_all_descendants(self, item): - out = [] - stack = [] - try: - for c in getattr(item, "_children", []) or []: - stack.append(c) - except Exception: - pass - while stack: - cur = stack.pop() - out.append(cur) - try: - for ch in getattr(cur, "_children", []) or []: - stack.append(ch) - except Exception: - pass - return out - - def _flatten_visible(self): - """Produce self._visible_items = [(item, depth), ...] following _is_open flags.""" - self._visible_items = [] - def _visit(nodes, depth=0): - for n in nodes: - self._visible_items.append((n, depth)) - try: - is_open = bool(getattr(n, "_is_open", False)) - except Exception: - is_open = False - if is_open: - try: - childs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - childs = getattr(n, "_children", []) or [] - if childs: - _visit(childs, depth + 1) - roots = list(getattr(self, "_items", []) or []) - _visit(roots, 0) - - def rebuildTree(self): - """Recompute visible items and restore selection from item.selected() or last_selected_ids.""" - # preserve items selection if any - try: - self._flatten_visible() - # if there are previously saved last_selected_ids, prefer them - selected_ids = set(self._last_selected_ids) if self._last_selected_ids else set() - # if none, collect from items' selected() property - if not selected_ids: - try: - def _collect_selected(nodes): - out = [] - for n in nodes: - try: - sel = False - if hasattr(n, "selected") and callable(getattr(n, "selected")): - sel = n.selected() - else: - sel = bool(getattr(n, "_selected", False)) - if sel: - out.append(n) - except Exception: - pass - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - out.extend(_collect_selected(chs)) - return out - pre_selected = _collect_selected(list(getattr(self, "_items", []) or [])) - for p in pre_selected: - selected_ids.add(id(p)) - except Exception: - pass - # build logical selected list and last_selected_ids - sel_items = [] - for itm, _d in self._visible_items: - try: - if id(itm) in selected_ids: - sel_items.append(itm) - except Exception: - pass - # also include non-visible selected nodes (descendants) if recursive selection used - if selected_ids: - try: - # scan full tree - def _all_nodes(nodes): - out = [] - for n in nodes: - out.append(n) - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - out.extend(_all_nodes(chs)) - return out - all_nodes = _all_nodes(list(getattr(self, "_items", []) or [])) - for n in all_nodes: - if id(n) in selected_ids and n not in sel_items: - sel_items.append(n) - except Exception: - pass - # apply selected flags to items consistently - try: - # clear all first - def _clear(nodes): - for n in nodes: - try: - n.setSelected(False) - except Exception: - pass - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - _clear(chs) - _clear(list(getattr(self, "_items", []) or [])) - except Exception: - pass - for it in sel_items: - try: - it.setSelected(True) - except Exception: - pass - self._selected_items = list(sel_items) - self._last_selected_ids = set(id(i) for i in self._selected_items) - # ensure hover_index valid - if self._hover_index >= len(self._visible_items): - self._hover_index = max(0, len(self._visible_items) - 1) - self._ensure_hover_visible() - except Exception: - pass - - def _ensure_hover_visible(self, height=None): - """Adjust scroll offset so hover visible in given height area (if None use last draw height).""" - try: - # height param is number of rows available for items display (excluding label) - if height is None: - height = max(1, getattr(self, "_height", 1)) - visible = max(1, height) - if self._hover_index < self._scroll_offset: - self._scroll_offset = self._hover_index - elif self._hover_index >= self._scroll_offset + visible: - self._scroll_offset = self._hover_index - visible + 1 - except Exception: - pass - - def _toggle_expand(self, item): - try: - self._suppress_selection_handler = True - except Exception: - pass - try: - try: - cur = item.isOpen() - item.setOpen(not cur) - except Exception: - try: - cur = bool(getattr(item, "_is_open", False)) - item._is_open = not cur - except Exception: - pass - # preserve selected ids and rebuild - try: - self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) - except Exception: - self._last_selected_ids = set() - self.rebuildTree() - finally: - try: - self._suppress_selection_handler = False - except Exception: - pass - - def _handle_selection_action(self, item): - """Toggle selection (ENTER) respecting multi/single & recursive semantics.""" - if item is None: - return - try: - if self._multi: - # toggle membership - if item in self._selected_items: - # deselect item and (if recursive) descendants - if self._recursive: - to_remove = {item} | set(self._collect_all_descendants(item)) - self._selected_items = [it for it in self._selected_items if it not in to_remove] - for it in to_remove: - try: - it.setSelected(False) - except Exception: - try: - setattr(it, "_selected", False) - except Exception: - pass - else: - try: - self._selected_items.remove(item) - except Exception: - pass - try: - item.setSelected(False) - except Exception: - try: - setattr(item, "_selected", False) - except Exception: - pass - else: - # select item and possibly descendants - if self._recursive: - to_add = [item] + self._collect_all_descendants(item) - for it in to_add: - if it not in self._selected_items: - self._selected_items.append(it) - try: - it.setSelected(True) - except Exception: - try: - setattr(it, "_selected", True) - except Exception: - pass - else: - self._selected_items.append(item) - try: - item.setSelected(True) - except Exception: - try: - setattr(item, "_selected", True) - except Exception: - pass - else: - # single selection: clear all others and set this one - try: - for it in list(getattr(self, "_items", []) or []): - try: - it.setSelected(False) - except Exception: - try: - setattr(it, "_selected", False) - except Exception: - pass - except Exception: - pass - self._selected_items = [item] - try: - item.setSelected(True) - except Exception: - try: - setattr(item, "_selected", True) - except Exception: - pass - except Exception: - pass - - # update last_selected_ids and notify - try: - self._last_selected_ids = set(id(i) for i in self._selected_items) - except Exception: - self._last_selected_ids = set() - if self._immediate and self.notify(): - dlg = self.findDialog() - if dlg: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - - def _draw(self, window, y, x, width, height): - """Draw tree in provided rectangle. Expects height rows available.""" - try: - # compute drawing area for items (first row may be label) - line = y - start_line = line - label_rows = 1 if self._label else 0 - - # Draw label - if self._label: - try: - window.addstr(line, x, self._label[:width], curses.A_BOLD) - except curses.error: - pass - line += 1 - - # Actual rows given by parent for items - available_rows = max(0, height - label_rows) - # Keep _height as the current viewport rows (items area), not the preferred minimum - self._height = max(1, available_rows) - - # record last draw height for navigation/ensure logic - self._height = available_rows - # rebuild visible items (safe cheap operation) - self._flatten_visible() - total = len(self._visible_items) - if total == 0: - try: - if available_rows > 0: - window.addstr(line, x, "(empty)", curses.A_DIM) - except curses.error: - pass - return - - # Clamp scroll/hover to the viewport - self._ensure_hover_visible(height=self._height) - - # Draw only inside the allocated rectangle - draw_rows = min(available_rows, max(0, total - self._scroll_offset)) - for i in range(draw_rows): - idx = self._scroll_offset + i - if idx >= total: - break - itm, depth = self._visible_items[idx] - is_selected = itm in self._selected_items - # expander, text, attrs... - try: - has_children = bool(getattr(itm, "_children", []) or (callable(getattr(itm, "children", None)) and (itm.children() or []))) - except Exception: - has_children = False - try: - is_open = bool(getattr(itm, "_is_open", False)) - except Exception: - is_open = False - exp = "▾" if (has_children and is_open) else ("▸" if has_children else " ") - checkbox = "*" if is_selected else " " - indent = " " * (depth * 2) - text = f"{indent}{exp} [{checkbox}] {itm.label()}" - if len(text) > width: - text = text[:max(0, width - 3)] + "..." - attr = curses.A_REVERSE if (self._focused and idx == self._hover_index and self.isEnabled()) else curses.A_NORMAL - if not self.isEnabled(): - attr |= curses.A_DIM - try: - window.addstr(line + i, x, text.ljust(width), attr) - except curses.error: - pass - - # Scroll indicators based on actual viewport rows - try: - if self._scroll_offset > 0 and available_rows > 0: - window.addch(y + label_rows, x + max(0, width - 1), '^') - if (self._scroll_offset + available_rows) < total and available_rows > 0: - window.addch(y + label_rows + min(available_rows - 1, total - 1), x + max(0, width - 1), 'v') - except curses.error: - pass - except Exception: - pass - - def _handle_key(self, key): - """Keyboard handling: navigation, expand (SPACE), select (ENTER).""" - if not self._focused or not self.isEnabled(): - return False - handled = True - total = len(self._visible_items) - if key == curses.KEY_UP: - if self._hover_index > 0: - self._hover_index -= 1 - self._ensure_hover_visible(self._height) - elif key == curses.KEY_DOWN: - if self._hover_index < max(0, total - 1): - self._hover_index += 1 - self._ensure_hover_visible(self._height) - elif key == curses.KEY_PPAGE: - step = max(1, self._height) - self._hover_index = max(0, self._hover_index - step) - self._ensure_hover_visible(self._height) - elif key == curses.KEY_NPAGE: - step = max(1, self._height) - self._hover_index = min(max(0, total - 1), self._hover_index + step) - self._ensure_hover_visible(self._height) - elif key == curses.KEY_HOME: - self._hover_index = 0 - self._ensure_hover_visible(self._height) - elif key == curses.KEY_END: - self._hover_index = max(0, total - 1) - self._ensure_hover_visible(self._height) - elif key in (ord(' '),): # SPACE toggles expansion per dialog footer convention - if 0 <= self._hover_index < total: - itm, _ = self._visible_items[self._hover_index] - # Toggle expand/collapse without changing selection - self._toggle_expand(itm) - elif key in (ord('\n'),): # ENTER toggles selection - if 0 <= self._hover_index < total: - itm, _ = self._visible_items[self._hover_index] - self._handle_selection_action(itm) - else: - handled = False - return handled - - def currentItem(self): - try: - # Prefer explicit selected_items; if empty return hovered visible item (useful after selection) - if self._selected_items: - return self._selected_items[0] - # fallback: return hovered visible item if any - if 0 <= self._hover_index < len(getattr(self, "_visible_items", [])): - return self._visible_items[self._hover_index][0] - return None - except Exception: - return None - - def getSelectedItems(self): - return list(self._selected_items) - - def selectItem(self, item, selected=True): - """Programmatic select/deselect that respects recursive flag.""" - if item is None: - return - try: - if selected: - if not self._multi: - # clear others - try: - for it in list(getattr(self, "_items", []) or []): - try: - it.setSelected(False) - except Exception: - try: - setattr(it, "_selected", False) - except Exception: - pass - item.setSelected(True) - except Exception: - try: - setattr(item, "_selected", True) - except Exception: - pass - self._selected_items = [item] - else: - if item not in self._selected_items: - try: - item.setSelected(True) - except Exception: - try: - setattr(item, "_selected", True) - except Exception: - pass - self._selected_items.append(item) - if self._recursive: - for d in self._collect_all_descendants(item): - if d not in self._selected_items: - try: - d.setSelected(True) - except Exception: - try: - setattr(d, "_selected", True) - except Exception: - pass - self._selected_items.append(d) - else: - # deselect - if item in self._selected_items: - try: - self._selected_items.remove(item) - except Exception: - pass - try: - item.setSelected(False) - except Exception: - try: - setattr(item, "_selected", False) - except Exception: - pass - if self._recursive: - for d in self._collect_all_descendants(item): - if d in self._selected_items: - try: - self._selected_items.remove(d) - except Exception: - pass - try: - d.setSelected(False) - except Exception: - try: - setattr(d, "_selected", False) - except Exception: - pass - # update last ids - try: - self._last_selected_ids = set(id(i) for i in self._selected_items) - except Exception: - self._last_selected_ids = set() - except Exception: - pass - -class YFrameCurses(YSingleChildContainerWidget): - """ - NCurses implementation of YFrame. - - Draws a framed box with a title. - - Hosts a single child inside the frame with inner margins so the child's - own label does not overlap the frame title. - - Reports stretchability based on its child. - """ - def __init__(self, parent=None, label=""): - super().__init__(parent) - self._label = label or "" - self._backend_widget = None - # Preferred minimal height is computed from child (see _update_min_height) - self._height = 3 - # inner top padding to separate frame title from child's label - self._inner_top_padding = 1 - - def widgetClass(self): - return "YFrame" - - def _update_min_height(self): - """Recompute minimal height: at least 3 rows or child layout min + borders + padding.""" - try: - child = getattr(self, "_child", None) - inner_min = _curses_recursive_min_height(child) if child is not None else 1 - self._height = max(3, 2 + self._inner_top_padding + inner_min) - except Exception: - self._height = max(self._height, 3) - - def label(self): - return self._label - - def setLabel(self, new_label): - try: - self._label = str(new_label) - except Exception: - self._label = new_label - - def stretchable(self, dim): - """Frame is stretchable if its child is stretchable or has a weight.""" - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is None: - return False - try: - if bool(child.stretchable(dim)): - return True - except Exception: - pass - try: - if bool(child.weight(dim)): - return True - except Exception: - pass - except Exception: - pass - return False - - def _create_backend_widget(self): - # curses backend does not create a separate widget object for frames; - # drawing is performed in _draw by the parent container. - self._backend_widget = None - # Update minimal height based on the child - self._update_min_height() - - def _set_backend_enabled(self, enabled): - """Propagate enabled state to the child.""" - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is not None and hasattr(child, "setEnabled"): - try: - child.setEnabled(enabled) - except Exception: - pass - except Exception: - pass - - def addChild(self, child): - try: - super().addChild(child) - except Exception: - self._child = child - # ensure traversal lists contain the child - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - if child not in self._children: - self._children.append(child) - try: - child._parent = self - except Exception: - pass - except Exception: - pass - # Update minimal height based on the child - self._update_min_height() - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - try: - self._children = [child] - try: - child._parent = self - except Exception: - pass - except Exception: - pass - # Update minimal height based on the child - self._update_min_height() - - def _draw(self, window, y, x, width, height): - """Draw frame border and title, then draw child inside inner area with margins.""" - try: - if width <= 0 or height <= 0: - return - # Ensure minimal height based on child layout before drawing - self._update_min_height() - # Graceful fallback for very small areas - if height < 3 or width < 4: - try: - if self._label and height >= 1 and width > 2: - title = f" {self._label} " - title = title[:max(0, width - 2)] - window.addstr(y, x, title, curses.A_BOLD) - except curses.error: - pass - return - # Choose box characters (prefer ACS if available) - try: - hline = curses.ACS_HLINE - vline = curses.ACS_VLINE - tl = curses.ACS_ULCORNER - tr = curses.ACS_URCORNER - bl = curses.ACS_LLCORNER - br = curses.ACS_LRCORNER - except Exception: - hline = ord('-') - vline = ord('|') - tl = ord('+') - tr = ord('+') - bl = ord('+') - br = ord('+') - - # Draw corners and edges - try: - window.addch(y, x, tl) - window.addch(y, x + width - 1, tr) - window.addch(y + height - 1, x, bl) - window.addch(y + height - 1, x + width - 1, br) - for cx in range(x + 1, x + width - 1): - window.addch(y, cx, hline) - window.addch(y + height - 1, cx, hline) - for cy in range(y + 1, y + height - 1): - window.addch(cy, x, vline) - window.addch(cy, x + width - 1, vline) - except curses.error: - # best-effort: ignore drawing errors when area is too small - pass - - # Draw title centered on top border (leave at least one space from corners) - if self._label: - try: - title = f" {self._label} " - max_title_len = max(0, width - 4) - if len(title) > max_title_len: - title = title[:max(0, max_title_len - 3)] + "..." - start_x = x + max(1, (width - len(title)) // 2) - # overwrite part of top border with title text - window.addstr(y, start_x, title, curses.A_BOLD) - except curses.error: - pass - - # Compute inner content rectangle - inner_x = x + 1 - inner_y = y + 1 - inner_w = max(0, width - 2) - inner_h = max(0, height - 2) - - pad_top = min(self._inner_top_padding, max(0, inner_h)) - content_y = inner_y + pad_top - content_h = max(0, inner_h - pad_top) - - child = getattr(self, "_child", None) - if child is None: - return - - # Clamp content height to at least the child layout minimal height - needed = _curses_recursive_min_height(child) - # Do not exceed available area; this only influences the draw area passed down - content_h = min(max(content_h, needed), inner_h) - - if content_h <= 0 or inner_w <= 0: - return - if hasattr(child, "_draw"): - child._draw(window, content_y, inner_x, inner_w, content_h) - except Exception: - pass From 8056a874402cfd36773c688f3ecb99e8c3cb1e72 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 7 Dec 2025 22:52:45 +0100 Subject: [PATCH 116/523] removed custom import --- manatools/aui/yui_curses.py | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index e369ca0..68b754c 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -7,37 +7,8 @@ import sys import os import time -import importlib from .yui_common import * - -# Import backend symbols only into this shim module. -def _import_curses_backend_symbols(): - mod = None - try: - # Package-relative import - mod = importlib.import_module(".backends.curses", __package__) - except Exception: - try: - # Absolute fallback - mod = importlib.import_module("manatools.aui.backends.curses") - except Exception: - mod = None - if not mod: - return - names = getattr(mod, "__all__", None) - if names: - for name in names: - try: - globals()[name] = getattr(mod, name) - except Exception: - pass - else: - # Fallback: import non-private names - for name, obj in mod.__dict__.items(): - if not name.startswith("_"): - globals()[name] = obj - -_import_curses_backend_symbols() +from .backends.curses import * class YUICurses: def __init__(self): From b45494093884e3de21475c4e2fc6bb4195515d97 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 7 Dec 2025 22:53:41 +0100 Subject: [PATCH 117/523] removed custom import --- manatools/aui/yui_qt.py | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 7d0b128..2842e9f 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -3,39 +3,10 @@ """ import sys -import importlib from PySide6 import QtWidgets, QtCore, QtGui import os from .yui_common import * - -# Import backend symbols only into this shim module. -def _import_qt_backend_symbols(): - mod = None - try: - # Package-relative import - mod = importlib.import_module(".backends.qt", __package__) - except Exception: - try: - # Absolute fallback - mod = importlib.import_module("manatools.aui.backends.qt") - except Exception: - mod = None - if not mod: - return - names = getattr(mod, "__all__", None) - if names: - for name in names: - try: - globals()[name] = getattr(mod, name) - except Exception: - pass - else: - # Fallback: import non-private names - for name, obj in mod.__dict__.items(): - if not name.startswith("_"): - globals()[name] = obj - -_import_qt_backend_symbols() +from .backends.qt import * class YUIQt: def __init__(self): From 5198eae8371596745aa6e6becaee348c19ba9ee2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 7 Dec 2025 22:54:35 +0100 Subject: [PATCH 118/523] removed custom import --- manatools/aui/yui_gtk.py | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 9d4d9e8..fb6f7d3 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -8,37 +8,8 @@ import cairo import threading import os -import importlib from .yui_common import * - -# Import backend symbols only into this shim module. -def _import_gtk_backend_symbols(): - mod = None - try: - # Package-relative import - mod = importlib.import_module(".backends.gtk", __package__) - except Exception: - try: - # Absolute fallback - mod = importlib.import_module("manatools.aui.backends.gtk") - except Exception: - mod = None - if not mod: - return - names = getattr(mod, "__all__", None) - if names: - for name in names: - try: - globals()[name] = getattr(mod, name) - except Exception: - pass - else: - # Fallback: import non-private names - for name, obj in mod.__dict__.items(): - if not name.startswith("_"): - globals()[name] = obj - -_import_gtk_backend_symbols() +from .backends.gtk import * class YUIGtk: def __init__(self): From dd251b61fe314956d2791685621f32ddd475977d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 18:43:02 +0100 Subject: [PATCH 119/523] Added YCheckBoxFrameQt --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/checkboxframeqt.py | 309 +++++++++++++++++++ manatools/aui/yui_qt.py | 5 +- 3 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/qt/checkboxframeqt.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 1e35ce0..f0eefa1 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -10,6 +10,7 @@ from .frameqt import YFrameQt from .inputfieldqt import YInputFieldQt from .selectionboxqt import YSelectionBoxQt +from .checkboxframeqt import YCheckBoxFrameQt __all__ = [ @@ -25,5 +26,6 @@ "YCheckBoxQt", "YComboBoxQt", "YAlignmentQt", + "YCheckBoxFrameQt", # ... ] diff --git a/manatools/aui/backends/qt/checkboxframeqt.py b/manatools/aui/backends/qt/checkboxframeqt.py new file mode 100644 index 0000000..25b26f0 --- /dev/null +++ b/manatools/aui/backends/qt/checkboxframeqt.py @@ -0,0 +1,309 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore +from ...yui_common import * + +class YCheckBoxFrameQt(YSingleChildContainerWidget): + """ + Qt backend for YCheckBoxFrame: a frame with a checkbox that can enable/disable its child. + """ + def __init__(self, parent=None, label: str = "", checked: bool = False): + super().__init__(parent) + self._label = label or "" + self._checked = bool(checked) + self._auto_enable = True + self._invert_auto = False + self._backend_widget = None + self._checkbox = None + self._content_widget = None + self._content_layout = None + + def widgetClass(self): + return "YCheckBoxFrame" + + def label(self): + return self._label + + def setLabel(self, new_label): + try: + self._label = str(new_label) + if self._checkbox is not None: + try: + self._checkbox.setText(self._label) + except Exception: + pass + except Exception: + pass + + def setValue(self, isChecked: bool): + try: + self._checked = bool(isChecked) + if self._checkbox is not None: + try: + self._checkbox.blockSignals(True) + self._checkbox.setChecked(self._checked) + self._checkbox.blockSignals(False) + except Exception: + pass + # propagate enablement based on new value + self.handleChildrenEnablement(self._checked) + except Exception: + pass + + def value(self): + try: + if self._checkbox is not None: + return bool(self._checkbox.isChecked()) + except Exception: + pass + return bool(self._checked) + + def autoEnable(self): + return bool(self._auto_enable) + + def setAutoEnable(self, autoEnable: bool): + try: + self._auto_enable = bool(autoEnable) + # re-evaluate children enablement + self.handleChildrenEnablement(self.value()) + except Exception: + pass + + def invertAutoEnable(self): + return bool(self._invert_auto) + + def setInvertAutoEnable(self, invert: bool): + try: + self._invert_auto = bool(invert) + self.handleChildrenEnablement(self.value()) + except Exception: + pass + + def _create_backend_widget(self): + """Create widget: top-level checkbox + content area for single child.""" + try: + container = QtWidgets.QWidget() + vlayout = QtWidgets.QVBoxLayout(container) + vlayout.setContentsMargins(6, 6, 6, 6) + vlayout.setSpacing(4) + + cb = QtWidgets.QCheckBox(self._label) + cb.setChecked(self._checked) + vlayout.addWidget(cb, 0, QtCore.Qt.AlignTop) + + content = QtWidgets.QWidget() + content_layout = QtWidgets.QVBoxLayout(content) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(4) + vlayout.addWidget(content) + + self._backend_widget = container + self._checkbox = cb + self._content_widget = content + self._content_layout = content_layout + + cb.toggled.connect(self._on_checkbox_toggled) + + # attach existing child if present + try: + if getattr(self, "_child", None): + self._attach_child_backend() + except Exception: + pass + except Exception: + self._backend_widget = None + self._checkbox = None + self._content_widget = None + self._content_layout = None + + def _attach_child_backend(self): + """Attach child's backend widget into content area.""" + if not (self._backend_widget and self._content_layout and getattr(self, "_child", None)): + return + try: + # clear content layout + while self._content_layout.count(): + it = self._content_layout.takeAt(0) + if it and it.widget(): + it.widget().setParent(None) + except Exception: + pass + try: + child = self._child + w = None + try: + w = child.get_backend_widget() + except Exception: + w = None + if w is None: + # if backend not created, attempt to create it + try: + child._create_backend_widget() + w = child.get_backend_widget() + except Exception: + w = None + if w is not None: + try: + self._content_layout.addWidget(w) + except Exception: + try: + w.setParent(self._content_widget) + except Exception: + pass + # apply current enablement state + self.handleChildrenEnablement(self.value()) + except Exception: + pass + + def _on_checkbox_toggled(self, checked): + try: + self._checked = bool(checked) + if self._auto_enable: + self.handleChildrenEnablement(self._checked) + # notify logical selection change through YWidgetEvent if needed + try: + if self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + except Exception: + pass + + def handleChildrenEnablement(self, isChecked: bool): + """Enable/disable child widgets according to autoEnable and invertAutoEnable.""" + try: + if not self._auto_enable: + return + state = bool(isChecked) + if self._invert_auto: + state = not state + # propagate to logical child(s) + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + for c in chs: + try: + c.setEnabled(state) + except Exception: + pass + return + try: + child.setEnabled(state) + except Exception: + # best-effort: if child has backend widget, set it sensitive + try: + w = child.get_backend_widget() + if w is not None: + try: + w.setEnabled(state) + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + try: + self._child = child + child._parent = self + except Exception: + pass + try: + if getattr(self, "_backend_widget", None): + self._attach_child_backend() + except Exception: + pass + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + try: + self._child = child + child._parent = self + except Exception: + pass + try: + if getattr(self, "_backend_widget", None): + self._attach_child_backend() + except Exception: + pass + + def _set_backend_enabled(self, enabled: bool): + try: + if self._backend_widget is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate to logical child(s) + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + for c in chs: + try: + c.setEnabled(enabled) + except Exception: + pass + return + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def setProperty(self, propertyName, val): + try: + if propertyName == "label": + self.setLabel(str(val)) + return True + if propertyName == "value" or propertyName == "checked": + self.setValue(bool(val)) + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "label": + return self.label() + if propertyName == "value" or propertyName == "checked": + return self.value() + except Exception: + pass + return None + + def propertySet(self): + try: + props = YPropertySet() + try: + props.add(YProperty("label", YPropertyType.YStringProperty)) + props.add(YProperty("value", YPropertyType.YBoolProperty)) + except Exception: + pass + return props + except Exception: + return None diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 2842e9f..7305989 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -241,4 +241,7 @@ def createTree(self, parent, label, multiselection=False, recursiveselection = F def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameQt(parent, label) - + + def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): + """Create a CheckBox Frame widget.""" + return YCheckBoxFrameQt(parent, label, checked) From 10f55bf746858a1c31c70cbd7b9e39473a1de392 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 18:55:01 +0100 Subject: [PATCH 120/523] Added a border like YFrameQt --- manatools/aui/backends/qt/checkboxframeqt.py | 59 +++++++++++++------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/manatools/aui/backends/qt/checkboxframeqt.py b/manatools/aui/backends/qt/checkboxframeqt.py index 25b26f0..d69272e 100644 --- a/manatools/aui/backends/qt/checkboxframeqt.py +++ b/manatools/aui/backends/qt/checkboxframeqt.py @@ -36,9 +36,13 @@ def label(self): def setLabel(self, new_label): try: self._label = str(new_label) - if self._checkbox is not None: + if getattr(self, "_checkbox", None) is not None: try: - self._checkbox.setText(self._label) + # QGroupBox uses setTitle; keep compatibility if _checkbox is a QCheckBox + if hasattr(self._checkbox, "setTitle"): + self._checkbox.setTitle(self._label) + else: + self._checkbox.setText(self._label) except Exception: pass except Exception: @@ -49,9 +53,16 @@ def setValue(self, isChecked: bool): self._checked = bool(isChecked) if self._checkbox is not None: try: - self._checkbox.blockSignals(True) - self._checkbox.setChecked(self._checked) - self._checkbox.blockSignals(False) + # QGroupBox supports setChecked when checkable + if hasattr(self._checkbox, "setChecked"): + self._checkbox.blockSignals(True) + self._checkbox.setChecked(self._checked) + self._checkbox.blockSignals(False) + else: + # fallback for plain checkbox widget + self._checkbox.blockSignals(True) + self._checkbox.setChecked(self._checked) + self._checkbox.blockSignals(False) except Exception: pass # propagate enablement based on new value @@ -62,7 +73,11 @@ def setValue(self, isChecked: bool): def value(self): try: if self._checkbox is not None: - return bool(self._checkbox.isChecked()) + # QGroupBox isChecked exists when checkable; otherwise fallback + if hasattr(self._checkbox, "isChecked"): + return bool(self._checkbox.isChecked()) + if hasattr(self._checkbox, "isChecked"): + return bool(self._checkbox.isChecked()) except Exception: pass return bool(self._checked) @@ -89,29 +104,35 @@ def setInvertAutoEnable(self, invert: bool): pass def _create_backend_widget(self): - """Create widget: top-level checkbox + content area for single child.""" + """Create widget: use QGroupBox checkable (theme-aware) so the checkbox is in the title.""" try: - container = QtWidgets.QWidget() - vlayout = QtWidgets.QVBoxLayout(container) - vlayout.setContentsMargins(6, 6, 6, 6) - vlayout.setSpacing(4) - - cb = QtWidgets.QCheckBox(self._label) - cb.setChecked(self._checked) - vlayout.addWidget(cb, 0, QtCore.Qt.AlignTop) + # Use QGroupBox to present a themed frame with a checkable title area. + grp = QtWidgets.QGroupBox() + grp.setTitle(self._label) + grp.setCheckable(True) + grp.setChecked(self._checked) + layout = QtWidgets.QVBoxLayout(grp) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(4) content = QtWidgets.QWidget() content_layout = QtWidgets.QVBoxLayout(content) content_layout.setContentsMargins(0, 0, 0, 0) content_layout.setSpacing(4) - vlayout.addWidget(content) + layout.addWidget(content) - self._backend_widget = container - self._checkbox = cb + self._backend_widget = grp + # keep attribute name _checkbox for compatibility, but it's a QGroupBox now + self._checkbox = grp self._content_widget = content self._content_layout = content_layout - cb.toggled.connect(self._on_checkbox_toggled) + # connect group toggled signal + try: + grp.toggled.connect(self._on_checkbox_toggled) + except Exception: + # older bindings or non-checkable objects may not have toggled + pass # attach existing child if present try: From eb6b8b0de01dbcfce74a183254d5c932b77380f9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 18:55:39 +0100 Subject: [PATCH 121/523] Changed as CheckBoxFrame --- test/test_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_frame.py b/test/test_frame.py index ecbee78..8cd774e 100644 --- a/test/test_frame.py +++ b/test/test_frame.py @@ -43,7 +43,7 @@ def test_selectionbox(backend_name=None): selBox.addItem( "Ravioli" ) selBox.addItem( "Trofie al pesto" ) # Ligurian specialty - frame1 = factory.createFrame( hbox , "SelectionBox Options") + frame1 = factory.createCheckBoxFrame( hbox , "SelectionBox Options") vbox = factory.createVBox( frame1 ) align = factory.createTop(vbox) notifyCheckBox = factory.createCheckBox( align, "Notify on change", selBox.notify() ) From 6d190e5aeb715ce25ab73552d472e7fb8fb57e2f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 19:01:40 +0100 Subject: [PATCH 122/523] Added YCheckBoxFrameQt --- manatools/aui/backends/gtk/__init__.py | 2 + .../aui/backends/gtk/checkboxframegtk.py | 422 ++++++++++++++++++ manatools/aui/yui_gtk.py | 3 + 3 files changed, 427 insertions(+) create mode 100644 manatools/aui/backends/gtk/checkboxframegtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 81e5906..f90d1d3 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -10,6 +10,7 @@ from .framegtk import YFrameGtk from .inputfieldgtk import YInputFieldGtk from .selectionboxgtk import YSelectionBoxGtk +from .checkboxframegtk import YCheckBoxFrameGtk __all__ = [ @@ -25,5 +26,6 @@ "YCheckBoxGtk", "YComboBoxGtk", "YAlignmentGtk", + "YCheckBoxFrameGtk", # ... ] diff --git a/manatools/aui/backends/gtk/checkboxframegtk.py b/manatools/aui/backends/gtk/checkboxframegtk.py new file mode 100644 index 0000000..24c5548 --- /dev/null +++ b/manatools/aui/backends/gtk/checkboxframegtk.py @@ -0,0 +1,422 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + +class YCheckBoxFrameGtk(YSingleChildContainerWidget): + """ + GTK backend for YCheckBoxFrame: a frame with a checkbutton in the title area + that enables/disables its child. The implementation prefers to place the + CheckButton into the Frame's label widget (if supported) so the theme draws + the frame title and border consistently. + """ + def __init__(self, parent=None, label: str = "", checked: bool = False): + super().__init__(parent) + self._label = label or "" + self._checked = bool(checked) + self._auto_enable = True + self._invert_auto = False + + self._backend_widget = None + self._checkbox = None + self._content_box = None + self._label_widget = None + + def widgetClass(self): + return "YCheckBoxFrame" + + def label(self): + return self._label + + def setLabel(self, new_label: str): + try: + self._label = new_label or "" + if getattr(self, "_checkbox", None) is not None: + try: + # Gtk.CheckButton uses set_label via property or set_text + if hasattr(self._checkbox, "set_label"): + self._checkbox.set_label(self._label) + elif hasattr(self._checkbox, "set_text"): + self._checkbox.set_text(self._label) + else: + # fallback: recreate label if necessary (best-effort) + pass + except Exception: + pass + # if we used a separate label widget, update it too + try: + if getattr(self, "_label_widget", None) is not None: + self._label_widget.set_text(self._label) + except Exception: + pass + except Exception: + pass + + def value(self): + try: + if getattr(self, "_checkbox", None) is not None: + return bool(self._checkbox.get_active()) + except Exception: + pass + return bool(self._checked) + + def setValue(self, isChecked: bool): + try: + self._checked = bool(isChecked) + if getattr(self, "_checkbox", None) is not None: + try: + self._checkbox.handler_block_by_func(self._on_toggled) + except Exception: + pass + try: + self._checkbox.set_active(self._checked) + except Exception: + try: + self._checkbox.set_active(self._checked) + except Exception: + pass + try: + self._checkbox.handler_unblock_by_func(self._on_toggled) + except Exception: + pass + # apply enable/disable to children + self._apply_children_enablement(self._checked) + except Exception: + pass + + def autoEnable(self): + return bool(self._auto_enable) + + def setAutoEnable(self, autoEnable: bool): + try: + self._auto_enable = bool(autoEnable) + self._apply_children_enablement(self.value()) + except Exception: + pass + + def invertAutoEnable(self): + return bool(self._invert_auto) + + def setInvertAutoEnable(self, invert: bool): + try: + self._invert_auto = bool(invert) + self._apply_children_enablement(self.value()) + except Exception: + pass + + def _create_backend_widget(self): + """Create Gtk.Frame (or fallback container) and place a CheckButton in the title area.""" + try: + # Try Gtk.Frame and place a checkbutton as label widget (theme-aware) + try: + frame = Gtk.Frame() + # create inner content box + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + try: + content.set_hexpand(True) + content.set_vexpand(True) + except Exception: + pass + + # Create checkbutton and try to set it as the frame's label widget + check = Gtk.CheckButton(label=self._label) + check.set_active(self._checked) + self._checkbox = check + + # If Gtk.Frame supports set_label_widget use it for themed title + if hasattr(frame, "set_label_widget"): + try: + frame.set_label_widget(check) + except Exception: + # fallback: append check above content + pass + + # Attach content inside frame + try: + if hasattr(frame, "set_child"): + frame.set_child(content) + else: + frame.add(content) + except Exception: + try: + frame.add(content) + except Exception: + pass + + self._backend_widget = frame + self._content_box = content + self._label_widget = None + except Exception: + # Fallback: container with top CheckButton and then content box + container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + check = Gtk.CheckButton(label=self._label) + check.set_active(self._checked) + container.append(check) + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + try: + content.set_hexpand(True) + content.set_vexpand(True) + except Exception: + pass + container.append(content) + self._backend_widget = container + self._checkbox = check + self._content_box = content + self._label_widget = None + + # Ensure a little top margin between title and content + try: + if hasattr(self._content_box, "set_margin_top"): + self._content_box.set_margin_top(6) + else: + try: + self._content_box.set_spacing(6) + except Exception: + pass + except Exception: + pass + + # Connect toggled handler + try: + self._checkbox.connect("toggled", self._on_toggled) + except Exception: + try: + self._checkbox.connect("toggled", self._on_toggled) + except Exception: + pass + + # attach existing child if any + try: + if getattr(self, "_child", None): + self._attach_child_backend() + except Exception: + pass + except Exception: + self._backend_widget = None + self._checkbox = None + self._content_box = None + self._label_widget = None + + def _attach_child_backend(self): + """Attach the logical child backend widget into the content box (clear previous).""" + if self._content_box is None or self._backend_widget is None: + return + try: + # clear existing children + try: + while True: + first = self._content_box.get_first_child() + if first is None: + break + try: + self._content_box.remove(first) + except Exception: + break + except Exception: + # fallback for Gtk.Box append/remove API + try: + for child in list(self._content_box.get_children()): + try: + self._content_box.remove(child) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return + + try: + w = None + try: + w = child.get_backend_widget() + except Exception: + w = None + if w is None: + try: + if hasattr(child, "_create_backend_widget"): + child._create_backend_widget() + w = child.get_backend_widget() + except Exception: + w = None + if w is not None: + try: + self._content_box.append(w) + except Exception: + try: + self._content_box.add(w) + except Exception: + pass + # propagate expansion hints + try: + if child.stretchable(YUIDimension.YD_VERT): + if hasattr(w, "set_vexpand"): + w.set_vexpand(True) + if child.stretchable(YUIDimension.YD_HORIZ): + if hasattr(w, "set_hexpand"): + w.set_hexpand(True) + except Exception: + pass + except Exception: + pass + + # apply enablement state + self._apply_children_enablement(self.value()) + + def _on_toggled(self, widget): + try: + val = bool(self._checkbox.get_active()) + self._checked = val + if self._auto_enable: + self._apply_children_enablement(val) + try: + if self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + except Exception: + pass + + def _apply_children_enablement(self, isChecked: bool): + try: + if not self._auto_enable: + return + state = bool(isChecked) + if self._invert_auto: + state = not state + child = getattr(self, "_child", None) + if child is None: + for c in (getattr(self, "_children", None) or []): + try: + c.setEnabled(state) + except Exception: + pass + return + try: + child.setEnabled(state) + except Exception: + try: + w = child.get_backend_widget() + if w is not None: + try: + w.set_sensitive(state) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + try: + self._child = child + child._parent = self + except Exception: + pass + try: + if getattr(self, "_backend_widget", None): + self._attach_child_backend() + except Exception: + pass + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + try: + self._child = child + child._parent = self + except Exception: + pass + try: + if getattr(self, "_backend_widget", None): + self._attach_child_backend() + except Exception: + pass + + def _set_backend_enabled(self, enabled: bool): + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.set_sensitive(bool(enabled)) + except Exception: + pass + except Exception: + pass + # propagate to logical children + try: + child = getattr(self, "_child", None) + if child is None: + for c in (getattr(self, "_children", None) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + return + try: + child.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def setProperty(self, propertyName, val): + try: + if propertyName == "label": + self.setLabel(str(val)) + return True + if propertyName in ("value", "checked"): + self.setValue(bool(val)) + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "label": + return self.label() + if propertyName in ("value", "checked"): + return self.value() + except Exception: + pass + return None + + def propertySet(self): + try: + props = YPropertySet() + try: + props.add(YProperty("label", YPropertyType.YStringProperty)) + props.add(YProperty("value", YPropertyType.YBoolProperty)) + except Exception: + pass + return props + except Exception: + return None + diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index fb6f7d3..68c9ff9 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -254,3 +254,6 @@ def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) + def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): + """Create a CheckBox Frame widget.""" + return YCheckBoxFrameGtk(parent, label, checked) \ No newline at end of file From fc82bce6ffab424141d0cd2a93289cd87ebbabd4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 19:09:46 +0100 Subject: [PATCH 123/523] Added YCheckBoxFrameCurses --- manatools/aui/backends/curses/__init__.py | 2 + .../backends/curses/checkboxframecurses.py | 294 ++++++++++++++++++ manatools/aui/yui_curses.py | 5 +- 3 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 manatools/aui/backends/curses/checkboxframecurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index a402e63..4203599 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -10,6 +10,7 @@ from .framecurses import YFrameCurses from .inputfieldcurses import YInputFieldCurses from .selectionboxcurses import YSelectionBoxCurses +from .checkboxframecurses import YCheckBoxFrameCurses __all__ = [ @@ -25,5 +26,6 @@ "YCheckBoxCurses", "YComboBoxCurses", "YAlignmentCurses", + "YCheckBoxFrameCurses", # ... ] diff --git a/manatools/aui/backends/curses/checkboxframecurses.py b/manatools/aui/backends/curses/checkboxframecurses.py new file mode 100644 index 0000000..d96e1a1 --- /dev/null +++ b/manatools/aui/backends/curses/checkboxframecurses.py @@ -0,0 +1,294 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * +from .commoncurses import _curses_recursive_min_height + +class YCheckBoxFrameCurses(YSingleChildContainerWidget): + """ + NCurses implementation of a framed container with a checkbox in the title. + The checkbox enables/disables the inner child (autoEnable/invertAutoEnable supported). + Drawing reuses frame style from framecurses but prepends a checkbox marker to the title. + """ + def __init__(self, parent=None, label: str = "", checked: bool = False): + super().__init__(parent) + self._label = label or "" + self._checked = bool(checked) + self._auto_enable = True + self._invert_auto = False + self._backend_widget = None + # minimal height (will be computed from child) + self._height = 3 + self._inner_top_padding = 1 + + def widgetClass(self): + return "YCheckBoxFrame" + + def label(self): + return self._label + + def setLabel(self, new_label): + try: + self._label = str(new_label) + except Exception: + self._label = new_label + + def value(self): + try: + return bool(self._checked) + except Exception: + return False + + def setValue(self, isChecked: bool): + try: + self._checked = bool(isChecked) + # propagate enablement to children if autoEnable + if self._auto_enable: + self._apply_children_enablement(self._checked) + # notify listeners + try: + if self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + except Exception: + pass + + def autoEnable(self): + return bool(self._auto_enable) + + def setAutoEnable(self, autoEnable: bool): + try: + self._auto_enable = bool(autoEnable) + self._apply_children_enablement(self._checked) + except Exception: + pass + + def invertAutoEnable(self): + return bool(self._invert_auto) + + def setInvertAutoEnable(self, invert: bool): + try: + self._invert_auto = bool(invert) + self._apply_children_enablement(self._checked) + except Exception: + pass + + def stretchable(self, dim): + """Frame is stretchable if its child is.""" + try: + child = getattr(self, "_child", None) + if child is None: + chs = getattr(self, "_children", None) or [] + child = chs[0] if chs else None + if child is None: + return False + try: + if bool(child.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(child.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def _update_min_height(self): + """Recompute minimal height: borders + padding + child's minimal layout.""" + try: + child = getattr(self, "_child", None) + inner_min = _curses_recursive_min_height(child) if child is not None else 1 + self._height = max(3, 2 + self._inner_top_padding + inner_min) + except Exception: + self._height = max(self._height, 3) + + def _create_backend_widget(self): + # no persistent backend object for curses + self._backend_widget = None + self._update_min_height() + + def _apply_children_enablement(self, isChecked: bool): + try: + if not self._auto_enable: + return + state = bool(isChecked) + if self._invert_auto: + state = not state + child = getattr(self, "_child", None) + if child is None: + for c in (getattr(self, "_children", None) or []): + try: + c.setEnabled(state) + except Exception: + pass + return + try: + child.setEnabled(state) + except Exception: + # best-effort try backend widget sensitivity + try: + w = child.get_backend_widget() + if w is not None: + try: + w.set_sensitive(state) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def _set_backend_enabled(self, enabled: bool): + try: + # logical propagation + self._apply_children_enablement(self._checked if self._auto_enable else enabled) + except Exception: + pass + + def addChild(self, child): + try: + super().addChild(child) + except Exception: + self._child = child + try: + if not hasattr(self, "_children") or self._children is None: + self._children = [] + if child not in self._children: + self._children.append(child) + try: + child._parent = self + except Exception: + pass + except Exception: + pass + self._update_min_height() + + def setChild(self, child): + try: + super().setChild(child) + except Exception: + self._child = child + try: + self._children = [child] + try: + child._parent = self + except Exception: + pass + except Exception: + pass + self._update_min_height() + + def _on_toggle_request(self): + """Toggle value (helper for key handling if needed).""" + try: + self.setValue(not self._checked) + except Exception: + pass + + def _draw(self, window, y, x, width, height): + """ + Draw framed box with checkbox marker in the label, then delegate to child. + Title shows "[x]" or "[ ]" before the label. + """ + try: + if width <= 0 or height <= 0: + return + self._update_min_height() + # if not enough space for full frame, draw compact title line and return + if height < 3 or width < 4: + try: + chk = "x" if self._checked else " " + title = f"[{chk}] {self._label}" if self._label else f"[{chk}]" + title = title[:max(0, width)] + window.addstr(y, x, title, curses.A_BOLD) + except curses.error: + pass + return + + # choose box chars + try: + hline = curses.ACS_HLINE + vline = curses.ACS_VLINE + tl = curses.ACS_ULCORNER + tr = curses.ACS_URCORNER + bl = curses.ACS_LLCORNER + br = curses.ACS_LRCORNER + except Exception: + hline = ord('-') + vline = ord('|') + tl = ord('+') + tr = ord('+') + bl = ord('+') + br = ord('+') + + # draw border + try: + window.addch(y, x, tl) + window.addch(y, x + width - 1, tr) + window.addch(y + height - 1, x, bl) + window.addch(y + height - 1, x + width - 1, br) + for cx in range(x + 1, x + width - 1): + window.addch(y, cx, hline) + window.addch(y + height - 1, cx, hline) + for cy in range(y + 1, y + height - 1): + window.addch(cy, x, vline) + window.addch(cy, x + width - 1, vline) + except curses.error: + pass + + # build title with checkbox marker + try: + chk = "x" if self._checked else " " + title_body = f"[{chk}] {self._label}" if self._label else f"[{chk}]" + max_title_len = max(0, width - 4) + if len(title_body) > max_title_len: + title_body = title_body[:max(0, max_title_len - 3)] + "..." + start_x = x + max(1, (width - len(title_body)) // 2) + window.addstr(y, start_x, title_body, curses.A_BOLD) + except curses.error: + pass + + # inner rect + inner_x = x + 1 + inner_y = y + 1 + inner_w = max(0, width - 2) + inner_h = max(0, height - 2) + + pad_top = min(self._inner_top_padding, max(0, inner_h)) + content_y = inner_y + pad_top + content_h = max(0, inner_h - pad_top) + + child = getattr(self, "_child", None) + if child is None: + return + + needed = _curses_recursive_min_height(child) + # ensure we give at least needed rows if available + content_h = min(max(content_h, needed), inner_h) + + if content_h <= 0 or inner_w <= 0: + return + + if hasattr(child, "_draw"): + child._draw(window, content_y, inner_x, inner_w, content_h) + except Exception: + pass diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 68b754c..30107ad 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -202,5 +202,6 @@ def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameCurses(parent, label) - - + def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): + """Create a CheckBox Frame widget.""" + return YCheckBoxFrameCurses(parent, label, checked) From cf50a93faab9b8c298026e842fd7392c3f0b2b16 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 19:11:45 +0100 Subject: [PATCH 124/523] Label checkbox is now selectable --- .../backends/curses/checkboxframecurses.py | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/manatools/aui/backends/curses/checkboxframecurses.py b/manatools/aui/backends/curses/checkboxframecurses.py index d96e1a1..6c7a0cf 100644 --- a/manatools/aui/backends/curses/checkboxframecurses.py +++ b/manatools/aui/backends/curses/checkboxframecurses.py @@ -33,6 +33,12 @@ def __init__(self, parent=None, label: str = "", checked: bool = False): # minimal height (will be computed from child) self._height = 3 self._inner_top_padding = 1 + # allow focusing the frame title/checkbox and track focus state + try: + self._can_focus = True + except Exception: + pass + self._focused = False def widgetClass(self): return "YCheckBoxFrame" @@ -125,6 +131,11 @@ def _create_backend_widget(self): # no persistent backend object for curses self._backend_widget = None self._update_min_height() + # ensure children enablement matches checkbox initial state + try: + self._apply_children_enablement(self._checked) + except Exception: + pass def _apply_children_enablement(self, isChecked: bool): try: @@ -160,7 +171,24 @@ def _apply_children_enablement(self, isChecked: bool): def _set_backend_enabled(self, enabled: bool): try: # logical propagation - self._apply_children_enablement(self._checked if self._auto_enable else enabled) + # If auto_enable is enabled, child's enabled state follows checkbox. + # Otherwise follow explicit enabled flag. + if self._auto_enable: + self._apply_children_enablement(self._checked) + else: + # propagate explicit enabled/disabled to logical children + child = getattr(self, "_child", None) + if child is None: + for c in (getattr(self, "_children", None) or []): + try: + c.setEnabled(enabled) + except Exception: + pass + else: + try: + child.setEnabled(enabled) + except Exception: + pass except Exception: pass @@ -181,6 +209,10 @@ def addChild(self, child): except Exception: pass self._update_min_height() + try: + self._apply_children_enablement(self._checked) + except Exception: + pass def setChild(self, child): try: @@ -196,6 +228,10 @@ def setChild(self, child): except Exception: pass self._update_min_height() + try: + self._apply_children_enablement(self._checked) + except Exception: + pass def _on_toggle_request(self): """Toggle value (helper for key handling if needed).""" @@ -204,6 +240,27 @@ def _on_toggle_request(self): except Exception: pass + def _handle_key(self, key): + """Handle keyboard toggling when this frame is focused.""" + try: + if key in (ord(' '), 10, 13, curses.KEY_ENTER): + # toggle checkbox + try: + self.setValue(not self._checked) + except Exception: + pass + return True + except Exception: + pass + return False + + def _set_focus(self, focused: bool): + """Called by container when focus moves; track focus for drawing.""" + try: + self._focused = bool(focused) + except Exception: + self._focused = False + def _draw(self, window, y, x, width, height): """ Draw framed box with checkbox marker in the label, then delegate to child. @@ -219,7 +276,20 @@ def _draw(self, window, y, x, width, height): chk = "x" if self._checked else " " title = f"[{chk}] {self._label}" if self._label else f"[{chk}]" title = title[:max(0, width)] - window.addstr(y, x, title, curses.A_BOLD) + # highlight when focused, dim when disabled + attr = curses.A_BOLD + try: + if getattr(self, "isEnabled", None): + enabled = bool(self.isEnabled()) + else: + enabled = True + except Exception: + enabled = True + if not enabled: + attr |= curses.A_DIM + if getattr(self, "_focused", False): + attr |= curses.A_REVERSE + window.addstr(y, x, title, attr) except curses.error: pass return @@ -263,7 +333,20 @@ def _draw(self, window, y, x, width, height): if len(title_body) > max_title_len: title_body = title_body[:max(0, max_title_len - 3)] + "..." start_x = x + max(1, (width - len(title_body)) // 2) - window.addstr(y, start_x, title_body, curses.A_BOLD) + # choose attributes depending on focus/enable state + attr = curses.A_BOLD + try: + if getattr(self, "isEnabled", None): + enabled = bool(self.isEnabled()) + else: + enabled = True + except Exception: + enabled = True + if not enabled: + attr |= curses.A_DIM + if getattr(self, "_focused", False): + attr |= curses.A_REVERSE + window.addstr(y, start_x, title_body, attr) except curses.error: pass From b367fac5d07fd64384f65aaa9749fcba2e48a726 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 22:55:20 +0100 Subject: [PATCH 125/523] Honor enabling settings --- manatools/aui/backends/qt/pushbuttonqt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index 5d1e3be..cc4f25c 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -52,6 +52,7 @@ def _create_backend_widget(self): pass except Exception: pass + self._backend_widget.setEnabled(bool(self._enabled)) self._backend_widget.clicked.connect(self._on_clicked) def _set_backend_enabled(self, enabled): From 81ba1b09eca8cbd7dd55145756fe60ef8e68976f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 8 Dec 2025 22:57:23 +0100 Subject: [PATCH 126/523] fixed disable on startup --- manatools/aui/backends/gtk/pushbuttongtk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index de59b2a..bb7c4cb 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -50,6 +50,7 @@ def _create_backend_widget(self): except Exception: pass try: + self._backend_widget.set_sensitive(self._enabled) self._backend_widget.connect("clicked", self._on_clicked) except Exception: pass From cb111360a4132318638d336b8281f135291aa85a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 9 Dec 2025 18:42:21 +0100 Subject: [PATCH 127/523] Removed _child and managed YSingleChildContainerWidget with _children[0] --- .../aui/backends/curses/alignmentcurses.py | 69 ++++--------------- .../backends/curses/checkboxframecurses.py | 66 +++--------------- manatools/aui/backends/curses/commoncurses.py | 6 +- manatools/aui/backends/curses/dialogcurses.py | 27 +++----- manatools/aui/backends/curses/framecurses.py | 47 ++----------- manatools/aui/yui_common.py | 17 +++-- 6 files changed, 51 insertions(+), 181 deletions(-) diff --git a/manatools/aui/backends/curses/alignmentcurses.py b/manatools/aui/backends/curses/alignmentcurses.py index 937b947..58a77e2 100644 --- a/manatools/aui/backends/curses/alignmentcurses.py +++ b/manatools/aui/backends/curses/alignmentcurses.py @@ -38,68 +38,26 @@ def stretchable(self, dim: YUIDimension): * this dimension or if the child widget has a layout weight in * this dimension. ''' - if self._child: - expand = bool(self._child.stretchable(dim)) - weight = bool(self._child.weight(dim)) + if self.hasChildren(): + # get the only child + child = self.child() + expand = bool(child.stretchable(dim)) + weight = bool(child.weight(dim)) if expand or weight: return True return False - def addChild(self, child): - try: - super().addChild(child) - except Exception: - self._child = child - # Ensure child is visible to traversal (dialog looks at widget._children) - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - if child not in self._children: - self._children.append(child) - # keep parent pointer consistent - try: - setattr(child, "_parent", self) - except Exception: - pass - except Exception: - pass - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - # Mirror to _children so focus traversal finds it - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - # replace existing children with this single child to avoid stale entries - if self._children != [child]: - self._children = [child] - try: - setattr(child, "_parent", self) - except Exception: - pass - except Exception: - pass - def _create_backend_widget(self): - self._backend_widget = None - self._height = max(1, getattr(self._child, "_height", 1) if self._child else 1) + self._backend_widget = None # no real widget for curses + self._height = max(1, getattr(self.child(), "_height", 1) if self.hasChildren() else 1) def _set_backend_enabled(self, enabled): """Enable/disable alignment container and propagate to its logical child.""" try: # propagate to logical child so it updates its own focusability/state - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - if child is not None and hasattr(child, "setEnabled"): - try: - child.setEnabled(enabled) - except Exception: - pass + child = self.child() + if child is not None: + child.setEnabled(enabled) # nothing else to do for curses backend (no real widget object) except Exception: pass @@ -119,11 +77,12 @@ def _child_min_width(self, child, max_width): return max(1, min(10, max_width)) def _draw(self, window, y, x, width, height): - if not self._child or not hasattr(self._child, "_draw"): + + if not self.hasChildren() or not hasattr(self.child(), "_draw"): return try: # width to give to the child: minimal needed (so it can be pushed) - ch_min_w = self._child_min_width(self._child, width) + ch_min_w = self._child_min_width(self.child(), width) # Horizontal position if self._halign_spec == YAlignmentType.YAlignEnd: cx = x + max(0, width - ch_min_w) @@ -138,6 +97,6 @@ def _draw(self, window, y, x, width, height): cy = y + max(0, height - 1) else: cy = y - self._child._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, getattr(self._child, "_height", 1))) + self.child()._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, getattr(self.child(), "_height", 1))) except Exception: pass diff --git a/manatools/aui/backends/curses/checkboxframecurses.py b/manatools/aui/backends/curses/checkboxframecurses.py index 6c7a0cf..bbe2e6b 100644 --- a/manatools/aui/backends/curses/checkboxframecurses.py +++ b/manatools/aui/backends/curses/checkboxframecurses.py @@ -97,11 +97,8 @@ def setInvertAutoEnable(self, invert: bool): def stretchable(self, dim): """Frame is stretchable if its child is.""" - try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + try: + child = self.child() if child is None: return False try: @@ -121,7 +118,7 @@ def stretchable(self, dim): def _update_min_height(self): """Recompute minimal height: borders + padding + child's minimal layout.""" try: - child = getattr(self, "_child", None) + child = self.child() inner_min = _curses_recursive_min_height(child) if child is not None else 1 self._height = max(3, 2 + self._inner_top_padding + inner_min) except Exception: @@ -144,13 +141,8 @@ def _apply_children_enablement(self, isChecked: bool): state = bool(isChecked) if self._invert_auto: state = not state - child = getattr(self, "_child", None) + child = self.child() if child is None: - for c in (getattr(self, "_children", None) or []): - try: - c.setEnabled(state) - except Exception: - pass return try: child.setEnabled(state) @@ -177,56 +169,14 @@ def _set_backend_enabled(self, enabled: bool): self._apply_children_enablement(self._checked) else: # propagate explicit enabled/disabled to logical children - child = getattr(self, "_child", None) - if child is None: - for c in (getattr(self, "_children", None) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - else: - try: - child.setEnabled(enabled) - except Exception: - pass + child = self.child() + if child is not None: + child.setEnabled(enabled) except Exception: pass def addChild(self, child): - try: - super().addChild(child) - except Exception: - self._child = child - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - if child not in self._children: - self._children.append(child) - try: - child._parent = self - except Exception: - pass - except Exception: - pass - self._update_min_height() - try: - self._apply_children_enablement(self._checked) - except Exception: - pass - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - try: - self._children = [child] - try: - child._parent = self - except Exception: - pass - except Exception: - pass + super().addChild(child) self._update_min_height() try: self._apply_children_enablement(self._checked) diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py index f93cff3..14a3b2b 100644 --- a/manatools/aui/backends/curses/commoncurses.py +++ b/manatools/aui/backends/curses/commoncurses.py @@ -42,10 +42,10 @@ def _curses_recursive_min_height(widget): tallest = max(tallest, _curses_recursive_min_height(c)) return max(1, tallest) elif cls == "YAlignment": - child = getattr(widget, "_child", None) + child = widget.child() return max(1, _curses_recursive_min_height(child)) - elif cls == "YFrame": - child = getattr(widget, "_child", None) + elif cls == "YFrame" or cls == "YCheckBoxFrame": + child = widget.child() inner_top = max(0, getattr(widget, "_inner_top_padding", 1)) inner_min = _curses_recursive_min_height(child) return max(3, 2 + inner_top + inner_min) # borders(2) + padding + inner diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index d2aff98..5262074 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -106,16 +106,10 @@ def _set_backend_enabled(self, enabled): try: # propagate logical enabled state to entire subtree using setEnabled on children # so each widget's hook executes and updates its state. - if getattr(self, "_child", None): - try: - self._child.setEnabled(enabled) - except Exception: - pass - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass + child = self.child() + if child is not None: + child.setEnabled(enabled) + # If disabling and dialog had focused widget, clear focus if not enabled: try: @@ -181,7 +175,7 @@ def _draw_dialog(self): content_x = 2 # Draw child content - if self._child: + if self.hasChildren(): self._draw_child_content(content_y, content_x, content_width, content_height) # Draw footer with instructions @@ -208,12 +202,13 @@ def _draw_dialog(self): def _draw_child_content(self, start_y, start_x, max_width, max_height): """Draw the child widget content respecting container hierarchy""" - if not self._child: + if not self.hasChildren(): return # Draw only the root child - it will handle drawing its own children - if hasattr(self._child, '_draw'): - self._child._draw(self._backend_widget, start_y, start_x, max_width, max_height) + child = self.child() + if hasattr(child, '_draw'): + child._draw(self._backend_widget, start_y, start_x, max_width, max_height) def _cycle_focus(self, forward=True): @@ -262,8 +257,8 @@ def find_in_widget(widget): for child in widget._children: find_in_widget(child) - if self._child: - find_in_widget(self._child) + if self.hasChildren(): + find_in_widget(self.child()) return focusable diff --git a/manatools/aui/backends/curses/framecurses.py b/manatools/aui/backends/curses/framecurses.py index 9da0dd4..c68d1b2 100644 --- a/manatools/aui/backends/curses/framecurses.py +++ b/manatools/aui/backends/curses/framecurses.py @@ -41,7 +41,7 @@ def widgetClass(self): def _update_min_height(self): """Recompute minimal height: at least 3 rows or child layout min + borders + padding.""" try: - child = getattr(self, "_child", None) + child = self.child() inner_min = _curses_recursive_min_height(child) if child is not None else 1 self._height = max(3, 2 + self._inner_top_padding + inner_min) except Exception: @@ -59,10 +59,7 @@ def setLabel(self, new_label): def stretchable(self, dim): """Frame is stretchable if its child is stretchable or has a weight.""" try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is None: return False try: @@ -89,10 +86,7 @@ def _create_backend_widget(self): def _set_backend_enabled(self, enabled): """Propagate enabled state to the child.""" try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is not None and hasattr(child, "setEnabled"): try: child.setEnabled(enabled) @@ -102,38 +96,7 @@ def _set_backend_enabled(self, enabled): pass def addChild(self, child): - try: - super().addChild(child) - except Exception: - self._child = child - # ensure traversal lists contain the child - try: - if not hasattr(self, "_children") or self._children is None: - self._children = [] - if child not in self._children: - self._children.append(child) - try: - child._parent = self - except Exception: - pass - except Exception: - pass - # Update minimal height based on the child - self._update_min_height() - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - try: - self._children = [child] - try: - child._parent = self - except Exception: - pass - except Exception: - pass + super().addChild(child) # Update minimal height based on the child self._update_min_height() @@ -209,7 +172,7 @@ def _draw(self, window, y, x, width, height): content_y = inner_y + pad_top content_h = max(0, inner_h - pad_top) - child = getattr(self, "_child", None) + child = self.child() if child is None: return diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index ec556d0..6589252 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -206,8 +206,7 @@ def setEnabled(self, enabled=True): the change to the actual backend widget. ''' self._enabled = enabled - if self._backend_widget: - self._set_backend_enabled(enabled) + self._set_backend_enabled(enabled) def setDisabled(self): self.setEnabled(False) @@ -272,13 +271,17 @@ def get_backend_widget(self): class YSingleChildContainerWidget(YWidget): def __init__(self, parent=None): super().__init__(parent) - self._child = None + def child(self): + ''' + Returns the only child of this single-child container, or None if no child is set. + ''' + return self.firstChild() + def addChild(self, child): - if self._child is not None: - self.removeChild(self._child) - self._child = child - child._parent = self + if self.hasChildren(): + raise YUIInvalidWidgetException("YSingleChildContainerWidget can only have one child") + super().addChild(child) class YSelectionWidget(YWidget): def __init__(self, parent=None): From 98a4114fca70df83ea6472d32e27f920612f9024 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 9 Dec 2025 19:14:00 +0100 Subject: [PATCH 128/523] removed _child from YSingleChildContainerWidget implementation --- manatools/aui/backends/gtk/alignmentgtk.py | 40 ++-------- .../aui/backends/gtk/checkboxframegtk.py | 80 ++++--------------- manatools/aui/backends/gtk/dialoggtk.py | 7 +- manatools/aui/backends/gtk/framegtk.py | 53 ++---------- 4 files changed, 37 insertions(+), 143 deletions(-) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index ac18f9b..1f366c3 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -89,9 +89,9 @@ def stretchable(self, dim: YUIDimension): * this dimension or if the child widget has a layout weight in * this dimension. ''' - if self._child: - expand = bool(self._child.stretchable(dim)) - weight = bool(self._child.weight(dim)) + if self.child(): + expand = bool(self.child().stretchable(dim)) + weight = bool(self.child().weight(dim)) if expand or weight: return True return False @@ -140,19 +140,7 @@ def _on_draw(self, widget, cr): def addChild(self, child): """Keep base behavior and ensure we attempt to attach child's backend.""" - try: - super().addChild(child) - except Exception: - self._child = child - self._child_attached = False - self._schedule_attach_child() - - def setChild(self, child): - """Keep base behavior and ensure we attempt to attach child's backend.""" - try: - super().setChild(child) - except Exception: - self._child = child + super().addChild(child) self._child_attached = False self._schedule_attach_child() @@ -182,14 +170,8 @@ def _ensure_child_attached(self): self._create_backend_widget() return - # choose child reference (support _child or _children storage) - child = getattr(self, "_child", None) - if child is None: - try: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None - except Exception: - child = None + # choose child reference + child = self.child() if child is None: return @@ -348,14 +330,8 @@ def _set_backend_enabled(self, enabled): pass # propagate to logical child so child's backend updates too try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is not None: - try: - child.setEnabled(enabled) - except Exception: - pass + child.setEnabled(enabled) except Exception: pass diff --git a/manatools/aui/backends/gtk/checkboxframegtk.py b/manatools/aui/backends/gtk/checkboxframegtk.py index 24c5548..9a2a1ea 100644 --- a/manatools/aui/backends/gtk/checkboxframegtk.py +++ b/manatools/aui/backends/gtk/checkboxframegtk.py @@ -202,7 +202,7 @@ def _create_backend_widget(self): # attach existing child if any try: - if getattr(self, "_child", None): + if self.hasChildren(): self._attach_child_backend() except Exception: pass @@ -240,10 +240,7 @@ def _attach_child_backend(self): except Exception: pass - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is None: return @@ -307,58 +304,24 @@ def _apply_children_enablement(self, isChecked: bool): state = bool(isChecked) if self._invert_auto: state = not state - child = getattr(self, "_child", None) - if child is None: - for c in (getattr(self, "_children", None) or []): - try: - c.setEnabled(state) - except Exception: - pass - return - try: + child = self.child() + if child is not None: child.setEnabled(state) - except Exception: - try: - w = child.get_backend_widget() - if w is not None: - try: - w.set_sensitive(state) - except Exception: - pass - except Exception: - pass + #try: + # w = child.get_backend_widget() + # if w is not None: + # try: + # w.set_sensitive(state) + # except Exception: + # pass + #except Exception: + # pass except Exception: pass def addChild(self, child): - try: - super().addChild(child) - except Exception: - try: - self._child = child - child._parent = self - except Exception: - pass - try: - if getattr(self, "_backend_widget", None): - self._attach_child_backend() - except Exception: - pass - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - try: - self._child = child - child._parent = self - except Exception: - pass - try: - if getattr(self, "_backend_widget", None): - self._attach_child_backend() - except Exception: - pass + super().addChild(child) + self._attach_child_backend() def _set_backend_enabled(self, enabled: bool): try: @@ -371,18 +334,9 @@ def _set_backend_enabled(self, enabled: bool): pass # propagate to logical children try: - child = getattr(self, "_child", None) - if child is None: - for c in (getattr(self, "_children", None) or []): - try: - c.setEnabled(enabled) - except Exception: - pass - return - try: + child = self.child() + if child is not None: child.setEnabled(enabled) - except Exception: - pass except Exception: pass diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index 347bc68..10ec211 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -229,9 +229,10 @@ def _create_backend_widget(self): content.set_margin_end(10) content.set_margin_top(10) content.set_margin_bottom(10) - - if self._child: - child_widget = self._child.get_backend_widget() + + child = self.child() + if child: + child_widget = child.get_backend_widget() # ensure child is shown properly try: content.append(child_widget) diff --git a/manatools/aui/backends/gtk/framegtk.py b/manatools/aui/backends/gtk/framegtk.py index 948a5bd..6a14210 100644 --- a/manatools/aui/backends/gtk/framegtk.py +++ b/manatools/aui/backends/gtk/framegtk.py @@ -70,10 +70,7 @@ def stretchable(self, dim: YUIDimension): The frame is stretchable when its child is stretchable or has a layout weight. """ try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is None: return False try: @@ -97,10 +94,7 @@ def _attach_child_backend(self): return if self._content_box is None: return - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is None: return try: @@ -161,37 +155,9 @@ def _attach_child_backend(self): def addChild(self, child): """Add logical child and attach backend if possible.""" - try: - super().addChild(child) - except Exception: - # best-effort fallback - try: - self._child = child - child._parent = self - except Exception: - pass - # attach to backend if ready - try: - if getattr(self, "_backend_widget", None) is not None: - self._attach_child_backend() - except Exception: - pass - - def setChild(self, child): - """Set single logical child and attach backend if possible.""" - try: - super().setChild(child) - except Exception: - try: - self._child = child - child._parent = self - except Exception: - pass - try: - if getattr(self, "_backend_widget", None) is not None: - self._attach_child_backend() - except Exception: - pass + super().addChild(child) + # best-effort fallback + self._attach_child_backend() def _create_backend_widget(self): """ @@ -229,7 +195,7 @@ def _create_backend_widget(self): self._content_box = content # attach existing child if any try: - if getattr(self, "_child", None): + if self.hasChildren(): self._attach_child_backend() except Exception: pass @@ -252,7 +218,7 @@ def _create_backend_widget(self): self._label_widget = lbl self._backend_widget = container self._content_box = content - if getattr(self, "_child", None): + if self.hasChildren(): try: self._attach_child_backend() except Exception: @@ -277,10 +243,7 @@ def _set_backend_enabled(self, enabled): pass # propagate to logical child try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is not None: try: child.setEnabled(enabled) From aa79cd48f82edeb1f5745b160a4d8fd0f349f471 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 9 Dec 2025 19:17:17 +0100 Subject: [PATCH 129/523] removed last forgotten _child --- manatools/aui/backends/curses/checkboxframecurses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/curses/checkboxframecurses.py b/manatools/aui/backends/curses/checkboxframecurses.py index bbe2e6b..f5d73fb 100644 --- a/manatools/aui/backends/curses/checkboxframecurses.py +++ b/manatools/aui/backends/curses/checkboxframecurses.py @@ -310,7 +310,7 @@ def _draw(self, window, y, x, width, height): content_y = inner_y + pad_top content_h = max(0, inner_h - pad_top) - child = getattr(self, "_child", None) + child = self.child() if child is None: return From a9628f950d7c96060629fc3d86305b3787f6fe29 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 9 Dec 2025 19:29:20 +0100 Subject: [PATCH 130/523] removed setChild --- manatools/aui/backends/curses/hboxcurses.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py index 83b4ee8..ea653f2 100644 --- a/manatools/aui/backends/curses/hboxcurses.py +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -54,18 +54,6 @@ def addChild(self, child): pass self._recompute_min_height() - def setChild(self, child): - """Not typical for HBox, but keep parity with containers.""" - try: - super().setChild(child) - except Exception: - try: - self._children = [child] - child._parent = self - except Exception: - pass - self._recompute_min_height() - def _set_backend_enabled(self, enabled): """Enable/disable HBox and propagate to logical children.""" try: From f21345cfa4aab48dddbbcae60a3627c4cbcf1a81 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 9 Dec 2025 19:30:10 +0100 Subject: [PATCH 131/523] removed _child and used _children[0] --- manatools/aui/backends/qt/alignmentqt.py | 40 ++++------- manatools/aui/backends/qt/checkboxframeqt.py | 74 +++----------------- manatools/aui/backends/qt/dialogqt.py | 14 ++-- manatools/aui/backends/qt/frameqt.py | 48 +++---------- 4 files changed, 36 insertions(+), 140 deletions(-) diff --git a/manatools/aui/backends/qt/alignmentqt.py b/manatools/aui/backends/qt/alignmentqt.py index 3a25aef..10ddc51 100644 --- a/manatools/aui/backends/qt/alignmentqt.py +++ b/manatools/aui/backends/qt/alignmentqt.py @@ -70,9 +70,9 @@ def stretchable(self, dim: YUIDimension): # Otherwise honor child's own stretchability/weight try: - if self._child: - expand = bool(self._child.stretchable(dim)) - weight = bool(self._child.weight(dim)) + if self.child(): + expand = bool(self.child().stretchable(dim)) + weight = bool(self.child().weight(dim)) if expand or weight: return True except Exception: @@ -85,10 +85,10 @@ def setAlignment(self, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, self._reapply_alignment() def _reapply_alignment(self): - if not (self._layout and self._child): + if not (self._layout and self.child()): return try: - w = self._child.get_backend_widget() + w = self.child().get_backend_widget() if w: self._layout.removeWidget(w) flags = QtCore.Qt.AlignmentFlag(0) @@ -103,26 +103,15 @@ def _reapply_alignment(self): pass def addChild(self, child): - try: - super().addChild(child) - except Exception: - self._child = child - if self._backend_widget: - self._attach_child_backend() + super().addChild(child) + self._attach_child_backend() - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - if self._backend_widget: - self._attach_child_backend() def _attach_child_backend(self): - if not (self._backend_widget and self._layout and self._child): + if not (self._backend_widget and self._layout and self.child()): return try: - w = self._child.get_backend_widget() + w = self.child().get_backend_widget() if w: # clear previous try: @@ -138,7 +127,7 @@ def _attach_child_backend(self): flags |= va # If the child requests horizontal stretch, set its QSizePolicy to Expanding try: - if self._child and self._child.stretchable(YUIDimension.YD_HORIZ): + if self.child() and self.child().stretchable(YUIDimension.YD_HORIZ): sp = w.sizePolicy() try: sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) @@ -149,7 +138,7 @@ def _attach_child_backend(self): pass w.setSizePolicy(sp) # If child requests vertical stretch, set vertical policy - if self._child and self._child.stretchable(YUIDimension.YD_VERT): + if self.child() and self.child().stretchable(YUIDimension.YD_VERT): sp = w.sizePolicy() try: sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) @@ -185,7 +174,7 @@ def _create_backend_widget(self): self._backend_widget = container self._layout = grid - if getattr(self, "_child", None): + if self.hasChildren(): self._attach_child_backend() def _set_backend_enabled(self, enabled): @@ -200,10 +189,7 @@ def _set_backend_enabled(self, enabled): pass # propagate to logical child try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is not None: try: child.setEnabled(enabled) diff --git a/manatools/aui/backends/qt/checkboxframeqt.py b/manatools/aui/backends/qt/checkboxframeqt.py index d69272e..7e48187 100644 --- a/manatools/aui/backends/qt/checkboxframeqt.py +++ b/manatools/aui/backends/qt/checkboxframeqt.py @@ -136,7 +136,7 @@ def _create_backend_widget(self): # attach existing child if present try: - if getattr(self, "_child", None): + if self.hasChildren(): self._attach_child_backend() except Exception: pass @@ -148,7 +148,7 @@ def _create_backend_widget(self): def _attach_child_backend(self): """Attach child's backend widget into content area.""" - if not (self._backend_widget and self._content_layout and getattr(self, "_child", None)): + if not (self._backend_widget and self._content_layout and self.child()): return try: # clear content layout @@ -159,7 +159,7 @@ def _attach_child_backend(self): except Exception: pass try: - child = self._child + child = self.child() w = None try: w = child.get_backend_widget() @@ -211,62 +211,16 @@ def handleChildrenEnablement(self, isChecked: bool): state = not state # propagate to logical child(s) try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - for c in chs: - try: - c.setEnabled(state) - except Exception: - pass - return - try: - child.setEnabled(state) - except Exception: - # best-effort: if child has backend widget, set it sensitive - try: - w = child.get_backend_widget() - if w is not None: - try: - w.setEnabled(state) - except Exception: - pass - except Exception: - pass + child = self.child() + child.setEnabled(state) except Exception: pass except Exception: pass def addChild(self, child): - try: - super().addChild(child) - except Exception: - try: - self._child = child - child._parent = self - except Exception: - pass - try: - if getattr(self, "_backend_widget", None): - self._attach_child_backend() - except Exception: - pass - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - try: - self._child = child - child._parent = self - except Exception: - pass - try: - if getattr(self, "_backend_widget", None): - self._attach_child_backend() - except Exception: - pass + super().addChild(child) + self._attach_child_backend() def _set_backend_enabled(self, enabled: bool): try: @@ -279,19 +233,9 @@ def _set_backend_enabled(self, enabled: bool): pass # propagate to logical child(s) try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - for c in chs: - try: - c.setEnabled(enabled) - except Exception: - pass - return - try: + child = self.child() + if child is not None: child.setEnabled(enabled) - except Exception: - pass except Exception: pass diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index 0bc9803..dd636b5 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -158,9 +158,9 @@ def _create_backend_widget(self): central_widget = QtWidgets.QWidget() self._qwidget.setCentralWidget(central_widget) - if self._child: + if self.child(): layout = QtWidgets.QVBoxLayout(central_widget) - layout.addWidget(self._child.get_backend_widget()) + layout.addWidget(self.child().get_backend_widget()) self._backend_widget = self._qwidget self._qwidget.closeEvent = self._on_close_event @@ -177,17 +177,11 @@ def _set_backend_enabled(self, enabled): pass # propagate logical enabled state to contained YWidget(s) try: - if getattr(self, "_child", None): + if self.child(): try: - self._child.setEnabled(enabled) + self.child().setEnabled(enabled) except Exception: pass - else: - for c in list(getattr(self, "_children", []) or []): - try: - c.setEnabled(enabled) - except Exception: - pass except Exception: pass diff --git a/manatools/aui/backends/qt/frameqt.py b/manatools/aui/backends/qt/frameqt.py index e28d3e7..5ef67d8 100644 --- a/manatools/aui/backends/qt/frameqt.py +++ b/manatools/aui/backends/qt/frameqt.py @@ -27,10 +27,7 @@ def stretchable(self, dim: YUIDimension): """ try: # prefer explicit single child - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is None: return False try: @@ -64,10 +61,10 @@ def setLabel(self, newLabel): def _attach_child_backend(self): """Attach existing child backend widget to the groupbox layout.""" - if not (self._backend_widget and self._group_layout and getattr(self, "_child", None)): + if not (self._backend_widget and self._group_layout and self.child()): return try: - w = self._child.get_backend_widget() + w = self.child().get_backend_widget() if w: # clear any existing widgets in layout (defensive) try: @@ -83,30 +80,8 @@ def _attach_child_backend(self): def addChild(self, child): """Override to attach backend child when available.""" - try: - super().addChild(child) - except Exception: - # best-effort fallback - self._child = child - child._parent = self - # if backend exists, attach new child's backend - if getattr(self, "_backend_widget", None): - try: - self._attach_child_backend() - except Exception: - pass - - def setChild(self, child): - try: - super().setChild(child) - except Exception: - self._child = child - child._parent = self - if getattr(self, "_backend_widget", None): - try: - self._attach_child_backend() - except Exception: - pass + super().addChild(child) + self._attach_child_backend() def _create_backend_widget(self): """Create the QGroupBox + layout and attach child if present.""" @@ -119,9 +94,9 @@ def _create_backend_widget(self): self._group_layout = layout # attach child widget if already set - if getattr(self, "_child", None): + if self.child(): try: - w = self._child.get_backend_widget() + w = self.child().get_backend_widget() if w: layout.addWidget(w) except Exception: @@ -135,9 +110,9 @@ def _create_backend_widget(self): layout.setSpacing(4) self._backend_widget = container self._group_layout = layout - if getattr(self, "_child", None): + if self.child(): try: - w = self._child.get_backend_widget() + w = self.child().get_backend_widget() if w: layout.addWidget(w) except Exception: @@ -158,10 +133,7 @@ def _set_backend_enabled(self, enabled): pass # propagate to logical child try: - child = getattr(self, "_child", None) - if child is None: - chs = getattr(self, "_children", None) or [] - child = chs[0] if chs else None + child = self.child() if child is not None: try: child.setEnabled(enabled) From f112033f26bb327c101e65a4cdcf6e068ad4b26c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 11 Dec 2025 20:04:31 +0100 Subject: [PATCH 132/523] Enable/Disable at creation according to widget property --- manatools/aui/backends/qt/alignmentqt.py | 1 + manatools/aui/backends/qt/checkboxframeqt.py | 1 + manatools/aui/backends/qt/checkboxqt.py | 1 + manatools/aui/backends/qt/comboboxqt.py | 1 + manatools/aui/backends/qt/dialogqt.py | 1 + manatools/aui/backends/qt/frameqt.py | 1 + manatools/aui/backends/qt/hboxqt.py | 1 + manatools/aui/backends/qt/inputfieldqt.py | 1 + manatools/aui/backends/qt/labelqt.py | 1 + manatools/aui/backends/qt/selectionboxqt.py | 1 + manatools/aui/backends/qt/treeqt.py | 2 +- manatools/aui/backends/qt/vboxqt.py | 3 +-- 12 files changed, 12 insertions(+), 3 deletions(-) diff --git a/manatools/aui/backends/qt/alignmentqt.py b/manatools/aui/backends/qt/alignmentqt.py index 10ddc51..3167981 100644 --- a/manatools/aui/backends/qt/alignmentqt.py +++ b/manatools/aui/backends/qt/alignmentqt.py @@ -173,6 +173,7 @@ def _create_backend_widget(self): self._backend_widget = container self._layout = grid + self._backend_widget.setEnabled(bool(self._enabled)) if self.hasChildren(): self._attach_child_backend() diff --git a/manatools/aui/backends/qt/checkboxframeqt.py b/manatools/aui/backends/qt/checkboxframeqt.py index 7e48187..b419e9b 100644 --- a/manatools/aui/backends/qt/checkboxframeqt.py +++ b/manatools/aui/backends/qt/checkboxframeqt.py @@ -126,6 +126,7 @@ def _create_backend_widget(self): self._checkbox = grp self._content_widget = content self._content_layout = content_layout + self._backend_widget.setEnabled(bool(self._enabled)) # connect group toggled signal try: diff --git a/manatools/aui/backends/qt/checkboxqt.py b/manatools/aui/backends/qt/checkboxqt.py index 6a0dd85..7769119 100644 --- a/manatools/aui/backends/qt/checkboxqt.py +++ b/manatools/aui/backends/qt/checkboxqt.py @@ -44,6 +44,7 @@ def _create_backend_widget(self): self._backend_widget = QtWidgets.QCheckBox(self._label) self._backend_widget.setChecked(self._is_checked) self._backend_widget.stateChanged.connect(self._on_state_changed) + self._backend_widget.setEnabled(bool(self._enabled)) def _set_backend_enabled(self, enabled): """Enable/disable the QCheckBox backend.""" diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index 3cdd8e6..7bc0df6 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -70,6 +70,7 @@ def _create_backend_widget(self): self._backend_widget = container self._combo_widget = combo + self._backend_widget.setEnabled(bool(self._enabled)) def _set_backend_enabled(self, enabled): """Enable/disable the combobox and its container.""" diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index dd636b5..9064fe4 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -164,6 +164,7 @@ def _create_backend_widget(self): self._backend_widget = self._qwidget self._qwidget.closeEvent = self._on_close_event + self._backend_widget.setEnabled(bool(self._enabled)) def _set_backend_enabled(self, enabled): """Enable/disable the dialog window and propagate to logical child widgets.""" diff --git a/manatools/aui/backends/qt/frameqt.py b/manatools/aui/backends/qt/frameqt.py index 5ef67d8..90d230f 100644 --- a/manatools/aui/backends/qt/frameqt.py +++ b/manatools/aui/backends/qt/frameqt.py @@ -92,6 +92,7 @@ def _create_backend_widget(self): layout.setSpacing(4) self._backend_widget = grp self._group_layout = layout + self._backend_widget.setEnabled(bool(self._enabled)) # attach child widget if already set if self.child(): diff --git a/manatools/aui/backends/qt/hboxqt.py b/manatools/aui/backends/qt/hboxqt.py index ecee483..52e8bcc 100644 --- a/manatools/aui/backends/qt/hboxqt.py +++ b/manatools/aui/backends/qt/hboxqt.py @@ -57,6 +57,7 @@ def _create_backend_widget(self): widget.setSizePolicy(sp) except Exception: pass + self._backend_widget.setEnabled(bool(self._enabled)) print( f"YHBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) diff --git a/manatools/aui/backends/qt/inputfieldqt.py b/manatools/aui/backends/qt/inputfieldqt.py index d583025..62abb61 100644 --- a/manatools/aui/backends/qt/inputfieldqt.py +++ b/manatools/aui/backends/qt/inputfieldqt.py @@ -54,6 +54,7 @@ def _create_backend_widget(self): self._backend_widget = container self._entry_widget = entry + self._backend_widget.setEnabled(bool(self._enabled)) def _set_backend_enabled(self, enabled): """Enable/disable the input field: entry and container.""" diff --git a/manatools/aui/backends/qt/labelqt.py b/manatools/aui/backends/qt/labelqt.py index bc39818..0538171 100644 --- a/manatools/aui/backends/qt/labelqt.py +++ b/manatools/aui/backends/qt/labelqt.py @@ -37,6 +37,7 @@ def _create_backend_widget(self): font.setBold(True) font.setPointSize(font.pointSize() + 2) self._backend_widget.setFont(font) + self._backend_widget.setEnabled(bool(self._enabled)) def _set_backend_enabled(self, enabled): """Enable/disable the QLabel backend.""" diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index c6af7d9..d678caf 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -107,6 +107,7 @@ def _create_backend_widget(self): layout.addWidget(list_widget) self._backend_widget = container + self._backend_widget.setEnabled(bool(self._enabled)) self._list_widget = list_widget def _set_backend_enabled(self, enabled): diff --git a/manatools/aui/backends/qt/treeqt.py b/manatools/aui/backends/qt/treeqt.py index 843a827..ecb10a0 100644 --- a/manatools/aui/backends/qt/treeqt.py +++ b/manatools/aui/backends/qt/treeqt.py @@ -62,7 +62,7 @@ def _create_backend_widget(self): layout.addWidget(tree) self._backend_widget = container self._tree_widget = tree - + self._backend_widget.setEnabled(bool(self._enabled)) # populate if items already present try: self.rebuildTree() diff --git a/manatools/aui/backends/qt/vboxqt.py b/manatools/aui/backends/qt/vboxqt.py index 4d8ab15..ff84b6c 100644 --- a/manatools/aui/backends/qt/vboxqt.py +++ b/manatools/aui/backends/qt/vboxqt.py @@ -58,8 +58,7 @@ def _create_backend_widget(self): except Exception: pass - - + self._backend_widget.setEnabled(bool(self._enabled)) print( f"YVBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug layout.addWidget(widget, stretch=expand) From 4428b124fb73790babac43a6e84344ea1878fce2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 11 Dec 2025 20:05:53 +0100 Subject: [PATCH 133/523] Parent disabled force disabling children --- manatools/aui/yui_common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 6589252..a0ef04e 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -180,6 +180,8 @@ def addChild(self, child): if child not in self._children: self._children.append(child) child._parent = self + if self.isEnabled() is False: + child._enabled = False def removeChild(self, child): if child in self._children: From 0de4c9ec4c9b8b10e1f2362bdbe2b9949b0b2b96 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 11 Dec 2025 20:59:25 +0100 Subject: [PATCH 134/523] Managed disable widget at creation --- manatools/aui/backends/gtk/alignmentgtk.py | 1 + manatools/aui/backends/gtk/checkboxframegtk.py | 4 +++- manatools/aui/backends/gtk/checkboxgtk.py | 1 + manatools/aui/backends/gtk/comboboxgtk.py | 1 + manatools/aui/backends/gtk/dialoggtk.py | 1 + manatools/aui/backends/gtk/framegtk.py | 1 + manatools/aui/backends/gtk/hboxgtk.py | 1 + manatools/aui/backends/gtk/inputfieldgtk.py | 1 + manatools/aui/backends/gtk/labelgtk.py | 1 + manatools/aui/backends/gtk/selectionboxgtk.py | 1 + manatools/aui/backends/gtk/treegtk.py | 1 + manatools/aui/backends/gtk/vboxgtk.py | 3 ++- 12 files changed, 15 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index 1f366c3..dbb7fac 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -277,6 +277,7 @@ def _create_backend_widget(self): box = Gtk.Box() self._backend_widget = box + self._backend_widget.set_sensitive(self._enabled) # Connect draw handler if we have a background pixbuf if self._background_pixbuf and not self._signal_id: diff --git a/manatools/aui/backends/gtk/checkboxframegtk.py b/manatools/aui/backends/gtk/checkboxframegtk.py index 9a2a1ea..4acd071 100644 --- a/manatools/aui/backends/gtk/checkboxframegtk.py +++ b/manatools/aui/backends/gtk/checkboxframegtk.py @@ -178,6 +178,7 @@ def _create_backend_widget(self): self._checkbox = check self._content_box = content self._label_widget = None + self._backend_widget.set_sensitive(self._enabled) # Ensure a little top margin between title and content try: @@ -279,7 +280,8 @@ def _attach_child_backend(self): pass # apply enablement state - self._apply_children_enablement(self.value()) + if self.isEnabled(): + self._apply_children_enablement(self.value()) def _on_toggled(self, widget): try: diff --git a/manatools/aui/backends/gtk/checkboxgtk.py b/manatools/aui/backends/gtk/checkboxgtk.py index 4124014..42e2c82 100644 --- a/manatools/aui/backends/gtk/checkboxgtk.py +++ b/manatools/aui/backends/gtk/checkboxgtk.py @@ -50,6 +50,7 @@ def _create_backend_widget(self): self._backend_widget.connect("toggled", self._on_toggled) except Exception: pass + self._backend_widget.set_sensitive(self._enabled) def _on_toggled(self, button): try: diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index 52a1444..ee1590a 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -117,6 +117,7 @@ def _create_backend_widget(self): hbox.append(entry) self._backend_widget = hbox + self._backend_widget.set_sensitive(self._enabled) def _set_backend_enabled(self, enabled): """Enable/disable the combobox/backing widget and its entry/dropdown.""" diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index 10ec211..84de8cb 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -252,6 +252,7 @@ def _create_backend_widget(self): pass self._backend_widget = self._window + self._backend_widget.set_sensitive(self._enabled) # Connect destroy/close handlers try: # Gtk4: use 'close-request' if available, otherwise 'destroy' diff --git a/manatools/aui/backends/gtk/framegtk.py b/manatools/aui/backends/gtk/framegtk.py index 6a14210..4c0452f 100644 --- a/manatools/aui/backends/gtk/framegtk.py +++ b/manatools/aui/backends/gtk/framegtk.py @@ -193,6 +193,7 @@ def _create_backend_widget(self): pass self._backend_widget = frame self._content_box = content + self._backend_widget.set_sensitive(self._enabled) # attach existing child if any try: if self.hasChildren(): diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py index 82012de..f77215e 100644 --- a/manatools/aui/backends/gtk/hboxgtk.py +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -65,6 +65,7 @@ def _create_backend_widget(self): self._backend_widget.add(widget) except Exception: pass + self._backend_widget.set_sensitive(self._enabled) def _set_backend_enabled(self, enabled): """Enable/disable the HBox and propagate to children.""" diff --git a/manatools/aui/backends/gtk/inputfieldgtk.py b/manatools/aui/backends/gtk/inputfieldgtk.py index e4b555c..3d7ee17 100644 --- a/manatools/aui/backends/gtk/inputfieldgtk.py +++ b/manatools/aui/backends/gtk/inputfieldgtk.py @@ -81,6 +81,7 @@ def _create_backend_widget(self): self._backend_widget = hbox self._entry_widget = entry + self._backend_widget.set_sensitive(self._enabled) def _on_changed(self, entry): try: diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py index d06bc42..e07df72 100644 --- a/manatools/aui/backends/gtk/labelgtk.py +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -56,6 +56,7 @@ def _create_backend_widget(self): self._backend_widget.set_markup(markup) except Exception: pass + self._backend_widget.set_sensitive(self._enabled) def _set_backend_enabled(self, enabled): """Enable/disable the label widget backend.""" diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index f647919..05a2997 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -266,6 +266,7 @@ def _create_backend_widget(self): self._backend_widget = vbox self._listbox = listbox + self._backend_widget.set_sensitive(self._enabled) def _set_backend_enabled(self, enabled): """Enable/disable the selection box and its listbox/rows.""" diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 0d17444..9af6027 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -101,6 +101,7 @@ def _create_backend_widget(self): self._backend_widget = vbox self._listbox = listbox + self._backend_widget.set_sensitive(self._enabled) try: vbox.append(sw) diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index d870fea..2356f92 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -65,7 +65,8 @@ def _create_backend_widget(self): widget.set_halign(Gtk.Align.START) except Exception: pass - + + self._backend_widget.set_sensitive(self._enabled) # Gtk4: use append instead of pack_start try: self._backend_widget.append(widget) From cf185196388c8f7b4440aaaa4373112019b8b671 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 11 Dec 2025 21:21:56 +0100 Subject: [PATCH 135/523] Managing disable at widget creation --- manatools/aui/backends/curses/checkboxframecurses.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/checkboxframecurses.py b/manatools/aui/backends/curses/checkboxframecurses.py index f5d73fb..ea7cafe 100644 --- a/manatools/aui/backends/curses/checkboxframecurses.py +++ b/manatools/aui/backends/curses/checkboxframecurses.py @@ -130,7 +130,8 @@ def _create_backend_widget(self): self._update_min_height() # ensure children enablement matches checkbox initial state try: - self._apply_children_enablement(self._checked) + if self.isEnabled(): + self._apply_children_enablement(self._checked) except Exception: pass @@ -138,7 +139,7 @@ def _apply_children_enablement(self, isChecked: bool): try: if not self._auto_enable: return - state = bool(isChecked) + state = bool(isChecked) if self.isEnabled() else False if self._invert_auto: state = not state child = self.child() From 27fdd62e02dcae0d3766e67e8b65fa1e4fa5ab18 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 11:58:40 +0100 Subject: [PATCH 136/523] fix vbox enable --- manatools/aui/backends/gtk/vboxgtk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index 2356f92..753db07 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -66,7 +66,6 @@ def _create_backend_widget(self): except Exception: pass - self._backend_widget.set_sensitive(self._enabled) # Gtk4: use append instead of pack_start try: self._backend_widget.append(widget) @@ -75,6 +74,8 @@ def _create_backend_widget(self): self._backend_widget.add(widget) except Exception: pass + self._backend_widget.set_sensitive(self._enabled) + def _set_backend_enabled(self, enabled): """Enable/disable the VBox and propagate to children.""" From 5fd89b69e87a4767b207f3794a6cdc973bdbec7a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 17:40:18 +0100 Subject: [PATCH 137/523] Fixed alignment --- manatools/aui/backends/gtk/alignmentgtk.py | 175 +++++++++++---------- manatools/aui/backends/gtk/hboxgtk.py | 17 +- manatools/aui/backends/gtk/vboxgtk.py | 26 +-- 3 files changed, 97 insertions(+), 121 deletions(-) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index dbb7fac..312d8cf 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -24,8 +24,9 @@ class YAlignmentGtk(YSingleChildContainerWidget): """ GTK4 implementation of YAlignment. - - Uses a Gtk.Box as a lightweight container that requests expansion when - needed so child halign/valign can take effect (matches the small GTK sample). + - Uses a Gtk.Grid as a lightweight container that expands to provide space + so child halign/valign can take effect. Gtk.Grid honors child halign/valign + within its allocation, which matches YAlignment.h semantics. - Applies halign/valign hints to the child's backend widget. - Defers attaching the child if its backend is not yet created (GLib.idle_add). - Supports an optional repeating background pixbuf painted in the draw signal. @@ -37,6 +38,8 @@ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUn self._background_pixbuf = None self._signal_id = None self._backend_widget = None + # get a reference to the single row container + self._row = [] # schedule guard for deferred attach self._attach_scheduled = False # Track if we've already attached a child @@ -46,7 +49,7 @@ def widgetClass(self): return "YAlignment" def _to_gtk_halign(self): - """Convert Horizontal YAlignmentType to Gtk.Align or None.""" + """Convert Horizontal YAlignmentType to Gtk.Align or Gtk.Align.CENTER.""" if self._halign_spec: if self._halign_spec == YAlignmentType.YAlignBegin: return Gtk.Align.START @@ -54,10 +57,11 @@ def _to_gtk_halign(self): return Gtk.Align.CENTER if self._halign_spec == YAlignmentType.YAlignEnd: return Gtk.Align.END - return None + #default + return Gtk.Align.CENTER def _to_gtk_valign(self): - """Convert Vertical YAlignmentType to Gtk.Align or None.""" + """Convert Vertical YAlignmentType to Gtk.Align or Gtk.Align.CENTER.""" if self._valign_spec: if self._valign_spec == YAlignmentType.YAlignBegin: return Gtk.Align.START @@ -65,7 +69,8 @@ def _to_gtk_valign(self): return Gtk.Align.CENTER if self._valign_spec == YAlignmentType.YAlignEnd: return Gtk.Align.END - return None + #default + return Gtk.Align.CENTER #def stretchable(self, dim): # """Report whether this alignment should expand in given dimension. @@ -84,16 +89,27 @@ def _to_gtk_valign(self): # return False def stretchable(self, dim: YUIDimension): - ''' Returns the stretchability of the layout box: - * The layout box is stretchable if the child is stretchable in - * this dimension or if the child widget has a layout weight in - * this dimension. - ''' - if self.child(): - expand = bool(self.child().stretchable(dim)) - weight = bool(self.child().weight(dim)) - if expand or weight: - return True + """Stretchability semantics consistent with YAlignment.h: + - In the aligned dimension (Begin/Center/End), the alignment container is stretchable + so there is space for alignment to take effect. + - In the unchanged dimension, reflect the child's stretchability or layout weight. + """ + try: + if dim == YUIDimension.YD_HORIZ: + if self._halign_spec is not None and self._halign_spec != YAlignmentType.YAlignUnchanged: + return True + if dim == YUIDimension.YD_VERT: + if self._valign_spec is not None and self._valign_spec != YAlignmentType.YAlignUnchanged: + return True + except Exception: + pass + + child = self.child() + if child: + try: + return bool(child.stretchable(dim) or child.weight(dim)) + except Exception: + return False return False def setBackgroundPixmap(self, filename): @@ -148,6 +164,7 @@ def _schedule_attach_child(self): """Schedule a single idle callback to attach child backend later.""" if self._attach_scheduled or self._child_attached: return + print("Scheduling child attach in idle") self._attach_scheduled = True def _idle_cb(): @@ -165,7 +182,10 @@ def _idle_cb(): _idle_cb() def _ensure_child_attached(self): - """Attach child's backend to our container, apply alignment hints.""" + """Attach child's backend to our container using a 3x3 grid built + from a vertical Gtk.Box with three Gtk.CenterBox rows. Position the + child in the appropriate row and slot according to halign/valign. + """ if self._backend_widget is None: self._create_backend_widget() return @@ -184,6 +204,7 @@ def _ensure_child_attached(self): if cw is None: # child backend not yet ready; schedule again if not self._child_attached: + print(f"Child {child.widgetClass()} {child.debugLabel()} backend not ready; deferring attach") self._schedule_attach_child() return @@ -191,98 +212,80 @@ def _ensure_child_attached(self): hal = self._to_gtk_halign() val = self._to_gtk_valign() - # Apply alignment and expansion hints to child try: - # Set horizontal alignment and expansion - if hasattr(cw, "set_halign"): - if hal is not None: - cw.set_halign(hal) - else: - cw.set_halign(Gtk.Align.FILL) - - # Request expansion for alignment to work properly - cw.set_hexpand(True) - - # Set vertical alignment and expansion - if hasattr(cw, "set_valign"): - if val is not None: - cw.set_valign(val) - else: - cw.set_valign(Gtk.Align.FILL) - - # Request expansion for alignment to work properly - cw.set_vexpand(True) - - except Exception as e: - print(f"Error setting alignment properties: {e}") - # If the child widget is already parented to us, nothing to do - parent_of_cw = None - try: - if hasattr(cw, 'get_parent'): - parent_of_cw = cw.get_parent() - except Exception: - parent_of_cw = None + # Determine row index based on vertical alignment + row_index = 0 if val == Gtk.Align.START else 2 if val == Gtk.Align.END else 1 # center default + target_cb = self._row[row_index] - if parent_of_cw == self._backend_widget: - self._child_attached = True - return + # Place child in start/center/end based on horizontal alignment + # Default to center if unspecified + try: + # Clear any existing widgets in the target centerbox slots + target_cb.set_start_widget(None) + target_cb.set_center_widget(None) + target_cb.set_end_widget(None) + except Exception: + pass + cw.set_halign(hal) + cw.set_valign(val) - # Remove any existing children from our container - try: - # In GTK4, we need to remove all existing children - while True: - child_widget = self._backend_widget.get_first_child() - if child_widget is None: - break - self._backend_widget.remove(child_widget) - except Exception as e: - print(f"Error removing existing children: {e}") + if hal == Gtk.Align.START: + target_cb.set_start_widget(cw) + elif hal == Gtk.Align.END: + target_cb.set_end_widget(cw) + else: + target_cb.set_center_widget(cw) - # Append child to our box - this is the critical fix for GTK4 - try: - self._backend_widget.append(cw) + self._backend_widget.set_halign(hal) + self._backend_widget.set_valign(val) + #self._backend_widget.set_hexpand(True) + #self._backend_widget.set_vexpand(True) self._child_attached = True - print(f"Successfully attached child {child.widgetClass()} {child.debugLabel()} to alignment container") + + col_index = 0 if hal == Gtk.Align.START else 2 if hal == Gtk.Align.END else 1 # center default + print(f"Successfully attached child {child.widgetClass()} {child.label()} [{row_index},{col_index}]") except Exception as e: - print(f"Error appending child: {e}") - # Try alternative method for GTK4 - try: - self._backend_widget.set_child(cw) - self._child_attached = True - print(f"Successfully set child {child.widgetClass()} {child.debugLabel()} using set_child()") - except Exception as e2: - print(f"Error setting child: {e2}") + print(f"Error building CenterBox layout: {e}") def _create_backend_widget(self): - """Create a Box container oriented to allow alignment to work. + """Create a container for the 3x3 alignment layout. - In GTK4, we use a simple Box that expands in both directions - to provide space for the child widget to align within. + Use a simple Gtk.Box as root container; actual 3x3 is built on attach. """ try: - # Use a box that can expand in both directions - box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + root.set_hexpand(True) + root.set_vexpand(True) + root.set_halign(Gtk.Align.FILL) + root.set_valign(Gtk.Align.FILL) - # Make the box expand to fill available space - box.set_hexpand(True) - box.set_vexpand(True) + for _ in range(3): + cb = Gtk.CenterBox() + cb.set_hexpand(True) + cb.set_vexpand(True) + cb.set_halign(Gtk.Align.FILL) + cb.set_valign(Gtk.Align.FILL) + cb.set_margin_start(0) + cb.set_margin_end(0) + self._row.append(cb) + root.append(cb) - # Set the box to fill its allocation so child has space to align - box.set_halign(Gtk.Align.FILL) - box.set_valign(Gtk.Align.FILL) + for cb in self._row: + cb.set_vexpand(True) + except Exception as e: print(f"Error creating backend widget: {e}") - box = Gtk.Box() + root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self._backend_widget = box + self._backend_widget = root self._backend_widget.set_sensitive(self._enabled) # Connect draw handler if we have a background pixbuf if self._background_pixbuf and not self._signal_id: try: - self._signal_id = box.connect("draw", self._on_draw) + self._signal_id = self._backend_widget.connect("draw", self._on_draw) except Exception as e: print(f"Error connecting draw signal: {e}") self._signal_id = None diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py index f77215e..a280188 100644 --- a/manatools/aui/backends/gtk/hboxgtk.py +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -41,20 +41,11 @@ def _create_backend_widget(self): for child in self._children: print("HBox child: ", child.widgetClass()) widget = child.get_backend_widget() - expand = bool(child.stretchable(YUIDimension.YD_HORIZ)) - fill = True - padding = 0 try: - if expand: - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(True) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.FILL) - else: - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(False) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.START) + widget.set_hexpand(True) + widget.set_vexpand(True) + #if hasattr(widget, "set_halign"): + # widget.set_halign(Gtk.Align.FILL) except Exception: pass diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index 753db07..a9eeb3e 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -40,29 +40,11 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() - expand = bool(child.stretchable(YUIDimension.YD_VERT)) - fill = True - padding = 0 - try: - if expand: - if hasattr(widget, "set_vexpand"): - widget.set_vexpand(True) - if hasattr(widget, "set_valign"): - widget.set_valign(Gtk.Align.FILL) - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(True) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.FILL) - else: - if hasattr(widget, "set_vexpand"): - widget.set_vexpand(False) - if hasattr(widget, "set_valign"): - widget.set_valign(Gtk.Align.START) - if hasattr(widget, "set_hexpand"): - widget.set_hexpand(False) - if hasattr(widget, "set_halign"): - widget.set_halign(Gtk.Align.START) + widget.set_vexpand(True) + widget.set_hexpand(True) + #widget.set_valign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_VERT) else Gtk.Align.START) + #widget.set_halign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_HORIZ) else Gtk.Align.START) except Exception: pass From 03bb9894185d8d60c1b67606efbf54e64c9b5fde Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 17:40:55 +0100 Subject: [PATCH 138/523] Fixed stretchable creation --- manatools/aui/backends/gtk/pushbuttongtk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index bb7c4cb..4f3ae2c 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -43,10 +43,10 @@ def _create_backend_widget(self): self._backend_widget = Gtk.Button(label=self._label) # Prevent button from being stretched horizontally by default. try: - if hasattr(self._backend_widget, "set_hexpand"): - self._backend_widget.set_hexpand(False) - if hasattr(self._backend_widget, "set_halign"): - self._backend_widget.set_halign(Gtk.Align.START) + self._backend_widget.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + self._backend_widget.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + self._backend_widget.set_halign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_HORIZ) else Gtk.Align.CENTER) + self._backend_widget.set_valign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_VERT) else Gtk.Align.CENTER) except Exception: pass try: From 91bf0c87611171e96d16be913955aa5764aee77e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 17:52:00 +0100 Subject: [PATCH 139/523] Honour strethable value --- manatools/aui/backends/qt/pushbuttonqt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index cc4f25c..1e60dc3 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -37,7 +37,8 @@ def _create_backend_widget(self): sp = self._backend_widget.sizePolicy() # PySide6 may expect enum class; try both styles defensively try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Minimum) + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Minimum if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Fixed) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Minimum if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed) except Exception: try: sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) From 2e25ce5e2cbb3ed0a33ab5df39e5066e1885c756 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 19:09:16 +0100 Subject: [PATCH 140/523] First implementation of progress bar --- manatools/aui/backends/qt/progressbarqt.py | 164 +++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 manatools/aui/backends/qt/progressbarqt.py diff --git a/manatools/aui/backends/qt/progressbarqt.py b/manatools/aui/backends/qt/progressbarqt.py new file mode 100644 index 0000000..76232a3 --- /dev/null +++ b/manatools/aui/backends/qt/progressbarqt.py @@ -0,0 +1,164 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YProgressBarQt(YWidget): + def __init__(self, parent=None, label="", maxValue=100): + super().__init__(parent) + self._label = label + self._max_value = int(maxValue) if maxValue is not None else 100 + self._value = 0 + self._backend_widget = None + self._label_widget = None + self._progress_widget = None + + def widgetClass(self): + return "YProgressBar" + + def label(self): + return self._label + + def setLabel(self, newLabel): + try: + self._label = str(newLabel) + if getattr(self, "_label_widget", None) is not None: + try: + self._label_widget.setText(self._label) + except Exception: + pass + except Exception: + pass + + def maxValue(self): + return int(self._max_value) + + def value(self): + return int(self._value) + + def setValue(self, newValue): + try: + v = int(newValue) + if v < 0: + v = 0 + if v > self._max_value: + v = self._max_value + self._value = v + if getattr(self, "_progress_widget", None) is not None: + try: + self._progress_widget.setValue(self._value) + except Exception: + pass + except Exception: + pass + + def _create_backend_widget(self): + try: + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + # Create a horizontal row to keep label attached to the progress bar + row = QtWidgets.QWidget() + h = QtWidgets.QHBoxLayout(row) + h.setContentsMargins(0, 0, 0, 0) + h.setSpacing(0) + + # optional label + lbl = QtWidgets.QLabel(self._label) if self._label else None + if lbl is not None: + lbl.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + h.addWidget(lbl) + + prog = QtWidgets.QProgressBar() + prog.setRange(0, max(1, int(self._max_value))) + prog.setValue(int(self._value)) + prog.setTextVisible(True) + # let progress bar expand to take remaining horizontal space + prog.setSizePolicy(QtWidgets.QSizePolicy.Preferred, + QtWidgets.QSizePolicy.Preferred if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed) + h.addWidget(prog) + + layout.addWidget(row) + + self._backend_widget = container + self._label_widget = lbl + self._progress_widget = prog + self._backend_widget.setEnabled(bool(self._enabled)) + except Exception: + self._backend_widget = None + self._label_widget = None + self._progress_widget = None + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def setProperty(self, propertyName, val): + try: + if propertyName == "label": + self.setLabel(str(val)) + return True + if propertyName == "value": + try: + self.setValue(int(val)) + except Exception: + # if val is YPropertyValue + try: + self.setValue(int(val.integerVal())) + except Exception: + pass + return True + except Exception: + pass + return False + + def getProperty(self, propertyName): + try: + if propertyName == "label": + return self.label() + if propertyName == "value": + return self.value() + if propertyName == "maxValue": + return self.maxValue() + except Exception: + pass + return None + + def propertySet(self): + try: + props = YPropertySet() + try: + props.add(YProperty("label", YPropertyType.YStringProperty)) + props.add(YProperty("value", YPropertyType.YIntegerProperty)) + props.add(YProperty("maxValue", YPropertyType.YIntegerProperty)) + except Exception: + pass + return props + except Exception: + return None + + def stretchable(self, dim: YUIDimension): + # Progress bars usually expand horizontally but not vertically + try: + if dim == YUIDimension.YD_HORIZ: + return True + return False + except Exception: + return False From 5f70d0d55915cda8f4828f49ecef15c8f2af7982 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 19:24:07 +0100 Subject: [PATCH 141/523] fixed label --- manatools/aui/backends/qt/__init__.py | 2 ++ manatools/aui/backends/qt/progressbarqt.py | 25 ++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index f0eefa1..a4f852e 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -11,6 +11,7 @@ from .inputfieldqt import YInputFieldQt from .selectionboxqt import YSelectionBoxQt from .checkboxframeqt import YCheckBoxFrameQt +from .progressbarqt import YProgressBarQt __all__ = [ @@ -27,5 +28,6 @@ "YComboBoxQt", "YAlignmentQt", "YCheckBoxFrameQt", + "YProgressBarQt", # ... ] diff --git a/manatools/aui/backends/qt/progressbarqt.py b/manatools/aui/backends/qt/progressbarqt.py index 76232a3..35fdc1a 100644 --- a/manatools/aui/backends/qt/progressbarqt.py +++ b/manatools/aui/backends/qt/progressbarqt.py @@ -68,28 +68,25 @@ def _create_backend_widget(self): layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) - # Create a horizontal row to keep label attached to the progress bar - row = QtWidgets.QWidget() - h = QtWidgets.QHBoxLayout(row) - h.setContentsMargins(0, 0, 0, 0) - h.setSpacing(0) + # container vertical stretching is allowed only if widget is stretchable vertically + h_policy = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Preferred + v_policy = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed + container.setSizePolicy(h_policy, v_policy) - # optional label + # Place label above the progress bar with no spacing so they remain attached lbl = QtWidgets.QLabel(self._label) if self._label else None if lbl is not None: - lbl.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - h.addWidget(lbl) + lbl.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + layout.addWidget(lbl) prog = QtWidgets.QProgressBar() prog.setRange(0, max(1, int(self._max_value))) prog.setValue(int(self._value)) prog.setTextVisible(True) - # let progress bar expand to take remaining horizontal space - prog.setSizePolicy(QtWidgets.QSizePolicy.Preferred, - QtWidgets.QSizePolicy.Preferred if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed) - h.addWidget(prog) - - layout.addWidget(row) + # progress bar horizontal expand; vertical policy mirrors container vertical policy + prog.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed) + layout.addWidget(prog) self._backend_widget = container self._label_widget = lbl From 5d20ee610a65d8b377531a97a36f906593e2f82d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 19:24:27 +0100 Subject: [PATCH 142/523] added test progress bar --- test/test_progressbar.py | 91 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 test/test_progressbar.py diff --git a/test/test_progressbar.py b/test/test_progressbar.py new file mode 100644 index 0000000..d8b7e36 --- /dev/null +++ b/test/test_progressbar.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + + +def test_progressbar(backend_name=None): + """Test simple dialog with a progress bar and OK button""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + try: + import time + from manatools.aui.yui import YUI, YUI_ui + from manatools.aui.yui_common import YTimeoutEvent + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + ui.application().setApplicationTitle("Progress bar Application") + dialog = factory.createMainDialog() + + vbox = factory.createVBox(dialog) + pb = factory.createProgressBar(vbox, "Progress", 100) + + # ensure initial value is 0 + pb.setValue(0) + + factory.createPushButton(vbox, "OK") + dialog.open() + + # Main loop: wait 500ms, increment progress by 1 on timeout. + # When reaching 100: wait 1s (allowing event handling), reset to 0, + # then wait 3s before restarting counting. Any non-timeout event (e.g. OK) + # will break the loop and close the dialog. + value = 0 + timeout_ms = 500 + phase = 0 # Normal counting + while True: + ev = dialog.waitForEvent(timeout_ms) + if isinstance(ev, YTimeoutEvent): + # increment + if phase == 0: + value = min(100, value + 1) + elif phase == 1: + value = 0 + timeout_ms = 3000 + phase = 3 # Waiting before restart + elif phase == 3: + value = 1 + timeout_ms = 500 + phase = 0 # Normal counting + try: + pb.setValue(value) + except Exception: + try: + pb.setProperty('value', value) + except Exception: + pass + + if value >= 100: + # wait 1 second (still via waitForEvent to allow user interaction) + timeout_ms = 1000 + phase = 1 # Waiting for reset + else: + # any other event (button press, close) break + break + + dialog.destroy() + + except Exception as e: + print(f"Error testing ProgressBar with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_progressbar(sys.argv[1]) + else: + test_progressbar() \ No newline at end of file From c15ddc0ec55401b5f0ca0a80d3235a2fb79857a7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 20:23:05 +0100 Subject: [PATCH 143/523] Removed warning on timeout cancellation --- manatools/aui/backends/gtk/dialoggtk.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index 84de8cb..5829c65 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -125,7 +125,15 @@ def waitForEvent(self, timeout_millisec=0): def on_timeout(): # post timeout event and quit loop - self._event_result = YTimeoutEvent() + try: + self._event_result = YTimeoutEvent() + except Exception: + pass + # mark timeout id consumed so cleanup won't try to remove it again + try: + self._timeout_id = None + except Exception: + pass try: if self._glib_loop.is_running(): self._glib_loop.quit() From c02f2aa0ce30d9b17097befc64906cd354f209fd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 20:23:56 +0100 Subject: [PATCH 144/523] Added Progress bar for Gtk --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/progressbargtk.py | 89 ++++++++++++++++++++ manatools/aui/yui_gtk.py | 5 +- 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/gtk/progressbargtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index f90d1d3..64ae603 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -11,6 +11,7 @@ from .inputfieldgtk import YInputFieldGtk from .selectionboxgtk import YSelectionBoxGtk from .checkboxframegtk import YCheckBoxFrameGtk +from .progressbargtk import YProgressBarGtk __all__ = [ @@ -27,5 +28,6 @@ "YComboBoxGtk", "YAlignmentGtk", "YCheckBoxFrameGtk", + "YProgressBarGtk", # ... ] diff --git a/manatools/aui/backends/gtk/progressbargtk.py b/manatools/aui/backends/gtk/progressbargtk.py new file mode 100644 index 0000000..56c8bf5 --- /dev/null +++ b/manatools/aui/backends/gtk/progressbargtk.py @@ -0,0 +1,89 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + +class YProgressBarGtk(YWidget): + def __init__(self, parent=None, label="", maxValue=100): + super().__init__(parent) + self._label = label + self._max_value = int(maxValue) if maxValue is not None else 100 + self._value = 0 + self._backend_widget = None + self._label_widget = None + self._progress_widget = None + + def widgetClass(self): + return "YProgressBar" + + def label(self): + return self._label + + def setLabel(self, newLabel): + try: + self._label = str(newLabel) + if getattr(self, "_label_widget", None) is not None: + try: + self._label_widget.set_text(self._label) + except Exception: + pass + except Exception: + pass + + def maxValue(self): + return int(self._max_value) + + def value(self): + return int(self._value) + + def setValue(self, newValue): + try: + v = int(newValue) + if v < 0: + v = 0 + if v > self._max_value: + v = self._max_value + self._value = v + if getattr(self, "_progress_widget", None) is not None: + try: + self._progress_widget.set_fraction(float(self._value) / float(self._max_value) if self._max_value > 0 else 0.0) + except Exception: + pass + except Exception: + pass + + def _create_backend_widget(self): + container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + container.set_hexpand(True) + container.set_halign(Gtk.Align.FILL) + + # Label + self._label_widget = Gtk.Label(label=self._label) + self._label_widget.set_halign(Gtk.Align.START) + container.append(self._label_widget) + + # Progress Bar + self._progress_widget = Gtk.ProgressBar() + self._progress_widget.set_fraction(float(self._value) / float(self._max_value) if self._max_value > 0 else 0.0) + self._progress_widget.set_hexpand(True) + self._progress_widget.set_halign(Gtk.Align.FILL) + self._progress_widget.set_show_text(True) + container.append(self._progress_widget) + + self._backend_widget = container \ No newline at end of file diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 68c9ff9..e52f29f 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -256,4 +256,7 @@ def createFrame(self, parent, label: str=""): def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): """Create a CheckBox Frame widget.""" - return YCheckBoxFrameGtk(parent, label, checked) \ No newline at end of file + return YCheckBoxFrameGtk(parent, label, checked) + + def createProgressBar(self, parent, label, max_value=100): + return YProgressBarGtk(parent, label, max_value) From 65094dd11d02c541ad75b9022703fde4fc4168f9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 20:38:45 +0100 Subject: [PATCH 145/523] Added ncurses progress bar --- manatools/aui/backends/curses/__init__.py | 3 +- .../aui/backends/curses/progressbarcurses.py | 141 ++++++++++++++++++ manatools/aui/yui_curses.py | 4 + 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/progressbarcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 4203599..19cd868 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -11,7 +11,7 @@ from .inputfieldcurses import YInputFieldCurses from .selectionboxcurses import YSelectionBoxCurses from .checkboxframecurses import YCheckBoxFrameCurses - +from .progressbarcurses import YProgressBarCurses __all__ = [ "YDialogCurses", @@ -27,5 +27,6 @@ "YComboBoxCurses", "YAlignmentCurses", "YCheckBoxFrameCurses", + "YProgressBarCurses", # ... ] diff --git a/manatools/aui/backends/curses/progressbarcurses.py b/manatools/aui/backends/curses/progressbarcurses.py new file mode 100644 index 0000000..73a326f --- /dev/null +++ b/manatools/aui/backends/curses/progressbarcurses.py @@ -0,0 +1,141 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + +class YProgressBarCurses(YWidget): + def __init__(self, parent=None, label="", maxValue=100): + super().__init__(parent) + self._label = label + self._max_value = int(maxValue) if maxValue is not None else 100 + self._value = 0 + # progress bar occupies 2 rows when label present, otherwise 1 + self._height = 2 if self._label else 1 + self._backend_widget = None + + def widgetClass(self): + return "YProgressBar" + + def label(self): + return self._label + + def setLabel(self, newLabel): + try: + self._label = str(newLabel) + # adjust height when label toggles + try: + self._height = 2 if self._label else 1 + except Exception: + pass + # request immediate redraw of parent dialog if present + dlg = self.findDialog() + if dlg is not None: + try: + dlg._last_draw_time = 0 + except Exception: + pass + except Exception: + pass + + def maxValue(self): + return int(self._max_value) + + def value(self): + return int(self._value) + + def setValue(self, newValue): + try: + v = int(newValue) + if v < 0: + v = 0 + if v > self._max_value: + v = self._max_value + self._value = v + # request immediate redraw of parent dialog + dlg = self.findDialog() + if dlg is not None: + try: + dlg._last_draw_time = 0 + except Exception: + pass + except Exception: + pass + + def _create_backend_widget(self): + # curses backend widgets don't wrap native widgets; keep None + self._backend_widget = None + + def _draw(self, window, y, x, width, height): + try: + if width <= 0 or height <= 0: + return + + # Determine where to draw label and bar + draw_label = bool(self._label) + label_y = y if draw_label else None + bar_y = y + (1 if draw_label else 0) + + # Draw label (single line) above the bar + if draw_label and label_y is not None and width > 0: + try: + attr = 0 + if not self.isEnabled(): + attr |= curses.A_DIM + text = str(self._label)[:max(0, width)] + window.addstr(label_y, x, text, attr) + except curses.error: + pass + + # Draw progress bar line + try: + # Compute fill fraction and percent text + maxv = max(1, int(self._max_value)) + val = max(0, int(self._value)) + frac = float(val) / float(maxv) + fill = int(frac * width) + + # Bar characters + filled_char = '=' + empty_char = ' ' + bar_str = (filled_char * fill) + (empty_char * max(0, width - fill)) + + # Center percentage text + perc = int(frac * 100) + perc_text = f"{perc}%" + pt_len = len(perc_text) + pt_x = x + max(0, (width - pt_len) // 2) + + # Attrs + bar_attr = curses.A_REVERSE if self.isEnabled() else curses.A_DIM + perc_attr = curses.A_BOLD if self.isEnabled() else curses.A_DIM + + # Draw base bar + try: + window.addstr(bar_y, x, bar_str[:width], bar_attr) + except curses.error: + pass + + # Overlay percentage text + try: + # Ensure percentage fits inside bar + if pt_x + pt_len <= x + width: + window.addstr(bar_y, pt_x, perc_text, perc_attr) + except curses.error: + pass + except Exception: + pass + except Exception: + pass \ No newline at end of file diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 30107ad..624a10b 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -205,3 +205,7 @@ def createFrame(self, parent, label: str=""): def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): """Create a CheckBox Frame widget.""" return YCheckBoxFrameCurses(parent, label, checked) + + def createProgressBar(self, parent, label, max_value=100): + """Create a Progress Bar widget.""" + return YProgressBarCurses(parent, label, max_value) From c31bfddcf3e4d1305b25b1828e26e886e64f482e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 13 Dec 2025 20:39:05 +0100 Subject: [PATCH 146/523] Added info on BE on title --- test/test_progressbar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_progressbar.py b/test/test_progressbar.py index d8b7e36..4f467ba 100644 --- a/test/test_progressbar.py +++ b/test/test_progressbar.py @@ -28,7 +28,7 @@ def test_progressbar(backend_name=None): ui = YUI_ui() factory = ui.widgetFactory() - ui.application().setApplicationTitle("Progress bar Application") + ui.application().setApplicationTitle(f"Progress bar {backend.value} application") dialog = factory.createMainDialog() vbox = factory.createVBox(dialog) @@ -88,4 +88,4 @@ def test_progressbar(backend_name=None): if len(sys.argv) > 1: test_progressbar(sys.argv[1]) else: - test_progressbar() \ No newline at end of file + test_progressbar() From 96ea1fb4b91a1027083ae6b1278e279510f74d1d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 15:17:52 +0100 Subject: [PATCH 147/523] PySide6 and Gtk4 --- manatools/aui/yui.py | 10 ++++++---- test/test_multi_backend.py | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/manatools/aui/yui.py b/manatools/aui/yui.py index 9825163..c3a933e 100644 --- a/manatools/aui/yui.py +++ b/manatools/aui/yui.py @@ -30,15 +30,17 @@ def _detect_backend(cls): return Backend.NCURSES # Auto-detect based on available imports + # Require PySide6 (Qt6) try: - import PyQt5.QtWidgets + import PySide6.QtWidgets return Backend.QT except ImportError: pass - + + # GTK: require GTK4 try: import gi - gi.require_version('Gtk', '3.0') + gi.require_version('Gtk', '4.0') from gi.repository import Gtk return Backend.GTK except (ImportError, ValueError): @@ -50,7 +52,7 @@ def _detect_backend(cls): except ImportError: pass - raise RuntimeError("No UI backend available. Install PyQt5, PyGObject, or curses.") + raise RuntimeError("No UI backend available. Install PySide6, PyGObject (GTK4), or curses.") @classmethod def ui(cls): diff --git a/test/test_multi_backend.py b/test/test_multi_backend.py index 22e704a..a989a31 100644 --- a/test/test_multi_backend.py +++ b/test/test_multi_backend.py @@ -95,19 +95,20 @@ def test_all_backends(): backends_to_test = [] # Check which backends are available + # Require PySide6 (Qt6) try: - import PyQt5.QtWidgets + import PySide6.QtWidgets backends_to_test.append('qt') - print("✓ Qt backend available") + print("✓ Qt backend available (PySide6)") except ImportError: - print("✗ Qt backend not available") + print("✗ Qt backend not available (PySide6 required)") try: import gi - gi.require_version('Gtk', '3.0') + gi.require_version('Gtk', '4.0') from gi.repository import Gtk backends_to_test.append('gtk') - print("✓ GTK backend available") + print("✓ GTK backend available (GTK4)") except (ImportError, ValueError) as e: print(f"✗ GTK backend not available: {e}") From f5bd97ba7c1497f3cafd1fe0900ffaf7c5c58417 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 15:55:48 +0100 Subject: [PATCH 148/523] removed access to private attributes --- test/test_progressbar.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/test_progressbar.py b/test/test_progressbar.py index 4f467ba..971a2a5 100644 --- a/test/test_progressbar.py +++ b/test/test_progressbar.py @@ -19,10 +19,6 @@ def test_progressbar(backend_name=None): from manatools.aui.yui import YUI, YUI_ui from manatools.aui.yui_common import YTimeoutEvent - # Force re-detection - YUI._instance = None - YUI._backend = None - backend = YUI.backend() print(f"Using backend: {backend.value}") From cef0a6d61decd397e8102d19cf30327ae8c77da0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 15:57:32 +0100 Subject: [PATCH 149/523] Default value --- manatools/aui/yui_gtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index e52f29f..fb8e45a 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -35,7 +35,7 @@ def yApp(self): class YApplicationGtk: def __init__(self): self._application_title = "manatools GTK Application" - self._product_name = "manatools YUI GTK" + self._product_name = "manatools AUI Gtk" self._icon_base_path = None self._icon = "" # cached resolved GdkPixbuf.Pixbuf (or None) From 061e43ce49c7c2d41ebf702982d31d5c1e1ad3a4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 15:58:29 +0100 Subject: [PATCH 150/523] simplify access to application title --- manatools/aui/backends/gtk/dialoggtk.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index 5829c65..aeedfed 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -18,6 +18,7 @@ import threading import os from ...yui_common import * +from ... import yui as yui_mod class YDialogGtk(YSingleChildContainerWidget): _open_dialogs = [] @@ -175,23 +176,12 @@ def currentDialog(cls, doThrow=True): def _create_backend_widget(self): # Determine window title from YApplicationGtk instance stored on the YUI backend - title = "Manatools YUI GTK Dialog" + title = "Manatools GTK Dialog" try: - from . import yui as yui_mod - appobj = None - # YUI._backend may hold the backend instance (YUIGtk) - backend = getattr(yui_mod.YUI, "_backend", None) - if backend and hasattr(backend, "application"): - appobj = backend.application() - # fallback: YUI._instance might be set and expose application/yApp - if not appobj: - inst = getattr(yui_mod.YUI, "_instance", None) - if inst and hasattr(inst, "application"): - appobj = inst.application() - if appobj and hasattr(appobj, "applicationTitle"): - atitle = appobj.applicationTitle() - if atitle: - title = atitle + appobj = yui_mod.YUI.ui().application() + atitle = appobj.applicationTitle() + if atitle: + title = atitle # try to obtain resolved pixbuf from application and store for window icon _resolved_pixbuf = None try: @@ -199,6 +189,7 @@ def _create_backend_widget(self): except Exception: _resolved_pixbuf = None except Exception: + print("Warning: could not determine application title for dialog") pass # Create Gtk4 Window From 6bb27120a14b2a9cf8dd50e6ed662f53a665440c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 16:04:30 +0100 Subject: [PATCH 151/523] fixing application title --- manatools/aui/backends/qt/dialogqt.py | 25 +++++++------------------ manatools/aui/yui_qt.py | 2 +- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index 9064fe4..c688941 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -12,6 +12,8 @@ from PySide6 import QtWidgets, QtCore, QtGui from ...yui_common import YSingleChildContainerWidget, YUIDimension, YPropertySet, YProperty, YPropertyType, YUINoDialogException, YDialogType, YDialogColorMode, YEvent, YCancelEvent, YTimeoutEvent +from ... import yui as yui_mod +import os class YDialogQt(YSingleChildContainerWidget): _open_dialogs = [] @@ -91,26 +93,13 @@ def currentDialog(cls, doThrow=True): def _create_backend_widget(self): self._qwidget = QtWidgets.QMainWindow() # Determine window title:from YApplicationQt instance stored on the YUI backend - title = "Manatools YUI Qt Dialog" + title = "Manatools Qt Dialog" try: - from . import yui as yui_mod - appobj = None - # YUI._backend may hold the backend instance (YUIQt) - backend = getattr(yui_mod.YUI, "_backend", None) - if backend: - if hasattr(backend, "application"): - appobj = backend.application() - # fallback: YUI._instance might be set and expose application/yApp - if not appobj: - inst = getattr(yui_mod.YUI, "_instance", None) - if inst: - if hasattr(inst, "application"): - appobj = inst.application() - if appobj and hasattr(appobj, "applicationTitle"): - atitle = appobj.applicationTitle() - if atitle: - title = atitle + appobj = yui_mod.YUI.ui().application() + atitle = appobj.applicationTitle() + if atitle: + title = atitle # try to obtain a resolved QIcon from the application backend if available app_qicon = None if appobj: diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 7305989..066d28e 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -36,7 +36,7 @@ def yApp(self): class YApplicationQt: def __init__(self): self._application_title = "manatools Qt Application" - self._product_name = "manatools YUI Qt" + self._product_name = "manatools AUI Qt" self._icon_base_path = None self._icon = "" # cached QIcon resolved from _icon (None if not resolved) From d20a8de38897ed20ffcdc4e13918ef69a7568195 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 16:07:45 +0100 Subject: [PATCH 152/523] Fixed application title --- manatools/aui/backends/curses/dialogcurses.py | 27 +++++-------------- manatools/aui/yui_curses.py | 2 +- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index 5262074..06cb6ae 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -15,6 +15,7 @@ import os import time from ...yui_common import * +from ... import yui as yui_mod class YDialogCurses(YSingleChildContainerWidget): _open_dialogs = [] @@ -142,26 +143,12 @@ def _draw_dialog(self): # Draw title title = " manatools YUI NCurses Dialog " - try: - from . import yui as yui_mod - appobj = None - # YUI._backend may hold the backend instance (YUIQt) - backend = getattr(yui_mod.YUI, "_backend", None) - if backend: - if hasattr(backend, "application"): - appobj = backend.application() - # fallback: YUI._instance might be set and expose application/yApp - if not appobj: - inst = getattr(yui_mod.YUI, "_instance", None) - if inst: - if hasattr(inst, "application"): - appobj = inst.application() - if appobj and hasattr(appobj, "applicationTitle"): - atitle = appobj.applicationTitle() - if atitle: - title = atitle - if appobj: - appobj.setApplicationTitle(title) + try: + appobj = yui_mod.YUI.ui().application() + atitle = appobj.applicationTitle() + if atitle: + title = atitle + appobj.setApplicationTitle(title) except Exception: # ignore and keep default pass diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 624a10b..de605ba 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -77,7 +77,7 @@ def yApp(self): class YApplicationCurses: def __init__(self): self._application_title = "manatools Curses Application" - self._product_name = "manatools YUI Curses" + self._product_name = "manatools AUI Curses" self._icon_base_path = "" self._icon = "" From 99034ab5466c9e8367d2750237dc697f2edf06cc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 16:10:19 +0100 Subject: [PATCH 153/523] progress --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 26bab0c..def0abd 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -14,7 +14,7 @@ Missing Widgets comparing libyui: [X] YTree [X] YFrame [ ] YTable - [ ] YProgressBar + [X] YProgressBar [ ] YRichText [ ] YMultiLineEdit [ ] YIntField From 33790450d9c15e62e14f7353fa16ba37f728fe53 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 16:15:24 +0100 Subject: [PATCH 154/523] typo --- test/{test_selctionbox-vbox.py => test_selectionbox-vbox.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{test_selctionbox-vbox.py => test_selectionbox-vbox.py} (100%) diff --git a/test/test_selctionbox-vbox.py b/test/test_selectionbox-vbox.py similarity index 100% rename from test/test_selctionbox-vbox.py rename to test/test_selectionbox-vbox.py From bcb4a6e4ad025b489f0e401e566923f4025796e9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 16:51:55 +0100 Subject: [PATCH 155/523] Added Qt RadioButton --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/radiobuttonqt.py | 102 +++++++++++++++++++++ manatools/aui/yui_qt.py | 4 + 3 files changed, 108 insertions(+) create mode 100644 manatools/aui/backends/qt/radiobuttonqt.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index a4f852e..36c7812 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -12,6 +12,7 @@ from .selectionboxqt import YSelectionBoxQt from .checkboxframeqt import YCheckBoxFrameQt from .progressbarqt import YProgressBarQt +from .radiobuttonqt import YRadioButtonQt __all__ = [ @@ -29,5 +30,6 @@ "YAlignmentQt", "YCheckBoxFrameQt", "YProgressBarQt", + "YRadioButtonQt", # ... ] diff --git a/manatools/aui/backends/qt/radiobuttonqt.py b/manatools/aui/backends/qt/radiobuttonqt.py new file mode 100644 index 0000000..017753c --- /dev/null +++ b/manatools/aui/backends/qt/radiobuttonqt.py @@ -0,0 +1,102 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +from ...yui_common import * + +class YRadioButtonQt(YWidget): + def __init__(self, parent=None, label="", isChecked=False): + super().__init__(parent) + self._label = label + self._is_checked = bool(isChecked) + self._backend_widget = None + + def widgetClass(self): + return "YRadioButton" + + def label(self): + return self._label + + def setLabel(self, newLabel): + try: + self._label = str(newLabel) + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setText(self._label) + except Exception: + pass + except Exception: + pass + + def isChecked(self): + return bool(self._is_checked) + + def setChecked(self, checked): + try: + self._is_checked = bool(checked) + if getattr(self, "_backend_widget", None) is not None: + try: + # avoid emitting signals while programmatically changing state + self._backend_widget.blockSignals(True) + self._backend_widget.setChecked(self._is_checked) + finally: + try: + self._backend_widget.blockSignals(False) + except Exception: + pass + except Exception: + pass + + # Compatibility with other widgets: provide value()/setValue() + def value(self): + return self.isChecked() + + def setValue(self, checked): + return self.setChecked(checked) + + def _create_backend_widget(self): + try: + self._backend_widget = QtWidgets.QRadioButton(self._label) + self._backend_widget.setChecked(self._is_checked) + self._backend_widget.toggled.connect(self._on_toggled) + try: + self._backend_widget.setEnabled(bool(self._enabled)) + except Exception: + pass + except Exception: + self._backend_widget = None + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_toggled(self, checked): + try: + self._is_checked = bool(checked) + except Exception: + self._is_checked = False + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + # best-effort debug output when no dialog + try: + print(f"RadioButton toggled: {self._label} = {self._is_checked}") + except Exception: + pass \ No newline at end of file diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 066d28e..453326f 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -245,3 +245,7 @@ def createFrame(self, parent, label: str=""): def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): """Create a CheckBox Frame widget.""" return YCheckBoxFrameQt(parent, label, checked) + + def createRadioButton(self, parent, label:str = "", isChecked:bool = False): + """Create a Radio Button widget.""" + return YRadioButtonQt(parent, label, isChecked) \ No newline at end of file From 798e1e264048815cb201bc8d85134e4c56511cee Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 20:16:34 +0100 Subject: [PATCH 156/523] First attempt to make gtk radiobutton --- manatools/aui/backends/gtk/__init__.py | 3 +- manatools/aui/backends/gtk/radiobuttongtk.py | 111 +++++++++++++++++++ manatools/aui/yui_gtk.py | 4 + 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/gtk/radiobuttongtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 64ae603..567ea5a 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -12,7 +12,7 @@ from .selectionboxgtk import YSelectionBoxGtk from .checkboxframegtk import YCheckBoxFrameGtk from .progressbargtk import YProgressBarGtk - +from .radiobuttongtk import YRadioButtonGtk __all__ = [ "YDialogGtk", @@ -29,5 +29,6 @@ "YAlignmentGtk", "YCheckBoxFrameGtk", "YProgressBarGtk", + "YRadioButtonGtk", # ... ] diff --git a/manatools/aui/backends/gtk/radiobuttongtk.py b/manatools/aui/backends/gtk/radiobuttongtk.py new file mode 100644 index 0000000..6c665cb --- /dev/null +++ b/manatools/aui/backends/gtk/radiobuttongtk.py @@ -0,0 +1,111 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +from ...yui_common import * + +class YRadioButtonGtk(YWidget): + def __init__(self, parent=None, label="", isChecked=False): + super().__init__(parent) + self._label = label + self._is_checked = bool(isChecked) + self._backend_widget = None + + def widgetClass(self): + return "YRadioButton" + + def label(self): + return self._label + + def setLabel(self, newLabel): + try: + self._label = str(newLabel) + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.set_label(self._label) + except Exception: + pass + except Exception: + pass + + def isChecked(self): + return bool(self._is_checked) + + def setChecked(self, checked): + try: + self._is_checked = bool(checked) + if getattr(self, "_backend_widget", None) is not None: + try: + # avoid emitting signals while programmatically changing state + self._backend_widget.handler_block_by_func(self._on_toggled) + self._backend_widget.set_active(self._is_checked) + finally: + try: + self._backend_widget.handler_unblock_by_func(self._on_toggled) + except Exception: + pass + except Exception: + pass + + def _create_backend_widget(self): + self._backend_widget = Gtk.CheckButton(label=self._label) + + # Prevent radio button from being stretched horizontally by default. + try: + self._backend_widget.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + self._backend_widget.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + self._backend_widget.set_halign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_HORIZ) else Gtk.Align.CENTER) + self._backend_widget.set_valign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_VERT) else Gtk.Align.CENTER) + except Exception: + pass + try: + self._backend_widget.set_sensitive(self._enabled) + self._backend_widget.connect("toggled", self._on_toggled) + self._backend_widget.set_active(self._is_checked) + except Exception: + pass + + def _on_toggled(self, button): + try: + self._is_checked = bool(button.get_active()) + except Exception: + try: + self._is_checked = bool(self._is_checked) + except Exception: + self._is_checked = False + + if self.notify() is False: + return + + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + """Enable/disable the Gtk.RadioButton backend.""" + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass \ No newline at end of file diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index fb8e45a..3a5eed7 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -260,3 +260,7 @@ def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): def createProgressBar(self, parent, label, max_value=100): return YProgressBarGtk(parent, label, max_value) + + def createRadioButton(self, parent, label:str = "", isChecked:bool = False): + """Create a Radio Button widget.""" + return YRadioButtonGtk(parent, label, isChecked) From 89dfebf2a1542d71846d3ca22665e3e8c2d4db3f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 21:20:04 +0100 Subject: [PATCH 157/523] Two radio button groups to test --- test/test_radiobutton.py | 84 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/test_radiobutton.py diff --git a/test/test_radiobutton.py b/test/test_radiobutton.py new file mode 100644 index 0000000..69f6fa7 --- /dev/null +++ b/test/test_radiobutton.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + + +def test_radiobutton(backend_name=None): + """Test dialog with 3 radio buttons, a label showing selection, and OK button""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + import time + from manatools.aui.yui import YUI, YUI_ui + from manatools.aui.yui_common import YTimeoutEvent, YWidgetEvent, YCancelEvent + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + ui.application().setApplicationTitle(f"RadioButton {backend.value} test") + + dialog = factory.createMainDialog() + vbox = factory.createVBox(dialog) + + # First radio group (3 options) + frame = factory.createFrame(vbox, "Options") + inner1 = factory.createVBox(frame) + + rb1 = factory.createRadioButton(inner1, "Option 1", False) + rb2 = factory.createRadioButton(inner1, "Option 2", True) + rb3 = factory.createRadioButton(inner1, "Option 3", False) + selected_label1 = factory.createLabel(vbox, "Selected: Option 2") + + # Second radio group (2 test options) + frame2 = factory.createFrame(vbox, "Tests") + inner2 = factory.createVBox(frame2) + tr1 = factory.createRadioButton(inner2, "Test 1", True) + tr2 = factory.createRadioButton(inner2, "Test 2", False) + + selected_label2 = factory.createLabel(vbox, "Selected: Test 1") + + ok_button = factory.createPushButton(vbox, "OK") + + dialog.open() + + # Event loop: wait for events; on widget events update the label; + # OK button closes the dialog. + while True: + ev = dialog.waitForEvent(500) + if isinstance(ev, YTimeoutEvent): + continue + if isinstance(ev, YCancelEvent): + break + if isinstance(ev, YWidgetEvent): + w = ev.widget() + # If OK pressed -> exit + if w == ok_button: + break + elif w in (rb1, rb2, rb3): + selected_label1.setText(f"Selected: {w.label()}") + elif w in (tr1, tr2): + selected_label2.setText(f"Selected: {w.label()}") + + dialog.destroy() + + except Exception as e: + print(f"Error testing RadioButton with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_radiobutton(sys.argv[1]) + else: + test_radiobutton() From 994428e1f3da0d76d28a596a7f8bae280c7b00ce Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 21:20:49 +0100 Subject: [PATCH 158/523] Fixed gtk radio button --- manatools/aui/backends/gtk/radiobuttongtk.py | 52 ++++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/gtk/radiobuttongtk.py b/manatools/aui/backends/gtk/radiobuttongtk.py index 6c665cb..6de76fa 100644 --- a/manatools/aui/backends/gtk/radiobuttongtk.py +++ b/manatools/aui/backends/gtk/radiobuttongtk.py @@ -25,6 +25,29 @@ def __init__(self, parent=None, label="", isChecked=False): self._label = label self._is_checked = bool(isChecked) self._backend_widget = None + # determine radio-group membership among siblings + self._group = None + try: + brothers = getattr(parent, '_children', []) if parent is not None else [] + for b in brothers: + try: + if b is self: + continue + if getattr(b, 'widgetClass', None) and b.widgetClass() == 'YRadioButton': + # adopt existing sibling's group if set, otherwise use sibling as group leader + grp = getattr(b, '_group', None) + if grp is not None: + self._group = grp + else: + self._group = b # should not happen + break + except Exception: + pass + if self._group is None: + # no sibling found: become group leader + self._group = self + except Exception: + self._group = self def widgetClass(self): return "YRadioButton" @@ -63,7 +86,27 @@ def setChecked(self, checked): pass def _create_backend_widget(self): - self._backend_widget = Gtk.CheckButton(label=self._label) + # Create a check-like radio using Gtk.CheckButton (GTK4 bindings may + # not provide Gtk.RadioButton reliably). If a sibling group's backend + # widget exists, try to join its GTK group via `set_group`. + try: + self._backend_widget = Gtk.CheckButton(label=self._label) + if getattr(self, '_group', None) is not None and self._group is not self: + ref_w = getattr(self._group, '_backend_widget', None) + if ref_w is not None: + try: + if hasattr(self._backend_widget, 'set_group'): + try: + self._backend_widget.set_group(ref_w) + except Exception: + pass + except Exception: + pass + except Exception: + try: + self._backend_widget = Gtk.CheckButton(label=self._label) + except Exception: + self._backend_widget = None # Prevent radio button from being stretched horizontally by default. try: @@ -74,9 +117,10 @@ def _create_backend_widget(self): except Exception: pass try: - self._backend_widget.set_sensitive(self._enabled) - self._backend_widget.connect("toggled", self._on_toggled) - self._backend_widget.set_active(self._is_checked) + if self._backend_widget is not None: + self._backend_widget.set_sensitive(self._enabled) + self._backend_widget.connect("toggled", self._on_toggled) + self._backend_widget.set_active(self._is_checked) except Exception: pass From 6c39bbb3a0c6d7eb2f137a6ac76c6033457b794f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 21:27:11 +0100 Subject: [PATCH 159/523] Ncurses radio button --- manatools/aui/backends/curses/__init__.py | 2 + .../aui/backends/curses/radiobuttoncurses.py | 162 ++++++++++++++++++ manatools/aui/yui_curses.py | 4 + 3 files changed, 168 insertions(+) create mode 100644 manatools/aui/backends/curses/radiobuttoncurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 19cd868..c90b91f 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -12,6 +12,7 @@ from .selectionboxcurses import YSelectionBoxCurses from .checkboxframecurses import YCheckBoxFrameCurses from .progressbarcurses import YProgressBarCurses +from .radiobuttoncurses import YRadioButtonCurses __all__ = [ "YDialogCurses", @@ -28,5 +29,6 @@ "YAlignmentCurses", "YCheckBoxFrameCurses", "YProgressBarCurses", + "YRadioButtonCurses", # ... ] diff --git a/manatools/aui/backends/curses/radiobuttoncurses.py b/manatools/aui/backends/curses/radiobuttoncurses.py new file mode 100644 index 0000000..0bbdb3e --- /dev/null +++ b/manatools/aui/backends/curses/radiobuttoncurses.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import sys +import os +import time +from ...yui_common import * + + +class YRadioButtonCurses(YWidget): + def __init__(self, parent=None, label="", is_checked=False): + super().__init__(parent) + self._label = label + self._is_checked = bool(is_checked) + self._focused = False + self._can_focus = True + self._height = 1 + + def widgetClass(self): + return "YRadioButton" + + def value(self): + return self._is_checked + + def setValue(self, checked): + # Programmatic set: enforce single-selection among siblings when setting True + try: + self._is_checked = bool(checked) + if self._is_checked: + # uncheck sibling radio buttons under same parent + try: + parent = getattr(self, '_parent', None) + if parent is not None: + for sib in list(getattr(parent, '_children', []) or []): + try: + if sib is not self and getattr(sib, 'widgetClass', None) and sib.widgetClass() == 'YRadioButton': + if getattr(sib, '_is_checked', False): + try: + sib._is_checked = False + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def label(self): + return self._label + + def _create_backend_widget(self): + # No native curses widget; state is kept here + pass + + def _set_backend_enabled(self, enabled): + """Enable/disable radio button: update focusability and collapse focus if disabling.""" + try: + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = True + self._can_focus = False + if getattr(self, "_focused", False): + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + # Use parentheses style for radio: '(*)' if checked else '( )' + radio_symbol = "(*)" if self._is_checked else "( )" + text = f"{radio_symbol} {self._label}" + if len(text) > width: + text = text[:max(0, width - 3)] + "..." + + attr_on = False + if self._focused and self.isEnabled(): + window.attron(curses.A_REVERSE) + attr_on = True + elif not self.isEnabled(): + window.attron(curses.A_DIM) + attr_on = True + + window.addstr(y, x, text) + + if attr_on: + try: + if self._focused and self.isEnabled(): + window.attroff(curses.A_REVERSE) + else: + window.attroff(curses.A_DIM) + except Exception: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self.isEnabled(): + return False + # Space or Enter to select radio + if key in (ord(' '), ord('\n'), curses.KEY_ENTER): + self._select() + return True + return False + + def _select(self): + """Select this radio and unselect siblings; post event.""" + try: + if not getattr(self, '_is_checked', False): + # set self selected + self._is_checked = True + # uncheck siblings under same parent + try: + parent = getattr(self, '_parent', None) + if parent is not None: + for sib in list(getattr(parent, '_children', []) or []): + try: + if sib is not self and getattr(sib, 'widgetClass', None) and sib.widgetClass() == 'YRadioButton': + try: + sib._is_checked = False + except Exception: + pass + except Exception: + pass + except Exception: + pass + + # Post notification event + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + try: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + else: + try: + print(f"RadioButton selected (no dialog): {self._label}") + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index de605ba..0786760 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -209,3 +209,7 @@ def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): def createProgressBar(self, parent, label, max_value=100): """Create a Progress Bar widget.""" return YProgressBarCurses(parent, label, max_value) + + def createRadioButton(self, parent, label="", isChecked=False): + """Create a Radio Button widget.""" + return YRadioButtonCurses(parent, label, isChecked) \ No newline at end of file From 941ebb72e794a2a9ba0487495f9c6becf445cbd5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 14 Dec 2025 21:28:12 +0100 Subject: [PATCH 160/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index def0abd..4e0dd80 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -23,7 +23,7 @@ Missing Widgets comparing libyui: [ ] YPackageSelector [ ] YSpacing, YAlignment [ ] YReplacePoint - [ ] YRadioButton, YRadioButtonGroup + [X] YRadioButton, (YRadioButtonGroup avoid by now) To check how to manage YEvents [X] and YItems [ ] (verify selection attirbute). From c6e9b064a14f9c3f69afead322107346be754ca7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 16 Dec 2025 23:40:18 +0100 Subject: [PATCH 161/523] added addItem and deleteAllItems --- manatools/aui/backends/qt/selectionboxqt.py | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index d678caf..6fc7e7d 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -129,6 +129,51 @@ def _set_backend_enabled(self, enabled): except Exception: pass + def addItem(self, item): + """Add a single item to the selection box (model + Qt view).""" + # Let base class normalize strings to YItem and append to _items + super().addItem(item) + + # The newly added item is the last in the model list + try: + new_item = self._items[-1] + except Exception: + return + + # Ensure index is set + try: + new_item.setIndex(len(self._items) - 1) + except Exception: + pass + + # If the backend list widget exists, append a visual entry + try: + if getattr(self, '_list_widget', None) is not None: + try: + self._list_widget.addItem(new_item.label()) + # If the item is marked selected in the model, reflect it + if new_item.selected(): + idx = self._list_widget.count() - 1 + list_item = self._list_widget.item(idx) + if list_item is not None: + list_item.setSelected(True) + if new_item not in self._selected_items: + self._selected_items.append(new_item) + if not self._value: + self._value = new_item.label() + except Exception: + pass + except Exception: + pass + + def addItems(self, items): + """Add multiple items to the selection box.""" + for it in items: + try: + self.addItem(it) + except Exception: + pass + def _on_selection_changed(self): """Handle selection change in the list widget""" if hasattr(self, '_list_widget') and self._list_widget: @@ -152,3 +197,21 @@ def _on_selection_changed(self): dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) except Exception: pass + + def deleteAllItems(self): + """Remove all items from the selection box, both in the model and the Qt view.""" + # Clear internal model state + super().deleteAllItems() + self._value = "" + self._selected_items = [] + + # Clear Qt list widget if present + try: + if getattr(self, '_list_widget', None) is not None: + try: + self._list_widget.clear() + except Exception: + pass + except Exception: + pass + From c6594a186a58a6e21c32ec8d7580dcddcd59e665 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 16 Dec 2025 23:41:42 +0100 Subject: [PATCH 162/523] Changing items example --- test/test_selectionbox-changing-items.py | 136 +++++++++++++++++++++++ test/test_selectionbox-vbox.py | 97 ---------------- 2 files changed, 136 insertions(+), 97 deletions(-) create mode 100644 test/test_selectionbox-changing-items.py delete mode 100644 test/test_selectionbox-vbox.py diff --git a/test/test_selectionbox-changing-items.py b/test/test_selectionbox-changing-items.py new file mode 100644 index 0000000..c24adee --- /dev/null +++ b/test/test_selectionbox-changing-items.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_selectionbox(backend_name=None): + """Test ComboBox widget specifically""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + ui.app().setApplicationTitle(f"SelectionBox {backend.value} Test") + + dialog = factory.createPopupDialog() + vbox = factory.createVBox( dialog ) + # Radio buttons selector for menu category (use factory abstraction) + radios_container = factory.createHBox(vbox) + rb1 = factory.createRadioButton(radios_container, "First courses", True) + rb2 = factory.createRadioButton(radios_container, "Second courses", False) + rb3 = factory.createRadioButton(radios_container, "Desserts", False) + + selBox = factory.createSelectionBox( vbox, "Menu" ) + + # Default: first courses + selBox.addItem( "Spaghetti Carbonara" ) + selBox.addItem( "Penne Arrabbiata" ) + selBox.addItem( "Fettuccine" ) + selBox.addItem( "Lasagna" ) + selBox.addItem( "Ravioli" ) + selBox.addItem( "Trofie al pesto" ) # Ligurian specialty + + hbox = factory.createHBox( vbox ) + checkBox = factory.createCheckBox( hbox, "Notify on change", selBox.notify() ) + factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) + valueField = factory.createLabel(hbox, "") + #valueField.setStretchable( yui.YD_HORIZ, True ) # // allow stretching over entire dialog width + + valueButton = factory.createPushButton( vbox, "Value" ) + #factory.createVSpacing( vbox, 0.3 ) + + #rightAlignment = factory.createRight( vbox ) TODO + closeButton = factory.createPushButton( vbox, "Close" ) + + # + # Event loop + # + valueField.setText( "???" ) + while True: + event = dialog.waitForEvent() + if not event: + print("Empty") + next + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == closeButton: + dialog.destroy() + break + elif (wdg == valueButton): + item = selBox.selectedItem() + valueField.setText( item.label() if item else "" ) + elif (wdg == checkBox): + selBox.setNotify( checkBox.value() ) + elif (wdg == selBox): # selBox will only send events with setNotify() TODO + valueField.setText(selBox.value()) + elif wdg in (rb1, rb2, rb3): + # Category changed: replace selection box items accordingly + try: + selBox.deleteAllItems() + except Exception: + pass + try: + if wdg == rb1: + # First courses (default) + selBox.addItem( "Spaghetti Carbonara" ) + selBox.addItem( "Penne Arrabbiata" ) + selBox.addItem( "Fettuccine" ) + selBox.addItem( "Lasagna" ) + selBox.addItem( "Ravioli" ) + selBox.addItem( "Trofie al pesto" ) + elif wdg == rb2: + # Second courses: 4 meat, 2 vegan + selBox.addItem( "Beef Steak" ) + selBox.addItem( "Roast Chicken" ) + selBox.addItem( "Pork Chops" ) + selBox.addItem( "Lamb Ribs" ) + selBox.addItem( "Vegan Burger" ) + selBox.addItem( "Grilled Tofu" ) + elif wdg == rb3: + # Desserts: 3 typical American desserts + selBox.addItem( "Apple Pie" ) + selBox.addItem( "Cheesecake" ) + selBox.addItem( "Brownies" ) + except Exception: + pass + # update display to first item + try: + first = selBox.selectedItem() + valueField.setText(first.label() if first else "") + except Exception: + try: + valueField.setText("") + except Exception: + pass + + except Exception as e: + print(f"Error testing ComboBox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_selectionbox(sys.argv[1]) + else: + test_selectionbox() + + + + diff --git a/test/test_selectionbox-vbox.py b/test/test_selectionbox-vbox.py deleted file mode 100644 index 15de8c4..0000000 --- a/test/test_selectionbox-vbox.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys - -# Add parent directory to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -def test_selectionbox(backend_name=None): - """Test ComboBox widget specifically""" - if backend_name: - print(f"Setting backend to: {backend_name}") - os.environ['YUI_BACKEND'] = backend_name - else: - print("Using auto-detection") - - try: - from manatools.aui.yui import YUI, YUI_ui - import manatools.aui.yui_common as yui - - # Force re-detection - YUI._instance = None - YUI._backend = None - - backend = YUI.backend() - print(f"Using backend: {backend.value}") - - ui = YUI_ui() - factory = ui.widgetFactory() - - -############### - dialog = factory.createPopupDialog() - vbox = factory.createVBox( dialog ) - hbox = factory.createHBox( vbox ) - selBox = factory.createSelectionBox( vbox, "Menu" ) - - selBox.addItem( "Pizza Margherita" ) - selBox.addItem( "Pizza Capricciosa" ) - selBox.addItem( "Pizza Funghi" ) - selBox.addItem( "Pizza Prosciutto" ) - selBox.addItem( "Pizza Quattro Stagioni" ) - selBox.addItem( "Calzone" ) - - checkBox = factory.createCheckBox( hbox, "Notify on change", selBox.notify() ) - - hbox = factory.createHBox( vbox ) - factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) - valueField = factory.createLabel(vbox, "") - #valueField.setStretchable( yui.YD_HORIZ, True ) # // allow stretching over entire dialog width - - valueButton = factory.createPushButton( hbox, "Value" ) - #factory.createVSpacing( vbox, 0.3 ) - - #rightAlignment = factory.createRight( vbox ) TODO - closeButton = factory.createPushButton( vbox, "Close" ) - - # - # Event loop - # - valueField.setText( "???" ) - while True: - event = dialog.waitForEvent() - if not event: - print("Empty") - next - typ = event.eventType() - if typ == yui.YEventType.CancelEvent: - dialog.destroy() - break - elif typ == yui.YEventType.WidgetEvent: - wdg = event.widget() - if wdg == closeButton: - dialog.destroy() - break - elif (wdg == valueButton): - item = selBox.selectedItem() - valueField.setText( item.label() if item else "" ) - elif (wdg == checkBox): - selBox.setNotify( checkBox.value() ) - elif (wdg == selBox): # selBox will only send events with setNotify() TODO - valueField.setText(selBox.value()) - - except Exception as e: - print(f"Error testing ComboBox with backend {backend_name}: {e}") - import traceback - traceback.print_exc() - -if __name__ == "__main__": - if len(sys.argv) > 1: - test_selectionbox(sys.argv[1]) - else: - test_selectionbox() - - - - From 01d0f7422ea4d33c42d3d99d06551e40a1889687 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 16 Dec 2025 23:47:38 +0100 Subject: [PATCH 163/523] Added AddItems --- manatools/aui/yui_common.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index a0ef04e..848dfe3 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -303,6 +303,11 @@ def addItem(self, item): if isinstance(item, str): item = YItem(item) self._items.append(item) + + def addItems(self, items): + """Add multiple items to the selection widget.""" + for it in items: + self.addItem(it) def deleteAllItems(self): self._items.clear() From 5a9fb15d647d863260abbb1da6a1228c6a0f4e35 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 16 Dec 2025 23:47:51 +0100 Subject: [PATCH 164/523] removed addItems --- manatools/aui/backends/qt/selectionboxqt.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index 6fc7e7d..5f0afa2 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -166,14 +166,6 @@ def addItem(self, item): except Exception: pass - def addItems(self, items): - """Add multiple items to the selection box.""" - for it in items: - try: - self.addItem(it) - except Exception: - pass - def _on_selection_changed(self): """Handle selection change in the list widget""" if hasattr(self, '_list_widget') and self._list_widget: From 15723033111313044f6531edf7df954c83c2b701 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 16 Dec 2025 23:48:05 +0100 Subject: [PATCH 165/523] added deleteAllItems and addItem --- manatools/aui/backends/gtk/selectionboxgtk.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 05a2997..0c973a4 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -417,3 +417,97 @@ def _on_selected_rows_changed(self, listbox): if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + + def deleteAllItems(self): + """Remove all items from the selection box (model + GTK view).""" + # Clear internal model + super().deleteAllItems() + self._value = "" + self._selected_items = [] + + # Clear GTK rows/listbox + try: + rows = list(getattr(self, '_rows', []) or []) + for r in rows: + try: + if getattr(self, '_listbox', None) is not None: + try: + self._listbox.remove(r) + except Exception: + try: + # fallback: unparent the row + r.unparent() + except Exception: + pass + except Exception: + pass + self._rows = [] + except Exception: + pass + + def addItem(self, item): + """Add a single item to the selection box (model + GTK view).""" + super().addItem(item) + try: + new_item = self._items[-1] + except Exception: + return + + try: + new_item.setIndex(len(self._items) - 1) + except Exception: + pass + + # If listbox exists, create a new row and append + try: + if getattr(self, '_listbox', None) is not None: + row = Gtk.ListBoxRow() + lbl = Gtk.Label(label=new_item.label() or "") + try: + if hasattr(lbl, "set_xalign"): + lbl.set_xalign(0.0) + except Exception: + pass + try: + row.set_child(lbl) + except Exception: + try: + row.add(lbl) + except Exception: + pass + try: + row.set_selectable(True) + except Exception: + pass + + # reflect selected state + try: + if new_item.selected(): + try: + row.set_selected(True) + except Exception: + try: + setattr(row, '_selected_flag', True) + except Exception: + pass + if new_item not in self._selected_items: + self._selected_items.append(new_item) + if not self._value: + self._value = new_item.label() + except Exception: + pass + + try: + self._listbox.append(row) + except Exception: + try: + self._listbox.add(row) + except Exception: + pass + try: + self._rows.append(row) + except Exception: + pass + except Exception: + pass + From 2df80a7a3f1b6379299608d0270423aff4526d85 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 18 Dec 2025 22:10:25 +0100 Subject: [PATCH 166/523] Fixing Qt selection box item selection --- manatools/aui/backends/qt/selectionboxqt.py | 63 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index 5f0afa2..0744ae5 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -102,7 +102,46 @@ def _create_backend_widget(self): # Add items to list widget for item in self._items: list_widget.addItem(item.label()) - + + # Reflect model's selected flags into the view. + # If multi-selection is enabled, select all items flagged selected. + # If single-selection, only the last item with selected()==True should be selected. + try: + if self._multi_selection: + for idx, item in enumerate(self._items): + try: + if item.selected(): + li = list_widget.item(idx) + if li is not None: + li.setSelected(True) + if item not in self._selected_items: + self._selected_items.append(item) + if not self._value: + self._value = item.label() + except Exception: + pass + else: + last_selected_idx = None + for idx, item in enumerate(self._items): + try: + if item.selected(): + last_selected_idx = idx + except Exception: + pass + if last_selected_idx is not None: + li = list_widget.item(last_selected_idx) + if li is not None: + list_widget.setCurrentItem(li) + li.setSelected(True) + # update model internal selection list and value + try: + self._selected_items = [self._items[last_selected_idx]] + self._value = self._items[last_selected_idx].label() + except Exception: + pass + except Exception: + pass + list_widget.itemSelectionChanged.connect(self._on_selection_changed) layout.addWidget(list_widget) @@ -151,16 +190,34 @@ def addItem(self, item): if getattr(self, '_list_widget', None) is not None: try: self._list_widget.addItem(new_item.label()) - # If the item is marked selected in the model, reflect it + # If the item is marked selected in the model, reflect it. if new_item.selected(): idx = self._list_widget.count() - 1 list_item = self._list_widget.item(idx) if list_item is not None: + # For single-selection, clear previous selections so + # only the newly-added selected item remains selected. + if not self._multi_selection: + try: + self._list_widget.clearSelection() + except Exception: + pass + # Also clear model-side selected flags for other items + for it in self._items[:-1]: + try: + it.setSelected(False) + except Exception: + pass + self._selected_items = [] + list_item.setSelected(True) if new_item not in self._selected_items: self._selected_items.append(new_item) - if not self._value: + # Update value to the newly selected item (single or last) + try: self._value = new_item.label() + except Exception: + pass except Exception: pass except Exception: From b8a80dace1fdd3cfe6a980673aca817e7f78452e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 18 Dec 2025 22:17:04 +0100 Subject: [PATCH 167/523] fixing on selection change setting YItem accordingly --- manatools/aui/backends/qt/selectionboxqt.py | 68 ++++++++++++++++++--- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index 0744ae5..6e66ec7 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -58,12 +58,40 @@ def selectItem(self, item, selected=True): list_item = self._list_widget.item(i) if list_item.text() == item.label(): if selected: + # If single-selection, clear model flags for other items + if not self._multi_selection: + for it in self._items: + try: + if it is not item: + it.setSelected(False) + except Exception: + pass + try: + # clear visual selection + self._list_widget.clearSelection() + except Exception: + pass + self._selected_items = [] + self._list_widget.setCurrentItem(list_item) + try: + item.setSelected(True) + except Exception: + pass if item not in self._selected_items: self._selected_items.append(item) else: + try: + item.setSelected(False) + except Exception: + pass if item in self._selected_items: self._selected_items.remove(item) + # ensure internal state and notify + try: + self._on_selection_changed() + except Exception: + pass break def setMultiSelection(self, enabled): @@ -226,17 +254,43 @@ def addItem(self, item): def _on_selection_changed(self): """Handle selection change in the list widget""" if hasattr(self, '_list_widget') and self._list_widget: - # Update selected items - self._selected_items = [] + # Update model.selected flags and selected items list selected_indices = [index.row() for index in self._list_widget.selectedIndexes()] - - for idx in selected_indices: - if idx < len(self._items): - self._selected_items.append(self._items[idx]) - + new_selected = [] + for idx, it in enumerate(self._items): + try: + if idx in selected_indices: + try: + it.setSelected(True) + except Exception: + pass + new_selected.append(it) + else: + try: + it.setSelected(False) + except Exception: + pass + except Exception: + pass + + # In single-selection mode ensure only one model item remains selected + if not self._multi_selection and len(new_selected) > 1: + # Keep only the last selected (mimic addItem logic and selection expectations) + last = new_selected[-1] + for it in list(new_selected)[:-1]: + try: + it.setSelected(False) + except Exception: + pass + new_selected = [last] + + self._selected_items = new_selected + # Update value to first selected item if self._selected_items: self._value = self._selected_items[0].label() + else: + self._value = "" # Post selection-changed event to containing dialog try: From 0e2e1bf7cc12048394d22d43317b23b248bc7ec6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 18 Dec 2025 23:43:28 +0100 Subject: [PATCH 168/523] Managing selected item --- test/test_selectionbox-changing-items.py | 53 ++++++++++++++---------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/test/test_selectionbox-changing-items.py b/test/test_selectionbox-changing-items.py index c24adee..aaf7c65 100644 --- a/test/test_selectionbox-changing-items.py +++ b/test/test_selectionbox-changing-items.py @@ -35,14 +35,35 @@ def test_selectionbox(backend_name=None): selBox = factory.createSelectionBox( vbox, "Menu" ) - # Default: first courses - selBox.addItem( "Spaghetti Carbonara" ) - selBox.addItem( "Penne Arrabbiata" ) - selBox.addItem( "Fettuccine" ) - selBox.addItem( "Lasagna" ) - selBox.addItem( "Ravioli" ) - selBox.addItem( "Trofie al pesto" ) # Ligurian specialty + firstCourses = [ + yui.YItem("Spaghetti Carbonara"), + yui.YItem("Penne Arrabbiata"), + yui.YItem("Fettuccine"), + yui.YItem("Lasagna"), + yui.YItem("Ravioli"), + yui.YItem("Trofie al pesto") # Ligurian specialty + ] + firstCourses[0].setSelected( True ) + secondCourses = [ + yui.YItem("Beef Steak"), + yui.YItem("Roast Chicken"), + yui.YItem("Pork Chops"), + yui.YItem("Lamb Ribs"), + yui.YItem("Vegan Burger"), + yui.YItem("Grilled Tofu") + ] + secondCourses[0].setSelected( True ) + desserts = [ + yui.YItem("Apple Pie"), + yui.YItem("Ice Cream"), + yui.YItem("Cheesecake"), + yui.YItem("Brownies") + ] + desserts[0].setSelected( True ) + # Default: first courses + selBox.addItems( firstCourses ) + hbox = factory.createHBox( vbox ) checkBox = factory.createCheckBox( hbox, "Notify on change", selBox.notify() ) factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) @@ -89,25 +110,13 @@ def test_selectionbox(backend_name=None): try: if wdg == rb1: # First courses (default) - selBox.addItem( "Spaghetti Carbonara" ) - selBox.addItem( "Penne Arrabbiata" ) - selBox.addItem( "Fettuccine" ) - selBox.addItem( "Lasagna" ) - selBox.addItem( "Ravioli" ) - selBox.addItem( "Trofie al pesto" ) + selBox.addItems( firstCourses ) elif wdg == rb2: # Second courses: 4 meat, 2 vegan - selBox.addItem( "Beef Steak" ) - selBox.addItem( "Roast Chicken" ) - selBox.addItem( "Pork Chops" ) - selBox.addItem( "Lamb Ribs" ) - selBox.addItem( "Vegan Burger" ) - selBox.addItem( "Grilled Tofu" ) + selBox.addItems( secondCourses ) elif wdg == rb3: # Desserts: 3 typical American desserts - selBox.addItem( "Apple Pie" ) - selBox.addItem( "Cheesecake" ) - selBox.addItem( "Brownies" ) + selBox.addItems( desserts ) except Exception: pass # update display to first item From c7d1f9d931d7bc8d3f172821fc511402a2ec56f8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 18 Dec 2025 23:43:49 +0100 Subject: [PATCH 169/523] Start fixing selection --- manatools/aui/backends/gtk/selectionboxgtk.py | 205 ++++++++++++++---- 1 file changed, 165 insertions(+), 40 deletions(-) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 0c973a4..1091d47 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -66,8 +66,25 @@ def setValue(self, text): row.set_selected(False) except Exception: pass - # notify - self._on_selection_changed() + + # rebuild internal selection state and notify + try: + self._selected_items = [] + for i, r in enumerate(getattr(self, "_rows", [])): + try: + if self._row_is_selected(r) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + self._value = self._selected_items[0].label() if self._selected_items else None + except Exception: + self._selected_items = [] + self._value = None + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) def selectedItems(self): return list(self._selected_items) @@ -98,7 +115,24 @@ def selectItem(self, item, selected=True): except Exception: pass break - self._on_selection_changed() + # rebuild internal selection state and notify + try: + self._selected_items = [] + for i, r in enumerate(getattr(self, "_rows", [])): + try: + if self._row_is_selected(r) and i < len(self._items): + self._selected_items.append(self._items[i]) + except Exception: + pass + self._value = self._selected_items[0].label() if self._selected_items else None + except Exception: + self._selected_items = [] + self._value = None + + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) @@ -204,6 +238,59 @@ def _create_backend_widget(self): self._rows.append(row) listbox.append(row) + # Reflect items' selected flags into the view/model. + try: + if self._multi_selection: + for idx, it in enumerate(self._items): + try: + if it.selected(): + try: + self._rows[idx].set_selected(True) + except Exception: + try: + setattr(self._rows[idx], '_selected_flag', True) + except Exception: + pass + if it not in self._selected_items: + self._selected_items.append(it) + if not self._value: + self._value = it.label() + except Exception: + pass + else: + last_selected_idx = None + for idx, it in enumerate(self._items): + try: + if it.selected(): + last_selected_idx = idx + except Exception: + pass + if last_selected_idx is not None: + try: + for i, r in enumerate(self._rows): + try: + if i == last_selected_idx: + listbox.select_row( r ) + except Exception: + try: + setattr(r, '_selected_flag', (i == last_selected_idx)) + except Exception: + pass + try: + for i, it in enumerate(self._items): + try: + it.setSelected(i == last_selected_idx) + except Exception: + pass + except Exception: + pass + self._selected_items = [self._items[last_selected_idx]] + self._value = self._items[last_selected_idx].label() + except Exception: + pass + except Exception: + pass + sw = Gtk.ScrolledWindow() # allow scrolled window to expand vertically and horizontally try: @@ -317,36 +404,40 @@ def _on_row_selected(self, listbox, row): the provided row and rebuilds the selected items list. """ try: - if row is not None: - if self._multi_selection: - # toggle selection state for this row - try: - cur = self._row_is_selected(row) - try: - row.set_selected(not cur) - except Exception: - # fallback: store a flag when set_selected isn't available - setattr(row, "_selected_flag", not cur) - except Exception: - pass - else: - # single-selection: select provided row and deselect others - for r in getattr(self, "_rows", []): - try: - r.set_selected(r is row) - except Exception: - try: - setattr(r, "_selected_flag", (r is row)) - except Exception: - pass + #if row is not None: + # if self._multi_selection: + # # toggle selection state for this row + # try: + # cur = self._row_is_selected(row) + # try: + # if not cur: + # self._listbox.select_row( row ) + # except Exception: + # # fallback: store a flag when set_selected isn't available + # setattr(row, "_selected_flag", not cur) + # except Exception: + # pass + # else: + # # single-selection: select provided row and deselect others + # for r in getattr(self, "_rows", []): + # try: + # if r is row: + # self._listbox.select_row( r ) + # except Exception: + # try: + # setattr(r, "_selected_flag", (r is row)) + # except Exception: + # pass # rebuild selected_items scanning cached rows (works for both modes) self._selected_items = [] for i, r in enumerate(getattr(self, "_rows", [])): try: - if self._row_is_selected(r) and i < len(self._items): + if row is r and i < len(self._items): self._selected_items.append(self._items[i]) + self._items[i].setSelected( True ) except Exception: + print("Failed to check row selection") pass self._value = self._selected_items[0].label() if self._selected_items else None @@ -480,34 +571,68 @@ def addItem(self, item): except Exception: pass - # reflect selected state + # append the row first so selection APIs operate on attached rows + try: + try: + self._listbox.append(row) + except Exception: + try: + self._listbox.add(row) + except Exception: + pass + except Exception: + pass + try: + self._rows.append(row) + except Exception: + pass + + # reflect selected state (no notification on add) try: if new_item.selected(): + # single-selection: clear previous selections + if not self._multi_selection: + try: + for r in getattr(self, '_rows', []): + try: + self._listbox.unselect_row( r ) + #r.set_selected(False) + except Exception: + print("Failed to deselect row") + try: + setattr(r, '_selected_flag', False) + except Exception: + pass + except Exception: + pass + try: + for it in self._items[:-1]: + try: + it.setSelected(False) + except Exception: + pass + except Exception: + pass + try: - row.set_selected(True) + self._listbox.select_row( row ) except Exception: try: setattr(row, '_selected_flag', True) except Exception: pass + + try: + new_item.setSelected(True) + except Exception: + pass + if new_item not in self._selected_items: self._selected_items.append(new_item) if not self._value: self._value = new_item.label() except Exception: pass - - try: - self._listbox.append(row) - except Exception: - try: - self._listbox.add(row) - except Exception: - pass - try: - self._rows.append(row) - except Exception: - pass except Exception: pass From a642ec7b8a53b4ff762245d07fb8beae90c7a94e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 18 Dec 2025 23:53:08 +0100 Subject: [PATCH 170/523] Managed item selection --- .../aui/backends/curses/selectionboxcurses.py | 148 +++++++++++++++++- 1 file changed, 144 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index be97e10..25cccb1 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -54,8 +54,35 @@ def value(self): def setValue(self, text): """Select first item matching text.""" self._value = text - # update selected_items - self._selected_items = [it for it in self._items if it.label() == text][:1] + # update model flags and selected_items + self._selected_items = [] + try: + for it in self._items: + try: + if it.label() == text: + try: + it.setSelected(True) + except Exception: + pass + self._selected_items.append(it) + else: + if not self._multi_selection: + try: + it.setSelected(False) + except Exception: + pass + except Exception: + pass + if not self._multi_selection and len(self._selected_items) > 1: + last = self._selected_items[-1] + for it in list(self._selected_items)[:-1]: + try: + it.setSelected(False) + except Exception: + pass + self._selected_items = [last] + except Exception: + pass # update hover to first matching index for idx, it in enumerate(self._items): if it.label() == text: @@ -80,14 +107,33 @@ def selectItem(self, item, selected=True): if selected: if not self._multi_selection: + # clear other model flags + for it in self._items: + try: + if it is not self._items[idx]: + it.setSelected(False) + except Exception: + pass self._selected_items = [self._items[idx]] self._value = self._items[idx].label() + try: + self._items[idx].setSelected(True) + except Exception: + pass else: if self._items[idx] not in self._selected_items: self._selected_items.append(self._items[idx]) + try: + self._items[idx].setSelected(True) + except Exception: + pass else: if self._items[idx] in self._selected_items: self._selected_items.remove(self._items[idx]) + try: + self._items[idx].setSelected(False) + except Exception: + pass self._value = self._selected_items[0].label() if self._selected_items else "" # ensure hover and scroll reflect this item @@ -143,6 +189,37 @@ def _create_backend_widget(self): self._ensure_hover_visible() # reset the cached visible rows so future navigation uses the next draw's value self._current_visible_rows = None + # Reflect model YItem.selected flags into internal state so selection is visible + try: + sel = [] + if self._multi_selection: + for it in self._items: + try: + if it.selected(): + sel.append(it) + except Exception: + pass + else: + last = None + for it in self._items: + try: + if it.selected(): + last = it + except Exception: + pass + if last is not None: + sel = [last] + self._selected_items = sel + self._value = self._selected_items[0].label() if self._selected_items else "" + if self._selected_items: + try: + idx = self._items.index(self._selected_items[0]) + self._hover_index = idx + self._ensure_hover_visible() + except Exception: + pass + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable selection box: affect focusability and propagate to row items.""" @@ -259,17 +336,38 @@ def _handle_key(self, key): if 0 <= self._hover_index < len(self._items): item = self._items[self._hover_index] if self._multi_selection: - # toggle membership + # toggle membership and update model flag if item in self._selected_items: self._selected_items.remove(item) + try: + item.setSelected(False) + except Exception: + pass else: self._selected_items.append(item) + try: + item.setSelected(True) + except Exception: + pass # update primary value to first selected or empty self._value = self._selected_items[0].label() if self._selected_items else "" else: - # single selection: set as sole selected + # single selection: set as sole selected and clear other model flags + try: + for it in self._items: + try: + if it is not item: + it.setSelected(False) + except Exception: + pass + except Exception: + pass self._selected_items = [item] self._value = item.label() + try: + item.setSelected(True) + except Exception: + pass # notify dialog of selection change try: if getattr(self, "notify", lambda: True)(): @@ -282,3 +380,45 @@ def _handle_key(self, key): handled = False return handled + + + def addItem(self, item): + """Add item to model; if item has selected flag, update internal selection state. + + Do not emit notification on add. + """ + super().addItem(item) + try: + new_item = self._items[-1] + except Exception: + return + try: + new_item.setIndex(len(self._items) - 1) + except Exception: + pass + + try: + if new_item.selected(): + if not self._multi_selection: + try: + for it in self._items[:-1]: + try: + it.setSelected(False) + except Exception: + pass + except Exception: + pass + self._selected_items = [] + + try: + if new_item not in self._selected_items: + self._selected_items.append(new_item) + except Exception: + pass + + try: + self._value = new_item.label() + except Exception: + pass + except Exception: + pass From e9261980298b58d2af5db91ef8b08b8cd87d9b94 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 20 Dec 2025 17:11:21 +0100 Subject: [PATCH 171/523] fixed item selection and multi-selection mode --- manatools/aui/backends/gtk/selectionboxgtk.py | 105 ++++++------------ 1 file changed, 35 insertions(+), 70 deletions(-) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 1091d47..92398af 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -26,6 +26,7 @@ def __init__(self, parent=None, label=""): self._label = label self._value = "" self._selected_items = [] + self._old_selected_items = [] # for change detection self._multi_selection = False self._listbox = None self._backend_widget = None @@ -163,12 +164,10 @@ def setMultiSelection(self, enabled): try: hid = self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) self._signal_handlers['selected-rows-changed'] = hid - except Exception: - try: - hid = self._listbox.connect("row-selected", lambda lb, row: self._on_selected_rows_changed(lb)) - self._signal_handlers['row-selected_for_multi'] = hid - except Exception: - pass + hid = self._listbox.connect("row-activated", lambda lb, row: self._on_row_selected_for_multi(lb, row)) + self._signal_handlers['row-selected_for_multi'] = hid + except Exception: + pass else: try: hid = self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) @@ -245,12 +244,9 @@ def _create_backend_widget(self): try: if it.selected(): try: - self._rows[idx].set_selected(True) + listbox.select_row( self._rows[idx] ) except Exception: - try: - setattr(self._rows[idx], '_selected_flag', True) - except Exception: - pass + pass if it not in self._selected_items: self._selected_items.append(it) if not self._value: @@ -326,33 +322,12 @@ def _create_backend_widget(self): vbox.append(sw) except Exception: vbox.add(sw) - - # connect selection signal: choose appropriate signal per selection mode - # store handler ids so we can disconnect later if selection mode changes at runtime - self._signal_handlers = {} - try: - # ensure any previous handlers are disconnected (defensive) - try: - for hid in list(self._signal_handlers.values()): - if hid and isinstance(hid, int): - try: - listbox.disconnect(hid) - except Exception: - pass - except Exception: - pass - - # Use row-selected for both single and multi modes; handler will toggle for multi - try: - hid = listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) - self._signal_handlers['row-selected'] = hid - except Exception: - pass - except Exception: - pass - + self._backend_widget = vbox self._listbox = listbox + # connect selection signal: choose appropriate signal per selection mode + # if multi-selection has been set before widget creation, ensure correct mode + self.setMultiSelection( self._multi_selection ) self._backend_widget.set_sensitive(self._enabled) def _set_backend_enabled(self, enabled): @@ -404,44 +379,17 @@ def _on_row_selected(self, listbox, row): the provided row and rebuilds the selected items list. """ try: - #if row is not None: - # if self._multi_selection: - # # toggle selection state for this row - # try: - # cur = self._row_is_selected(row) - # try: - # if not cur: - # self._listbox.select_row( row ) - # except Exception: - # # fallback: store a flag when set_selected isn't available - # setattr(row, "_selected_flag", not cur) - # except Exception: - # pass - # else: - # # single-selection: select provided row and deselect others - # for r in getattr(self, "_rows", []): - # try: - # if r is row: - # self._listbox.select_row( r ) - # except Exception: - # try: - # setattr(r, "_selected_flag", (r is row)) - # except Exception: - # pass - # rebuild selected_items scanning cached rows (works for both modes) + old_item = self._selected_items[0] if self._selected_items else None + if old_item: + old_item.setSelected( False ) + idx = self._rows.index(row) self._selected_items = [] - for i, r in enumerate(getattr(self, "_rows", [])): - try: - if row is r and i < len(self._items): - self._selected_items.append(self._items[i]) - self._items[i].setSelected( True ) - except Exception: - print("Failed to check row selection") - pass - + self._selected_items.append(self._items[idx]) + self._items[idx].setSelected( True ) self._value = self._selected_items[0].label() if self._selected_items else None except Exception: + print("SelectionBoxGTK: failed to process row-selected event") # be defensive self._selected_items = [] self._value = None @@ -451,6 +399,20 @@ def _on_row_selected(self, listbox, row): if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + def _on_row_selected_for_multi(self, listbox, row): + """ + Handler for row selection in multi-selection mode: for de-selection. + """ + sel_rows = listbox.get_selected_rows() + idx = self._rows.index(row) + if self._items[idx] in self._old_selected_items: + self._listbox.unselect_row( row ) + self._items[idx].setSelected( False ) + self._on_selected_rows_changed(listbox) + else: + self._old_selected_items = self._selected_items + + def _on_selected_rows_changed(self, listbox): """ Handler for multi-selection (or bulk selection change). Rebuild selected list @@ -466,6 +428,7 @@ def _on_selected_rows_changed(self, listbox): except Exception: sel_rows = None + self._old_selected_items = self._selected_items self._selected_items = [] if sel_rows: # sel_rows may be list of Row objects or Paths; try to match by identity @@ -477,6 +440,7 @@ def _on_selected_rows_changed(self, listbox): idx = self._rows.index(r) if idx < len(self._items): self._selected_items.append(self._items[idx]) + self._items[idx].setSelected( True ) except Exception: pass else: @@ -485,6 +449,7 @@ def _on_selected_rows_changed(self, listbox): try: if self._row_is_selected(cr) and i < len(self._items): self._selected_items.append(self._items[i]) + self._items[i].setSelected( True ) except Exception: pass except Exception: From dbdb23c5d2dcec4c39d395875c8f05c931d74d5c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 20 Dec 2025 17:41:51 +0100 Subject: [PATCH 172/523] fixed children drawing --- manatools/aui/backends/curses/hboxcurses.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py index ea653f2..6c37fe2 100644 --- a/manatools/aui/backends/curses/hboxcurses.py +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -141,8 +141,15 @@ def _draw(self, window, y, x, width, height): w = widths[i] if w <= 0: continue - # If child is vertically stretchable, give full height; else give its minimum - if child.stretchable(YUIDimension.YD_VERT): + # Give full container height to vertically-stretchable children + # and to nested VBoxes so their internal layout can use the + # available vertical space. Otherwise fall back to the child's + # declared minimal height. + try: + cls = child.widgetClass() if hasattr(child, "widgetClass") else "" + except Exception: + cls = "" + if child.stretchable(YUIDimension.YD_VERT) or cls == "YVBox": ch = height else: ch = min(height, max(1, getattr(child, "_height", 1))) From d5de3343f2794918585e228766eef4ac681e84f8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 20 Dec 2025 18:11:30 +0100 Subject: [PATCH 173/523] clean up the code --- .../aui/backends/curses/selectionboxcurses.py | 48 ++++--------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index 25cccb1..65966f1 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -140,16 +140,6 @@ def selectItem(self, item, selected=True): self._hover_index = idx self._ensure_hover_visible() - if self.notify(): - # notify dialog - try: - if getattr(self, "notify", lambda: True)(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass - def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) # if disabling multi-selection, reduce to first selected item @@ -339,43 +329,25 @@ def _handle_key(self, key): # toggle membership and update model flag if item in self._selected_items: self._selected_items.remove(item) - try: - item.setSelected(False) - except Exception: - pass + item.setSelected(False) else: self._selected_items.append(item) - try: - item.setSelected(True) - except Exception: - pass + item.setSelected(True) # update primary value to first selected or empty self._value = self._selected_items[0].label() if self._selected_items else "" else: # single selection: set as sole selected and clear other model flags - try: - for it in self._items: - try: - if it is not item: - it.setSelected(False) - except Exception: - pass - except Exception: - pass + it = self._selected_items[0] if self._selected_items else None + if it is not None: + it.setSelected(False) self._selected_items = [item] self._value = item.label() - try: - item.setSelected(True) - except Exception: - pass + item.setSelected(True) # notify dialog of selection change - try: - if getattr(self, "notify", lambda: True)(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) else: handled = False From 452436e85120ba758c4daf10cdca59d4a500f034 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 12:10:40 +0100 Subject: [PATCH 174/523] cleanup code and better managing item selection/deselection --- .../aui/backends/curses/selectionboxcurses.py | 90 ++++++------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index 65966f1..6a88369 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -107,33 +107,21 @@ def selectItem(self, item, selected=True): if selected: if not self._multi_selection: - # clear other model flags - for it in self._items: - try: - if it is not self._items[idx]: - it.setSelected(False) - except Exception: - pass - self._selected_items = [self._items[idx]] - self._value = self._items[idx].label() - try: - self._items[idx].setSelected(True) - except Exception: - pass + if item not in self._selected_items: + selected_item = self._selected_items[0] if self._selected_items else None + if selected_item is not None: + selected_item.setSelected(False) + self._selected_items = [item] + self._value = item.label() + item.setSelected(True) else: - if self._items[idx] not in self._selected_items: - self._selected_items.append(self._items[idx]) - try: - self._items[idx].setSelected(True) - except Exception: - pass + if item not in self._selected_items: + self._selected_items.append(item) + item.setSelected(True) else: - if self._items[idx] in self._selected_items: - self._selected_items.remove(self._items[idx]) - try: - self._items[idx].setSelected(False) - except Exception: - pass + if item in self._selected_items: + self._selected_items.remove(item) + item.setSelected(False) self._value = self._selected_items[0].label() if self._selected_items else "" # ensure hover and scroll reflect this item @@ -145,6 +133,8 @@ def setMultiSelection(self, enabled): # if disabling multi-selection, reduce to first selected item if not self._multi_selection and len(self._selected_items) > 1: first = self._selected_items[0] + for it in list(self._selected_items)[1:]: + it.setSelected(False) self._selected_items = [first] self._value = first.label() @@ -174,11 +164,10 @@ def _create_backend_widget(self): # Keep minimal layout height small so parent can give more space. self._height = len(self._items) + (1 if self._label else 0) # reset scroll/hover if out of range - if self._hover_index >= len(self._items): - self._hover_index = max(0, len(self._items) - 1) - self._ensure_hover_visible() + self._hover_index = 0 # reset the cached visible rows so future navigation uses the next draw's value self._current_visible_rows = None + self._ensure_hover_visible() # Reflect model YItem.selected flags into internal state so selection is visible try: sel = [] @@ -194,6 +183,8 @@ def _create_backend_widget(self): for it in self._items: try: if it.selected(): + if last is not None: + last.setSelected(False) last = it except Exception: pass @@ -201,13 +192,6 @@ def _create_backend_widget(self): sel = [last] self._selected_items = sel self._value = self._selected_items[0].label() if self._selected_items else "" - if self._selected_items: - try: - idx = self._items.index(self._selected_items[0]) - self._hover_index = idx - self._ensure_hover_visible() - except Exception: - pass except Exception: pass @@ -356,41 +340,21 @@ def _handle_key(self, key): def addItem(self, item): """Add item to model; if item has selected flag, update internal selection state. - - Do not emit notification on add. + Do not emit notification on add. """ super().addItem(item) try: new_item = self._items[-1] - except Exception: - return - try: new_item.setIndex(len(self._items) - 1) - except Exception: - pass - - try: if new_item.selected(): if not self._multi_selection: - try: - for it in self._items[:-1]: - try: - it.setSelected(False) - except Exception: - pass - except Exception: - pass - self._selected_items = [] - - try: - if new_item not in self._selected_items: - self._selected_items.append(new_item) - except Exception: - pass - - try: + selected = self._selected_items[0] if self._selected_items else None + if selected is not None: + selected.setSelected(False) + self._selected_items = [new_item] self._value = new_item.label() - except Exception: - pass + else: + self._selected_items.append(new_item) + self._value = self._selected_items[0].label() if self._selected_items else "" except Exception: pass From 6d44b3e3513a82cf6d3fb7e8b608b95507e5f46a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 13:56:25 +0100 Subject: [PATCH 175/523] added logging --- manatools/aui/backends/curses/dialogcurses.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index 06cb6ae..69c0231 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -14,15 +14,30 @@ import sys import os import time +import logging from ...yui_common import * from ... import yui as yui_mod +# Module-level safe logging setup for dialog backend; main application may override +_mod_logger = logging.getLogger("manatools.aui.curses.dialog.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YDialogCurses(YSingleChildContainerWidget): _open_dialogs = [] _current_dialog = None def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): super().__init__() + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("YDialogCurses.__init__ dialog_type=%s color_mode=%s", dialog_type, color_mode) self._dialog_type = dialog_type self._color_mode = color_mode self._is_open = False @@ -61,6 +76,7 @@ def open(self): self._is_open = True YDialogCurses._current_dialog = self + self._logger.debug("dialog opened; current_dialog set") # Find first focusable widget focusable = self._find_focusable_widgets() @@ -81,6 +97,10 @@ def destroy(self, doThrow=True): YDialogCurses._open_dialogs.remove(self) if YDialogCurses._current_dialog == self: YDialogCurses._current_dialog = None + try: + self._logger.debug("dialog destroyed; remaining open=%d", len(YDialogCurses._open_dialogs)) + except Exception: + pass return True @classmethod @@ -101,6 +121,10 @@ def currentDialog(cls, doThrow=True): def _create_backend_widget(self): # Use the main screen self._backend_widget = curses.newwin(0, 0, 0, 0) + try: + self._logger.debug("_create_backend_widget created backend window") + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the dialog and propagate to contained widgets.""" From 83333501f3e0b24c5dd5add62dbf6bc61608c5d2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 13:57:17 +0100 Subject: [PATCH 176/523] Added logging, found why first selection box didn't work properly --- .../aui/backends/curses/selectionboxcurses.py | 63 ++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index 6a88369..625d213 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -14,12 +14,29 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level safe logging setup: add a StreamHandler by default only if +# the root logger has no handlers so main can fully configure logging later. +_mod_logger = logging.getLogger("manatools.aui.curses.selectionbox.module") +if not logging.getLogger().handlers: + h = logging.StreamHandler() + h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(h) + _mod_logger.setLevel(logging.INFO) + class YSelectionBoxCurses(YSelectionWidget): def __init__(self, parent=None, label=""): super().__init__(parent) + # per-instance logger named by package/backend/class + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + # ensure instance logger at least inherits module handler if root not configured + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("YSelectionBoxCurses.__init__ label=%s", label) self._label = label self._value = "" self._selected_items = [] @@ -30,7 +47,7 @@ def __init__(self, parent=None, label=""): self._height = 1 # preferred rows used for paging when no draw happened yet self._preferred_rows = 6 - + self._scroll_offset = 0 self._hover_index = 0 # index into self._items (global) self._can_focus = True @@ -192,8 +209,10 @@ def _create_backend_widget(self): sel = [last] self._selected_items = sel self._value = self._selected_items[0].label() if self._selected_items else "" + _mod_logger.debug("_create_backend_widget: <%s> selected_items=<%r> value=<%r>", self.debugLabel(), self._selected_items, self._value) except Exception: pass + self._backend_widget = self def _set_backend_enabled(self, enabled): """Enable/disable selection box: affect focusability and propagate to row items.""" @@ -283,15 +302,18 @@ def _draw(self, window, y, x, width, height): def _handle_key(self, key): if not self._focused or not self.isEnabled(): return False + self._logger.debug("_handle_key called key=%r focused=%s hover_index=%d", key, self._focused, self._hover_index) handled = True if key == curses.KEY_UP: if self._hover_index > 0: self._hover_index -= 1 self._ensure_hover_visible() + self._logger.debug("hover moved up -> %d", self._hover_index) elif key == curses.KEY_DOWN: if self._hover_index < max(0, len(self._items) - 1): self._hover_index += 1 self._ensure_hover_visible() + self._logger.debug("hover moved down -> %d", self._hover_index) elif key == curses.KEY_PPAGE: # PageUp step = self._visible_row_count() or 1 self._hover_index = max(0, self._hover_index - step) @@ -311,22 +333,38 @@ def _handle_key(self, key): item = self._items[self._hover_index] if self._multi_selection: # toggle membership and update model flag - if item in self._selected_items: + was_selected = item in self._selected_items + if was_selected: self._selected_items.remove(item) - item.setSelected(False) + try: + item.setSelected(False) + except Exception: + pass + self._logger.info("item deselected: <%s>", item.label()) else: self._selected_items.append(item) - item.setSelected(True) + try: + item.setSelected(True) + except Exception: + pass + self._logger.info("item selected: <%s>", item.label()) # update primary value to first selected or empty self._value = self._selected_items[0].label() if self._selected_items else "" else: # single selection: set as sole selected and clear other model flags it = self._selected_items[0] if self._selected_items else None if it is not None: - it.setSelected(False) + try: + it.setSelected(False) + except Exception: + pass self._selected_items = [item] self._value = item.label() - item.setSelected(True) + try: + item.setSelected(True) + except Exception: + pass + self._logger.info("single selection set: %s", item.label()) # notify dialog of selection change if self.notify(): dlg = self.findDialog() @@ -346,15 +384,24 @@ def addItem(self, item): try: new_item = self._items[-1] new_item.setIndex(len(self._items) - 1) - if new_item.selected(): + selected_flag = False + try: + selected_flag = bool(new_item.selected()) + except Exception: + selected_flag = False + if selected_flag: if not self._multi_selection: selected = self._selected_items[0] if self._selected_items else None if selected is not None: - selected.setSelected(False) + try: + selected.setSelected(False) + except Exception: + pass self._selected_items = [new_item] self._value = new_item.label() else: self._selected_items.append(new_item) self._value = self._selected_items[0].label() if self._selected_items else "" + self._logger.debug("addItem: label=<%s> selected=<%s> value=<%r>", new_item.label(), selected_flag, self._value) except Exception: pass From 5346c5c259cfe3cc59bb1782486b9d294d9e00b0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 13:59:22 +0100 Subject: [PATCH 177/523] Added another selectionbox example and logging to improve problem debugging --- test/test_selectionbox.py | 38 +++++++++-- test/test_selectionbox2.py | 134 +++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 test/test_selectionbox2.py diff --git a/test/test_selectionbox.py b/test/test_selectionbox.py index 7fa9ffa..ce6df6b 100644 --- a/test/test_selectionbox.py +++ b/test/test_selectionbox.py @@ -5,6 +5,29 @@ # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") def test_selectionbox(backend_name=None): """Test Selection Box widget specifically""" @@ -42,14 +65,14 @@ def test_selectionbox(backend_name=None): selBox.addItem( "Ravioli" ) selBox.addItem( "Trofie al pesto" ) # Ligurian specialty + #selBox.setMultiSelection(True) + vbox = factory.createVBox( hbox ) - align = factory.createTop(vbox) - notifyCheckBox = factory.createCheckBox( align, "Notify on change", selBox.notify() ) + notifyCheckBox = factory.createCheckBox( vbox, "Notify on change", selBox.notify() ) notifyCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) multiSelectionCheckBox = factory.createCheckBox( vbox, "Multi-selection", selBox.multiSelection() ) multiSelectionCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) - align = factory.createBottom( vbox ) - disableSelectionBox = factory.createCheckBox( align, "disable selection box", not selBox.isEnabled() ) + disableSelectionBox = factory.createCheckBox( vbox, "disable selection box", not selBox.isEnabled() ) disableSelectionBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) disableValue = factory.createCheckBox( vbox, "disable value button", False ) disableValue.setStretchable( yui.YUIDimension.YD_HORIZ, True ) @@ -61,6 +84,13 @@ def test_selectionbox(backend_name=None): label.setStretchable( yui.YUIDimension.YD_HORIZ, True ) valueField = factory.createLabel(hbox, "") valueField.setStretchable( yui.YUIDimension.YD_HORIZ, True ) # // allow stretching over entire dialog width + if selBox.multiSelection(): + labels = [item.label() for item in selBox.selectedItems()] + valueField.setText( ", ".join(labels) ) + else: + item = selBox.selectedItem() + valueField.setText( item.label() if item else "" ) + #factory.createVSpacing( vbox, 0.3 ) diff --git a/test/test_selectionbox2.py b/test/test_selectionbox2.py new file mode 100644 index 0000000..9d93e9b --- /dev/null +++ b/test/test_selectionbox2.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + # Avoid adding duplicate file handlers for repeated imports + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + +def test_two_selectionbox(backend_name=None): + """Two selection boxes side by side; label below shows which box and value.""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + ui.app().setApplicationTitle(f"Two SelectionBox {backend.value} Test") + + dialog = factory.createPopupDialog() + mainVbox = factory.createVBox(dialog) + + # HBox with two selection boxes + hbox = factory.createHBox(mainVbox) + sel1 = factory.createSelectionBox(hbox, "First Box") + sel2 = factory.createSelectionBox(hbox, "Second Box") + + # Populate items (use YItem so we can set initial selection) + items1 = [ + yui.YItem("Apple"), + yui.YItem("Banana"), + yui.YItem("Cherry") + ] + items1[1].setSelected(True) + + items2 = [ + yui.YItem("Red"), + yui.YItem("Green"), + yui.YItem("Blue") + ] + items2[1].setSelected(True) + + sel1.addItems(items1) + sel2.addItems(items2) + + # Label below the hbox that reports which box sent the event and its value + label_box = factory.createVBox(mainVbox) + infoLabel = factory.createLabel(label_box, "") + infoLabel.setStretchable(yui.YUIDimension.YD_HORIZ, True) + + # OK button to exit + okButton = factory.createPushButton(label_box, "OK") + + # Initial display + try: + v1 = sel1.value() or (sel1.selectedItem().label() if sel1.selectedItem() else "") + except Exception: + v1 = "" + try: + v2 = sel2.value() or (sel2.selectedItem().label() if sel2.selectedItem() else "") + except Exception: + v2 = "" + infoLabel.setText(f"Box1: {v1} | Box2: {v2}") + + # Event loop + while True: + event = dialog.waitForEvent() + if not event: + continue + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == okButton: + dialog.destroy() + break + elif wdg == sel1 or wdg == sel2: + # Update label with which box and its value + try: + v1 = sel1.value() or (sel1.selectedItem().label() if sel1.selectedItem() else "") + except Exception: + v1 = "" + try: + v2 = sel2.value() or (sel2.selectedItem().label() if sel2.selectedItem() else "") + except Exception: + v2 = "" + who = "Box1" if wdg == sel1 else "Box2" + infoLabel.setText(f"Now({who}: {wdg.value()}) [Box1: {v1}] [Box2: {v2}]") + + except Exception as e: + print(f"Error in test_two_selectionbox with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_two_selectionbox(sys.argv[1]) + else: + test_two_selectionbox() From b4b4ff68f0c9879877ad37de02d6e534420bd2bd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 14:18:42 +0100 Subject: [PATCH 178/523] Added logging --- .../aui/backends/curses/alignmentcurses.py | 27 ++++++++++- .../aui/backends/curses/checkboxcurses.py | 26 ++++++++++- .../backends/curses/checkboxframecurses.py | 45 ++++++++++++++----- .../aui/backends/curses/comboboxcurses.py | 40 ++++++++++++++--- manatools/aui/backends/curses/framecurses.py | 39 +++++++++++++--- manatools/aui/backends/curses/hboxcurses.py | 24 +++++++++- .../aui/backends/curses/inputfieldcurses.py | 31 +++++++++++-- manatools/aui/backends/curses/labelcurses.py | 31 +++++++++++-- .../aui/backends/curses/progressbarcurses.py | 32 +++++++++++-- .../aui/backends/curses/pushbuttoncurses.py | 35 +++++++++++++-- .../aui/backends/curses/radiobuttoncurses.py | 45 ++++++++++++++++--- 11 files changed, 326 insertions(+), 49 deletions(-) diff --git a/manatools/aui/backends/curses/alignmentcurses.py b/manatools/aui/backends/curses/alignmentcurses.py index 58a77e2..648f074 100644 --- a/manatools/aui/backends/curses/alignmentcurses.py +++ b/manatools/aui/backends/curses/alignmentcurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for curses alignment backend +_mod_logger = logging.getLogger("manatools.aui.curses.alignment.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YAlignmentCurses(YSingleChildContainerWidget): """ @@ -27,6 +36,12 @@ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUn self._halign_spec = horAlign self._valign_spec = vertAlign self._backend_widget = None # not used by curses + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ halign=%s valign=%s", self.__class__.__name__, horAlign, vertAlign) self._height = 1 def widgetClass(self): @@ -48,8 +63,16 @@ def stretchable(self, dim: YUIDimension): return False def _create_backend_widget(self): - self._backend_widget = None # no real widget for curses - self._height = max(1, getattr(self.child(), "_height", 1) if self.hasChildren() else 1) + try: + # for curses we don't create a real window; associate backend_widget to self + self._backend_widget = self + self._height = max(1, getattr(self.child(), "_height", 1) if self.hasChildren() else 1) + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable alignment container and propagate to its logical child.""" diff --git a/manatools/aui/backends/curses/checkboxcurses.py b/manatools/aui/backends/curses/checkboxcurses.py index 1785e22..b19fa0d 100644 --- a/manatools/aui/backends/curses/checkboxcurses.py +++ b/manatools/aui/backends/curses/checkboxcurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for curses checkbox backend +_mod_logger = logging.getLogger("manatools.aui.curses.checkbox.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YCheckBoxCurses(YWidget): def __init__(self, parent=None, label="", is_checked=False): @@ -25,6 +34,12 @@ def __init__(self, parent=None, label="", is_checked=False): self._focused = False self._can_focus = True self._height = 1 + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s checked=%s", self.__class__.__name__, label, is_checked) def widgetClass(self): return "YCheckBox" @@ -39,8 +54,15 @@ def label(self): return self._label def _create_backend_widget(self): - # In curses, there's no actual backend widget, just internal state - pass + try: + # In curses, there's no actual backend widget; associate placeholder to self + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable checkbox: update focusability and collapse focus if disabling.""" diff --git a/manatools/aui/backends/curses/checkboxframecurses.py b/manatools/aui/backends/curses/checkboxframecurses.py index ea7cafe..5af5cb2 100644 --- a/manatools/aui/backends/curses/checkboxframecurses.py +++ b/manatools/aui/backends/curses/checkboxframecurses.py @@ -14,9 +14,18 @@ import sys import os import time +import logging from ...yui_common import * from .commoncurses import _curses_recursive_min_height +# Module-level logger for checkbox frame curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.checkboxframe.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YCheckBoxFrameCurses(YSingleChildContainerWidget): """ NCurses implementation of a framed container with a checkbox in the title. @@ -39,6 +48,12 @@ def __init__(self, parent=None, label: str = "", checked: bool = False): except Exception: pass self._focused = False + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s checked=%s", self.__class__.__name__, self._label, self._checked) def widgetClass(self): return "YCheckBoxFrame" @@ -125,15 +140,22 @@ def _update_min_height(self): self._height = max(self._height, 3) def _create_backend_widget(self): - # no persistent backend object for curses - self._backend_widget = None - self._update_min_height() - # ensure children enablement matches checkbox initial state try: - if self.isEnabled(): - self._apply_children_enablement(self._checked) - except Exception: - pass + # no persistent backend object for curses; associate placeholder + self._backend_widget = self + self._update_min_height() + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + # ensure children enablement matches checkbox initial state + try: + if self.isEnabled(): + self._apply_children_enablement(self._checked) + except Exception as e: + self._logger.error("_create_backend_widget inner error: %s", e, exc_info=True) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _apply_children_enablement(self, isChecked: bool): try: @@ -158,8 +180,11 @@ def _apply_children_enablement(self, isChecked: bool): pass except Exception: pass - except Exception: - pass + except Exception as e: + try: + self._logger.error("_draw error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled: bool): try: diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py index 987428d..a3405ca 100644 --- a/manatools/aui/backends/curses/comboboxcurses.py +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for combobox curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.combobox.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YComboBoxCurses(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): @@ -31,6 +40,12 @@ def __init__(self, parent=None, label="", editable=False): self._combo_x = 0 self._combo_y = 0 self._combo_width = 0 + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s editable=%s", self.__class__.__name__, label, editable) def widgetClass(self): return "YComboBox" @@ -51,7 +66,15 @@ def editable(self): return self._editable def _create_backend_widget(self): - self._backend_widget = None + try: + # associate a placeholder backend widget to avoid None + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable combobox: affect focusability, expanded state and focused state.""" @@ -129,8 +152,11 @@ def _draw(self, window, y, x, width, height): # Draw expanded list if active and enabled if self._expanded and self.isEnabled(): self._draw_expanded_list(window) - except curses.error: - pass + except curses.error as e: + try: + self._logger.error("_draw curses.error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw curses.error: %s", e, exc_info=True) def _draw_expanded_list(self, window): @@ -187,9 +213,11 @@ def _draw_expanded_list(self, window): except curses.error: pass # Ignore out-of-bounds errors - except curses.error: - # Ignore drawing errors - pass + except curses.error as e: + try: + self._logger.error("_draw_expanded_list curses.error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw_expanded_list curses.error: %s", e, exc_info=True) def _handle_key(self, key): if not self._focused or not self.isEnabled(): diff --git a/manatools/aui/backends/curses/framecurses.py b/manatools/aui/backends/curses/framecurses.py index c68d1b2..fc1edd5 100644 --- a/manatools/aui/backends/curses/framecurses.py +++ b/manatools/aui/backends/curses/framecurses.py @@ -14,10 +14,19 @@ import sys import os import time +import logging from ...yui_common import * from .commoncurses import _curses_recursive_min_height +# Module-level logger for frame curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.frame.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YFrameCurses(YSingleChildContainerWidget): """ NCurses implementation of YFrame. @@ -34,6 +43,12 @@ def __init__(self, parent=None, label=""): self._height = 3 # inner top padding to separate frame title from child's label self._inner_top_padding = 1 + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, self._label) def widgetClass(self): return "YFrame" @@ -77,11 +92,18 @@ def stretchable(self, dim): return False def _create_backend_widget(self): - # curses backend does not create a separate widget object for frames; - # drawing is performed in _draw by the parent container. - self._backend_widget = None - # Update minimal height based on the child - self._update_min_height() + try: + # curses backend does not create a separate widget object for frames; + # associate placeholder backend_widget to self to avoid None problems + self._backend_widget = self + # Update minimal height based on the child + self._update_min_height() + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Propagate enabled state to the child.""" @@ -92,8 +114,11 @@ def _set_backend_enabled(self, enabled): child.setEnabled(enabled) except Exception: pass - except Exception: - pass + except Exception as e: + try: + self._logger.error("_draw error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw error: %s", e, exc_info=True) def addChild(self, child): super().addChild(child) diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py index 6c37fe2..fe893d2 100644 --- a/manatools/aui/backends/curses/hboxcurses.py +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -14,21 +14,43 @@ import sys import os import time +import logging from ...yui_common import * from .commoncurses import _curses_recursive_min_height +# Module-level logger for hbox curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.hbox.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YHBoxCurses(YWidget): def __init__(self, parent=None): super().__init__(parent) # Minimum height will be computed from children self._height = 1 + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__", self.__class__.__name__) def widgetClass(self): return "YHBox" def _create_backend_widget(self): - self._backend_widget = None + try: + # No real curses widget; associate placeholder to self + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _recompute_min_height(self): """Compute minimal height for this horizontal box as the tallest child's minimum.""" diff --git a/manatools/aui/backends/curses/inputfieldcurses.py b/manatools/aui/backends/curses/inputfieldcurses.py index ac9ec29..33bc535 100644 --- a/manatools/aui/backends/curses/inputfieldcurses.py +++ b/manatools/aui/backends/curses/inputfieldcurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for inputfield curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.inputfield.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YInputFieldCurses(YWidget): def __init__(self, parent=None, label="", password_mode=False): @@ -27,6 +36,12 @@ def __init__(self, parent=None, label="", password_mode=False): self._focused = False self._can_focus = True self._height = 1 + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s password_mode=%s", self.__class__.__name__, label, password_mode) def widgetClass(self): return "YInputField" @@ -42,7 +57,14 @@ def label(self): return self._label def _create_backend_widget(self): - self._backend_widget = None + try: + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable the input field: affect focusability and focused state.""" @@ -118,8 +140,11 @@ def _draw(self, window, y, x, width, height): cursor_display_pos = min(self._cursor_pos, width - 1) if cursor_display_pos < len(display_value): window.chgat(y, x + cursor_display_pos, 1, curses.A_REVERSE | curses.A_BOLD) - except curses.error: - pass + except curses.error as e: + try: + self._logger.error("_draw curses.error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw curses.error: %s", e, exc_info=True) def _handle_key(self, key): if not self._focused or not self.isEnabled(): diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index 1fd97cf..b4b247e 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for label curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.label.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YLabelCurses(YWidget): def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): @@ -26,6 +35,12 @@ def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): self._height = 1 self._focused = False self._can_focus = False # Labels don't get focus + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ text=%s heading=%s", self.__class__.__name__, text, isHeading) def widgetClass(self): return "YLabel" @@ -37,7 +52,14 @@ def setText(self, new_text): self._text = new_text def _create_backend_widget(self): - self._backend_widget = None + try: + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable label: labels are not focusable; just keep enabled state for drawing.""" @@ -60,5 +82,8 @@ def _draw(self, window, y, x, width, height): # Truncate text to fit available width display_text = self._text[:max(0, width-1)] window.addstr(y, x, display_text, attr) - except curses.error: - pass + except curses.error as e: + try: + self._logger.error("_draw curses.error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw curses.error: %s", e, exc_info=True) diff --git a/manatools/aui/backends/curses/progressbarcurses.py b/manatools/aui/backends/curses/progressbarcurses.py index 73a326f..49461ee 100644 --- a/manatools/aui/backends/curses/progressbarcurses.py +++ b/manatools/aui/backends/curses/progressbarcurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for progressbar curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.progressbar.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YProgressBarCurses(YWidget): def __init__(self, parent=None, label="", maxValue=100): super().__init__(parent) @@ -25,6 +34,11 @@ def __init__(self, parent=None, label="", maxValue=100): # progress bar occupies 2 rows when label present, otherwise 1 self._height = 2 if self._label else 1 self._backend_widget = None + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s maxValue=%s", self.__class__.__name__, label, maxValue) def widgetClass(self): return "YProgressBar" @@ -75,8 +89,15 @@ def setValue(self, newValue): pass def _create_backend_widget(self): - # curses backend widgets don't wrap native widgets; keep None - self._backend_widget = None + try: + # associate placeholder backend widget to self + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _draw(self, window, y, x, width, height): try: @@ -137,5 +158,8 @@ def _draw(self, window, y, x, width, height): pass except Exception: pass - except Exception: - pass \ No newline at end of file + except Exception as e: + try: + self._logger.error("_draw error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw error: %s", e, exc_info=True) \ No newline at end of file diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index 58424dc..4f9f56c 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for pushbutton curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.pushbutton.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YPushButtonCurses(YWidget): def __init__(self, parent=None, label=""): @@ -24,6 +33,11 @@ def __init__(self, parent=None, label=""): self._focused = False self._can_focus = True self._height = 1 # Fixed height - buttons are always one line + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, label) def widgetClass(self): return "YPushButton" @@ -35,7 +49,14 @@ def setLabel(self, label): self._label = label def _create_backend_widget(self): - self._backend_widget = None + try: + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable push button: update focusability and collapse focus if disabling.""" @@ -74,8 +95,11 @@ def _draw(self, window, y, x, width, height): attr |= curses.A_BOLD window.addstr(y, text_x, button_text, attr) - except curses.error: - pass + except curses.error as e: + try: + self._logger.error("_draw curses.error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw curses.error: %s", e, exc_info=True) def _handle_key(self, key): if not self._focused or not self.isEnabled(): @@ -88,6 +112,9 @@ def _handle_key(self, key): try: dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) except Exception: - pass + try: + self._logger.error("_handle_key post event error", exc_info=True) + except Exception: + _mod_logger.error("_handle_key post event error", exc_info=True) return True return False diff --git a/manatools/aui/backends/curses/radiobuttoncurses.py b/manatools/aui/backends/curses/radiobuttoncurses.py index 0bbdb3e..722f5f3 100644 --- a/manatools/aui/backends/curses/radiobuttoncurses.py +++ b/manatools/aui/backends/curses/radiobuttoncurses.py @@ -15,8 +15,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for radiobutton curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.radiobutton.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YRadioButtonCurses(YWidget): def __init__(self, parent=None, label="", is_checked=False): @@ -26,6 +35,12 @@ def __init__(self, parent=None, label="", is_checked=False): self._focused = False self._can_focus = True self._height = 1 + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ label=%s checked=%s", self.__class__.__name__, label, is_checked) def widgetClass(self): return "YRadioButton" @@ -61,8 +76,15 @@ def label(self): return self._label def _create_backend_widget(self): - # No native curses widget; state is kept here - pass + try: + # associate placeholder backend widget + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable radio button: update focusability and collapse focus if disabling.""" @@ -111,8 +133,11 @@ def _draw(self, window, y, x, width, height): window.attroff(curses.A_DIM) except Exception: pass - except curses.error: - pass + except curses.error as e: + try: + self._logger.error("_draw curses.error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_draw curses.error: %s", e, exc_info=True) def _handle_key(self, key): if not self.isEnabled(): @@ -151,12 +176,18 @@ def _select(self): if dlg is not None: try: dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - except Exception: - pass + except Exception as e: + try: + self._logger.error("_select post event error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_select post event error: %s", e, exc_info=True) else: try: print(f"RadioButton selected (no dialog): {self._label}") except Exception: pass except Exception: - pass + try: + self._logger.error("_select error", exc_info=True) + except Exception: + _mod_logger.error("_select error", exc_info=True) From 3830b5d11657d150e5b72c2bb9460e2ec0f4de31 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 14:27:40 +0100 Subject: [PATCH 179/523] Added logging --- manatools/aui/backends/curses/commoncurses.py | 15 +++++++- manatools/aui/backends/curses/treecurses.py | 34 ++++++++++++++++-- manatools/aui/backends/curses/vboxcurses.py | 36 ++++++++++++++++--- 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py index 14a3b2b..4dee676 100644 --- a/manatools/aui/backends/curses/commoncurses.py +++ b/manatools/aui/backends/curses/commoncurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for common curses helpers +_mod_logger = logging.getLogger("manatools.aui.curses.common.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + __all__ = ["_curses_recursive_min_height"] @@ -51,5 +60,9 @@ def _curses_recursive_min_height(widget): return max(3, 2 + inner_top + inner_min) # borders(2) + padding + inner else: return max(1, getattr(widget, "_height", 1)) - except Exception: + except Exception as e: + try: + _mod_logger.error("_curses_recursive_min_height error: %s", e, exc_info=True) + except Exception: + pass return max(1, getattr(widget, "_height", 1)) diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index eca697c..b8ac2ba 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -14,8 +14,17 @@ import sys import os import time +import logging from ...yui_common import * +# Module-level logger for tree curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.tree.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YTreeCurses(YSelectionWidget): """ @@ -47,6 +56,16 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti self._suppress_selection_handler = False self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + # debuglabel: class and initial label + try: + self._logger.debug("%s.__init__ label=%s multiselection=%s recursive=%s", self.__class__.__name__, label, multiselection, recursiveselection) + except Exception: + pass def widgetClass(self): return "YTree" @@ -63,9 +82,18 @@ def setImmediateMode(self, on:bool=True): self.setNotify(on) def _create_backend_widget(self): - # Keep preferred minimum for the layout (items + optional label) - self._height = max(self._height, self._min_height + (1 if self._label else 0)) - self.rebuildTree() + try: + # Keep preferred minimum for the layout (items + optional label) + self._height = max(self._height, self._min_height + (1 if self._label else 0)) + # associate placeholder backend widget to avoid None + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + self.rebuildTree() + except Exception as e: + try: + self._logger.critical("_create_backend_widget critical error: %s", e, exc_info=True) + except Exception: + _mod_logger.critical("_create_backend_widget critical error: %s", e, exc_info=True) def addItem(self, item): """Ensure base storage gets the item and rebuild visible list immediately.""" diff --git a/manatools/aui/backends/curses/vboxcurses.py b/manatools/aui/backends/curses/vboxcurses.py index 2cf717d..e974604 100644 --- a/manatools/aui/backends/curses/vboxcurses.py +++ b/manatools/aui/backends/curses/vboxcurses.py @@ -14,13 +14,28 @@ import sys import os import time +import logging from ...yui_common import * from .commoncurses import _curses_recursive_min_height +# Module-level logger for vbox curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.vbox.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + class YVBoxCurses(YWidget): def __init__(self, parent=None): super().__init__(parent) + # per-instance logger + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__", self.__class__.__name__) def widgetClass(self): return "YVBox" @@ -40,7 +55,11 @@ def stretchable(self, dim): return False def _create_backend_widget(self): - self._backend_widget = None + try: + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable VBox and propagate to logical children.""" @@ -49,9 +68,15 @@ def _set_backend_enabled(self, enabled): try: c.setEnabled(enabled) except Exception: - pass + try: + self._logger.error("_set_backend_enabled child setEnabled error", exc_info=True) + except Exception: + _mod_logger.error("_set_backend_enabled child setEnabled error", exc_info=True) except Exception: - pass + try: + self._logger.error("_set_backend_enabled error", exc_info=True) + except Exception: + _mod_logger.error("_set_backend_enabled error", exc_info=True) def _draw(self, window, y, x, width, height): # Vertical layout with spacing; give stretchable children more than their minimum @@ -134,7 +159,10 @@ def _draw(self, window, y, x, width, height): if hasattr(child, "_draw"): child._draw(window, cy, x, width, ch) except Exception: - pass + try: + self._logger.error("_draw child error: %s", child, exc_info=True) + except Exception: + _mod_logger.error("_draw child error", exc_info=True) cy += ch if i < num_children - 1 and cy < (y + height): cy += 1 # one-line spacing From a716cef19efdb4c4d1c41d7746e4099ccf36bf45 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 14:51:23 +0100 Subject: [PATCH 180/523] Added logging --- manatools/aui/backends/gtk/alignmentgtk.py | 24 ++++++++++++------- .../aui/backends/gtk/checkboxframegtk.py | 7 ++++++ manatools/aui/backends/gtk/checkboxgtk.py | 8 ++++++- manatools/aui/backends/gtk/comboboxgtk.py | 10 ++++++++ manatools/aui/backends/gtk/dialoggtk.py | 11 ++++++++- manatools/aui/backends/gtk/framegtk.py | 9 +++++++ manatools/aui/backends/gtk/hboxgtk.py | 11 ++++++++- manatools/aui/backends/gtk/inputfieldgtk.py | 6 +++++ manatools/aui/backends/gtk/labelgtk.py | 6 +++++ manatools/aui/backends/gtk/progressbargtk.py | 12 +++++++++- manatools/aui/backends/gtk/pushbuttongtk.py | 9 +++++++ manatools/aui/backends/gtk/radiobuttongtk.py | 9 +++++++ manatools/aui/backends/gtk/selectionboxgtk.py | 18 +++++++++++--- manatools/aui/backends/gtk/treegtk.py | 14 ++++++++++- manatools/aui/backends/gtk/vboxgtk.py | 6 +++++ 15 files changed, 143 insertions(+), 17 deletions(-) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index 312d8cf..fef0635 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -33,6 +34,7 @@ class YAlignmentGtk(YSingleChildContainerWidget): """ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") self._halign_spec = horAlign self._valign_spec = vertAlign self._background_pixbuf = None @@ -132,7 +134,7 @@ def setBackgroundPixmap(self, filename): self._signal_id = self._backend_widget.connect("draw", self._on_draw) self._backend_widget.queue_draw() # Trigger redraw except Exception as e: - print(f"Failed to load background image: {e}") + self._logger.error("Failed to load background image: %s", e, exc_info=True) self._background_pixbuf = None def _on_draw(self, widget, cr): @@ -151,7 +153,7 @@ def _on_draw(self, widget, cr): cr.rectangle(0, 0, width, height) cr.fill() except Exception as e: - print(f"Error drawing background: {e}") + self._logger.error("Error drawing background: %s", e, exc_info=True) return False def addChild(self, child): @@ -164,7 +166,7 @@ def _schedule_attach_child(self): """Schedule a single idle callback to attach child backend later.""" if self._attach_scheduled or self._child_attached: return - print("Scheduling child attach in idle") + self._logger.debug("Scheduling child attach in idle") self._attach_scheduled = True def _idle_cb(): @@ -172,7 +174,7 @@ def _idle_cb(): try: self._ensure_child_attached() except Exception as e: - print(f"Error attaching child: {e}") + self._logger.error("Error attaching child: %s", e, exc_info=True) return False try: @@ -204,7 +206,7 @@ def _ensure_child_attached(self): if cw is None: # child backend not yet ready; schedule again if not self._child_attached: - print(f"Child {child.widgetClass()} {child.debugLabel()} backend not ready; deferring attach") + self._logger.debug("Child %s %s backend not ready; deferring attach", child.widgetClass(), child.debugLabel()) self._schedule_attach_child() return @@ -244,9 +246,9 @@ def _ensure_child_attached(self): self._child_attached = True col_index = 0 if hal == Gtk.Align.START else 2 if hal == Gtk.Align.END else 1 # center default - print(f"Successfully attached child {child.widgetClass()} {child.label()} [{row_index},{col_index}]") + self._logger.debug("Successfully attached child %s %s [%d,%d]", child.widgetClass(), child.label(), row_index, col_index) except Exception as e: - print(f"Error building CenterBox layout: {e}") + self._logger.error("Error building CenterBox layout: %s", e, exc_info=True) def _create_backend_widget(self): """Create a container for the 3x3 alignment layout. @@ -276,7 +278,7 @@ def _create_backend_widget(self): except Exception as e: - print(f"Error creating backend widget: {e}") + self._logger.error("Error creating backend widget: %s", e, exc_info=True) root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._backend_widget = root @@ -287,11 +289,15 @@ def _create_backend_widget(self): try: self._signal_id = self._backend_widget.connect("draw", self._on_draw) except Exception as e: - print(f"Error connecting draw signal: {e}") + self._logger.error("Error connecting draw signal: %s", e, exc_info=True) self._signal_id = None # Mark that backend is ready and attempt to attach child self._ensure_child_attached() + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def get_backend_widget(self): """Return the backend GTK widget.""" diff --git a/manatools/aui/backends/gtk/checkboxframegtk.py b/manatools/aui/backends/gtk/checkboxframegtk.py index 4acd071..027459e 100644 --- a/manatools/aui/backends/gtk/checkboxframegtk.py +++ b/manatools/aui/backends/gtk/checkboxframegtk.py @@ -36,6 +36,7 @@ def __init__(self, parent=None, label: str = "", checked: bool = False): self._checkbox = None self._content_box = None self._label_widget = None + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YCheckBoxFrame" @@ -208,10 +209,16 @@ def _create_backend_widget(self): except Exception: pass except Exception: + self._logger.critical("_create_backend_widget failed", exc_info=True) self._backend_widget = None self._checkbox = None self._content_box = None self._label_widget = None + return + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _attach_child_backend(self): """Attach the logical child backend widget into the content box (clear previous).""" diff --git a/manatools/aui/backends/gtk/checkboxgtk.py b/manatools/aui/backends/gtk/checkboxgtk.py index 42e2c82..d9f5d2c 100644 --- a/manatools/aui/backends/gtk/checkboxgtk.py +++ b/manatools/aui/backends/gtk/checkboxgtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -25,6 +26,7 @@ def __init__(self, parent=None, label="", is_checked=False): super().__init__(parent) self._label = label self._is_checked = is_checked + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YCheckBox" @@ -49,8 +51,12 @@ def _create_backend_widget(self): self._backend_widget.set_active(self._is_checked) self._backend_widget.connect("toggled", self._on_toggled) except Exception: - pass + self._logger.error("_create_backend_widget toggle setup failed", exc_info=True) self._backend_widget.set_sensitive(self._enabled) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _on_toggled(self, button): try: diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index ee1590a..10b4cce 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -28,6 +29,7 @@ def __init__(self, parent=None, label="", editable=False): self._value = "" self._selected_items = [] self._combo_widget = None + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YComboBox" @@ -118,6 +120,10 @@ def _create_backend_widget(self): self._backend_widget = hbox self._backend_widget.set_sensitive(self._enabled) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the combobox/backing widget and its entry/dropdown.""" @@ -236,3 +242,7 @@ def _on_changed_dropdown(self, dropdown): dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + try: + self._logger.debug("_on_changed_dropdown: value=%s selected_items=%s", self._value, [it.label() for it in self._selected_items]) + except Exception: + pass diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index aeedfed..940ec03 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * from ... import yui as yui_mod @@ -32,6 +33,7 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._event_result = None self._glib_loop = None YDialogGtk._open_dialogs.append(self) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YDialog" @@ -189,7 +191,10 @@ def _create_backend_widget(self): except Exception: _resolved_pixbuf = None except Exception: - print("Warning: could not determine application title for dialog") + try: + self._logger.warning("Could not determine application title for dialog", exc_info=True) + except Exception: + pass pass # Create Gtk4 Window @@ -263,6 +268,10 @@ def _create_backend_widget(self): self._window.connect("destroy", self._on_destroy) except Exception: pass + except Exception: + self._logger.error("Failed to connect window close/destroy handlers", exc_info=True) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass diff --git a/manatools/aui/backends/gtk/framegtk.py b/manatools/aui/backends/gtk/framegtk.py index 4c0452f..c879538 100644 --- a/manatools/aui/backends/gtk/framegtk.py +++ b/manatools/aui/backends/gtk/framegtk.py @@ -35,6 +35,7 @@ def __init__(self, parent=None, label: str = ""): self._label = label or "" self._backend_widget = None self._content_box = None + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YFrame" @@ -200,6 +201,10 @@ def _create_backend_widget(self): self._attach_child_backend() except Exception: pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass return except Exception: # fallback to a boxed container with a visible border using CSS if Frame creation fails @@ -231,6 +236,10 @@ def _create_backend_widget(self): except Exception: self._backend_widget = None self._content_box = None + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the frame and propagate to child.""" diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py index a280188..483003c 100644 --- a/manatools/aui/backends/gtk/hboxgtk.py +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -17,12 +17,14 @@ import cairo import threading import os +import logging from ...yui_common import * class YHBoxGtk(YWidget): def __init__(self, parent=None): super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YHBox" @@ -39,7 +41,10 @@ def _create_backend_widget(self): self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) for child in self._children: - print("HBox child: ", child.widgetClass()) + try: + self._logger.debug("HBox child: %s", child.widgetClass()) + except Exception: + pass widget = child.get_backend_widget() try: widget.set_hexpand(True) @@ -57,6 +62,10 @@ def _create_backend_widget(self): except Exception: pass self._backend_widget.set_sensitive(self._enabled) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the HBox and propagate to children.""" diff --git a/manatools/aui/backends/gtk/inputfieldgtk.py b/manatools/aui/backends/gtk/inputfieldgtk.py index 3d7ee17..cbb1406 100644 --- a/manatools/aui/backends/gtk/inputfieldgtk.py +++ b/manatools/aui/backends/gtk/inputfieldgtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -26,6 +27,7 @@ def __init__(self, parent=None, label="", password_mode=False): self._label = label self._value = "" self._password_mode = password_mode + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YInputField" @@ -82,6 +84,10 @@ def _create_backend_widget(self): self._backend_widget = hbox self._entry_widget = entry self._backend_widget.set_sensitive(self._enabled) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _on_changed(self, entry): try: diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py index e07df72..5dc4f30 100644 --- a/manatools/aui/backends/gtk/labelgtk.py +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -26,6 +27,7 @@ def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): self._text = text self._is_heading = isHeading self._is_output_field = isOutputField + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YLabel" @@ -57,6 +59,10 @@ def _create_backend_widget(self): except Exception: pass self._backend_widget.set_sensitive(self._enabled) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the label widget backend.""" diff --git a/manatools/aui/backends/gtk/progressbargtk.py b/manatools/aui/backends/gtk/progressbargtk.py index 56c8bf5..177bdb0 100644 --- a/manatools/aui/backends/gtk/progressbargtk.py +++ b/manatools/aui/backends/gtk/progressbargtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * class YProgressBarGtk(YWidget): @@ -28,6 +29,7 @@ def __init__(self, parent=None, label="", maxValue=100): self._backend_widget = None self._label_widget = None self._progress_widget = None + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YProgressBar" @@ -86,4 +88,12 @@ def _create_backend_widget(self): self._progress_widget.set_show_text(True) container.append(self._progress_widget) - self._backend_widget = container \ No newline at end of file + self._backend_widget = container + try: + self._backend_widget.set_sensitive(self._enabled) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass \ No newline at end of file diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index 4f3ae2c..1212e7f 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -24,6 +25,7 @@ class YPushButtonGtk(YWidget): def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YPushButton" @@ -52,6 +54,13 @@ def _create_backend_widget(self): try: self._backend_widget.set_sensitive(self._enabled) self._backend_widget.connect("clicked", self._on_clicked) + except Exception: + try: + self._logger.error("_create_backend_widget setup failed", exc_info=True) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass diff --git a/manatools/aui/backends/gtk/radiobuttongtk.py b/manatools/aui/backends/gtk/radiobuttongtk.py index 6de76fa..a1ac685 100644 --- a/manatools/aui/backends/gtk/radiobuttongtk.py +++ b/manatools/aui/backends/gtk/radiobuttongtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * class YRadioButtonGtk(YWidget): @@ -48,6 +49,7 @@ def __init__(self, parent=None, label="", isChecked=False): self._group = self except Exception: self._group = self + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YRadioButton" @@ -121,6 +123,13 @@ def _create_backend_widget(self): self._backend_widget.set_sensitive(self._enabled) self._backend_widget.connect("toggled", self._on_toggled) self._backend_widget.set_active(self._is_checked) + except Exception: + try: + self._logger.error("_create_backend_widget failed to finalize", exc_info=True) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 92398af..7fbf72d 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -37,6 +38,7 @@ def __init__(self, parent=None, label=""): self._preferred_rows = 6 self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YSelectionBox" @@ -329,6 +331,7 @@ def _create_backend_widget(self): # if multi-selection has been set before widget creation, ensure correct mode self.setMultiSelection( self._multi_selection ) self._backend_widget.set_sensitive(self._enabled) + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) def _set_backend_enabled(self, enabled): """Enable/disable the selection box and its listbox/rows.""" @@ -389,7 +392,10 @@ def _on_row_selected(self, listbox, row): self._items[idx].setSelected( True ) self._value = self._selected_items[0].label() if self._selected_items else None except Exception: - print("SelectionBoxGTK: failed to process row-selected event") + try: + self._logger.error("SelectionBoxGTK: failed to process row-selected event", exc_info=True) + except Exception: + pass # be defensive self._selected_items = [] self._value = None @@ -424,7 +430,10 @@ def _on_selected_rows_changed(self, listbox): try: # Some bindings may provide get_selected_rows() sel_rows = listbox.get_selected_rows() - print(f"Using get_selected_rows() {len(sel_rows)} API") + try: + self._logger.debug("Using get_selected_rows() API, count=%d", len(sel_rows)) + except Exception: + pass except Exception: sel_rows = None @@ -563,7 +572,10 @@ def addItem(self, item): self._listbox.unselect_row( r ) #r.set_selected(False) except Exception: - print("Failed to deselect row") + try: + self._logger.error("Failed to deselect row", exc_info=True) + except Exception: + pass try: setattr(r, '_selected_flag', False) except Exception: diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 9af6027..ab5e710 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -17,6 +17,7 @@ import cairo import threading import os +import logging from ...yui_common import * @@ -39,6 +40,7 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti self._immediate = self.notify() self._backend_widget = None self._listbox = None + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") # cached rows and mappings self._rows = [] # ordered list of Gtk.ListBoxRow self._row_to_item = {} # row -> YTreeItem @@ -97,7 +99,10 @@ def _create_backend_widget(self): try: listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) except Exception: - pass + try: + self._logger.error("Failed to connect row-selected handler", exc_info=True) + except Exception: + pass self._backend_widget = vbox self._listbox = listbox @@ -115,6 +120,13 @@ def _create_backend_widget(self): try: if getattr(self, "_items", None): self.rebuildTree() + except Exception: + try: + self._logger.error("rebuildTree failed during _create_backend_widget", exc_info=True) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index a9eeb3e..055a869 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -17,11 +17,13 @@ import cairo import threading import os +import logging from ...yui_common import * class YVBoxGtk(YWidget): def __init__(self, parent=None): super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YVBox" @@ -57,6 +59,10 @@ def _create_backend_widget(self): except Exception: pass self._backend_widget.set_sensitive(self._enabled) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): From e1716b5b73857ad7d8be8671629ee1c73a27754b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 15:02:37 +0100 Subject: [PATCH 181/523] Added logging --- manatools/aui/backends/qt/alignmentqt.py | 6 ++++++ manatools/aui/backends/qt/checkboxframeqt.py | 6 ++++++ manatools/aui/backends/qt/checkboxqt.py | 11 ++++++++++- manatools/aui/backends/qt/comboboxqt.py | 6 ++++++ manatools/aui/backends/qt/dialogqt.py | 6 ++++++ manatools/aui/backends/qt/frameqt.py | 10 ++++++++++ manatools/aui/backends/qt/hboxqt.py | 11 ++++++++++- manatools/aui/backends/qt/inputfieldqt.py | 6 ++++++ manatools/aui/backends/qt/labelqt.py | 6 ++++++ manatools/aui/backends/qt/progressbarqt.py | 6 ++++++ manatools/aui/backends/qt/pushbuttonqt.py | 11 ++++++++++- manatools/aui/backends/qt/radiobuttonqt.py | 8 +++++++- manatools/aui/backends/qt/selectionboxqt.py | 6 ++++++ manatools/aui/backends/qt/treeqt.py | 13 +++++++++++++ manatools/aui/backends/qt/vboxqt.py | 11 ++++++++++- 15 files changed, 118 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/qt/alignmentqt.py b/manatools/aui/backends/qt/alignmentqt.py index 3167981..e817865 100644 --- a/manatools/aui/backends/qt/alignmentqt.py +++ b/manatools/aui/backends/qt/alignmentqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets, QtCore +import logging from ...yui_common import * class YAlignmentQt(YSingleChildContainerWidget): @@ -20,6 +21,7 @@ class YAlignmentQt(YSingleChildContainerWidget): """ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUnchanged, vertAlign: YAlignmentType=YAlignmentType.YAlignUnchanged): super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") self._halign_spec = horAlign self._valign_spec = vertAlign self._backend_widget = None @@ -177,6 +179,10 @@ def _create_backend_widget(self): if self.hasChildren(): self._attach_child_backend() + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the alignment container and propagate to its logical child.""" diff --git a/manatools/aui/backends/qt/checkboxframeqt.py b/manatools/aui/backends/qt/checkboxframeqt.py index b419e9b..41c34c9 100644 --- a/manatools/aui/backends/qt/checkboxframeqt.py +++ b/manatools/aui/backends/qt/checkboxframeqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets, QtCore +import logging from ...yui_common import * class YCheckBoxFrameQt(YSingleChildContainerWidget): @@ -26,6 +27,7 @@ def __init__(self, parent=None, label: str = "", checked: bool = False): self._checkbox = None self._content_widget = None self._content_layout = None + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YCheckBoxFrame" @@ -146,6 +148,10 @@ def _create_backend_widget(self): self._checkbox = None self._content_widget = None self._content_layout = None + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _attach_child_backend(self): """Attach child's backend widget into content area.""" diff --git a/manatools/aui/backends/qt/checkboxqt.py b/manatools/aui/backends/qt/checkboxqt.py index 7769119..24fdf5a 100644 --- a/manatools/aui/backends/qt/checkboxqt.py +++ b/manatools/aui/backends/qt/checkboxqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets, QtCore +import logging from ...yui_common import * class YCheckBoxQt(YWidget): @@ -17,6 +18,7 @@ def __init__(self, parent=None, label="", is_checked=False): super().__init__(parent) self._label = label self._is_checked = is_checked + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YCheckBox" @@ -45,6 +47,10 @@ def _create_backend_widget(self): self._backend_widget.setChecked(self._is_checked) self._backend_widget.stateChanged.connect(self._on_state_changed) self._backend_widget.setEnabled(bool(self._enabled)) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the QCheckBox backend.""" @@ -68,4 +74,7 @@ def _on_state_changed(self, state): if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) else: - print(f"CheckBox state changed (no dialog found): {self._label} = {self._is_checked}") + try: + self._logger.warning("CheckBox state changed (no dialog found): %s = %s", self._label, self._is_checked) + except Exception: + pass diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index 7bc0df6..63105e0 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YComboBoxQt(YSelectionWidget): @@ -19,6 +20,7 @@ def __init__(self, parent=None, label="", editable=False): self._editable = editable self._value = "" self._selected_items = [] + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YComboBox" @@ -71,6 +73,10 @@ def _create_backend_widget(self): self._backend_widget = container self._combo_widget = combo self._backend_widget.setEnabled(bool(self._enabled)) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the combobox and its container.""" diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index c688941..2af7c91 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -14,6 +14,7 @@ from ...yui_common import YSingleChildContainerWidget, YUIDimension, YPropertySet, YProperty, YPropertyType, YUINoDialogException, YDialogType, YDialogColorMode, YEvent, YCancelEvent, YTimeoutEvent from ... import yui as yui_mod import os +import logging class YDialogQt(YSingleChildContainerWidget): _open_dialogs = [] @@ -27,6 +28,7 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._event_result = None self._qt_event_loop = None YDialogQt._open_dialogs.append(self) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YDialog" @@ -154,6 +156,10 @@ def _create_backend_widget(self): self._backend_widget = self._qwidget self._qwidget.closeEvent = self._on_close_event self._backend_widget.setEnabled(bool(self._enabled)) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the dialog window and propagate to logical child widgets.""" diff --git a/manatools/aui/backends/qt/frameqt.py b/manatools/aui/backends/qt/frameqt.py index 90d230f..cfa3d44 100644 --- a/manatools/aui/backends/qt/frameqt.py +++ b/manatools/aui/backends/qt/frameqt.py @@ -3,6 +3,7 @@ """ from PySide6 import QtWidgets +import logging from ...yui_common import * class YFrameQt(YSingleChildContainerWidget): @@ -17,6 +18,7 @@ def __init__(self, parent=None, label=""): self._label = label self._backend_widget = None self._group_layout = None + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YFrame" @@ -102,6 +104,10 @@ def _create_backend_widget(self): layout.addWidget(w) except Exception: pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass except Exception: # fallback to a plain QWidget container if QGroupBox creation fails try: @@ -118,6 +124,10 @@ def _create_backend_widget(self): layout.addWidget(w) except Exception: pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass except Exception: self._backend_widget = None self._group_layout = None diff --git a/manatools/aui/backends/qt/hboxqt.py b/manatools/aui/backends/qt/hboxqt.py index 52e8bcc..8a9e8f5 100644 --- a/manatools/aui/backends/qt/hboxqt.py +++ b/manatools/aui/backends/qt/hboxqt.py @@ -10,11 +10,13 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YHBoxQt(YWidget): def __init__(self, parent=None): super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YHBox" @@ -58,8 +60,15 @@ def _create_backend_widget(self): except Exception: pass self._backend_widget.setEnabled(bool(self._enabled)) - print( f"YHBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug + try: + self._logger.debug("YHBoxQt: adding child %s expand=%s", child.widgetClass(), expand) + except Exception: + pass layout.addWidget(widget, stretch=expand) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the HBox container and propagate to children.""" diff --git a/manatools/aui/backends/qt/inputfieldqt.py b/manatools/aui/backends/qt/inputfieldqt.py index 62abb61..4d79863 100644 --- a/manatools/aui/backends/qt/inputfieldqt.py +++ b/manatools/aui/backends/qt/inputfieldqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YInputFieldQt(YWidget): @@ -18,6 +19,7 @@ def __init__(self, parent=None, label="", password_mode=False): self._label = label self._value = "" self._password_mode = password_mode + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YInputField" @@ -55,6 +57,10 @@ def _create_backend_widget(self): self._backend_widget = container self._entry_widget = entry self._backend_widget.setEnabled(bool(self._enabled)) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the input field: entry and container.""" diff --git a/manatools/aui/backends/qt/labelqt.py b/manatools/aui/backends/qt/labelqt.py index 0538171..908d752 100644 --- a/manatools/aui/backends/qt/labelqt.py +++ b/manatools/aui/backends/qt/labelqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YLabelQt(YWidget): @@ -18,6 +19,7 @@ def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): self._text = text self._is_heading = isHeading self._is_output_field = isOutputField + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YLabel" @@ -38,6 +40,10 @@ def _create_backend_widget(self): font.setPointSize(font.pointSize() + 2) self._backend_widget.setFont(font) self._backend_widget.setEnabled(bool(self._enabled)) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the QLabel backend.""" diff --git a/manatools/aui/backends/qt/progressbarqt.py b/manatools/aui/backends/qt/progressbarqt.py index 35fdc1a..a62a211 100644 --- a/manatools/aui/backends/qt/progressbarqt.py +++ b/manatools/aui/backends/qt/progressbarqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YProgressBarQt(YWidget): @@ -21,6 +22,7 @@ def __init__(self, parent=None, label="", maxValue=100): self._backend_widget = None self._label_widget = None self._progress_widget = None + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YProgressBar" @@ -92,6 +94,10 @@ def _create_backend_widget(self): self._label_widget = lbl self._progress_widget = prog self._backend_widget.setEnabled(bool(self._enabled)) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass except Exception: self._backend_widget = None self._label_widget = None diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index 1e60dc3..1862670 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * @@ -17,6 +18,7 @@ class YPushButtonQt(YWidget): def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YPushButton" @@ -55,6 +57,10 @@ def _create_backend_widget(self): pass self._backend_widget.setEnabled(bool(self._enabled)) self._backend_widget.clicked.connect(self._on_clicked) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the QPushButton backend.""" @@ -74,4 +80,7 @@ def _on_clicked(self): dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) else: # fallback logging for now - print(f"Button clicked (no dialog found): {self._label}") + try: + self._logger.warning("Button clicked (no dialog found): %s", self._label) + except Exception: + pass diff --git a/manatools/aui/backends/qt/radiobuttonqt.py b/manatools/aui/backends/qt/radiobuttonqt.py index 017753c..72973ee 100644 --- a/manatools/aui/backends/qt/radiobuttonqt.py +++ b/manatools/aui/backends/qt/radiobuttonqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YRadioButtonQt(YWidget): @@ -18,6 +19,7 @@ def __init__(self, parent=None, label="", isChecked=False): self._label = label self._is_checked = bool(isChecked) self._backend_widget = None + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YRadioButton" @@ -73,6 +75,10 @@ def _create_backend_widget(self): pass except Exception: self._backend_widget = None + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): try: @@ -97,6 +103,6 @@ def _on_toggled(self, checked): else: # best-effort debug output when no dialog try: - print(f"RadioButton toggled: {self._label} = {self._is_checked}") + self._logger.warning("RadioButton toggled on None dialog: %s = %s", self._label, self._is_checked) except Exception: pass \ No newline at end of file diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index 6e66ec7..34d6071 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YSelectionBoxQt(YSelectionWidget): @@ -21,6 +22,7 @@ def __init__(self, parent=None, label=""): self._multi_selection = False self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YSelectionBox" @@ -176,6 +178,10 @@ def _create_backend_widget(self): self._backend_widget = container self._backend_widget.setEnabled(bool(self._enabled)) self._list_widget = list_widget + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the selection box and its list widget; propagate where applicable.""" diff --git a/manatools/aui/backends/qt/treeqt.py b/manatools/aui/backends/qt/treeqt.py index ecb10a0..b128cd5 100644 --- a/manatools/aui/backends/qt/treeqt.py +++ b/manatools/aui/backends/qt/treeqt.py @@ -10,6 +10,7 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YTreeQt(YSelectionWidget): @@ -66,6 +67,18 @@ def _create_backend_widget(self): # populate if items already present try: self.rebuildTree() + except Exception: + try: + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._logger.error("rebuildTree failed during _create_backend_widget", exc_info=True) + except Exception: + pass + pass + try: + # ensure logger exists and emit debug + if not hasattr(self, "_logger"): + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass diff --git a/manatools/aui/backends/qt/vboxqt.py b/manatools/aui/backends/qt/vboxqt.py index ff84b6c..30d012b 100644 --- a/manatools/aui/backends/qt/vboxqt.py +++ b/manatools/aui/backends/qt/vboxqt.py @@ -10,11 +10,13 @@ @package manatools.aui.backends.qt ''' from PySide6 import QtWidgets +import logging from ...yui_common import * class YVBoxQt(YWidget): def __init__(self, parent=None): super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YVBox" @@ -59,8 +61,15 @@ def _create_backend_widget(self): pass self._backend_widget.setEnabled(bool(self._enabled)) - print( f"YVBoxQt: adding child {child.widgetClass()} expand={expand}" ) #TODO remove debug + try: + self._logger.debug("YVBoxQt: adding child %s expand=%s", child.widgetClass(), expand) + except Exception: + pass layout.addWidget(widget, stretch=expand) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the VBox container and propagate to children.""" From fd41151ce1fd94ce6c85162fb1c2f6c814a6896e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 19:21:14 +0100 Subject: [PATCH 182/523] Removed setValue (nonsense). Fixed item(s) selection, avoiding text comparing --- manatools/aui/backends/qt/selectionboxqt.py | 143 ++++++++------------ 1 file changed, 53 insertions(+), 90 deletions(-) diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index 34d6071..8da02f6 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -22,6 +22,7 @@ def __init__(self, parent=None, label=""): self._multi_selection = False self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) + self._list_widget = None self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): @@ -29,23 +30,7 @@ def widgetClass(self): def value(self): return self._value - - def setValue(self, text): - self._value = text - if hasattr(self, '_list_widget') and self._list_widget: - # Find and select the item with matching text - for i in range(self._list_widget.count()): - item = self._list_widget.item(i) - if item.text() == text: - self._list_widget.setCurrentItem(item) - break - # Update selected_items to keep internal state consistent - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break - + def label(self): return self._label @@ -56,44 +41,30 @@ def selectedItems(self): def selectItem(self, item, selected=True): """Select or deselect a specific item""" if hasattr(self, '_list_widget') and self._list_widget: - for i in range(self._list_widget.count()): - list_item = self._list_widget.item(i) - if list_item.text() == item.label(): - if selected: - # If single-selection, clear model flags for other items - if not self._multi_selection: - for it in self._items: - try: - if it is not item: - it.setSelected(False) - except Exception: - pass - try: - # clear visual selection - self._list_widget.clearSelection() - except Exception: - pass - self._selected_items = [] - - self._list_widget.setCurrentItem(list_item) - try: - item.setSelected(True) - except Exception: - pass - if item not in self._selected_items: - self._selected_items.append(item) + for idx, it in enumerate(self._items): + if it is item: + if self.multiSelection(): + if selected: + if item not in self._selected_items: + self._selected_items.append(item) + else: + if item in self._selected_items: + self._selected_items.remove(item) else: - try: - item.setSelected(False) - except Exception: - pass - if item in self._selected_items: - self._selected_items.remove(item) - # ensure internal state and notify - try: - self._on_selection_changed() - except Exception: - pass + old_selected = self._selected_items[0] if self._selected_items else None + self._list_widget.clearSelection() + if selected: + if old_selected is not None: + old_selected.setSelected(False) + self._selected_items = [item] + else: + self._selected_items = [] + #fix item selection in the view and YItem + if selected: + self._list_widget.setCurrentItem(self._list_widget.item(idx)) + self._list_widget.item(idx).setSelected(bool(selected)) + item.setSelected(bool(selected)) + self._value = self._selected_items[0].label() if self._selected_items else "" break def setMultiSelection(self, enabled): @@ -109,8 +80,17 @@ def setMultiSelection(self, enabled): first = selected[0] self._list_widget.clearSelection() first.setSelected(True) - # update internal state to reflect change - self._on_selection_changed() + self._list_widget.setCurrentItem(first) + selected_indices = [index.row() for index in self._list_widget.selectedIndexes()] + new_selected = [] + for idx, it in enumerate(self._items): + if idx in selected_indices: + it.setSelected(True) + new_selected.append(it) + else: + it.setSelected(False) + self._selected_items = new_selected + self._value = self._selected_items[0].label() if self._selected_items else "" def multiSelection(self): """Return whether multi-selection is enabled.""" @@ -146,8 +126,6 @@ def _create_backend_widget(self): li.setSelected(True) if item not in self._selected_items: self._selected_items.append(item) - if not self._value: - self._value = item.label() except Exception: pass else: @@ -155,6 +133,9 @@ def _create_backend_widget(self): for idx, item in enumerate(self._items): try: if item.selected(): + if last_selected_idx is not None: + # clear previous selection in model + self._items[last_selected_idx].setSelected(False) last_selected_idx = idx except Exception: pass @@ -164,11 +145,7 @@ def _create_backend_widget(self): list_widget.setCurrentItem(li) li.setSelected(True) # update model internal selection list and value - try: - self._selected_items = [self._items[last_selected_idx]] - self._value = self._items[last_selected_idx].label() - except Exception: - pass + self._selected_items = [self._items[last_selected_idx]] except Exception: pass @@ -178,6 +155,7 @@ def _create_backend_widget(self): self._backend_widget = container self._backend_widget.setEnabled(bool(self._enabled)) self._list_widget = list_widget + self._value = self._selected_items[0].label() if self._selected_items else "" try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -263,31 +241,19 @@ def _on_selection_changed(self): # Update model.selected flags and selected items list selected_indices = [index.row() for index in self._list_widget.selectedIndexes()] new_selected = [] - for idx, it in enumerate(self._items): - try: - if idx in selected_indices: - try: - it.setSelected(True) - except Exception: - pass - new_selected.append(it) - else: - try: - it.setSelected(False) - except Exception: - pass - except Exception: - pass - + for idx, it in enumerate(self._items): + if idx in selected_indices: + it.setSelected(True) + new_selected.append(it) + else: + it.setSelected(False) + # In single-selection mode ensure only one model item remains selected if not self._multi_selection and len(new_selected) > 1: # Keep only the last selected (mimic addItem logic and selection expectations) last = new_selected[-1] for it in list(new_selected)[:-1]: - try: - it.setSelected(False) - except Exception: - pass + it.setSelected(False) new_selected = [last] self._selected_items = new_selected @@ -299,13 +265,10 @@ def _on_selection_changed(self): self._value = "" # Post selection-changed event to containing dialog - try: - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception: - pass + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) def deleteAllItems(self): """Remove all items from the selection box, both in the model and the Qt view.""" From daad1d5a6f8ad44e9393f0ab3d3ccbd37d20f904 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 19:24:14 +0100 Subject: [PATCH 183/523] removed setValue --- .../aui/backends/curses/selectionboxcurses.py | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index 625d213..aa7eff2 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -68,46 +68,6 @@ def label(self): def value(self): return self._value - def setValue(self, text): - """Select first item matching text.""" - self._value = text - # update model flags and selected_items - self._selected_items = [] - try: - for it in self._items: - try: - if it.label() == text: - try: - it.setSelected(True) - except Exception: - pass - self._selected_items.append(it) - else: - if not self._multi_selection: - try: - it.setSelected(False) - except Exception: - pass - except Exception: - pass - if not self._multi_selection and len(self._selected_items) > 1: - last = self._selected_items[-1] - for it in list(self._selected_items)[:-1]: - try: - it.setSelected(False) - except Exception: - pass - self._selected_items = [last] - except Exception: - pass - # update hover to first matching index - for idx, it in enumerate(self._items): - if it.label() == text: - self._hover_index = idx - # adjust scroll offset to make hovered visible - self._ensure_hover_visible() - break - def selectedItems(self): return list(self._selected_items) From 0208c57096d7b7b3b14135cacaf232cf3cbb829d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 19:26:10 +0100 Subject: [PATCH 184/523] removed setValue and wrong notification --- manatools/aui/backends/gtk/selectionboxgtk.py | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 7fbf72d..1360290 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -49,46 +49,6 @@ def label(self): def value(self): return self._value - def setValue(self, text): - """Select first item matching text.""" - self._value = text - self._selected_items = [it for it in self._items if it.label() == text] - if self._listbox is None: - return - # find and select corresponding row using the cached rows list - for i, row in enumerate(getattr(self, "_rows", [])): - if i >= len(self._items): - continue - try: - if self._items[i].label() == text: - row.set_selectable(True) - row.set_selected(True) - else: - # ensure others are not selected in single-selection mode - if not self._multi_selection: - row.set_selected(False) - except Exception: - pass - - # rebuild internal selection state and notify - try: - self._selected_items = [] - for i, r in enumerate(getattr(self, "_rows", [])): - try: - if self._row_is_selected(r) and i < len(self._items): - self._selected_items.append(self._items[i]) - except Exception: - pass - self._value = self._selected_items[0].label() if self._selected_items else None - except Exception: - self._selected_items = [] - self._value = None - - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - def selectedItems(self): return list(self._selected_items) @@ -132,11 +92,6 @@ def selectItem(self, item, selected=True): self._selected_items = [] self._value = None - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) # If listbox already created, update its selection mode at runtime. From 773b8d3c60eed0c497fa324b30ab78ab72c8618f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 19:58:56 +0100 Subject: [PATCH 185/523] Fixed setMultiselection --- manatools/aui/backends/gtk/selectionboxgtk.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 1360290..e5089b6 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -94,6 +94,7 @@ def selectItem(self, item, selected=True): def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) + old_selected = list(self._selected_items) # If listbox already created, update its selection mode at runtime. if self._listbox is None: return @@ -131,7 +132,24 @@ def setMultiSelection(self, enabled): self._signal_handlers['row-selected'] = hid except Exception: pass + + selected_item = old_selected[0] if old_selected else None + if selected_item: + if len(old_selected) > 1: + for it in old_selected[1:]: + it.setSelected(False) + self._selected_items = [selected_item] + self._value = self._selected_items[0].label() if self._selected_items else "" + if self._selected_items and self._listbox: + try: + idx = self._items.index( self._selected_items[0] ) + row = self._rows[idx] + self._listbox.select_row( row ) + except Exception: + pass + self._logger.debug("setMultiSelection: mode set to %s - value=%r", "MULTIPLE" if self._multi_selection else "SINGLE", self._value) except Exception: + self._logger.error("setMultiSelection: failed in multi-selection update", exc_info=True) pass def multiSelection(self): From e6772662e32d937b35e80a41eb44a3dcb680ab03 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 20:10:53 +0100 Subject: [PATCH 186/523] Fixed selectItem --- manatools/aui/backends/gtk/selectionboxgtk.py | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index e5089b6..c134d02 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -53,44 +53,29 @@ def selectedItems(self): return list(self._selected_items) def selectItem(self, item, selected=True): - if selected: - if not self._multi_selection: - self._selected_items = [item] - self._value = item.label() - else: + """Select or deselect a specific item""" + if self.multiSelection(): + if selected: if item not in self._selected_items: self._selected_items.append(item) + else: + if item in self._selected_items: + self._selected_items.remove(item) else: - if item in self._selected_items: - self._selected_items.remove(item) - self._value = self._selected_items[0].label() if self._selected_items else "" - - if self._listbox is None: - return - - # reflect change in UI - rows = getattr(self, "_rows", []) - for i, it in enumerate(self._items): - if it is item or it.label() == item.label(): - try: - row = rows[i] - row.set_selected(selected) - except Exception: - pass - break - # rebuild internal selection state and notify - try: - self._selected_items = [] - for i, r in enumerate(getattr(self, "_rows", [])): - try: - if self._row_is_selected(r) and i < len(self._items): - self._selected_items.append(self._items[i]) - except Exception: - pass - self._value = self._selected_items[0].label() if self._selected_items else None - except Exception: - self._selected_items = [] - self._value = None + old_selected = self._selected_items[0] if self._selected_items else None + if selected: + if old_selected is not None: + old_selected.setSelected(False) + self._selected_items = [item] + idx = self._items.index( self._selected_items[0] ) + row = self._rows[idx] + if self._listbox is not None: + self._listbox.select_row( row ) + else: + self._selected_items = [] + + item.setSelected(bool(selected)) + self._value = self._selected_items[0].label() if self._selected_items else None def setMultiSelection(self, enabled): self._multi_selection = bool(enabled) From 0fb2c0acb8fa0b563f1a71187ef4c15a15c4335f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 21 Dec 2025 20:13:08 +0100 Subject: [PATCH 187/523] updated --- sow/TODO.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 4e0dd80..cd77656 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -20,10 +20,11 @@ Missing Widgets comparing libyui: [ ] YIntField [ ] YMenuButton, YMenuBar [ ] YWizard - [ ] YPackageSelector + [-] ~~YPackageSelector~~ [ ] YSpacing, YAlignment [ ] YReplacePoint - [X] YRadioButton, (YRadioButtonGroup avoid by now) + [X] YRadioButton, + [-] ~~YRadioButtonGroup~~ To check how to manage YEvents [X] and YItems [ ] (verify selection attirbute). From d18afe16c65466bf16803d5c63bd1b647a6ce9b8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 14:10:05 +0100 Subject: [PATCH 188/523] Added icon to selection box item --- manatools/aui/backends/gtk/commongtk.py | 121 +++++++++++++++++ manatools/aui/backends/gtk/selectionboxgtk.py | 127 +++++++++++++++++- 2 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 manatools/aui/backends/gtk/commongtk.py diff --git a/manatools/aui/backends/gtk/commongtk.py b/manatools/aui/backends/gtk/commongtk.py new file mode 100644 index 0000000..bfc7764 --- /dev/null +++ b/manatools/aui/backends/gtk/commongtk.py @@ -0,0 +1,121 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import cairo +import threading +import os +import logging +from ...yui_common import * + + +__all__ = ["_curses_recursive_min_height"] + + +def _resolve_icon(icon_name, size=16): + """Return a `Gtk.Image` for the given icon_name or None. + + Resolution policy: + - If `icon_name` looks like a filesystem path (absolute or contains path + separator) or the file exists, load from file. If no extension, also + try the same path with `.png` appended. + - Otherwise, strip any extension and try to load from the system + icon theme. If that fails, try creating an image from the original + name (some engines accept full names). + """ + if not icon_name: + return None + + # Helper function to load from file + def load_from_file(filename): + if os.path.exists(filename): + try: + # Try as a file path + picture = Gtk.Picture.new_for_filename(filename) + if picture.get_paintable(): + image = Gtk.Image.new_from_paintable(picture.get_paintable()) + if image.get_paintable(): + return image + except Exception: + pass + + # Alternative: try GdkPixbuf + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + if pixbuf: + return Gtk.Image.new_from_pixbuf(pixbuf) + except Exception: + pass + return None + + # Step 1: Try as file path + try: + if (os.path.isabs(icon_name) or + os.path.sep in icon_name or + os.path.exists(icon_name)): + + result = load_from_file(icon_name) + if result: + return result + + # If no extension, try with .png + base, ext = os.path.splitext(icon_name) + if not ext: + result = load_from_file(icon_name + '.png') + if result: + return result + except Exception: + pass + + # Step 2: Try icon theme + try: + # Strip extension for icon name + base_name = os.path.splitext(icon_name)[0] if '.' in icon_name else icon_name + + # Get default display and theme + display = Gdk.Display.get_default() + if display: + theme = Gtk.IconTheme.get_for_display(display) + + # Try to lookup the icon + paint = theme.lookup_icon( + base_name, + fallbacks=None, + size=size, + scale=1, + direction=Gtk.TextDirection.LTR, + flags=Gtk.IconLookupFlags.FORCE_REGULAR + ) + + if paint: + return Gtk.Image.new_from_paintable(paint) + + # Fallback: simple icon name creation + image = Gtk.Image.new_from_icon_name(base_name) + if image.get_icon_name(): # Check if icon was actually set + return image + + except Exception: + pass + + # Step 3: Final attempt with original name + try: + image = Gtk.Image.new_from_icon_name(icon_name) + if image.get_icon_name(): + return image + except Exception: + pass + + return None \ No newline at end of file diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index c134d02..7b7a7fb 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -19,6 +19,7 @@ import os import logging from ...yui_common import * +from .commongtk import _resolve_icon class YSelectionBoxGtk(YSelectionWidget): @@ -167,19 +168,78 @@ def _create_backend_widget(self): self._rows = [] for it in self._items: row = Gtk.ListBoxRow() + # build a horizontal container to hold optional icon + label + try: + widget_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + except Exception: + widget_box = Gtk.Box() + + # try to resolve icon for this item + icon_name = None + try: + fn = getattr(it, 'iconName', None) + if callable(fn): + icon_name = fn() + else: + icon_name = fn + except Exception: + icon_name = None + + img = None + try: + img = _resolve_icon(icon_name, size=16) + except Exception: + img = None + lbl = Gtk.Label(label=it.label() or "") try: if hasattr(lbl, "set_xalign"): lbl.set_xalign(0.0) except Exception: pass + try: - row.set_child(lbl) + if img is not None: + try: + widget_box.append(img) + except Exception: + try: + widget_box.add(img) + except Exception: + pass + # append label after icon (or alone) + try: + widget_box.append(lbl) + except Exception: + try: + widget_box.add(lbl) + except Exception: + pass except Exception: + # worst case: fall back to label only on the row try: - row.add(lbl) + row.set_child(lbl) except Exception: - pass + try: + row.add(lbl) + except Exception: + pass + + # attach widget_box into the row if not already attached + try: + row.set_child(widget_box) + except Exception: + try: + row.add(widget_box) + except Exception: + # if that fails, ensure label at least + try: + row.set_child(lbl) + except Exception: + try: + row.add(lbl) + except Exception: + pass # Make every row selectable so users can multi-select if mode allows. try: @@ -485,19 +545,74 @@ def addItem(self, item): try: if getattr(self, '_listbox', None) is not None: row = Gtk.ListBoxRow() + # build widget with optional icon + label + try: + widget_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + except Exception: + widget_box = Gtk.Box() + + icon_name = None + try: + fn = getattr(new_item, 'iconName', None) + if callable(fn): + icon_name = fn() + else: + icon_name = fn + except Exception: + icon_name = None + + img = None + try: + img = _resolve_icon(icon_name, size=16) + except Exception: + img = None + lbl = Gtk.Label(label=new_item.label() or "") try: if hasattr(lbl, "set_xalign"): lbl.set_xalign(0.0) except Exception: pass + try: - row.set_child(lbl) + if img is not None: + try: + widget_box.append(img) + except Exception: + try: + widget_box.add(img) + except Exception: + pass + try: + widget_box.append(lbl) + except Exception: + try: + widget_box.add(lbl) + except Exception: + pass except Exception: try: - row.add(lbl) + row.set_child(lbl) except Exception: - pass + try: + row.add(lbl) + except Exception: + pass + + try: + row.set_child(widget_box) + except Exception: + try: + row.add(widget_box) + except Exception: + try: + row.set_child(lbl) + except Exception: + try: + row.add(lbl) + except Exception: + pass + try: row.set_selectable(True) except Exception: From a8170c00f0ec732df08044b2e2033bebbc522988 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 14:10:35 +0100 Subject: [PATCH 189/523] Added icon to selection box items --- manatools/aui/backends/qt/commonqt.py | 72 +++++++++++++++++++++ manatools/aui/backends/qt/selectionboxqt.py | 71 ++++++++++++++++++-- 2 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 manatools/aui/backends/qt/commonqt.py diff --git a/manatools/aui/backends/qt/commonqt.py b/manatools/aui/backends/qt/commonqt.py new file mode 100644 index 0000000..a48429c --- /dev/null +++ b/manatools/aui/backends/qt/commonqt.py @@ -0,0 +1,72 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtGui +import os +import logging +from ...yui_common import * + + +__all__ = ["_resolve_icon"] + +def _resolve_icon(icon_name): + """Resolve an icon name to a QtGui.QIcon or None. + + - If icon_name is an existing absolute or relative path -> load from path. + - If icon_name contains a path separator or exists on filesystem -> treat as path. + - Otherwise strip extension (if any) and try QIcon.fromTheme, then fallback to QIcon(name). + """ + try: + if not icon_name: + return None + # If icon_name looks like a filesystem path (absolute or contains a + # path separator), prefer loading from disk. If it has no + # extension, also try the same path with a .png suffix to help + # debugging/test cases where a directory+basename is provided. + try: + if os.path.isabs(icon_name) or os.path.sep in icon_name: + # exact file + if os.path.exists(icon_name): + return QtGui.QIcon(icon_name) + # if there's no extension, try .png + base, ext = os.path.splitext(icon_name) + if not ext: + png_candidate = icon_name + '.png' + if os.path.exists(png_candidate): + return QtGui.QIcon(png_candidate) + # not found on filesystem: fall through to theme/name + else: + # non-path might still be a relative file name + if os.path.exists(icon_name): + return QtGui.QIcon(icon_name) + except Exception: + pass + base = icon_name + try: + if "." in base: + base = os.path.splitext(base)[0] + except Exception: + pass + try: + ico = QtGui.QIcon.fromTheme(base) + if ico and not ico.isNull(): + return ico + except Exception: + pass + try: + ico = QtGui.QIcon(icon_name) + if ico and not ico.isNull(): + return ico + except Exception: + pass + except Exception: + pass + return None \ No newline at end of file diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index 8da02f6..066b254 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -11,7 +11,9 @@ ''' from PySide6 import QtWidgets import logging +import os from ...yui_common import * +from .commonqt import _resolve_icon class YSelectionBoxQt(YSelectionWidget): def __init__(self, parent=None, label=""): @@ -33,7 +35,7 @@ def value(self): def label(self): return self._label - + def selectedItems(self): """Get list of selected items""" return self._selected_items @@ -109,9 +111,41 @@ def _create_backend_widget(self): mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi_selection else QtWidgets.QAbstractItemView.SingleSelection list_widget.setSelectionMode(mode) - # Add items to list widget + # Add items to list widget (use QListWidgetItem to support icons) for item in self._items: - list_widget.addItem(item.label()) + try: + text = item.label() if hasattr(item, 'label') else str(item) + except Exception: + try: + text = str(item) + except Exception: + text = "" + try: + li = QtWidgets.QListWidgetItem(text) + icon_name = None + try: + fn = getattr(item, 'iconName', None) + if callable(fn): + icon_name = fn() + else: + icon_name = fn + except Exception: + icon_name = None + icon = _resolve_icon(icon_name) + if icon is not None: + try: + li.setIcon(icon) + except Exception: + pass + list_widget.addItem(li) + except Exception: + try: + list_widget.addItem(item.label()) + except Exception: + try: + list_widget.addItem(str(item)) + except Exception: + pass # Reflect model's selected flags into the view. # If multi-selection is enabled, select all items flagged selected. @@ -201,7 +235,36 @@ def addItem(self, item): try: if getattr(self, '_list_widget', None) is not None: try: - self._list_widget.addItem(new_item.label()) + try: + text = new_item.label() if hasattr(new_item, 'label') else str(new_item) + except Exception: + text = str(new_item) + try: + li = QtWidgets.QListWidgetItem(text) + icon_name = None + try: + fn = getattr(new_item, 'iconName', None) + if callable(fn): + icon_name = fn() + else: + icon_name = fn + except Exception: + icon_name = None + icon = _resolve_icon(icon_name) + if icon is not None: + try: + li.setIcon(icon) + except Exception: + pass + self._list_widget.addItem(li) + except Exception: + try: + self._list_widget.addItem(new_item.label()) + except Exception: + try: + self._list_widget.addItem(str(new_item)) + except Exception: + pass # If the item is marked selected in the model, reflect it. if new_item.selected(): idx = self._list_widget.count() - 1 From f5b52df1b7c2de7a61f8b46d6e56f9eab74dc03e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 14:11:43 +0100 Subject: [PATCH 190/523] right exported function name --- manatools/aui/backends/gtk/commongtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/commongtk.py b/manatools/aui/backends/gtk/commongtk.py index bfc7764..6b11c13 100644 --- a/manatools/aui/backends/gtk/commongtk.py +++ b/manatools/aui/backends/gtk/commongtk.py @@ -21,7 +21,7 @@ from ...yui_common import * -__all__ = ["_curses_recursive_min_height"] +__all__ = ["_resolve_icon"] def _resolve_icon(icon_name, size=16): From 0f0d09dd70fd5ae9f659c1cb7abbfb2d1302a92e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 14:23:35 +0100 Subject: [PATCH 191/523] Applied common resolve_icon call to have same way to load icon --- manatools/aui/backends/qt/dialogqt.py | 12 ++--------- manatools/aui/yui_qt.py | 31 ++------------------------- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index 2af7c91..1544f5c 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -12,6 +12,7 @@ from PySide6 import QtWidgets, QtCore, QtGui from ...yui_common import YSingleChildContainerWidget, YUIDimension, YPropertySet, YProperty, YPropertyType, YUINoDialogException, YDialogType, YDialogColorMode, YEvent, YCancelEvent, YTimeoutEvent +from .commonqt import _resolve_icon from ... import yui as yui_mod import os import logging @@ -112,16 +113,7 @@ def _create_backend_widget(self): try: icon_spec = appobj.applicationIcon() if icon_spec: - # use the application's iconBasePath if present - base = getattr(appobj, "_icon_base_path", None) - if base and not os.path.isabs(icon_spec): - p = os.path.join(base, icon_spec) - if os.path.exists(p): - app_qicon = QtGui.QIcon(p) - if not app_qicon: - q = QtGui.QIcon.fromTheme(icon_spec) - if not q.isNull(): - app_qicon = q + app_qicon = _resolve_icon(icon_spec) except Exception: pass # if we have a qicon, set it on the QApplication and the new window diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 453326f..3fef506 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -7,6 +7,7 @@ import os from .yui_common import * from .backends.qt import * +from .backends.qt.commonqt import _resolve_icon class YUIQt: def __init__(self): @@ -72,34 +73,6 @@ def setApplicationTitle(self, title): except Exception: pass - def _resolve_qicon(self, icon_spec): - """Resolve icon_spec (path or theme name) into a QtGui.QIcon or None. - If iconBasePath is set, prefer that as absolute path base. - """ - if not icon_spec: - return None - # if we have a base path and the spec is not absolute, try that first - try: - if self._icon_base_path: - cand = icon_spec - if not os.path.isabs(cand): - cand = os.path.join(self._icon_base_path, icon_spec) - if os.path.exists(cand): - return QtGui.QIcon(cand) - # if icon_spec looks like an absolute path, try it - if os.path.isabs(icon_spec) and os.path.exists(icon_spec): - return QtGui.QIcon(icon_spec) - except Exception: - pass - # fallback to theme lookup - try: - theme_icon = QtGui.QIcon.fromTheme(icon_spec) - if not theme_icon.isNull(): - return theme_icon - except Exception: - pass - return None - def setApplicationIcon(self, Icon): """Set application icon spec (theme name or path). Try to apply it to QApplication and active dialogs. @@ -111,7 +84,7 @@ def setApplicationIcon(self, Icon): self._icon = "" # resolve into a QIcon and cache try: - self._qt_icon = self._resolve_qicon(self._icon) + self._qt_icon = _resolve_icon(self._icon) except Exception: self._qt_icon = None From 2f81932af97c1246c305e5af31145f3dba680dc9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 18:17:58 +0100 Subject: [PATCH 192/523] Fixed addItem, selectItem, icons and item selection --- manatools/aui/backends/qt/treeqt.py | 195 +++++++++++++++++++-- test/test_tree_example.py | 255 ++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+), 17 deletions(-) create mode 100644 test/test_tree_example.py diff --git a/manatools/aui/backends/qt/treeqt.py b/manatools/aui/backends/qt/treeqt.py index b128cd5..0fb5366 100644 --- a/manatools/aui/backends/qt/treeqt.py +++ b/manatools/aui/backends/qt/treeqt.py @@ -9,9 +9,10 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtGui import logging from ...yui_common import * +from .commonqt import _resolve_icon class YTreeQt(YSelectionWidget): """ @@ -39,6 +40,7 @@ def __init__(self, parent=None, label="", multiSelection=False, recursiveSelecti self._suppress_selection_handler = False # remember last selected QTreeWidgetItem set to detect added/removed selections self._last_selected_qitems = set() + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): return "YTree" @@ -67,23 +69,13 @@ def _create_backend_widget(self): # populate if items already present try: self.rebuildTree() - except Exception: - try: - self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") - self._logger.error("rebuildTree failed during _create_backend_widget", exc_info=True) - except Exception: - pass - pass - try: - # ensure logger exists and emit debug - if not hasattr(self, "_logger"): - self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) - except Exception: - pass + except Exception: + self._logger.error("rebuildTree failed during _create_backend_widget", exc_info=True) + def rebuildTree(self): """Rebuild the QTreeWidget from self._items (calls helper recursively).""" + self._logger.debug("rebuildTree: rebuilding tree with %d items", len(self._items) if self._items else 0) if self._tree_widget is None: # ensure backend exists self._create_backend_widget() @@ -91,6 +83,8 @@ def rebuildTree(self): self._qitem_to_item.clear() self._item_to_qitem.clear() self._tree_widget.clear() + # collect selected candidates while building so we can apply single-selection rules + selected_candidates = [] def _add_recursive(parent_qitem, item): # item expected to provide label() and possibly children() iterable @@ -106,6 +100,25 @@ def _add_recursive(parent_qitem, item): # preserve mapping self._qitem_to_item[qitem] = item self._item_to_qitem[item] = qitem + # set icon for this item if provided + try: + icon_name = None + fn = getattr(item, 'iconName', None) + if callable(fn): + icon_name = fn() + else: + icon_name = fn + if icon_name: + ico = _resolve_icon(icon_name) + if ico is not None: + try: + self._logger.debug("Column count for item %d", qitem.columnCount()) + qitem.setIcon(0, ico) + except Exception: + pass + except Exception: + self._logger.error("Error setting icon for tree item %s", text, exc_info=True) + pass # attach to parent or top-level if parent_qitem is None: self._tree_widget.addTopLevelItem(qitem) @@ -120,6 +133,14 @@ def _add_recursive(parent_qitem, item): except Exception: pass + # remember selection candidates + try: + if getattr(item, 'selected', None) and callable(getattr(item, 'selected')): + if item.selected(): + selected_candidates.append((qitem, item)) + except Exception: + pass + # recurse on children if available try: children = getattr(item, "children", None) @@ -146,6 +167,33 @@ def _add_recursive(parent_qitem, item): _add_recursive(None, it) except Exception: pass + # Apply selection state according to collected candidates and selection mode + try: + self._selected_items = [] + if self._multi: + for qit, it in selected_candidates: + try: + qit.setSelected(True) + if it not in self._selected_items: + self._selected_items.append(it) + try: + it.setSelected(True) + except Exception: + pass + except Exception: + pass + else: + if selected_candidates: + qit, it = selected_candidates[-1] + try: + qit.setSelected(True) + it.setSelected(True) + self._selected_items = [it] + except Exception: + pass + except Exception: + pass + # do not call expandAll(); expansion is controlled per-item by _is_open def currentItem(self): @@ -329,9 +377,26 @@ def addItem(self, item): '''Add a YItem redefinition from YSelectionWidget to manage YTreeItems.''' if isinstance(item, str): item = YTreeItem(item) - self._items.append(item) - else: super().addItem(item) + elif isinstance(item, YTreeItem): + super().addItem(item) + else: + self._logger.error("YTree.addItem: invalid item type %s", type(item)) + raise TypeError("YTree.addItem expects a YTreeItem or string label") + # ensure index set + try: + item.setIndex(len(self._items) - 1) + except Exception: + pass + # if backend exists, refresh tree to reflect new item (including icon/selection) + try: + if getattr(self, '_tree_widget', None) is not None: + try: + self.rebuildTree() + except Exception: + pass + except Exception: + pass # property API hooks (minimal implementation) def setProperty(self, propertyName, val): @@ -371,3 +436,99 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def selectItem(self, item, selected=True): + """Select or deselect the given logical YTreeItem and reflect in the view.""" + try: + # update model flag + try: + item.setSelected(bool(selected)) + except Exception: + pass + # if no tree widget, only model is updated + if getattr(self, '_tree_widget', None) is None: + # maintain internal selected list + if selected: + if item not in self._selected_items: + if not self._multi: + self._selected_items = [item] + else: + self._selected_items.append(item) + else: + try: + if item in self._selected_items: + self._selected_items.remove(item) + except Exception: + pass + return + + qit = self._item_to_qitem.get(item, None) + if qit is None: + # item may be newly added — rebuild tree and retry + try: + self.rebuildTree() + qit = self._item_to_qitem.get(item, None) + except Exception: + qit = None + + if qit is None: + return + + # apply selection in view + try: + if not self._multi and selected: + try: + self._tree_widget.clearSelection() + except Exception: + pass + # if recursive selection is enabled, select/deselect descendants too + targets = [qit] + if selected and self._recursive: + targets = self._collect_descendant_qitems(qit) + for tq in targets: + try: + tq.setSelected(bool(selected)) + except Exception: + pass + except Exception: + pass + + # update internal selected items list + try: + new_selected = [] + for q in self._tree_widget.selectedItems(): + itm = self._qitem_to_item.get(q, None) + if itm is not None: + new_selected.append(itm) + # if not multi, keep only last + if not self._multi and len(new_selected) > 1: + new_selected = [new_selected[-1]] + self._selected_items = new_selected + except Exception: + pass + except Exception: + pass + + def deleteAllItems(self): + """Remove all items from model and QTreeWidget view.""" + try: + super().deleteAllItems() + except Exception: + self._items = [] + self._selected_items = [] + try: + self._qitem_to_item.clear() + except Exception: + pass + try: + self._item_to_qitem.clear() + except Exception: + pass + try: + if getattr(self, '_tree_widget', None) is not None: + try: + self._tree_widget.clear() + except Exception: + pass + except Exception: + pass diff --git a/test/test_tree_example.py b/test/test_tree_example.py new file mode 100644 index 0000000..823e2a6 --- /dev/null +++ b/test/test_tree_example.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 + +"""Example dialog to manually test YTree widgets. + +Layout: +- HBox with two trees (left numeric items, right lettered items). +- Button "Swap" that clears both trees and swaps their items, selecting different items. +- Labels show the selected item and selected lists for both trees. + +Run with: `python -m pytest -q test/test_tree_example.py::test_tree_example -s` or run directly. +""" + +import os +import sys + +# allow running from repo root +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + +from manatools.aui.yui import YUI, YUI_ui +import manatools.aui.yui_common as yui + + +def build_numeric_tree(tree_factory, tree_widget): + items = [] + for i in range(1, 6): + itm = yui.YTreeItem(f"Item {i}", is_open=(i == 1)) + for j in range(1, 4): + sub = yui.YTreeItem(f"SubItem {i}.{j}", parent=itm) + for k in range(1, 3): + yui.YTreeItem(f"SubItem {i}.{j}.{k}", parent=sub) + items.append(itm) + tree_widget.addItem(itm) + return items + + +def build_letter_tree(tree_widget): + items = [] + letters = ['A', 'B', 'C', 'D', 'E'] + for idx, L in enumerate(letters, start=1): + itm = yui.YTreeItem(f"Item {L}", is_open=(idx == 1), icon_name=("edit-cut" if L == 'A' else "")) + for j in range(1, 4): + sub = yui.YTreeItem(f"SubItem {L}.{j}", parent=itm) + for k in range(1, 3): + yui.YTreeItem(f"SubItem {L}.{j}.{k}", parent=sub) + items.append(itm) + tree_widget.addItem(itm) + return items + + +def test_tree_example(backend_name=None): + if backend_name: + os.environ['YUI_BACKEND'] = backend_name + + # Ensure fresh YUI detection + YUI._instance = None + YUI._backend = None + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Log program name and detected backend + try: + backend = YUI.backend() + logging.getLogger().debug("test_tree_example: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) + except Exception: + logging.getLogger().debug("test_tree_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) + + # prepare dialog + dlg = factory.createMainDialog() + vbox = factory.createVBox(dlg) + factory.createHeading(vbox, "Tree Swap Example") + factory.createLabel(vbox, "Two trees side-by-side. Press Swap to exchange items.") + + hbox = factory.createHBox(vbox) + left_tree = factory.createTree(hbox, "Left Tree") + right_tree = factory.createTree(hbox, "Right Tree") + + # populate both trees + left_items = build_numeric_tree(factory, left_tree) + right_items = build_letter_tree(right_tree) + + # select one item per tree initially + try: + # choose a sub-sub item on left and a subitem on right + left_selected = left_items[1].children() if hasattr(left_items[1], 'children') else None + except Exception: + left_selected = None + try: + # pick second subitem of first top item on right + right_selected = right_items[0].children() if hasattr(right_items[0], 'children') else None + except Exception: + right_selected = None + + # helper to pick a specific logical item if possible + def pick_initial_selections(): + # left: choose Item 2.1.1 if exists + try: + t = left_items[1] + # get first child's first child + sc = list(t.children()) if callable(getattr(t, 'children', None)) else getattr(t, '_children', []) + if sc: + sc2 = list(sc[0].children()) if callable(getattr(sc[0], 'children', None)) else getattr(sc[0], '_children', []) + if sc2: + left_tree.selectItem(sc2[0], True) + except Exception: + pass + # right: choose Item A.2 (second subitem of first top item) + try: + t = right_items[0] + sc = list(t.children()) if callable(getattr(t, 'children', None)) else getattr(t, '_children', []) + if sc and len(sc) >= 2: + right_tree.selectItem(sc[1], True) + except Exception: + pass + + pick_initial_selections() + + # labels showing status + left_status = factory.createLabel(vbox, "Left selected: None") + right_status = factory.createLabel(vbox, "Right selected: None") + both_status = factory.createLabel(vbox, "Left selected list: [] | Right selected list: []") + + # control buttons + ctrl_h = factory.createHBox(vbox) + swap_btn = factory.createPushButton(ctrl_h, "Swap") + quit_btn = factory.createPushButton(ctrl_h, "Quit") + + # track previous selected labels so we can choose different items after swap + prev_left_label = None + prev_right_label = None + + def update_labels(): + try: + lsel = left_tree.selectedItems() + rsel = right_tree.selectedItems() + l_label = lsel[0].label() if lsel else "None" + r_label = rsel[0].label() if rsel else "None" + left_status.setText(f"Left selected: {l_label}") + right_status.setText(f"Right selected: {r_label}") + left_list = [it.label() for it in lsel] + right_list = [it.label() for it in rsel] + both_status.setText(f"Left selected list: {left_list} | Right selected list: {right_list}") + except Exception: + pass + + update_labels() + + print("Opening Tree example dialog...") + + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dlg.destroy() + break + if et == yui.YEventType.WidgetEvent: + w = ev.widget() + reason = ev.reason() + # selection changed events from either tree + if w == left_tree and reason == yui.YEventReason.SelectionChanged: + update_labels() + elif w == right_tree and reason == yui.YEventReason.SelectionChanged: + update_labels() + elif w == swap_btn and reason == yui.YEventReason.Activated: + # perform swap: capture model items, clear, swap, and set new selections + try: + left_model = list(left_tree._items) + right_model = list(right_tree._items) + except Exception: + left_model = [] + right_model = [] + try: + left_tree.deleteAllItems() + right_tree.deleteAllItems() + except Exception: + pass + # add swapped + try: + for it in right_model: + left_tree.addItem(it) + for it in left_model: + right_tree.addItem(it) + except Exception: + pass + + # select different items than before: pick last top-level in each + try: + if left_tree.hasItems(): + # pick last top-level's first sub-sub if available + it = left_tree._items[-1] + children = list(getattr(it, 'children', lambda: [])()) if callable(getattr(it, 'children', None)) else getattr(it, '_children', []) + target = None + if children: + sc = children[0] + sc2 = list(getattr(sc, 'children', lambda: [])()) if callable(getattr(sc, 'children', None)) else getattr(sc, '_children', []) + if sc2: + target = sc2[-1] + if target is None: + target = it + left_tree.selectItem(target, True) + except Exception: + pass + try: + if right_tree.hasItems(): + it = right_tree._items[-1] + children = list(getattr(it, 'children', lambda: [])()) if callable(getattr(it, 'children', None)) else getattr(it, '_children', []) + target = None + if children and len(children) >= 2: + target = children[1] + elif children: + target = children[0] + if target is None: + target = it + right_tree.selectItem(target, True) + except Exception: + pass + + update_labels() + elif w == quit_btn and reason == yui.YEventReason.Activated: + dlg.destroy() + break + + print("Dialog closed") + + +if __name__ == '__main__': + # allow running directly + if len(sys.argv) > 1: + test_tree_example(sys.argv[1]) + else: + test_tree_example() From 9452f19b8702cfab5d9d1c93868b387cb9645d94 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 18:23:13 +0100 Subject: [PATCH 193/523] Added addItem, deleteAllItems, selectItem --- manatools/aui/backends/gtk/treegtk.py | 151 ++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index ab5e710..8bd3877 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -19,6 +19,7 @@ import os import logging from ...yui_common import * +from .commongtk import _resolve_icon class YTreeGtk(YSelectionWidget): @@ -264,6 +265,32 @@ def _on_pressed(gesture_obj, n_press, x, y, target_item=item): lbl.set_hexpand(True) except Exception: pass + # try to resolve icon and prepend image if available + try: + icon_name = None + fn = getattr(item, 'iconName', None) + if callable(fn): + icon_name = fn() + else: + icon_name = fn + except Exception: + icon_name = None + img = None + try: + img = _resolve_icon(icon_name, size=16) + except Exception: + img = None + try: + if img is not None: + try: + hbox.append(img) + except Exception: + try: + hbox.add(img) + except Exception: + pass + except Exception: + pass hbox.append(lbl) except Exception: pass @@ -763,3 +790,127 @@ def get_backend_widget(self): if self._backend_widget is None: self._create_backend_widget() return self._backend_widget + + def addItem(self, item): + """Add YTreeItem to model and refresh view when needed.""" + if isinstance(item, str): + item = YTreeItem(item) + super().addItem(item) + else: + super().addItem(item) + try: + item.setIndex(len(self._items) - 1) + except Exception: + pass + try: + if getattr(self, '_listbox', None) is not None: + try: + self.rebuildTree() + except Exception: + pass + except Exception: + pass + + def selectItem(self, item, selected=True): + """Select/deselect a logical YTreeItem and reflect changes in the Gtk.ListBox.""" + try: + try: + item.setSelected(bool(selected)) + except Exception: + pass + + # if no listbox, update model only + if getattr(self, '_listbox', None) is None: + if selected: + if not self._multi: + self._selected_items = [item] + else: + if item not in self._selected_items: + self._selected_items.append(item) + else: + try: + if item in self._selected_items: + self._selected_items.remove(item) + except Exception: + pass + return + + # ensure mapping exists + row = self._item_to_row.get(item, None) + if row is None: + try: + self.rebuildTree() + row = self._item_to_row.get(item, None) + except Exception: + row = None + if row is None: + return + + # apply selection to row; handle single-selection by clearing others + try: + if not self._multi and selected: + try: + self._listbox.unselect_all() + except Exception: + pass + try: + row.set_selected(bool(selected)) + except Exception: + try: + setattr(row, '_selected_flag', bool(selected)) + except Exception: + pass + except Exception: + pass + + # update logical selected list + try: + new_sel = [] + for r in list(self._rows or []): + try: + if self._row_is_selected(r): + it = self._row_to_item.get(r) + if it is not None: + new_sel.append(it) + except Exception: + pass + if not self._multi and len(new_sel) > 1: + new_sel = [new_sel[-1]] + self._selected_items = new_sel + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + pass + except Exception: + pass + + def deleteAllItems(self): + """Clear model and view rows for this tree.""" + try: + super().deleteAllItems() + except Exception: + self._items = [] + self._selected_items = [] + try: + self._rows = [] + self._row_to_item.clear() + self._item_to_row.clear() + self._visible_items = [] + except Exception: + pass + try: + if getattr(self, '_listbox', None) is not None: + try: + # remove visible rows + for r in list(self._listbox.get_children() or []) if hasattr(self._listbox, 'get_children') else []: + try: + self._listbox.remove(r) + except Exception: + pass + try: + self._listbox.unselect_all() + except Exception: + pass + except Exception: + pass + except Exception: + pass From d99157d822f95cafa427b0e0ea938c202e63f6ec Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 19:14:26 +0100 Subject: [PATCH 194/523] Cleaned up code --- manatools/aui/backends/gtk/treegtk.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 8bd3877..9f78d95 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -49,6 +49,8 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti self._visible_items = [] # list of (item, depth) self._suppress_selection_handler = False self._last_selected_ids = set() + # preferred visible rows to hint initial/min height (approx 24px per row) + self._preferred_rows = 8 self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) @@ -75,8 +77,9 @@ def _create_backend_widget(self): # Let listbox expand in available area listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + listbox.set_valign(Gtk.Align.FILL) except Exception: - pass + self._logger.debug("Failed to set expansion on tree listbox") sw = Gtk.ScrolledWindow() try: @@ -89,12 +92,23 @@ def _create_backend_widget(self): # Make scrolled window expand to fill container (so tree respects parent stretching) try: + sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + # In GTK4, scrolled windows may clamp to natural child height; disable propagation + sw.set_propagate_natural_height(False) + # hint a reasonable minimum height based on preferred rows + min_h = int(getattr(self, "_preferred_rows", 8) * 24) + sw.set_min_content_height(min_h) vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + vbox.set_valign(Gtk.Align.FILL) except Exception: - pass + self._logger.debug("Failed to set expansion on tree scrolled window / vbox") + sw.set_vexpand(True) + sw.set_hexpand(True) + vbox.set_vexpand(True) + vbox.set_hexpand(True) # connect selection signal; use defensive handler that scans rows try: From ec4a86933fb6ea2757ea0eafbf9cefb0b9650f16 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 19:24:54 +0100 Subject: [PATCH 195/523] Using Expander instead of button --- manatools/aui/backends/gtk/treegtk.py | 93 ++++++++++++--------------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 9f78d95..9c204a9 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -172,87 +172,76 @@ def _make_row(self, item, depth): if has_children: try: - btn = Gtk.Button(label="▾" if bool(getattr(item, "_is_open", False)) else "▸") + # Replace toggle button with Gtk.Expander to show standard tree affordance + exp = Gtk.Expander() try: - btn.set_relief(Gtk.ReliefStyle.NONE) + # keep the expander focused off to avoid selection side-effects + exp.set_can_focus(False) except Exception: pass - # prevent the toggle button from taking focus / causing selection side-effects try: - btn.set_focus_on_click(False) + # use an empty label so only the arrow is shown here + exp.set_label("") except Exception: pass try: - btn.set_can_focus(False) + # keep a compact width similar to the former button + exp.set_size_request(14, 1) except Exception: pass - # make button visually flat (no border/background) so it looks like a tree expander try: - btn.add_css_class("flat") + # reflect current open state + exp.set_expanded(bool(getattr(item, "_is_open", False))) except Exception: - # fallback: try another common class name - try: - btn.add_css_class("link") - except Exception: - pass + pass - # Use a GestureClick on the button to reliably receive a single-click action - # and avoid the occasional need for double clicks caused by focus/selection interplay. - try: - gesture = Gtk.GestureClick() - # accept any button; if set_button exists restrict to primary + def _on_expanded_changed(expander, pspec, target_item=item): try: - gesture.set_button(0) + self._suppress_selection_handler = True except Exception: pass - # pressed handler will toggle immediately - def _on_pressed(gesture_obj, n_press, x, y, target_item=item): - # run toggle synchronously and suppress selection handler while rebuilding + try: + # Sync model open state from expander try: - self._suppress_selection_handler = True + is_open = bool(getattr(expander, "get_expanded", None) and expander.get_expanded()) except Exception: - pass - try: - # toggle using public API if available - try: - cur = target_item.isOpen() - target_item.setOpen(not cur) - except Exception: - try: - cur = bool(getattr(target_item, "_is_open", False)) - target_item._is_open = not cur - except Exception: - pass - # preserve selection and rebuild - try: - self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) - except Exception: - self._last_selected_ids = set() try: - self.rebuildTree() + # fallback property access + is_open = bool(getattr(expander, "expanded", False)) except Exception: - pass - finally: + is_open = False + try: + target_item.setOpen(is_open) + except Exception: try: - self._suppress_selection_handler = False + target_item._is_open = is_open except Exception: pass - - gesture.connect("pressed", _on_pressed) - try: - btn.add_controller(gesture) - except Exception: + # preserve selection and rebuild + try: + self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) + except Exception: + self._last_selected_ids = set() + try: + self.rebuildTree() + except Exception: + pass + finally: try: - btn.add_controller(gesture) + self._suppress_selection_handler = False except Exception: pass + + try: + exp.connect("notify::expanded", _on_expanded_changed) except Exception: - # Fallback to clicked if GestureClick not available + # fallback: use activate if notify is not available try: - btn.connect("clicked", lambda b, it=item: self._on_toggle_clicked(it)) + exp.connect("activate", lambda e, it=item: self._on_toggle_clicked(it)) except Exception: pass - hbox.append(btn) + + hbox.append(exp) except Exception: # fallback spacer try: From 42f9fce3445a2dfb542f4a79a1722c07a59c8a8a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 22:03:34 +0100 Subject: [PATCH 196/523] _selected_item already defined in super class --- manatools/aui/backends/gtk/selectionboxgtk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 7b7a7fb..6d06be4 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -26,8 +26,7 @@ class YSelectionBoxGtk(YSelectionWidget): def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label - self._value = "" - self._selected_items = [] + self._value = "" self._old_selected_items = [] # for change detection self._multi_selection = False self._listbox = None From 75440a841e0f5555ad25f364b6ffd0099ef3aa74 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 23 Dec 2025 22:10:45 +0100 Subject: [PATCH 197/523] Improving item selection --- manatools/aui/backends/gtk/treegtk.py | 144 +++++++++++++++++++++----- 1 file changed, 116 insertions(+), 28 deletions(-) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 9c204a9..e80bf04 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -42,6 +42,7 @@ def __init__(self, parent=None, label="", multiselection=False, recursiveselecti self._backend_widget = None self._listbox = None self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._old_selected_items = [] # for change detection # cached rows and mappings self._rows = [] # ordered list of Gtk.ListBoxRow self._row_to_item = {} # row -> YTreeItem @@ -110,15 +111,6 @@ def _create_backend_widget(self): vbox.set_vexpand(True) vbox.set_hexpand(True) - # connect selection signal; use defensive handler that scans rows - try: - listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) - except Exception: - try: - self._logger.error("Failed to connect row-selected handler", exc_info=True) - except Exception: - pass - self._backend_widget = vbox self._listbox = listbox self._backend_widget.set_sensitive(self._enabled) @@ -140,10 +132,21 @@ def _create_backend_widget(self): self._logger.error("rebuildTree failed during _create_backend_widget", exc_info=True) except Exception: pass - try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) - except Exception: - pass + + # connect selection signal; use defensive handler that scans rows + if self._multi: + try: + self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) + self._listbox.connect("row-activated", lambda lb, row: self._on_row_selected_for_multi(lb, row)) + except Exception: + pass + else: + try: + self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + except Exception: + pass + + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) def _make_row(self, item, depth): """Create a ListBoxRow for item with indentation and (optional) toggle button.""" @@ -439,9 +442,9 @@ def _visit(nodes, depth=0): pass # restore previous selection (visible rows only) + self._suppress_selection_handler = True try: if self._last_selected_ids: - self._suppress_selection_handler = True try: self._listbox.unselect_all() except Exception: @@ -449,16 +452,15 @@ def _visit(nodes, depth=0): for row, item in list(self._row_to_item.items()): try: if id(item) in self._last_selected_ids: - try: - row.set_selected(True) - except Exception: - pass + self._listbox.select_row(row) except Exception: pass - self._suppress_selection_handler = False except Exception: - self._suppress_selection_handler = False + pass + # cleaning up old selection + for it in self._selected_items: + it.setSelected(False) # rebuild logical selected items from rows self._selected_items = [] for row in self._rows: @@ -470,10 +472,12 @@ def _visit(nodes, depth=0): if sel: it = self._row_to_item.get(row, None) if it is not None: + it.setSelected(True) self._selected_items.append(it) except Exception: pass + self._suppress_selection_handler = False self._last_selected_ids = set(id(i) for i in self._selected_items) except Exception: pass @@ -482,9 +486,7 @@ def _row_is_selected(self, r): """Robust helper to detect whether a ListBoxRow is selected.""" try: # preferred API - sel = getattr(r, "get_selected", None) - if callable(sel): - return bool(sel()) + return bool(r.is_selected()) except Exception: pass try: @@ -553,7 +555,10 @@ def _apply_desired_ids_to_rows(self, desired_ids): try: target = id(it) in desired_ids try: - row.set_selected(bool(target)) + if target: + self._listbox.select_row(row) + else: + self._listbox.unselect_row(row) except Exception: try: setattr(row, "_selected_flag", bool(target)) @@ -567,12 +572,28 @@ def _apply_desired_ids_to_rows(self, desired_ids): except Exception: pass - def _on_row_selected(self, listbox, row): - """Handle selection change; update logical selected items reliably. + def _on_row_selected_for_multi(self, listbox, row): + """ + Handler for row selection in multi-selection mode: for de-selection. + """ + self._logger.debug("_on_row_selected_for_multi called") + sel_rows = listbox.get_selected_rows() + it = self._row_to_item.get(row, None) + if it is not None: + if it in self._old_selected_items: + self._listbox.unselect_row( row ) + it.setSelected( False ) + self._on_selected_rows_changed(listbox) + else: + self._old_selected_items = self._selected_items - When recursive selection is enabled and multi-selection is on, - selecting/deselecting a parent will also select/deselect all its descendants. + + def _on_selected_rows_changed(self, listbox): + """ + Handler for multi-selection (or bulk selection change). Rebuild selected list + using either ListBox APIs (if available) or by scanning cached rows. """ + self._logger.debug("_on_selected_rows_changed called") # ignore if programmatic change in progress if self._suppress_selection_handler: return @@ -720,6 +741,73 @@ def _clear_flags(nodes): except Exception: pass + # store logical selection + self._old_selected_items = self._selected_items + self._selected_items = list(cur_selected_items) + self._last_selected_ids = set(id(i) for i in self._selected_items) + + # notify immediate mode + if self._immediate and self.notify(): + try: + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + except Exception: + pass + + def _on_row_selected(self, listbox, row): + """Handle selection change; update logical selected items reliably. + + When recursive selection is enabled and multi-selection is on, + selecting/deselecting a parent will also select/deselect all its descendants. + """ + self._logger.debug(f"_on_row_selected called {row if row else 'None'}") + # ignore if programmatic change in progress + if self._suppress_selection_handler: + return + + try: + selected_rows = self._gather_selected_rows() + + # map rows -> items + cur_selected_items = [] + for r in selected_rows: + try: + it = self._row_to_item.get(r, None) + if it is not None: + cur_selected_items.append(it) + except Exception: + pass + + # Update logical selection flags + try: + def _clear_flags(nodes): + for n in nodes: + try: + n.setSelected(False) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + _clear_flags(chs) + _clear_flags(list(getattr(self, "_items", []) or [])) + except Exception: + pass + + if len(cur_selected_items) > 1: + self._logger.warning(f"Multiple selected items: {[it.label() for it in cur_selected_items]}") + + for it in cur_selected_items: + try: + it.setSelected(True) + except Exception: + pass + # store logical selection self._selected_items = list(cur_selected_items) self._last_selected_ids = set(id(i) for i in self._selected_items) From d390b9ad432e24b7874b23e036439f3ef5911110 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 11:36:07 +0100 Subject: [PATCH 198/523] continue improving item selection, open parents if a subitem is selected --- manatools/aui/backends/gtk/treegtk.py | 120 +++++++++++++++++++------- 1 file changed, 88 insertions(+), 32 deletions(-) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index e80bf04..f0a3632 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -199,6 +199,8 @@ def _make_row(self, item, depth): pass def _on_expanded_changed(expander, pspec, target_item=item): + """Handler for expander expanded change: update model and rebuild.""" + self._logger.debug("_on_expanded_changed called for item <%s>", str(target_item)) try: self._suppress_selection_handler = True except Exception: @@ -428,6 +430,54 @@ def _visit(nodes, depth=0): _visit(childs, depth + 1) roots = list(getattr(self, "_items", []) or []) + + # Ensure that any selected item has its ancestors opened so it becomes visible. + try: + def _collect_all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_all_nodes(chs)) + return out + + all_nodes = _collect_all_nodes(roots) + for it in all_nodes: + try: + sel = False + if callable(getattr(it, 'selected', None)): + sel = it.selected() + else: + sel = bool(getattr(it, '_selected', False)) + if sel: + # walk up parent chain and open each ancestor + parent = None + try: + parent = it.parentItem() if callable(getattr(it, 'parentItem', None)) else getattr(it, '_parent_item', None) + except Exception: + parent = getattr(it, '_parent_item', None) + while parent: + try: + # prefer public API + try: + parent.setOpen(True) + except Exception: + parent._is_open = True + except Exception: + pass + try: + parent = parent.parentItem() if callable(getattr(parent, 'parentItem', None)) else getattr(parent, '_parent_item', None) + except Exception: + break + except Exception: + pass + except Exception: + pass + _visit(roots, 0) # create rows @@ -442,38 +492,24 @@ def _visit(nodes, depth=0): pass # restore previous selection (visible rows only) + # If caller provided _last_selected_ids (e.g. during toggle), honor that. + # Otherwise, honor any items that already have their model-selected flag set. self._suppress_selection_handler = True try: - if self._last_selected_ids: - try: - self._listbox.unselect_all() - except Exception: - pass - for row, item in list(self._row_to_item.items()): - try: - if id(item) in self._last_selected_ids: - self._listbox.select_row(row) - except Exception: - pass + self._listbox.unselect_all() except Exception: pass - - # cleaning up old selection - for it in self._selected_items: - it.setSelected(False) - # rebuild logical selected items from rows + self._selected_items = [] - for row in self._rows: + for it in list(getattr(self, "_items", []) or []): try: - if getattr(row, "get_selected", None): - sel = row.get_selected() - else: - sel = bool(getattr(row, "_selected_flag", False)) - if sel: - it = self._row_to_item.get(row, None) - if it is not None: - it.setSelected(True) - self._selected_items.append(it) + if it.selected(): + row = self._row_to_item.get(it, None) + if row is not None: + self._listbox.select_row(row) + else: + self._logger.debug("rebuildTree: item <%s> not visible yet checking parent", str(it)) + self._selected_items.append(it) except Exception: pass @@ -763,7 +799,18 @@ def _on_row_selected(self, listbox, row): When recursive selection is enabled and multi-selection is on, selecting/deselecting a parent will also select/deselect all its descendants. """ - self._logger.debug(f"_on_row_selected called {row if row else 'None'}") + try: + mapped = self._row_to_item.get(row, None) if row is not None else None + try: + mapped_label = mapped.label() if (mapped is not None and callable(getattr(mapped, 'label', None))) else str(mapped) + except Exception: + mapped_label = repr(mapped) + self._logger.debug("_on_row_selected called row=%r -> item=%r label=%r", row, mapped, mapped_label) + except Exception: + try: + self._logger.debug("_on_row_selected called (row=%r)", row) + except Exception: + pass # ignore if programmatic change in progress if self._suppress_selection_handler: return @@ -944,13 +991,22 @@ def selectItem(self, item, selected=True): self._listbox.unselect_all() except Exception: pass - try: - row.set_selected(bool(selected)) - except Exception: + if selected: try: - setattr(row, '_selected_flag', bool(selected)) + self._listbox.select_row(row) except Exception: - pass + try: + setattr(row, '_selected_flag', True) + except Exception: + pass + else: + try: + self._listbox.unselect_row(row) + except Exception: + try: + setattr(row, '_selected_flag', False) + except Exception: + pass except Exception: pass From c8fc371164dc61a98a6ee71cec42ecbb6492df0f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 11:59:32 +0100 Subject: [PATCH 199/523] simplify selectItem --- manatools/aui/backends/gtk/treegtk.py | 93 +++++++-------------------- 1 file changed, 22 insertions(+), 71 deletions(-) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index f0a3632..827b9e7 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -374,8 +374,10 @@ def _collect_all_descendants(self, item): def rebuildTree(self): """Flatten visible items according to _is_open and populate the ListBox.""" + _suppress_selection_handler = True if self._backend_widget is None or self._listbox is None: - self._create_backend_widget() + #self._create_backend_widget() + return try: # clear listbox rows robustly: repeatedly remove first child until none remain try: @@ -517,6 +519,7 @@ def _collect_all_nodes(nodes): self._last_selected_ids = set(id(i) for i in self._selected_items) except Exception: pass + _suppress_selection_handler = False def _row_is_selected(self, r): """Robust helper to detect whether a ListBoxRow is selected.""" @@ -612,6 +615,8 @@ def _on_row_selected_for_multi(self, listbox, row): """ Handler for row selection in multi-selection mode: for de-selection. """ + if self._suppress_selection_handler: + return self._logger.debug("_on_row_selected_for_multi called") sel_rows = listbox.get_selected_rows() it = self._row_to_item.get(row, None) @@ -952,81 +957,27 @@ def addItem(self, item): def selectItem(self, item, selected=True): """Select/deselect a logical YTreeItem and reflect changes in the Gtk.ListBox.""" try: - try: - item.setSelected(bool(selected)) - except Exception: - pass - - # if no listbox, update model only - if getattr(self, '_listbox', None) is None: - if selected: - if not self._multi: - self._selected_items = [item] - else: - if item not in self._selected_items: - self._selected_items.append(item) + if selected: + if not self._multi: + if len(self._selected_items) > 0: + self._selected_items[0].setSelected(False) + self._selected_items = [item] else: - try: - if item in self._selected_items: - self._selected_items.remove(item) - except Exception: - pass - return - - # ensure mapping exists - row = self._item_to_row.get(item, None) - if row is None: + if item not in self._selected_items: + self._selected_items.append(item) + else: try: - self.rebuildTree() - row = self._item_to_row.get(item, None) + if item in self._selected_items: + self._selected_items.remove(item) except Exception: - row = None - if row is None: - return + pass - # apply selection to row; handle single-selection by clearing others - try: - if not self._multi and selected: - try: - self._listbox.unselect_all() - except Exception: - pass - if selected: - try: - self._listbox.select_row(row) - except Exception: - try: - setattr(row, '_selected_flag', True) - except Exception: - pass - else: - try: - self._listbox.unselect_row(row) - except Exception: - try: - setattr(row, '_selected_flag', False) - except Exception: - pass - except Exception: - pass + item.setSelected(bool(selected)) - # update logical selected list - try: - new_sel = [] - for r in list(self._rows or []): - try: - if self._row_is_selected(r): - it = self._row_to_item.get(r) - if it is not None: - new_sel.append(it) - except Exception: - pass - if not self._multi and len(new_sel) > 1: - new_sel = [new_sel[-1]] - self._selected_items = new_sel - self._last_selected_ids = set(id(i) for i in self._selected_items) - except Exception: - pass + if getattr(self, '_listbox', None) is None: + return + # ensure mapping exists + self.rebuildTree() except Exception: pass From 9bd687a972940ba8320eaefc25160181a4f6f038 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 12:10:50 +0100 Subject: [PATCH 200/523] Added new api to get YTreeItem parent. --- manatools/aui/yui_common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 848dfe3..0bdf5a0 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -410,6 +410,9 @@ def __init__(self, label: str, parent: Optional["YTreeItem"] = None, is_open: bo if parent: parent.addChild(self) + def parentItem(self): + return self._parent_item + def hasChildren(self): return len(self._children) > 0 From 778b185ccc92a26f369c93f0c31eafd832a3656d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 12:11:24 +0100 Subject: [PATCH 201/523] Honor initial item selection --- manatools/aui/backends/gtk/treegtk.py | 83 +++++++++++++++++++++------ 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 827b9e7..6eabe3f 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -493,30 +493,77 @@ def _collect_all_nodes(nodes): except Exception: pass - # restore previous selection (visible rows only) - # If caller provided _last_selected_ids (e.g. during toggle), honor that. - # Otherwise, honor any items that already have their model-selected flag set. + # restore selection without emitting selection-changed while syncing UI to model + # Desired selection source: + # 1) If _last_selected_ids set (e.g., after expand/collapse), honor it. + # 2) Otherwise, collect all nodes in the tree with selected()==True. self._suppress_selection_handler = True try: - self._listbox.unselect_all() - except Exception: - pass - - self._selected_items = [] - for it in list(getattr(self, "_items", []) or []): try: - if it.selected(): - row = self._row_to_item.get(it, None) - if row is not None: - self._listbox.select_row(row) - else: - self._logger.debug("rebuildTree: item <%s> not visible yet checking parent", str(it)) - self._selected_items.append(it) + self._listbox.unselect_all() except Exception: pass - self._suppress_selection_handler = False - self._last_selected_ids = set(id(i) for i in self._selected_items) + # collect all nodes in the current model + def _collect_all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_all_nodes(chs)) + return out + all_nodes = _collect_all_nodes(roots) + + desired_ids = set(self._last_selected_ids or []) + if not desired_ids: + for n in all_nodes: + try: + if callable(getattr(n, 'selected', None)): + if n.selected(): + desired_ids.add(id(n)) + else: + if bool(getattr(n, '_selected', False)): + desired_ids.add(id(n)) + except Exception: + pass + + if desired_ids: + # apply to visible rows + try: + self._apply_desired_ids_to_rows(desired_ids) + except Exception: + # fallback per-row loop + try: + for row, item in list(self._row_to_item.items()): + if id(item) in desired_ids: + try: + self._listbox.select_row(row) + except Exception: + pass + except Exception: + pass + + # mirror selection flags back to items and build logical list + self._selected_items = [] + for n in all_nodes: + try: + sel = id(n) in desired_ids + try: + n.setSelected(sel) + except Exception: + pass + if sel: + self._selected_items.append(n) + except Exception: + pass + + self._last_selected_ids = set(id(i) for i in self._selected_items) + finally: + self._suppress_selection_handler = False except Exception: pass _suppress_selection_handler = False From ef72d0736f05899597be4b145ddb789ba2d3079d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 12:29:09 +0100 Subject: [PATCH 202/523] Added selectItem, deleteAllItem and honor item selected in creating widget --- manatools/aui/backends/curses/treecurses.py | 130 ++++++++++++++++---- 1 file changed, 109 insertions(+), 21 deletions(-) diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index b8ac2ba..7519be2 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -143,6 +143,30 @@ def clearItems(self): except Exception: pass + def deleteAllItems(self): + """Clear model and all internal state for this tree.""" + try: + try: + super().deleteAllItems() + except Exception: + self._items = [] + except Exception: + pass + # Reset selection and visibility state + try: + self._selected_items = [] + self._last_selected_ids = set() + self._visible_items = [] + self._hover_index = 0 + self._scroll_offset = 0 + except Exception: + pass + try: + # No items to show; nothing else to rebuild but ensure state consistent + self._flatten_visible() + except Exception: + pass + def _collect_all_descendants(self, item): out = [] stack = [] @@ -182,9 +206,67 @@ def _visit(nodes, depth=0): _visit(roots, 0) def rebuildTree(self): - """Recompute visible items and restore selection from item.selected() or last_selected_ids.""" + """Recompute visible items and restore selection from item.selected() or last_selected_ids. + + Ensures ancestors of selected items are opened so selections are visible. + """ # preserve items selection if any try: + # collect all nodes and desired selected ids (from last ids or model flags) + def _all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_all_nodes(chs)) + return out + roots = list(getattr(self, "_items", []) or []) + all_nodes = _all_nodes(roots) + + selected_ids = set(self._last_selected_ids) if self._last_selected_ids else set() + if not selected_ids: + for n in all_nodes: + try: + sel = False + if hasattr(n, "selected") and callable(getattr(n, "selected")): + sel = n.selected() + else: + sel = bool(getattr(n, "_selected", False)) + if sel: + selected_ids.add(id(n)) + except Exception: + pass + + # open ancestors for any selected node so it becomes visible + if selected_ids: + for n in list(all_nodes): + try: + if id(n) in selected_ids: + parent = None + try: + parent = n.parentItem() if callable(getattr(n, 'parentItem', None)) else getattr(n, '_parent_item', None) + except Exception: + parent = getattr(n, '_parent_item', None) + while parent: + try: + try: + parent.setOpen(True) + except Exception: + setattr(parent, '_is_open', True) + except Exception: + pass + try: + parent = parent.parentItem() if callable(getattr(parent, 'parentItem', None)) else getattr(parent, '_parent_item', None) + except Exception: + break + except Exception: + pass + + # now recompute visible list based on possibly opened parents self._flatten_visible() # if there are previously saved last_selected_ids, prefer them selected_ids = set(self._last_selected_ids) if self._last_selected_ids else set() @@ -224,27 +306,11 @@ def _collect_selected(nodes): sel_items.append(itm) except Exception: pass - # also include non-visible selected nodes (descendants) if recursive selection used + # also include non-visible selected nodes (descendants) when tracking logic if selected_ids: - try: - # scan full tree - def _all_nodes(nodes): - out = [] - for n in nodes: - out.append(n) - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - out.extend(_all_nodes(chs)) - return out - all_nodes = _all_nodes(list(getattr(self, "_items", []) or [])) - for n in all_nodes: - if id(n) in selected_ids and n not in sel_items: - sel_items.append(n) - except Exception: - pass + for n in all_nodes: + if id(n) in selected_ids and n not in sel_items: + sel_items.append(n) # apply selected flags to items consistently try: # clear all first @@ -587,6 +653,23 @@ def selectItem(self, item, selected=True): except Exception: pass self._selected_items.append(d) + # open parents so programmatically selected items are visible + try: + parent = item.parentItem() if callable(getattr(item, 'parentItem', None)) else getattr(item, '_parent_item', None) + except Exception: + parent = getattr(item, '_parent_item', None) + while parent: + try: + try: + parent.setOpen(True) + except Exception: + setattr(parent, '_is_open', True) + except Exception: + pass + try: + parent = parent.parentItem() if callable(getattr(parent, 'parentItem', None)) else getattr(parent, '_parent_item', None) + except Exception: + break else: # deselect if item in self._selected_items: @@ -620,5 +703,10 @@ def selectItem(self, item, selected=True): self._last_selected_ids = set(id(i) for i in self._selected_items) except Exception: self._last_selected_ids = set() + # after programmatic selection, rebuild visible list to reflect opened parents + try: + self.rebuildTree() + except Exception: + pass except Exception: pass From adab723dd1158a7b270bf9f6e1a81eedde505e7b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 12:44:57 +0100 Subject: [PATCH 203/523] Api change adding selected to YTreeItem as an option --- manatools/aui/yui_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 0bdf5a0..ab20ab7 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -400,10 +400,10 @@ def setData(self, new_data): self._data = new_data class YTreeItem(YItem): - def __init__(self, label: str, parent: Optional["YTreeItem"] = None, is_open: bool = False, icon_name: str = ""): + def __init__(self, label: str, parent: Optional["YTreeItem"] = None, selected: Optional[bool] = False, is_open: bool = False, icon_name: str = ""): ''' YTreeItem represents an item in a tree structure. It can have child items and can be expanded or collapsed.''' - super().__init__(label, False, icon_name) + super().__init__(label, selected, icon_name) self._children = [] self._is_open = is_open self._parent_item = parent From 4e72d9f73da2d96612c19e52db1b247937014859 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 13:01:45 +0100 Subject: [PATCH 204/523] improving selection on widget creation and readding items --- manatools/aui/backends/curses/treecurses.py | 44 +++++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index 7519be2..7295bb3 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -130,19 +130,6 @@ def removeItem(self, item): except Exception: pass - def clearItems(self): - """Clear items and rebuild.""" - try: - try: - super().clearItems() - except Exception: - self._items = [] - finally: - try: - self.rebuildTree() - except Exception: - pass - def deleteAllItems(self): """Clear model and all internal state for this tree.""" try: @@ -268,6 +255,25 @@ def _all_nodes(nodes): # now recompute visible list based on possibly opened parents self._flatten_visible() + + # In single-selection mode, clamp desired selection to a single item. + if selected_ids and not self._multi: + chosen_id = None + # Prefer a visible item + for itm, _d in self._visible_items: + try: + if id(itm) in selected_ids: + chosen_id = id(itm) + break + except Exception: + pass + if chosen_id is None: + try: + # fallback: arbitrary one + chosen_id = next(iter(selected_ids)) + except Exception: + chosen_id = None + selected_ids = {chosen_id} if chosen_id is not None else set() # if there are previously saved last_selected_ids, prefer them selected_ids = set(self._last_selected_ids) if self._last_selected_ids else set() # if none, collect from items' selected() property @@ -311,6 +317,18 @@ def _collect_selected(nodes): for n in all_nodes: if id(n) in selected_ids and n not in sel_items: sel_items.append(n) + # Ensure single-selection list contains only one item + if not self._multi and len(sel_items) > 1: + # Prefer the visible one if present + visible_ids = {id(itm) for itm, _d in self._visible_items} + chosen = None + for it in sel_items: + if id(it) in visible_ids: + chosen = it + break + if chosen is None: + chosen = sel_items[0] + sel_items = [chosen] # apply selected flags to items consistently try: # clear all first From 2c5d801c65e8ff80b29f6f95e2e1e96bed55d36b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 13:18:02 +0100 Subject: [PATCH 205/523] changed up and down arrows on scrolled bar --- manatools/aui/backends/curses/treecurses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index 7295bb3..2c2f340 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -562,9 +562,9 @@ def _draw(self, window, y, x, width, height): # Scroll indicators based on actual viewport rows try: if self._scroll_offset > 0 and available_rows > 0: - window.addch(y + label_rows, x + max(0, width - 1), '^') + window.addch(y + label_rows, x + max(0, width - 1), '↑', curses.A_REVERSE) if (self._scroll_offset + available_rows) < total and available_rows > 0: - window.addch(y + label_rows + min(available_rows - 1, total - 1), x + max(0, width - 1), 'v') + window.addch(y + label_rows + min(available_rows - 1, total - 1), x + max(0, width - 1), '↓', curses.A_REVERSE) except curses.error: pass except Exception: From 7eff136a660e0f7b11d8f6d82c8291b0e1fc8b06 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 14:56:09 +0100 Subject: [PATCH 206/523] Aligned Qt opening parents of a selected widget and fix selection items after deleting all the items and adding new ones --- manatools/aui/backends/qt/treeqt.py | 208 ++++++++++++++++++++++++---- 1 file changed, 182 insertions(+), 26 deletions(-) diff --git a/manatools/aui/backends/qt/treeqt.py b/manatools/aui/backends/qt/treeqt.py index 0fb5366..1449c2d 100644 --- a/manatools/aui/backends/qt/treeqt.py +++ b/manatools/aui/backends/qt/treeqt.py @@ -40,6 +40,8 @@ def __init__(self, parent=None, label="", multiSelection=False, recursiveSelecti self._suppress_selection_handler = False # remember last selected QTreeWidgetItem set to detect added/removed selections self._last_selected_qitems = set() + # track logical selected item ids to preserve across rebuilds/swaps + self._last_selected_ids = set() self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): @@ -76,9 +78,40 @@ def _create_backend_widget(self): def rebuildTree(self): """Rebuild the QTreeWidget from self._items (calls helper recursively).""" self._logger.debug("rebuildTree: rebuilding tree with %d items", len(self._items) if self._items else 0) + self._suppress_selection_handler = True if self._tree_widget is None: # ensure backend exists self._create_backend_widget() + # Ensure ancestors of any selected items are opened in the model so they will be expanded in the view + try: + self._logger.debug("rebuildTree: opening ancestors of selected items") + + def _all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + for c in chs: + out.extend(_all_nodes([c])) + return out + roots = list(getattr(self, "_items", []) or []) + for it in _all_nodes(roots): + try: + is_sel = it.selected() + if is_sel: + self._logger.debug("rebuildTree: opening ancestors of selected item %s", str(it.label())) + # open parent chain + parent = it.parentItem() + while parent: + parent.setOpen(True) + parent = parent.parentItem() + except Exception: + pass + except Exception: + pass # clear existing self._qitem_to_item.clear() self._item_to_qitem.clear() @@ -167,32 +200,97 @@ def _add_recursive(parent_qitem, item): _add_recursive(None, it) except Exception: pass - # Apply selection state according to collected candidates and selection mode + # Apply selection state strictly from items' selected() flags (YTreeItem wins) try: - self._selected_items = [] - if self._multi: - for qit, it in selected_candidates: + # build desired_ids by scanning items' selected flags only + roots = list(getattr(self, "_items", []) or []) + def _all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) try: - qit.setSelected(True) - if it not in self._selected_items: - self._selected_items.append(it) + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_all_nodes(chs)) + return out + desired_ids = set() + for it in _all_nodes(roots): + try: + sel = False + if hasattr(it, 'selected') and callable(getattr(it, 'selected')): + sel = it.selected() + else: + sel = bool(getattr(it, '_selected', False)) + if sel: + desired_ids.add(id(it)) + except Exception: + pass + + # map items to qitems for selection application + self._suppress_selection_handler = True + try: + self._tree_widget.clearSelection() + except Exception: + pass + self._selected_items = [] + if desired_ids: + if self._multi: + for it, qit in list(self._item_to_qitem.items()): try: - it.setSelected(True) + if id(it) in desired_ids: + qit.setSelected(True) + self._selected_items.append(it) + try: + it.setSelected(True) + except Exception: + pass + # expand parents in view + try: + pq = qit.parent() + while pq is not None: + pq.setExpanded(True) + pq = pq.parent() + except Exception: + pass except Exception: pass - except Exception: - pass - else: - if selected_candidates: - qit, it = selected_candidates[-1] - try: - qit.setSelected(True) - it.setSelected(True) - self._selected_items = [it] - except Exception: - pass - except Exception: - pass + else: + # choose one: prefer a visible item present in the view + chosen_it = None + chosen_q = None + for it, qit in list(self._item_to_qitem.items()): + if id(it) in desired_ids: + chosen_it = it + chosen_q = qit + break + if chosen_q is None: + # fallback to last candidate if any + if selected_candidates: + chosen_q, chosen_it = selected_candidates[-1] + if chosen_q is not None and chosen_it is not None: + try: + chosen_q.setSelected(True) + chosen_it.setSelected(True) + self._selected_items = [chosen_it] + # expand parents + try: + pq = chosen_q.parent() + while pq is not None: + pq.setExpanded(True) + pq = pq.parent() + except Exception: + pass + except Exception: + pass + # update preserved ids (not used to drive rebuild, kept for diagnostics) + try: + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + self._last_selected_ids = set() + finally: + self._suppress_selection_handler = False # do not call expandAll(); expansion is controlled per-item by _is_open @@ -258,12 +356,15 @@ def _on_selection_changed(self): # Defensive guard: when we change selection programmatically we don't want to re-enter here. if self._suppress_selection_handler: return + + self._logger.debug("_on_selection_changed: handling selection change") try: if not self._tree_widget: return sel_qitems = list(self._tree_widget.selectedItems()) + self._logger.debug("_on_selection_changed: %d items selected", len(sel_qitems)) current_set = set(sel_qitems) # If recursive selection is enabled and multi-selection is allowed, @@ -323,15 +424,25 @@ def _on_selection_changed(self): if itm is not None: new_selected.append(itm) - # Update internal selected items list (logical selection used by base class) + # Update selected flags across the entire tree (clear all, then set for new_selected) try: - # clear previous selection flags for all known items - for it in list(getattr(self, "_items", []) or []): + roots = list(getattr(self, "_items", []) or []) + def _all_nodes(nodes): + out = [] + for n in nodes: + out.append(n) + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_all_nodes(chs)) + return out + for it in _all_nodes(roots): try: it.setSelected(False) except Exception: pass - # set selection flag for newly selected items for it in new_selected: try: it.setSelected(True) @@ -341,13 +452,19 @@ def _on_selection_changed(self): pass self._selected_items = new_selected - + self._logger.debug("_on_selection_changed: %d new selected", len(new_selected)) # remember last selected QTreeWidgetItem set for next invocation try: self._last_selected_qitems = set(self._tree_widget.selectedItems()) except Exception: self._last_selected_qitems = set() + # preserve logical selected ids for diagnostics + try: + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + self._last_selected_ids = set() + # immediate mode: notify container/dialog try: if self._immediate and self.notify(): @@ -361,6 +478,7 @@ def _on_selection_changed(self): # item activated (double click / Enter) def _on_item_activated(self, qitem, column): + self._logger.debug("_on_item_activated: item activated") try: # map to logical item item = self._qitem_to_item.get(qitem, None) @@ -476,6 +594,30 @@ def selectItem(self, item, selected=True): # apply selection in view try: + # expand parents in model and view to ensure visibility + try: + # open parent chain in view + pq = qit.parent() + while pq is not None: + pq.setExpanded(True) + pq = pq.parent() + except Exception: + pass + try: + # also setOpen on logical parents + p = item.parentItem() if hasattr(item, 'parentItem') and callable(getattr(item, 'parentItem')) else getattr(item, '_parent_item', None) + while p is not None: + try: + p.setOpen(True) + except Exception: + try: + setattr(p, '_is_open', True) + except Exception: + pass + p = p.parentItem() if hasattr(p, 'parentItem') and callable(getattr(p, 'parentItem')) else getattr(p, '_parent_item', None) + except Exception: + pass + self._suppress_selection_handler = True if not self._multi and selected: try: self._tree_widget.clearSelection() @@ -490,7 +632,10 @@ def selectItem(self, item, selected=True): tq.setSelected(bool(selected)) except Exception: pass + # done applying; release suppression + self._suppress_selection_handler = False except Exception: + self._suppress_selection_handler = False pass # update internal selected items list @@ -504,6 +649,11 @@ def selectItem(self, item, selected=True): if not self._multi and len(new_selected) > 1: new_selected = [new_selected[-1]] self._selected_items = new_selected + # preserve ids for future rebuilds/swaps + try: + self._last_selected_ids = set(id(i) for i in self._selected_items) + except Exception: + self._last_selected_ids = set() except Exception: pass except Exception: @@ -511,11 +661,16 @@ def selectItem(self, item, selected=True): def deleteAllItems(self): """Remove all items from model and QTreeWidget view.""" + self._suppress_selection_handler = True try: super().deleteAllItems() except Exception: self._items = [] self._selected_items = [] + try: + self._last_selected_ids = set() + except Exception: + pass try: self._qitem_to_item.clear() except Exception: @@ -532,3 +687,4 @@ def deleteAllItems(self): pass except Exception: pass + self._suppress_selection_handler = False From 8764a25965c93356cc1409f832e234b5210d4a6d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 15:05:00 +0100 Subject: [PATCH 207/523] Aligned to qt behaviour --- manatools/aui/backends/gtk/treegtk.py | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 6eabe3f..7556e17 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -494,9 +494,7 @@ def _collect_all_nodes(nodes): pass # restore selection without emitting selection-changed while syncing UI to model - # Desired selection source: - # 1) If _last_selected_ids set (e.g., after expand/collapse), honor it. - # 2) Otherwise, collect all nodes in the tree with selected()==True. + # Desired selection source: collect all nodes in the tree with selected()==True. self._suppress_selection_handler = True try: try: @@ -518,18 +516,17 @@ def _collect_all_nodes(nodes): return out all_nodes = _collect_all_nodes(roots) - desired_ids = set(self._last_selected_ids or []) - if not desired_ids: - for n in all_nodes: - try: - if callable(getattr(n, 'selected', None)): - if n.selected(): - desired_ids.add(id(n)) - else: - if bool(getattr(n, '_selected', False)): - desired_ids.add(id(n)) - except Exception: - pass + desired_ids = set() + for n in all_nodes: + try: + if callable(getattr(n, 'selected', None)): + if n.selected(): + desired_ids.add(id(n)) + else: + if bool(getattr(n, '_selected', False)): + desired_ids.add(id(n)) + except Exception: + pass if desired_ids: # apply to visible rows @@ -1030,6 +1027,7 @@ def selectItem(self, item, selected=True): def deleteAllItems(self): """Clear model and view rows for this tree.""" + self._suppress_selection_handler = True try: super().deleteAllItems() except Exception: @@ -1040,6 +1038,10 @@ def deleteAllItems(self): self._row_to_item.clear() self._item_to_row.clear() self._visible_items = [] + try: + self._last_selected_ids = set() + except Exception: + pass except Exception: pass try: @@ -1059,3 +1061,4 @@ def deleteAllItems(self): pass except Exception: pass + self._suppress_selection_handler = False From 72502d6f21d4d3cc6198a224cffd6adc4cd9cabb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 15:05:19 +0100 Subject: [PATCH 208/523] Aligned to qt and gtk behaviors --- manatools/aui/backends/curses/treecurses.py | 110 +++++++++++--------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index 2c2f340..dcd1e86 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -132,6 +132,7 @@ def removeItem(self, item): def deleteAllItems(self): """Clear model and all internal state for this tree.""" + self._suppress_selection_handler = True try: try: super().deleteAllItems() @@ -153,6 +154,7 @@ def deleteAllItems(self): self._flatten_visible() except Exception: pass + self._suppress_selection_handler = False def _collect_all_descendants(self, item): out = [] @@ -172,6 +174,29 @@ def _collect_all_descendants(self, item): pass return out + def _clear_all_selected(self): + """Clear the selected flag recursively across the entire tree.""" + try: + roots = list(getattr(self, "_items", []) or []) + except Exception: + roots = [] + def _clear(nodes): + for n in nodes: + try: + n.setSelected(False) + except Exception: + try: + setattr(n, "_selected", False) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + _clear(chs) + _clear(roots) + def _flatten_visible(self): """Produce self._visible_items = [(item, depth), ...] following _is_open flags.""" self._visible_items = [] @@ -198,6 +223,7 @@ def rebuildTree(self): Ensures ancestors of selected items are opened so selections are visible. """ # preserve items selection if any + self._suppress_selection_handler = True try: # collect all nodes and desired selected ids (from last ids or model flags) def _all_nodes(nodes): @@ -274,36 +300,34 @@ def _all_nodes(nodes): except Exception: chosen_id = None selected_ids = {chosen_id} if chosen_id is not None else set() - # if there are previously saved last_selected_ids, prefer them - selected_ids = set(self._last_selected_ids) if self._last_selected_ids else set() - # if none, collect from items' selected() property - if not selected_ids: - try: - def _collect_selected(nodes): - out = [] - for n in nodes: - try: - sel = False - if hasattr(n, "selected") and callable(getattr(n, "selected")): - sel = n.selected() - else: - sel = bool(getattr(n, "_selected", False)) - if sel: - out.append(n) - except Exception: - pass - try: - chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] - except Exception: - chs = getattr(n, "_children", []) or [] - if chs: - out.extend(_collect_selected(chs)) - return out - pre_selected = _collect_selected(list(getattr(self, "_items", []) or [])) - for p in pre_selected: - selected_ids.add(id(p)) - except Exception: - pass + # collect from items' selected() property only (YTreeItem wins) + selected_ids = set() + try: + def _collect_selected(nodes): + out = [] + for n in nodes: + try: + sel = False + if hasattr(n, "selected") and callable(getattr(n, "selected")): + sel = n.selected() + else: + sel = bool(getattr(n, "_selected", False)) + if sel: + out.append(n) + except Exception: + pass + try: + chs = callable(getattr(n, "children", None)) and n.children() or getattr(n, "_children", []) or [] + except Exception: + chs = getattr(n, "_children", []) or [] + if chs: + out.extend(_collect_selected(chs)) + return out + pre_selected = _collect_selected(list(getattr(self, "_items", []) or [])) + for p in pre_selected: + selected_ids.add(id(p)) + except Exception: + pass # build logical selected list and last_selected_ids sel_items = [] for itm, _d in self._visible_items: @@ -360,6 +384,7 @@ def _clear(nodes): self._ensure_hover_visible() except Exception: pass + self._suppress_selection_handler = False def _ensure_hover_visible(self, height=None): """Adjust scroll offset so hover visible in given height area (if None use last draw height).""" @@ -458,16 +483,9 @@ def _handle_selection_action(self, item): except Exception: pass else: - # single selection: clear all others and set this one + # single selection: clear all flags recursively and set this one try: - for it in list(getattr(self, "_items", []) or []): - try: - it.setSelected(False) - except Exception: - try: - setattr(it, "_selected", False) - except Exception: - pass + self._clear_all_selected() except Exception: pass self._selected_items = [item] @@ -633,16 +651,12 @@ def selectItem(self, item, selected=True): try: if selected: if not self._multi: - # clear others + # clear others recursively and select only this one + try: + self._clear_all_selected() + except Exception: + pass try: - for it in list(getattr(self, "_items", []) or []): - try: - it.setSelected(False) - except Exception: - try: - setattr(it, "_selected", False) - except Exception: - pass item.setSelected(True) except Exception: try: From 2da95d48e53ca79169b75ce20cfbc91edb4edb33 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 24 Dec 2025 16:27:51 +0100 Subject: [PATCH 209/523] Added some changes to manage different widget behaviour --- test/test_aligment.py | 4 +- test/test_frame.py | 11 +++++- test/test_selectionbox2.py | 8 +++- test/test_tree.py | 33 +++++++++++++++- test/test_tree_example.py | 78 ++++++++++++++++++++------------------ 5 files changed, 93 insertions(+), 41 deletions(-) diff --git a/test/test_aligment.py b/test/test_aligment.py index d4450eb..a9bc35c 100644 --- a/test/test_aligment.py +++ b/test/test_aligment.py @@ -64,7 +64,9 @@ def test_Alignment(backend_name=None): align = factory.createVCenter( hbox ) factory.createPushButton( align, "HVCenter" ) - factory.createPushButton( vbox, "OK" ) + b = factory.createPushButton( vbox, "OK" ) + b.setStretchable(yui.YUIDimension.YD_HORIZ, True ) + b.setStretchable(yui.YUIDimension.YD_VERT, True ) dialog.open() event = dialog.waitForEvent() dialog.destroy() diff --git a/test/test_frame.py b/test/test_frame.py index 8cd774e..fea09a9 100644 --- a/test/test_frame.py +++ b/test/test_frame.py @@ -31,9 +31,13 @@ def test_selectionbox(backend_name=None): ############### ui.application().setApplicationTitle("Test Frame") dialog = factory.createPopupDialog() +# dialog.setEnabled(False) mainVbox = factory.createVBox( dialog ) +# mainVbox.setEnabled(False) hbox = factory.createHBox( mainVbox ) +# hbox.setEnabled(False) frame = factory.createFrame( hbox , "Pasta Menu") +# frame.setEnabled(False) selBox = factory.createSelectionBox( frame, "Choose your pasta" ) selBox.addItem( "Spaghetti Carbonara" ) @@ -43,8 +47,11 @@ def test_selectionbox(backend_name=None): selBox.addItem( "Ravioli" ) selBox.addItem( "Trofie al pesto" ) # Ligurian specialty - frame1 = factory.createCheckBoxFrame( hbox , "SelectionBox Options") + frame1 = factory.createCheckBoxFrame( hbox , "SelectionBox Options", True) +# frame1 = factory.createFrame( hbox , "SelectionBox Options") +# frame1.setEnabled(False) vbox = factory.createVBox( frame1 ) +# vbox.setEnabled(False) align = factory.createTop(vbox) notifyCheckBox = factory.createCheckBox( align, "Notify on change", selBox.notify() ) notifyCheckBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) @@ -55,9 +62,11 @@ def test_selectionbox(backend_name=None): disableSelectionBox.setStretchable( yui.YUIDimension.YD_HORIZ, True ) disableValue = factory.createCheckBox( vbox, "disable value button", False ) disableValue.setStretchable( yui.YUIDimension.YD_HORIZ, True ) +# disableValue.setEnabled(False) hbox = factory.createHBox( mainVbox ) valueButton = factory.createPushButton( hbox, "Value" ) +# valueButton.setEnabled(False) disableValue.setValue(not valueButton.isEnabled()) label = factory.createLabel(hbox, "SelectionBox") #factory.createOutputField( hbox, "" ) label.setStretchable( yui.YUIDimension.YD_HORIZ, True ) diff --git a/test/test_selectionbox2.py b/test/test_selectionbox2.py index 9d93e9b..b7e7ad6 100644 --- a/test/test_selectionbox2.py +++ b/test/test_selectionbox2.py @@ -67,10 +67,10 @@ def test_two_selectionbox(backend_name=None): items2 = [ yui.YItem("Red"), - yui.YItem("Green"), + yui.YItem("Green", icon_name="protected"), yui.YItem("Blue") ] - items2[1].setSelected(True) + items2[2].setSelected(True) sel1.addItems(items1) sel2.addItems(items2) @@ -83,7 +83,10 @@ def test_two_selectionbox(backend_name=None): # OK button to exit okButton = factory.createPushButton(label_box, "OK") + root_logger.debug("Opening dialog to set values into selection boxes") + dialog.open() # Initial display + sel2.selectItem(items2[2]) # trigger event to update label try: v1 = sel1.value() or (sel1.selectedItem().label() if sel1.selectedItem() else "") except Exception: @@ -93,6 +96,7 @@ def test_two_selectionbox(backend_name=None): except Exception: v2 = "" infoLabel.setText(f"Box1: {v1} | Box2: {v2}") + root_logger.debug(f"set values: Box1: {v1} | Box2: {v2}") # Event loop while True: diff --git a/test/test_tree.py b/test/test_tree.py index 0a2c9f9..1984436 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -6,6 +6,31 @@ # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + def test_tree(backend_name=None): """Test ComboBox widget specifically""" if backend_name: @@ -24,6 +49,7 @@ def test_tree(backend_name=None): backend = YUI.backend() print(f"Using backend: {backend.value}") + root_logger.info("test_tree: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) ui = YUI_ui() factory = ui.widgetFactory() @@ -46,7 +72,7 @@ def test_tree(backend_name=None): for i in range(5): item = yui.YTreeItem(f"Item {i+1}", is_open=(i==0)) for j in range(3): - subitem = yui.YTreeItem(f"SubItem {i+1}.{j+1}", parent=item) + subitem = yui.YTreeItem(f"SubItem {i+1}.{j+1}", parent=item, selected=(True if i==1 and j == 2 else False)) if i==1 and j == 1: for k in range(2): yui.YTreeItem(f"SubItem {i+1}.{j+1}.{k+1}", parent=subitem) @@ -60,6 +86,11 @@ def test_tree(backend_name=None): print("\nOpening ComboBox test dialog...") + dialog.open() + if tree.selectedItem() is not None: + selected.setText(f"Selected: '{tree.selectedItem().label()}'") + else: + selected.setText(f"Selected: None") while True: event = dialog.waitForEvent() typ = event.eventType() diff --git a/test/test_tree_example.py b/test/test_tree_example.py index 823e2a6..9bac848 100644 --- a/test/test_tree_example.py +++ b/test/test_tree_example.py @@ -187,6 +187,7 @@ def update_labels(): update_labels() elif w == swap_btn and reason == yui.YEventReason.Activated: # perform swap: capture model items, clear, swap, and set new selections + root_logger.debug("YTree swapping tree items...") try: left_model = list(left_tree._items) right_model = list(right_tree._items) @@ -200,44 +201,49 @@ def update_labels(): pass # add swapped try: - for it in right_model: - left_tree.addItem(it) - for it in left_model: - right_tree.addItem(it) - except Exception: - pass - - # select different items than before: pick last top-level in each - try: - if left_tree.hasItems(): - # pick last top-level's first sub-sub if available - it = left_tree._items[-1] - children = list(getattr(it, 'children', lambda: [])()) if callable(getattr(it, 'children', None)) else getattr(it, '_children', []) - target = None - if children: - sc = children[0] - sc2 = list(getattr(sc, 'children', lambda: [])()) if callable(getattr(sc, 'children', None)) else getattr(sc, '_children', []) - if sc2: - target = sc2[-1] - if target is None: - target = it - left_tree.selectItem(target, True) - except Exception: - pass - try: - if right_tree.hasItems(): - it = right_tree._items[-1] - children = list(getattr(it, 'children', lambda: [])()) if callable(getattr(it, 'children', None)) else getattr(it, '_children', []) - target = None - if children and len(children) >= 2: - target = children[1] - elif children: - target = children[0] - if target is None: - target = it - right_tree.selectItem(target, True) + right_tree.addItems(left_model) + left_tree.addItems(right_model) except Exception: pass + #try: + # for it in right_model: + # left_tree.addItem(it) + # for it in left_model: + # right_tree.addItem(it) + #except Exception: + # pass +# + ## select different items than before: pick last top-level in each + #try: + # if left_tree.hasItems(): + # # pick last top-level's first sub-sub if available + # it = left_tree._items[-1] + # children = list(getattr(it, 'children', lambda: [])()) if callable(getattr(it, 'children', None)) else getattr(it, '_children', []) + # target = None + # if children: + # sc = children[0] + # sc2 = list(getattr(sc, 'children', lambda: [])()) if callable(getattr(sc, 'children', None)) else getattr(sc, '_children', []) + # if sc2: + # target = sc2[-1] + # if target is None: + # target = it + # left_tree.selectItem(target, True) + #except Exception: + # pass + #try: + # if right_tree.hasItems(): + # it = right_tree._items[-1] + # children = list(getattr(it, 'children', lambda: [])()) if callable(getattr(it, 'children', None)) else getattr(it, '_children', []) + # target = None + # if children and len(children) >= 2: + # target = children[1] + # elif children: + # target = children[0] + # if target is None: + # target = it + # right_tree.selectItem(target, True) + #except Exception: + # pass update_labels() elif w == quit_btn and reason == yui.YEventReason.Activated: From bde2071651b8c34fb52537218cbb54769ba223dd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 16:15:48 +0100 Subject: [PATCH 210/523] added 'YTableHeader', 'YTableItem', 'YTableCell' (implementing also checkbox to avoid using 'MGA' variant) --- manatools/aui/yui.py | 4 +- manatools/aui/yui_common.py | 171 +++++++++++++++++++++++++++++++++++- 2 files changed, 171 insertions(+), 4 deletions(-) diff --git a/manatools/aui/yui.py b/manatools/aui/yui.py index c3a933e..35810b6 100644 --- a/manatools/aui/yui.py +++ b/manatools/aui/yui.py @@ -128,7 +128,7 @@ def YUI_ensureUICreated(): YEventType, YEventReason, YCheckBoxState, YButtonRole, # Base classes YWidget, YSingleChildContainerWidget, YSelectionWidget, - YSimpleInputField, YItem, YTreeItem, + YSimpleInputField, YItem, YTreeItem, YTableHeader, YTableItem, YTableCell, # Events YEvent, YWidgetEvent, YKeyEvent, YMenuEvent, YCancelEvent, # Exceptions @@ -143,7 +143,7 @@ def YUI_ensureUICreated(): 'YUIDimension', 'YAlignmentType', 'YDialogType', 'YDialogColorMode', 'YEventType', 'YEventReason', 'YCheckBoxState', 'YButtonRole', 'YWidget', 'YSingleChildContainerWidget', 'YSelectionWidget', - 'YSimpleInputField', 'YItem', 'YTreeItem', + 'YSimpleInputField', 'YItem', 'YTreeItem', 'YTableHeader', 'YTableItem', 'YTableCell', 'YEvent', 'YWidgetEvent', 'YKeyEvent', 'YMenuEvent', 'YCancelEvent', 'YUIException', 'YUIWidgetNotFoundException', 'YUINoDialogException', 'YProperty', 'YPropertyValue', 'YPropertySet', 'YShortcut', diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index ab20ab7..8d1c1d3 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -429,8 +429,175 @@ def addChild(self, item): def isOpen(self): return self._is_open - def setOpen(self, is_open=True): - self._is_open = is_open + +class YTableHeader: + """Helper class for table column properties. + + Tracks columns' header text, alignment and whether a column is a + checkbox column. + """ + def __init__(self): + self._columns = [] # list of dicts: {'header': str, 'alignment': YAlignmentType, 'checkbox': bool} + + def addColumn(self, header, checkBox : Optional[bool] = False, alignment=YAlignmentType.YAlignBegin): + """Add a column with header text and optional alignment (no checkbox).""" + self._columns.append({'header': str(header), 'alignment': alignment, 'checkbox': bool(checkBox)}) + + def columns(self): + return len(self._columns) + + def hasColumn(self, column): + return 0 <= int(column) < len(self._columns) + + def header(self, column): + try: + return self._columns[int(column)]['header'] + except Exception: + return "" + + def isCheckboxColumn(self, column): + try: + return bool(self._columns[int(column)]['checkbox']) + except Exception: + return False + + def alignment(self, column): + try: + return self._columns[int(column)]['alignment'] + except Exception: + return YAlignmentType.YAlignUnchanged + + +class YTableCell: + """One cell (one column in one row) of a YTableItem. + + Supports label, optional icon name, optional sort key and an optional + checkbox state. Cells can be created detached or with a parent/table + assigned via `reparent()`. + """ + def __init__(self, label: str = "", icon_name: str = "", sort_key: str = "", parent: Optional["YTableItem"] = None, column: int = -1, checked: Optional[bool] = None): + self._label = label + self._icon_name = icon_name + self._sort_key = sort_key + self._parent = parent + self._column = column + # checked: None means not-a-checkbox column; True/False represent checkbox state + self._checked = checked + + def label(self): + return self._label + + def setLabel(self, new_label: str): + self._label = new_label + + def iconName(self): + return self._icon_name + + def setIconName(self, new_icon_name: str): + self._icon_name = new_icon_name + + def hasIconName(self): + return bool(self._icon_name) + + def sortKey(self): + return self._sort_key + + def hasSortKey(self): + return bool(self._sort_key) + + def parent(self): + return self._parent + + def column(self): + return self._column + + def itemIndex(self): + return self._parent.index() if self._parent is not None else -1 + + def reparent(self, parent: "YTableItem", column: int): + if self._parent is not None: + raise Exception("Cell already has a parent") + self._parent = parent + self._column = column + + # checkbox API + def setChecked(self, val: bool = True): + self._checked = bool(val) + + def checked(self): + return bool(self._checked) if self._checked is not None else False + + +class YTableItem(YTreeItem): + """Table item (one row). Each item may contain multiple `YTableCell`. + + Provides convenience constructors and cell management similar to + the C++ `YTableItem` while also supporting checkbox cells. + """ + def __init__(self, label: str = "", parent: Optional["YTreeItem"] = None, is_open: bool = False, icon_name: str = ""): + super().__init__(label, parent, False, is_open, icon_name) + self._cells = [] # list of YTableCell + + def addCell(self, cell_or_label, icon_name: str = "", sort_key: str = ""): + """Add a cell instance or create one from label/icon/sort_key. + If a boolean is passed as first arg, treat it as a checkbox cell value. + """ + if isinstance(cell_or_label, YTableCell): + cell = cell_or_label + else: + # allow boolean-only constructor for checkbox column + if isinstance(cell_or_label, bool): + cell = YTableCell("", "", "", parent=self, column=len(self._cells), checked=cell_or_label) + else: + cell = YTableCell(str(cell_or_label), icon_name, sort_key, parent=self, column=len(self._cells)) + # set parent/column and append + try: + cell.reparent(self, len(self._cells)) + except Exception: + # already parented; update column if needed + cell._column = len(self._cells) + cell._parent = self + self._cells.append(cell) + + def addCells(self, *labels): + for lbl in labels: + self.addCell(lbl) + + def deleteCells(self): + self._cells = [] + + def cellsBegin(self): + return iter(self._cells) + + def cellsEnd(self): + return iter([]) + + def cell(self, index: int): + try: + return self._cells[index] + except Exception: + return None + + def cellCount(self): + return len(self._cells) + + def hasCell(self, index: int): + return 0 <= index < len(self._cells) + + def label(self, index: int = 0): + c = self.cell(index) + return c.label() if c is not None else "" + + def iconName(self, index: int = 0): + c = self.cell(index) + return c.iconName() if c is not None else "" + + def hasIconName(self, index: int = 0): + c = self.cell(index) + return c.hasIconName() if c is not None else False + + def debugLabel(self): + return f"{super().debugLabel()}[cells={self.cellCount()}]" # Property system class YPropertyType(Enum): From 5ed78e74c14f2ee0666f5e229183469992c1f143 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 16:17:02 +0100 Subject: [PATCH 211/523] Start implementing Qt YTable --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/tableqt.py | 355 ++++++++++++++++++++++++++ manatools/aui/yui_qt.py | 6 +- 3 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/qt/tableqt.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 36c7812..226a16d 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -13,6 +13,7 @@ from .checkboxframeqt import YCheckBoxFrameQt from .progressbarqt import YProgressBarQt from .radiobuttonqt import YRadioButtonQt +from .tableqt import YTableQt __all__ = [ @@ -31,5 +32,6 @@ "YCheckBoxFrameQt", "YProgressBarQt", "YRadioButtonQt", + "YTableQt", # ... ] diff --git a/manatools/aui/backends/qt/tableqt.py b/manatools/aui/backends/qt/tableqt.py new file mode 100644 index 0000000..ed709c6 --- /dev/null +++ b/manatools/aui/backends/qt/tableqt.py @@ -0,0 +1,355 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +Qt backend for YTable using QTableWidget. + +Supports multi-column rows via `YTableItem`/`YTableCell` from `yui_common`. +Cells that have a checkbox (their `checked` attribute is not None) are +rendered as checkboxes and the table forces single-selection mode in that +case (to keep curses/simple-mode compatibility). +""" +from PySide6 import QtWidgets, QtCore, QtGui +import logging +from ...yui_common import * + + +class YTableQt(YSelectionWidget): + def __init__(self, parent=None, header=None, multiSelection=False): + super().__init__(parent) + self._header = header + self._multi = bool(multiSelection) + self._immediate = self.notify() + self._table = None + self._row_to_item = {} + self._item_to_row = {} + self._suppress_selection_handler = False + self._suppress_item_change = False + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + + def widgetClass(self): + return "YTable" + + def _create_backend_widget(self): + tbl = QtWidgets.QTableWidget() + tbl.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi else QtWidgets.QAbstractItemView.SingleSelection + tbl.setSelectionMode(mode) + tbl.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + tbl.itemSelectionChanged.connect(self._on_selection_changed) + tbl.itemChanged.connect(self._on_item_changed) + self._table = tbl + self._backend_widget = tbl + # populate if items already present + try: + self.rebuildTable() + except Exception: + self._logger.exception("rebuildTable failed during _create_backend_widget") + + def _detect_columns_and_checkboxes(self): + # compute max columns and whether any checkbox cells present + max_cols = 0 + any_checkbox = False + for it in list(getattr(self, "_items", []) or []): + try: + cnt = it.cellCount() if hasattr(it, 'cellCount') else 0 + max_cols = max(max_cols, cnt) + for c in it.cellsBegin() if hasattr(it, 'cellsBegin') else []: + try: + if getattr(c, '_checked', None) is not None: + any_checkbox = True + break + except Exception: + pass + except Exception: + pass + return max_cols, any_checkbox + + def rebuildTable(self): + self._logger.debug("rebuildTable: rebuilding table with %d items", len(self._items) if self._items else 0) + if self._table is None: + self._create_backend_widget() + # determine columns and checkbox usage + cols, any_checkbox = self._detect_columns_and_checkboxes() + if cols <= 0: + cols = 1 + # enforce single-selection if checkbox columns used + if any_checkbox: + self._multi = False + mode = QtWidgets.QAbstractItemView.MultiSelection if self._multi else QtWidgets.QAbstractItemView.SingleSelection + try: + self._table.setSelectionMode(mode) + except Exception: + pass + + # clear existing + self._row_to_item.clear() + self._item_to_row.clear() + self._table.clear() + self._table.setRowCount(0) + self._table.setColumnCount(cols) + + # build rows + for row_idx, it in enumerate(list(getattr(self, '_items', []) or [])): + try: + self._table.insertRow(row_idx) + self._row_to_item[row_idx] = it + self._item_to_row[it] = row_idx + # populate cells + for col in range(cols): + cell = it.cell(col) if hasattr(it, 'cell') else None + if cell is None: + qit = QtWidgets.QTableWidgetItem("") + else: + text = cell.label() + qit = QtWidgets.QTableWidgetItem(text) + if getattr(cell, '_checked', None) is not None: + # checkbox column + qit.setFlags(qit.flags() | QtCore.Qt.ItemIsUserCheckable) + qit.setCheckState(QtCore.Qt.Checked if cell.checked() else QtCore.Qt.Unchecked) + try: + self._table.setItem(row_idx, col, qit) + except Exception: + pass + except Exception: + pass + + # apply selection from YTableItem.selected flags + desired_rows = [] + try: + for it, row in list(self._item_to_row.items()): + try: + sel = False + if hasattr(it, 'selected') and callable(getattr(it, 'selected')): + sel = it.selected() + else: + sel = bool(getattr(it, '_selected', False)) + if sel: + desired_rows.append(row) + except Exception: + pass + except Exception: + pass + + # set selection programmatically + try: + self._suppress_selection_handler = True + try: + self._table.clearSelection() + except Exception: + pass + if desired_rows: + if self._multi: + for r in desired_rows: + try: + self._table.selectRow(r) + except Exception: + pass + else: + # pick first desired row + try: + self._table.selectRow(desired_rows[0]) + except Exception: + pass + # update internal selected list + new_selected = [] + for r in desired_rows: + it = self._row_to_item.get(r, None) + if it is not None: + try: + it.setSelected(True) + except Exception: + pass + new_selected.append(it) + self._selected_items = new_selected + finally: + self._suppress_selection_handler = False + + def _on_selection_changed(self): + if self._suppress_selection_handler: + return + try: + sel_ranges = self._table.selectionModel().selectedRows() + new_selected = [] + for idx in sel_ranges: + try: + row = idx.row() + it = self._row_to_item.get(row, None) + if it is not None: + new_selected.append(it) + except Exception: + pass + + # clear all selected flags then set for new_selected + try: + for it in list(getattr(self, '_items', []) or []): + try: + it.setSelected(False) + except Exception: + pass + for it in new_selected: + try: + it.setSelected(True) + except Exception: + pass + except Exception: + pass + + # enforce single-selection semantics + if not self._multi and len(new_selected) > 1: + new_selected = [new_selected[-1]] + + self._selected_items = new_selected + + # notify immediate mode + try: + if self._immediate and self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + except Exception: + pass + + def _on_item_changed(self, qitem: QtWidgets.QTableWidgetItem): + # handle checkbox toggles + if self._suppress_item_change: + return + try: + if not (qitem.flags() & QtCore.Qt.ItemIsUserCheckable): + return + row = qitem.row() + col = qitem.column() + it = self._row_to_item.get(row, None) + if it is None: + return + cell = it.cell(col) + if cell is None: + return + # update model checkbox + try: + checked = (qitem.checkState() == QtCore.Qt.Checked) + cell.setChecked(checked) + except Exception: + pass + # when checkbox is used, assume this is a value change + try: + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + except Exception: + pass + + def addItem(self, item): + if isinstance(item, str): + item = YTableItem(item) + super().addItem(item) + elif isinstance(item, YTableItem): + super().addItem(item) + else: + self._logger.error("YTable.addItem: invalid item type %s", type(item)) + raise TypeError("YTable.addItem expects a YTableItem or string label") + try: + item.setIndex(len(self._items) - 1) + except Exception: + pass + try: + if getattr(self, '_table', None) is not None: + self.rebuildTable() + except Exception: + pass + + def addItems(self, items): + for it in items: + self.addItem(it) + + def selectItem(self, item, selected=True): + # update model and view + try: + try: + item.setSelected(bool(selected)) + except Exception: + pass + if getattr(self, '_table', None) is None: + # just update model + if selected: + if item not in self._selected_items: + if not self._multi: + self._selected_items = [item] + else: + self._selected_items.append(item) + else: + try: + if item in self._selected_items: + self._selected_items.remove(item) + except Exception: + pass + return + + row = self._item_to_row.get(item, None) + if row is None: + try: + self.rebuildTable() + row = self._item_to_row.get(item, None) + except Exception: + row = None + if row is None: + return + + try: + self._suppress_selection_handler = True + if not self._multi and selected: + try: + self._table.clearSelection() + except Exception: + pass + if selected: + self._table.selectRow(row) + else: + try: + # deselect programmatically + sel_model = self._table.selectionModel() + idx = sel_model.model().index(row, 0) + sel_model.select(idx, QtCore.QItemSelectionModel.Deselect | QtCore.QItemSelectionModel.Rows) + except Exception: + pass + finally: + self._suppress_selection_handler = False + + # update internal list + try: + new_selected = [] + for idx in self._table.selectionModel().selectedRows(): + it2 = self._row_to_item.get(idx.row(), None) + if it2 is not None: + new_selected.append(it2) + if not self._multi and len(new_selected) > 1: + new_selected = [new_selected[-1]] + self._selected_items = new_selected + except Exception: + pass + except Exception: + pass + + def deleteAllItems(self): + try: + super().deleteAllItems() + except Exception: + self._items = [] + self._selected_items = [] + try: + self._row_to_item.clear() + except Exception: + pass + try: + self._item_to_row.clear() + except Exception: + pass + try: + if getattr(self, '_table', None) is not None: + self._table.setRowCount(0) + self._table.clear() + except Exception: + pass diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 3fef506..0fcb859 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -221,4 +221,8 @@ def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): def createRadioButton(self, parent, label:str = "", isChecked:bool = False): """Create a Radio Button widget.""" - return YRadioButtonQt(parent, label, isChecked) \ No newline at end of file + return YRadioButtonQt(parent, label, isChecked) + + def createTable(self, parent, header: YTableHeader, multiSelection: bool = False): + """Create a Table widget.""" + return YTableQt(parent, header, multiSelection) From b09d9e20325044876c8599b5b1018a947d0c238a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 18:09:21 +0100 Subject: [PATCH 212/523] Added checked to get checked info --- manatools/aui/yui_common.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 8d1c1d3..c83ce8c 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -573,6 +573,8 @@ def cellsEnd(self): return iter([]) def cell(self, index: int): + if not self.hasCell(index): + return None try: return self._cells[index] except Exception: @@ -585,8 +587,16 @@ def hasCell(self, index: int): return 0 <= index < len(self._cells) def label(self, index: int = 0): + if not self.hasCell(index): + return "" c = self.cell(index) return c.label() if c is not None else "" + + def checked(self, index: int = 0): + if not self.hasCell(index): + return False + c = self.cell(index) + return c.checked() if c is not None else False def iconName(self, index: int = 0): c = self.cell(index) From 06d4a190d67ba8118b223257d885d6df1783333f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 18:09:56 +0100 Subject: [PATCH 213/523] Fixed header content and checked item information --- manatools/aui/backends/qt/tableqt.py | 214 ++++++++++++++++++++++----- 1 file changed, 180 insertions(+), 34 deletions(-) diff --git a/manatools/aui/backends/qt/tableqt.py b/manatools/aui/backends/qt/tableqt.py index ed709c6..c42155d 100644 --- a/manatools/aui/backends/qt/tableqt.py +++ b/manatools/aui/backends/qt/tableqt.py @@ -13,22 +13,59 @@ from ...yui_common import * +class YTableWidgetItem(QtWidgets.QTableWidgetItem): + """QTableWidgetItem subclass that prefers a stored sort key (UserRole) + when comparing for sorting. Falls back to the default behaviour. + """ + def __lt__(self, other): + try: + my_sort = self.data(QtCore.Qt.UserRole) + other_sort = other.data(QtCore.Qt.UserRole) + if my_sort is not None and other_sort is not None: + return str(my_sort) < str(other_sort) + except Exception: + pass + return super(YTableWidgetItem, self).__lt__(other) + + class YTableQt(YSelectionWidget): - def __init__(self, parent=None, header=None, multiSelection=False): + def __init__(self, parent, header: YTableHeader, multiSelection=False): super().__init__(parent) self._header = header self._multi = bool(multiSelection) - self._immediate = self.notify() + # force single-selection if any checkbox cells present + if self._header is not None: + try: + for c_idx in range(self._header.columns()): + if self._header.isCheckboxColumn(c_idx): + self._multi = False + break + except Exception: + pass + else: + raise ValueError("YTableQt requires a YTableHeader") + self._table = None self._row_to_item = {} self._item_to_row = {} self._suppress_selection_handler = False self._suppress_item_change = False self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._changed_item = None def widgetClass(self): return "YTable" + def _header_is_checkbox(self, col): + try: + if getattr(self, '_header', None) is None: + return False + if hasattr(self._header, 'isCheckboxColumn'): + return bool(self._header.isCheckboxColumn(col)) + return False + except Exception: + return False + def _create_backend_widget(self): tbl = QtWidgets.QTableWidget() tbl.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) @@ -69,9 +106,25 @@ def rebuildTable(self): if self._table is None: self._create_backend_widget() # determine columns and checkbox usage - cols, any_checkbox = self._detect_columns_and_checkboxes() + cols = 0 + any_checkbox = False + if getattr(self, '_header', None) is not None and hasattr(self._header, 'columns'): + try: + cols = int(self._header.columns()) + except Exception: + cols = 0 + # any_checkbox if header declares any checkbox column + try: + for c_idx in range(cols): + if self._header_is_checkbox(c_idx): + any_checkbox = True + break + except Exception: + any_checkbox = False if cols <= 0: - cols = 1 + cols, any_checkbox = self._detect_columns_and_checkboxes() + if cols <= 0: + cols = 1 # enforce single-selection if checkbox columns used if any_checkbox: self._multi = False @@ -81,37 +134,118 @@ def rebuildTable(self): except Exception: pass + # set column headers if available + try: + headers = [] + for c in range(cols): + if getattr(self, '_header', None) is not None and hasattr(self._header, 'hasColumn') and self._header.hasColumn(c): + headers.append(self._header.header(c)) + else: + headers.append("") + try: + self._table.setColumnCount(cols) + self._table.setHorizontalHeaderLabels(headers) + except Exception: + pass + except Exception: + pass + # clear existing self._row_to_item.clear() self._item_to_row.clear() - self._table.clear() + # clear contents only to preserve header labels + self._table.clearContents() self._table.setRowCount(0) - self._table.setColumnCount(cols) + # ensure column count already set above - # build rows - for row_idx, it in enumerate(list(getattr(self, '_items', []) or [])): - try: - self._table.insertRow(row_idx) - self._row_to_item[row_idx] = it - self._item_to_row[it] = row_idx - # populate cells - for col in range(cols): - cell = it.cell(col) if hasattr(it, 'cell') else None - if cell is None: - qit = QtWidgets.QTableWidgetItem("") - else: - text = cell.label() - qit = QtWidgets.QTableWidgetItem(text) - if getattr(cell, '_checked', None) is not None: - # checkbox column - qit.setFlags(qit.flags() | QtCore.Qt.ItemIsUserCheckable) - qit.setCheckState(QtCore.Qt.Checked if cell.checked() else QtCore.Qt.Unchecked) - try: - self._table.setItem(row_idx, col, qit) - except Exception: - pass - except Exception: - pass + # build rows (suppress item change notifications while programmatically populating) + try: + self._suppress_item_change = True + for row_idx, it in enumerate(list(getattr(self, '_items', []) or [])): + try: + self._table.insertRow(row_idx) + self._row_to_item[row_idx] = it + self._item_to_row[it] = row_idx + # populate cells: only up to 'cols' columns defined by header/detection + for col in range(cols): + cell = None + try: + cell = it.cell(col) if hasattr(it, 'cell') else None + except Exception: + cell = None + + text = "" + sort_key = None + if cell is not None: + try: + text = cell.label() + except Exception: + text = "" + try: + if hasattr(cell, 'hasSortKey') and cell.hasSortKey(): + sort_key = cell.sortKey() + except Exception: + sort_key = None + + # create item that supports sortKey via UserRole + qit = YTableWidgetItem(text) + if sort_key is not None: + try: + qit.setData(QtCore.Qt.UserRole, sort_key) + except Exception: + pass + + # determine if this column is a checkbox column according to header + is_checkbox_col = False + try: + if getattr(self, '_header', None) is not None: + is_checkbox_col = self._header_is_checkbox(col) + else: + # fallback: treat as checkbox if cell explicitly has _checked + is_checkbox_col = (getattr(cell, '_checked', None) is not None) + except Exception: + is_checkbox_col = (getattr(cell, '_checked', None) is not None) + + # apply checkbox flags only if header says so + try: + flags = qit.flags() | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + if is_checkbox_col: + flags |= QtCore.Qt.ItemIsUserCheckable + qit.setFlags(flags) + checked_state = QtCore.Qt.Unchecked + try: + if cell is not None and getattr(cell, '_checked', None) is not None and cell.checked(): + checked_state = QtCore.Qt.Checked + except Exception: + checked_state = QtCore.Qt.Unchecked + qit.setCheckState(checked_state) + else: + qit.setFlags(flags) + except Exception: + pass + + # alignment according to header + try: + if getattr(self, '_header', None) is not None and hasattr(self._header, 'alignment') and self._header.hasColumn(col): + align = self._header.alignment(col) + if align == YAlignmentType.YAlignCenter: + qit.setTextAlignment(QtCore.Qt.AlignCenter) + elif align == YAlignmentType.YAlignEnd: + qit.setTextAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + else: + qit.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + except Exception: + pass + + try: + self._table.setItem(row_idx, col, qit) + except Exception: + pass + except Exception: + pass + + finally: + self._suppress_item_change = False # apply selection from YTableItem.selected flags desired_rows = [] @@ -202,7 +336,7 @@ def _on_selection_changed(self): # notify immediate mode try: - if self._immediate and self.notify(): + if self.notify(): dlg = self.findDialog() if dlg is not None: dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) @@ -216,10 +350,16 @@ def _on_item_changed(self, qitem: QtWidgets.QTableWidgetItem): if self._suppress_item_change: return try: - if not (qitem.flags() & QtCore.Qt.ItemIsUserCheckable): + # Only treat as checkbox change if header declares this column as checkbox + col = qitem.column() + is_checkbox_col = False + try: + is_checkbox_col = self._header_is_checkbox(col) + except Exception: + is_checkbox_col = bool(qitem.flags() & QtCore.Qt.ItemIsUserCheckable) + if not is_checkbox_col: return row = qitem.row() - col = qitem.column() it = self._row_to_item.get(row, None) if it is None: return @@ -230,6 +370,7 @@ def _on_item_changed(self, qitem: QtWidgets.QTableWidgetItem): try: checked = (qitem.checkState() == QtCore.Qt.Checked) cell.setChecked(checked) + self._changed_item = it except Exception: pass # when checkbox is used, assume this is a value change @@ -336,6 +477,7 @@ def selectItem(self, item, selected=True): def deleteAllItems(self): try: super().deleteAllItems() + self._changed_item = None except Exception: self._items = [] self._selected_items = [] @@ -350,6 +492,10 @@ def deleteAllItems(self): try: if getattr(self, '_table', None) is not None: self._table.setRowCount(0) - self._table.clear() + # keep header labels intact + self._table.clearContents() except Exception: pass + + def changedItem(self): + return self._changed_item From f3474da7a88490dd41b00a1a01b2f80a6cd09708 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 18:54:01 +0100 Subject: [PATCH 214/523] Aligned checkbox too --- manatools/aui/backends/qt/tableqt.py | 85 ++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/manatools/aui/backends/qt/tableqt.py b/manatools/aui/backends/qt/tableqt.py index c42155d..e43514b 100644 --- a/manatools/aui/backends/qt/tableqt.py +++ b/manatools/aui/backends/qt/tableqt.py @@ -9,6 +9,7 @@ case (to keep curses/simple-mode compatibility). """ from PySide6 import QtWidgets, QtCore, QtGui +from functools import partial import logging from ...yui_common import * @@ -73,7 +74,7 @@ def _create_backend_widget(self): tbl.setSelectionMode(mode) tbl.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) tbl.itemSelectionChanged.connect(self._on_selection_changed) - tbl.itemChanged.connect(self._on_item_changed) + #tbl.itemChanged.connect(self._on_item_changed) self._table = tbl self._backend_widget = tbl # populate if items already present @@ -206,19 +207,18 @@ def rebuildTable(self): except Exception: is_checkbox_col = (getattr(cell, '_checked', None) is not None) - # apply checkbox flags only if header says so + # apply flags and, for checkbox columns, prefer a centered checkbox widget try: flags = qit.flags() | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled if is_checkbox_col: - flags |= QtCore.Qt.ItemIsUserCheckable + # keep item non-user-checkable; we'll install a centered QCheckBox widget qit.setFlags(flags) - checked_state = QtCore.Qt.Unchecked - try: - if cell is not None and getattr(cell, '_checked', None) is not None and cell.checked(): - checked_state = QtCore.Qt.Checked - except Exception: - checked_state = QtCore.Qt.Unchecked - qit.setCheckState(checked_state) + # set sortable value if no explicit sort key + if sort_key is None: + try: + qit.setData(QtCore.Qt.UserRole, 1 if (cell is not None and cell.checked()) else 0) + except Exception: + pass else: qit.setFlags(flags) except Exception: @@ -239,6 +239,37 @@ def rebuildTable(self): try: self._table.setItem(row_idx, col, qit) + # for checkbox columns, install a checkbox widget honoring header alignment and connect + if is_checkbox_col: + try: + chk = QtWidgets.QCheckBox() + try: + chk.setChecked(cell.checked() if cell is not None else False) + except Exception: + chk.setChecked(False) + chk.setFocusPolicy(QtCore.Qt.NoFocus) + container = QtWidgets.QWidget() + lay = QtWidgets.QHBoxLayout(container) + lay.setContentsMargins(0, 0, 0, 0) + # determine alignment from header + try: + align_type = self._header.alignment(col) + except Exception: + align_type = YAlignmentType.YAlignBegin + if align_type == YAlignmentType.YAlignCenter: + lay.addStretch(1) + lay.addWidget(chk, alignment=QtCore.Qt.AlignCenter) + lay.addStretch(1) + elif align_type == YAlignmentType.YAlignEnd: + lay.addStretch(1) + lay.addWidget(chk, alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + else: + lay.addWidget(chk, alignment=QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + lay.addStretch(1) + self._table.setCellWidget(row_idx, col, container) + chk.toggled.connect(partial(self._on_checkbox_toggled, row_idx, col)) + except Exception: + pass except Exception: pass except Exception: @@ -301,6 +332,7 @@ def rebuildTable(self): def _on_selection_changed(self): if self._suppress_selection_handler: return + self._logger.debug("_on_selection_changed") try: sel_ranges = self._table.selectionModel().selectedRows() new_selected = [] @@ -349,6 +381,7 @@ def _on_item_changed(self, qitem: QtWidgets.QTableWidgetItem): # handle checkbox toggles if self._suppress_item_change: return + self._logger.debug("_on_item_changed: row=%d col=%d", qitem.row(), qitem.column()) try: # Only treat as checkbox change if header declares this column as checkbox col = qitem.column() @@ -383,6 +416,38 @@ def _on_item_changed(self, qitem: QtWidgets.QTableWidgetItem): except Exception: pass + def _on_checkbox_toggled(self, row, col, checked): + # update model and sort role when the embedded checkbox widget toggles + self._logger.debug("_on_checkbox_toggled: row=%d col=%d checked=%s", row, col, checked) + try: + it = self._row_to_item.get(row, None) + if it is None: + return + cell = it.cell(col) + if cell is None: + return + try: + cell.setChecked(bool(checked)) + self._changed_item = it + except Exception: + pass + # keep sorting role consistent if no explicit sort key + try: + qit = self._table.item(row, col) + if qit is not None and (cell is not None) and not (hasattr(cell, 'hasSortKey') and cell.hasSortKey()): + qit.setData(QtCore.Qt.UserRole, 1 if checked else 0) + except Exception: + pass + # notify value change + try: + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + except Exception: + pass + def addItem(self, item): if isinstance(item, str): item = YTableItem(item) From 736b18d9b816297770ee4fe35db6c0361ec47a92 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 19:07:50 +0100 Subject: [PATCH 215/523] Implementing Gtk YTable --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/tablegtk.py | 428 +++++++++++++++++++++++++ manatools/aui/yui_gtk.py | 5 + 3 files changed, 435 insertions(+) create mode 100644 manatools/aui/backends/gtk/tablegtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 567ea5a..381b90c 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -13,6 +13,7 @@ from .checkboxframegtk import YCheckBoxFrameGtk from .progressbargtk import YProgressBarGtk from .radiobuttongtk import YRadioButtonGtk +from .tablegtk import YTableGtk __all__ = [ "YDialogGtk", @@ -30,5 +31,6 @@ "YCheckBoxFrameGtk", "YProgressBarGtk", "YRadioButtonGtk", + "YTableGtk", # ... ] diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py new file mode 100644 index 0000000..ffe2722 --- /dev/null +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -0,0 +1,428 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +GTK backend for YTable using Gtk4. + +Renders a header row via Gtk.Grid and rows via Gtk.ListBox. Supports: +- Column headers from `YTableHeader.header()` +- Column alignment from `YTableHeader.alignment()` +- Checkbox columns declared via `YTableHeader.isCheckboxColumn()` +- Selection driven by `YTableItem.selected()`; emits SelectionChanged on change +- Checkbox toggles emit ValueChanged and update `YTableCell.checked()` + +Sorting UI is not implemented; if needed we can add clickable headers. +""" + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GLib +import logging +from ...yui_common import * + + +class YTableGtk(YSelectionWidget): + def __init__(self, parent=None, header: YTableHeader = None, multiSelection=False): + super().__init__(parent) + if header is None: + raise ValueError("YTableGtk requires a YTableHeader") + self._header = header + self._multi = bool(multiSelection) + # force single-selection if any checkbox columns present + try: + for c_idx in range(self._header.columns()): + if self._header.isCheckboxColumn(c_idx): + self._multi = False + break + except Exception: + pass + self._backend_widget = None + self._header_grid = None + self._listbox = None + self._row_to_item = {} + self._item_to_row = {} + self._rows = [] + self._suppress_selection_handler = False + self._suppress_item_change = False + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._changed_item = None + + def widgetClass(self): + return "YTable" + + def _create_backend_widget(self): + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + + # Header grid + header_grid = Gtk.Grid(column_spacing=12, row_spacing=0) + try: + cols = self._header.columns() + except Exception: + cols = 0 + for col in range(cols): + try: + txt = self._header.header(col) + except Exception: + txt = "" + lbl = Gtk.Label(label=txt) + try: + align = self._header.alignment(col) + if align == YAlignmentType.YAlignCenter: + lbl.set_xalign(0.5) + elif align == YAlignmentType.YAlignEnd: + lbl.set_xalign(1.0) + else: + lbl.set_xalign(0.0) + except Exception: + pass + header_grid.attach(lbl, col, 0, 1, 1) + + # ListBox inside ScrolledWindow + listbox = Gtk.ListBox() + try: + mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE + listbox.set_selection_mode(mode) + except Exception: + pass + + sw = Gtk.ScrolledWindow() + try: + sw.set_child(listbox) + except Exception: + try: + sw.add(listbox) + except Exception: + pass + + # Make expand according to parent stretching + try: + vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + listbox.set_hexpand(True) + listbox.set_vexpand(True) + sw.set_hexpand(True) + sw.set_vexpand(True) + except Exception: + pass + + self._backend_widget = vbox + self._header_grid = header_grid + self._listbox = listbox + + try: + vbox.append(header_grid) + vbox.append(sw) + except Exception: + try: + vbox.add(header_grid) + vbox.add(sw) + except Exception: + pass + + # connect selection handlers + if self._multi: + try: + self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) + except Exception: + pass + else: + try: + self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + except Exception: + pass + + # populate if items exist + try: + if getattr(self, "_items", None): + self.rebuildTable() + except Exception: + self._logger.exception("rebuildTable failed during _create_backend_widget") + + def _header_is_checkbox(self, col): + try: + return bool(self._header.isCheckboxColumn(col)) + except Exception: + return False + + def rebuildTable(self): + self._logger.debug("rebuildTable: %d items", len(self._items) if self._items else 0) + if self._backend_widget is None or self._listbox is None: + self._create_backend_widget() + + # clear rows + try: + self._row_to_item.clear() + self._item_to_row.clear() + except Exception: + pass + try: + for row in list(self._rows): + try: + self._listbox.remove(row) + except Exception: + pass + self._rows = [] + except Exception: + pass + + # build rows + try: + cols = self._header.columns() + except Exception: + cols = 0 + if cols <= 0: + cols = 1 + + for row_idx, it in enumerate(list(getattr(self, '_items', []) or [])): + try: + row = Gtk.ListBoxRow() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + + for col in range(cols): + cell = it.cell(col) if hasattr(it, 'cell') else None + is_cb = self._header_is_checkbox(col) + # alignment for this column + try: + align_t = self._header.alignment(col) + except Exception: + align_t = YAlignmentType.YAlignBegin + + if is_cb: + # render a checkbox honoring alignment + try: + chk = Gtk.CheckButton() + try: + chk.set_active(cell.checked() if cell is not None else False) + except Exception: + chk.set_active(False) + # place inside a box to honor alignment + cell_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + if align_t == YAlignmentType.YAlignCenter: + cell_box.set_halign(Gtk.Align.CENTER) + elif align_t == YAlignmentType.YAlignEnd: + cell_box.set_halign(Gtk.Align.END) + else: + cell_box.set_halign(Gtk.Align.START) + cell_box.append(chk) + hbox.append(cell_box) + # connect toggle + def _on_toggled(btn, item=it, cindex=col): + try: + c = item.cell(cindex) + if c is not None: + c.setChecked(bool(btn.get_active())) + # track changed item + self._changed_item = item + # emit value changed + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + chk.connect("toggled", _on_toggled) + except Exception: + hbox.append(Gtk.Label(label="")) + else: + # render text label honoring alignment + txt = "" + try: + txt = cell.label() if cell is not None else "" + except Exception: + txt = "" + lbl = Gtk.Label(label=txt) + try: + if align_t == YAlignmentType.YAlignCenter: + lbl.set_xalign(0.5) + elif align_t == YAlignmentType.YAlignEnd: + lbl.set_xalign(1.0) + else: + lbl.set_xalign(0.0) + except Exception: + pass + hbox.append(lbl) + + row.set_child(hbox) + self._listbox.append(row) + self._row_to_item[row] = it + self._item_to_row[it] = row + self._rows.append(row) + except Exception: + pass + + # apply selection from model + try: + self._suppress_selection_handler = True + if not self._multi: + # single: select the first selected item + for it in list(getattr(self, '_items', []) or []): + if hasattr(it, 'selected') and it.selected(): + try: + row = self._item_to_row.get(it) + if row is not None: + self._listbox.select_row(row) + break + except Exception: + pass + else: + # multi: select all selected items + for it in list(getattr(self, '_items', []) or []): + if hasattr(it, 'selected') and it.selected(): + try: + row = self._item_to_row.get(it) + if row is not None: + self._listbox.select_row(row) + except Exception: + pass + finally: + self._suppress_selection_handler = False + + # selection handlers + def _on_row_selected(self, listbox, row): + if self._suppress_selection_handler: + return + try: + # update selected flags + for it in list(getattr(self, '_items', []) or []): + try: + it.setSelected(False) + except Exception: + pass + if row is not None: + it = self._row_to_item.get(row) + if it is not None: + try: + it.setSelected(True) + except Exception: + pass + self._selected_items = [it] + else: + self._selected_items = [] + # notify + try: + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + except Exception: + pass + + def _on_selected_rows_changed(self, listbox): + if self._suppress_selection_handler: + return + try: + selected_rows = listbox.get_selected_rows() or [] + new_selected = [] + for row in selected_rows: + it = self._row_to_item.get(row) + if it is not None: + new_selected.append(it) + # set flags + try: + for it in list(getattr(self, '_items', []) or []): + it.setSelected(False) + for it in new_selected: + it.setSelected(True) + except Exception: + pass + # clamp single-selection just in case + if not self._multi and len(new_selected) > 1: + new_selected = [new_selected[-1]] + self._selected_items = new_selected + # notify + try: + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + pass + except Exception: + pass + + # API + def addItem(self, item): + if isinstance(item, str): + item = YTableItem(item) + if not isinstance(item, YTableItem): + raise TypeError("YTableGtk.addItem expects a YTableItem or string label") + super().addItem(item) + try: + item.setIndex(len(self._items) - 1) + except Exception: + pass + try: + if getattr(self, '_listbox', None) is not None: + self.rebuildTable() + except Exception: + pass + + def addItems(self, items): + for it in items: + self.addItem(it) + + def selectItem(self, item, selected=True): + try: + item.setSelected(bool(selected)) + except Exception: + pass + if getattr(self, '_listbox', None) is None: + # only update model + if selected: + if item not in self._selected_items: + self._selected_items.append(item) + else: + try: + if item in self._selected_items: + self._selected_items.remove(item) + except Exception: + pass + return + try: + row = self._item_to_row.get(item) + if row is None: + self.rebuildTable() + row = self._item_to_row.get(item) + if row is None: + return + self._suppress_selection_handler = True + if selected: + if not self._multi: + try: + # GTK4 ListBox does not have clearSelection; manually unselect others + for r in list(self._listbox.get_selected_rows() or []): + self._listbox.unselect_row(r) + except Exception: + pass + self._listbox.select_row(row) + else: + try: + self._listbox.unselect_row(row) + except Exception: + pass + finally: + self._suppress_selection_handler = False + + def deleteAllItems(self): + try: + super().deleteAllItems() + except Exception: + self._items = [] + self._selected_items = [] + self._changed_item = None + try: + self._row_to_item.clear() + self._item_to_row.clear() + except Exception: + pass + try: + for row in list(self._listbox.get_children() or []): + try: + self._listbox.remove(row) + except Exception: + pass + except Exception: + pass + + def changedItem(self): + return getattr(self, "_changed_item", None) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 3a5eed7..ca3e010 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -250,6 +250,11 @@ def createTree(self, parent, label, multiselection=False, recursiveselection = F """Create a Tree widget.""" return YTreeGtk(parent, label, multiselection, recursiveselection) + def createTable(self, parent, header: YTableHeader, multiSelection: bool = False): + """Create a Table widget.""" + from .backends.gtk.tablegtk import YTableGtk + return YTableGtk(parent, header, multiSelection) + def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) From ef834f0cf5762067cd40e7497dbf722e6d0d752a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 19:08:17 +0100 Subject: [PATCH 216/523] Removed addItems --- manatools/aui/backends/qt/tableqt.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/manatools/aui/backends/qt/tableqt.py b/manatools/aui/backends/qt/tableqt.py index e43514b..615cfe4 100644 --- a/manatools/aui/backends/qt/tableqt.py +++ b/manatools/aui/backends/qt/tableqt.py @@ -467,10 +467,6 @@ def addItem(self, item): except Exception: pass - def addItems(self, items): - for it in items: - self.addItem(it) - def selectItem(self, item, selected=True): # update model and view try: From 17841e5c8fc0f06c3cc5eba8b225074e607744b9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 21:00:00 +0100 Subject: [PATCH 217/523] Start developing ncurses YTable --- manatools/aui/backends/curses/__init__.py | 2 + manatools/aui/backends/curses/tablecurses.py | 417 +++++++++++++++++++ manatools/aui/yui_curses.py | 6 +- 3 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/tablecurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index c90b91f..38e5ed5 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -13,6 +13,7 @@ from .checkboxframecurses import YCheckBoxFrameCurses from .progressbarcurses import YProgressBarCurses from .radiobuttoncurses import YRadioButtonCurses +from .tablecurses import YTableCurses __all__ = [ "YDialogCurses", @@ -30,5 +31,6 @@ "YCheckBoxFrameCurses", "YProgressBarCurses", "YRadioButtonCurses", + "YTableCurses", # ... ] diff --git a/manatools/aui/backends/curses/tablecurses.py b/manatools/aui/backends/curses/tablecurses.py new file mode 100644 index 0000000..e727443 --- /dev/null +++ b/manatools/aui/backends/curses/tablecurses.py @@ -0,0 +1,417 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import logging +from ...yui_common import * + +# Module-level logger for table curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.table.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + + +class YTableCurses(YSelectionWidget): + """ + NCurses implementation of a table widget. + - Renders column headers and rows in a fixed-width grid. + - Honors `YTableHeader` titles and alignments. + - Displays checkbox columns declared via `YTableHeader.isCheckboxColumn()` as [x]/[ ]. + - Selection driven by `YTableItem.selected()`; emits SelectionChanged on change. + - SPACE toggles the first checkbox column for the current row and emits ValueChanged. + - ENTER toggles row selection (multi or single as configured). + """ + def __init__(self, parent=None, header: YTableHeader = None, multiSelection: bool = False): + super().__init__(parent) + if header is None: + raise ValueError("YTableCurses requires a YTableHeader") + self._header = header + self._multi = bool(multiSelection) + # force single-selection if any checkbox column present + try: + for c_idx in range(self._header.columns()): + if self._header.isCheckboxColumn(c_idx): + self._multi = False + break + except Exception: + pass + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + # UI state + self._height = 3 # header + at least 2 rows + self._can_focus = True + self._focused = False + self._hover_row = 0 + self._scroll_offset = 0 + self._selected_items = [] + self._changed_item = None + self._current_visible_rows = None + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YTable" + + def _first_checkbox_col(self): + try: + for c in range(self._header.columns()): + if self._header.isCheckboxColumn(c): + return c + except Exception: + pass + return None + + def _create_backend_widget(self): + # Associate backend with self, compute minimal height, and reflect model selection. + self._backend_widget = self + self._height = max(3, 1 + min(len(self._items), 6)) + # Build internal selection list from item flags + sel = [] + try: + if self._multi: + for it in list(getattr(self, '_items', []) or []): + try: + if it.selected(): + sel.append(it) + except Exception: + pass + else: + chosen = None + for it in list(getattr(self, '_items', []) or []): + try: + if it.selected(): + chosen = it + except Exception: + pass + if chosen is not None: + sel = [chosen] + except Exception: + pass + self._selected_items = sel + self._hover_row = 0 + self._scroll_offset = 0 + self._current_visible_rows = None + self._logger.debug("_create_backend_widget: items=%d selected=%d", len(self._items) if self._items else 0, len(self._selected_items)) + + def _set_backend_enabled(self, enabled): + try: + self._can_focus = bool(enabled) + if not enabled: + self._focused = False + # propagate logical enabled state to contained items + for it in list(getattr(self, '_items', []) or []): + if hasattr(it, 'setEnabled'): + try: + it.setEnabled(enabled) + except Exception: + pass + except Exception: + pass + + def _visible_row_count(self): + # number of rows available excluding the header line + return max(1, getattr(self, "_preferred_rows", 6)) + + def _ensure_hover_visible(self): + visible = self._current_visible_rows if self._current_visible_rows is not None else self._visible_row_count() + if visible <= 0: + return + if self._hover_row < self._scroll_offset: + self._scroll_offset = self._hover_row + elif self._hover_row >= self._scroll_offset + visible: + self._scroll_offset = self._hover_row - visible + 1 + + def _col_widths(self, total_width): + try: + cols = max(1, int(self._header.columns())) + except Exception: + cols = 1 + # Divide width equally across columns, leaving 1 char separator + sep = 1 + usable = max(1, total_width - (cols - 1) * sep) + base = max(1, usable // cols) + widths = [base] * cols + remainder = usable - base * cols + for i in range(remainder): + widths[i] += 1 + return widths, sep + + def _align_text(self, text, width, align: YAlignmentType): + s = str(text)[:width] + if align == YAlignmentType.YAlignCenter: + pad = max(0, width - len(s)) + left = pad // 2 + right = pad - left + return (" " * left) + s + (" " * right) + elif align == YAlignmentType.YAlignEnd: + return s.rjust(width) + else: + return s.ljust(width) + + def _draw(self, window, y, x, width, height): + try: + line = y + # Header + widths, sep = self._col_widths(width) + headers = [] + try: + for c in range(self._header.columns()): + lbl = self._header.header(c) + align = self._header.alignment(c) + headers.append(self._align_text(lbl, widths[c], align)) + except Exception: + # fallback single empty header + headers = [" ".ljust(widths[0])] + header_line = (" " * sep).join(headers) + try: + window.addstr(line, x, header_line[:width], curses.A_BOLD) + except curses.error: + pass + line += 1 + + # Rows + available_rows = max(0, height - 1) + visible = min(len(self._items), available_rows) + if self.stretchable(YUIDimension.YD_VERT): + visible = min(len(self._items), available_rows) + else: + visible = min(len(self._items), self._visible_row_count(), available_rows) + self._current_visible_rows = visible + + for i in range(visible): + row_idx = self._scroll_offset + i + if row_idx >= len(self._items): + break + it = self._items[row_idx] + cells = [] + for c in range(len(widths)): + try: + cell = it.cell(c) + except Exception: + cell = None + is_cb = False + try: + is_cb = self._header.isCheckboxColumn(c) + except Exception: + is_cb = False + align = YAlignmentType.YAlignBegin + try: + align = self._header.alignment(c) + except Exception: + pass + if is_cb: + val = False + try: + val = cell.checked() if cell is not None else False + except Exception: + val = False + txt = "[x]" if val else "[ ]" + else: + txt = "" + try: + txt = cell.label() if cell is not None else "" + except Exception: + txt = "" + cells.append(self._align_text(txt, widths[c], align)) + row_text = (" " * sep).join(cells) + attr = curses.A_NORMAL + if not self.isEnabled(): + attr |= curses.A_DIM + if self._focused and row_idx == self._hover_row and self.isEnabled(): + attr |= curses.A_REVERSE + try: + window.addstr(line + i, x, row_text[:width].ljust(width), attr) + except curses.error: + pass + + # simple scroll indicators + if self._focused and len(self._items) > visible and width > 0 and self.isEnabled(): + try: + if self._scroll_offset > 0: + window.addch(y + 1, x + width - 1, '^') + if (self._scroll_offset + visible) < len(self._items): + window.addch(y + visible, x + width - 1, 'v') + except curses.error: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + handled = True + if key == curses.KEY_UP: + if self._hover_row > 0: + self._hover_row -= 1 + self._ensure_hover_visible() + elif key == curses.KEY_DOWN: + if self._hover_row < max(0, len(self._items) - 1): + self._hover_row += 1 + self._ensure_hover_visible() + elif key == curses.KEY_PPAGE: + step = self._visible_row_count() or 1 + self._hover_row = max(0, self._hover_row - step) + self._ensure_hover_visible() + elif key == curses.KEY_NPAGE: + step = self._visible_row_count() or 1 + self._hover_row = min(max(0, len(self._items) - 1), self._hover_row + step) + self._ensure_hover_visible() + elif key == curses.KEY_HOME: + self._hover_row = 0 + self._ensure_hover_visible() + elif key == curses.KEY_END: + self._hover_row = max(0, len(self._items) - 1) + self._ensure_hover_visible() + elif key in (ord(' '),): # toggle checkbox in the first checkbox column + col = self._first_checkbox_col() + if col is not None and 0 <= self._hover_row < len(self._items): + it = self._items[self._hover_row] + cell = None + try: + cell = it.cell(col) + except Exception: + cell = None + if cell is not None: + try: + cell.setChecked(not bool(cell.checked())) + self._changed_item = it + except Exception: + pass + # notify value changed + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + elif key in (ord('\n'),): # toggle row selection + if 0 <= self._hover_row < len(self._items): + it = self._items[self._hover_row] + if self._multi: + was_selected = it in self._selected_items + if was_selected: + try: + it.setSelected(False) + except Exception: + pass + try: + self._selected_items.remove(it) + except Exception: + pass + else: + try: + it.setSelected(True) + except Exception: + pass + self._selected_items.append(it) + else: + # single selection: make it sole selected + prev = self._selected_items[0] if self._selected_items else None + if prev is not None: + try: + prev.setSelected(False) + except Exception: + pass + try: + it.setSelected(True) + except Exception: + pass + self._selected_items = [it] + # notify selection change + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + else: + handled = False + return handled + + # API + def addItem(self, item): + if isinstance(item, str): + item = YTableItem(item) + if not isinstance(item, YTableItem): + raise TypeError("YTableCurses.addItem expects a YTableItem or string label") + super().addItem(item) + try: + item.setIndex(len(self._items) - 1) + except Exception: + pass + # reflect initial selected flag into internal list + try: + if item.selected(): + if not self._multi: + prev = self._selected_items[0] if self._selected_items else None + if prev is not None: + try: + prev.setSelected(False) + except Exception: + pass + self._selected_items = [item] + else: + if item not in self._selected_items: + self._selected_items.append(item) + except Exception: + pass + + def selectItem(self, item, selected=True): + try: + item.setSelected(bool(selected)) + except Exception: + pass + if selected: + if not self._multi: + prev = self._selected_items[0] if self._selected_items else None + if prev is not None: + try: + prev.setSelected(False) + except Exception: + pass + self._selected_items = [item] + else: + if item not in self._selected_items: + self._selected_items.append(item) + else: + try: + if item in self._selected_items: + self._selected_items.remove(item) + except Exception: + pass + # move hover to this item if present + try: + for i, it in enumerate(list(getattr(self, '_items', []) or [])): + if it is item: + self._hover_row = i + self._ensure_hover_visible() + break + except Exception: + pass + + def deleteAllItems(self): + try: + super().deleteAllItems() + except Exception: + self._items = [] + try: + self._selected_items = [] + self._hover_row = 0 + self._scroll_offset = 0 + self._current_visible_rows = None + self._changed_item = None + except Exception: + pass + + def changedItem(self): + return getattr(self, "_changed_item", None) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 0786760..b754055 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -212,4 +212,8 @@ def createProgressBar(self, parent, label, max_value=100): def createRadioButton(self, parent, label="", isChecked=False): """Create a Radio Button widget.""" - return YRadioButtonCurses(parent, label, isChecked) \ No newline at end of file + return YRadioButtonCurses(parent, label, isChecked) + + def createTable(self, parent, header: YTableHeader, multiSelection=False): + """Create a Table widget (curses backend).""" + return YTableCurses(parent, header, multiSelection) \ No newline at end of file From 1b9d68320ca48d957da859948bc885d30df85c41 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 21:11:40 +0100 Subject: [PATCH 218/523] fixed multiselection --- manatools/aui/backends/curses/tablecurses.py | 71 +++++++++++++++----- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/manatools/aui/backends/curses/tablecurses.py b/manatools/aui/backends/curses/tablecurses.py index e727443..15008e9 100644 --- a/manatools/aui/backends/curses/tablecurses.py +++ b/manatools/aui/backends/curses/tablecurses.py @@ -177,6 +177,14 @@ def _draw(self, window, y, x, width, height): # fallback single empty header headers = [" ".ljust(widths[0])] header_line = (" " * sep).join(headers) + # If multi-selection without checkbox columns, reserve left space for selection marker + use_selection_marker = False + try: + use_selection_marker = self._multi and (self._first_checkbox_col() is None) + except Exception: + use_selection_marker = False + if use_selection_marker: + header_line = " " + header_line try: window.addstr(line, x, header_line[:width], curses.A_BOLD) except curses.error: @@ -228,6 +236,13 @@ def _draw(self, window, y, x, width, height): txt = "" cells.append(self._align_text(txt, widths[c], align)) row_text = (" " * sep).join(cells) + # selection marker for multi-selection without checkbox columns + if use_selection_marker: + try: + marker = "[x] " if (it in self._selected_items) else "[ ] " + except Exception: + marker = "[ ] " + row_text = marker + row_text attr = curses.A_NORMAL if not self.isEnabled(): attr |= curses.A_DIM @@ -276,26 +291,52 @@ def _handle_key(self, key): elif key == curses.KEY_END: self._hover_row = max(0, len(self._items) - 1) self._ensure_hover_visible() - elif key in (ord(' '),): # toggle checkbox in the first checkbox column + elif key in (ord(' '),): # toggle checkbox or selection if no checkbox columns col = self._first_checkbox_col() - if col is not None and 0 <= self._hover_row < len(self._items): + if 0 <= self._hover_row < len(self._items): it = self._items[self._hover_row] - cell = None - try: - cell = it.cell(col) - except Exception: + if col is not None: + # Toggle checkbox value cell = None - if cell is not None: try: - cell.setChecked(not bool(cell.checked())) - self._changed_item = it + cell = it.cell(col) except Exception: - pass - # notify value changed - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + cell = None + if cell is not None: + try: + cell.setChecked(not bool(cell.checked())) + self._changed_item = it + except Exception: + pass + # notify value changed + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + else: + # Use SPACE to toggle row selection in multi-selection mode + if self._multi: + was_selected = it in self._selected_items + if was_selected: + try: + it.setSelected(False) + except Exception: + pass + try: + self._selected_items.remove(it) + except Exception: + pass + else: + try: + it.setSelected(True) + except Exception: + pass + self._selected_items.append(it) + # notify selection change + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) elif key in (ord('\n'),): # toggle row selection if 0 <= self._hover_row < len(self._items): it = self._items[self._hover_row] From 6b1fc68d76a9150f72ebead4e33774271de565a9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 21:19:46 +0100 Subject: [PATCH 219/523] Fixed multi selection when deselectiong --- manatools/aui/backends/gtk/tablegtk.py | 27 ++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index ffe2722..b6e88c8 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -44,6 +44,7 @@ def __init__(self, parent=None, header: YTableHeader = None, multiSelection=Fals self._suppress_selection_handler = False self._suppress_item_change = False self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._old_selected_items = [] # for change detection self._changed_item = None def widgetClass(self): @@ -121,8 +122,9 @@ def _create_backend_widget(self): # connect selection handlers if self._multi: try: - self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) - except Exception: + self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) + self._listbox.connect("row-activated", lambda lb, row: self._on_row_selected_for_multi(lb, row)) + except Exception: pass else: try: @@ -130,6 +132,8 @@ def _create_backend_widget(self): except Exception: pass + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + # populate if items exist try: if getattr(self, "_items", None): @@ -328,6 +332,8 @@ def _on_selected_rows_changed(self, listbox): # clamp single-selection just in case if not self._multi and len(new_selected) > 1: new_selected = [new_selected[-1]] + + self._old_selected_items = self._selected_items self._selected_items = new_selected # notify try: @@ -340,6 +346,23 @@ def _on_selected_rows_changed(self, listbox): except Exception: pass + def _on_row_selected_for_multi(self, listbox, row): + """ + Handler for row selection in multi-selection mode: for de-selection. + """ + if self._suppress_selection_handler: + return + self._logger.debug("_on_row_selected_for_multi called") + sel_rows = listbox.get_selected_rows() + it = self._row_to_item.get(row, None) + if it is not None: + if it in self._old_selected_items: + self._listbox.unselect_row( row ) + it.setSelected( False ) + self._on_selected_rows_changed(listbox) + else: + self._old_selected_items = self._selected_items + # API def addItem(self, item): if isinstance(item, str): From 927f0deb022ac65d24103b0e568cde4b7b59355e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 21:20:37 +0100 Subject: [PATCH 220/523] updated --- sow/TODO.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index cd77656..959504e 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -13,17 +13,18 @@ Missing Widgets comparing libyui: [X] YMultiSelectionBox [X] YTree [X] YFrame - [ ] YTable + [X] YTable [X] YProgressBar [ ] YRichText [ ] YMultiLineEdit [ ] YIntField - [ ] YMenuButton, YMenuBar - [ ] YWizard - [-] ~~YPackageSelector~~ - [ ] YSpacing, YAlignment + [ ] YMenuBar (YMenuButton?) + [ ] YSpacing + [X] YAlignment [ ] YReplacePoint [X] YRadioButton, + [ ] YWizard + [-] ~~YPackageSelector~~ [-] ~~YRadioButtonGroup~~ To check how to manage YEvents [X] and YItems [ ] (verify selection attirbute). From 08a736ec1a7800b7475035243108af21e37feef3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 21:24:27 +0100 Subject: [PATCH 221/523] Fill to expand vertically --- manatools/aui/backends/gtk/tablegtk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index b6e88c8..d56ad5c 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -98,10 +98,13 @@ def _create_backend_widget(self): try: vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) + vbox.set_valign(Gtk.Align.FILL) listbox.set_hexpand(True) listbox.set_vexpand(True) + listbox.set_valign(Gtk.Align.FILL) sw.set_hexpand(True) sw.set_vexpand(True) + sw.set_valign(Gtk.Align.FILL) except Exception: pass From 20d531f82d82c49254c417e7cbbce6b8b07475bd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 26 Dec 2025 21:38:27 +0100 Subject: [PATCH 222/523] Honored enabled property --- manatools/aui/backends/curses/tablecurses.py | 5 +++++ manatools/aui/backends/gtk/tablegtk.py | 22 ++++++++++++++++++++ manatools/aui/backends/qt/tableqt.py | 13 ++++++++++++ 3 files changed, 40 insertions(+) diff --git a/manatools/aui/backends/curses/tablecurses.py b/manatools/aui/backends/curses/tablecurses.py index 15008e9..1e9ef7b 100644 --- a/manatools/aui/backends/curses/tablecurses.py +++ b/manatools/aui/backends/curses/tablecurses.py @@ -106,6 +106,11 @@ def _create_backend_widget(self): self._scroll_offset = 0 self._current_visible_rows = None self._logger.debug("_create_backend_widget: items=%d selected=%d", len(self._items) if self._items else 0, len(self._selected_items)) + # respect initial enabled state + try: + self._set_backend_enabled(self.isEnabled()) + except Exception: + pass def _set_backend_enabled(self, enabled): try: diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index d56ad5c..604aada 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -122,6 +122,15 @@ def _create_backend_widget(self): except Exception: pass + # respect initial enabled state + try: + if hasattr(self._backend_widget, "set_sensitive"): + self._backend_widget.set_sensitive(bool(getattr(self, "_enabled", True))) + if hasattr(self._listbox, "set_sensitive"): + self._listbox.set_sensitive(bool(getattr(self, "_enabled", True))) + except Exception: + pass + # connect selection handlers if self._multi: try: @@ -452,3 +461,16 @@ def deleteAllItems(self): def changedItem(self): return getattr(self, "_changed_item", None) + + def _set_backend_enabled(self, enabled): + """Enable/disable the GTK table at runtime.""" + try: + if getattr(self, "_backend_widget", None) is not None and hasattr(self._backend_widget, "set_sensitive"): + self._backend_widget.set_sensitive(bool(enabled)) + except Exception: + pass + try: + if getattr(self, "_listbox", None) is not None and hasattr(self._listbox, "set_sensitive"): + self._listbox.set_sensitive(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/backends/qt/tableqt.py b/manatools/aui/backends/qt/tableqt.py index 615cfe4..ffb7827 100644 --- a/manatools/aui/backends/qt/tableqt.py +++ b/manatools/aui/backends/qt/tableqt.py @@ -77,6 +77,11 @@ def _create_backend_widget(self): #tbl.itemChanged.connect(self._on_item_changed) self._table = tbl self._backend_widget = tbl + # respect initial enabled state + try: + self._table.setEnabled(bool(getattr(self, "_enabled", True))) + except Exception: + pass # populate if items already present try: self.rebuildTable() @@ -560,3 +565,11 @@ def deleteAllItems(self): def changedItem(self): return self._changed_item + + def _set_backend_enabled(self, enabled): + """Enable/disable the Qt table at runtime.""" + try: + if getattr(self, "_table", None) is not None: + self._table.setEnabled(bool(enabled)) + except Exception: + pass From 7cd6914a02c58a1e2fd5ac264d7966bb1ad456ce Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 14:05:23 +0100 Subject: [PATCH 223/523] Managed ctrl-c --- manatools/aui/backends/gtk/dialoggtk.py | 38 +++++- manatools/aui/backends/qt/dialogqt.py | 147 ++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index 940ec03..bafe254 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -148,8 +148,35 @@ def on_timeout(): if timeout_millisec and timeout_millisec > 0: self._timeout_id = GLib.timeout_add(timeout_millisec, on_timeout) - # run nested loop - self._glib_loop.run() + # Handle Ctrl-C gracefully: add a GLib SIGINT source to quit the loop and post CancelEvent + self._sigint_source_id = None + try: + def _on_sigint(*_args): + try: + self._post_event(YCancelEvent()) + except Exception: + pass + try: + if self._glib_loop.is_running(): + self._glib_loop.quit() + except Exception: + pass + return True + # GLib.unix_signal_add is available on Linux; use default priority + if hasattr(GLib, 'unix_signal_add'): + self._sigint_source_id = GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, 2, _on_sigint) # 2 = SIGINT + except Exception: + self._sigint_source_id = None + + # run nested loop; suppress KeyboardInterrupt raised by GI fallback on exit + try: + self._glib_loop.run() + except KeyboardInterrupt: + # Convert to a cancel event and continue cleanup + try: + self._event_result = YCancelEvent() + except Exception: + pass # cleanup if self._timeout_id: @@ -158,6 +185,13 @@ def on_timeout(): except Exception: pass self._timeout_id = None + # remove SIGINT source if installed + try: + if self._sigint_source_id: + GLib.source_remove(self._sigint_source_id) + except Exception: + pass + self._sigint_source_id = None self._glib_loop = None return self._event_result if self._event_result is not None else YEvent() diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index 1544f5c..f62ea63 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -16,6 +16,8 @@ from ... import yui as yui_mod import os import logging +import signal +import fcntl class YDialogQt(YSingleChildContainerWidget): _open_dialogs = [] @@ -28,6 +30,12 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._qwidget = None self._event_result = None self._qt_event_loop = None + # SIGINT handling state + self._sigint_r = None + self._sigint_w = None + self._sigint_notifier = None + self._prev_wakeup_fd = None + self._prev_sigint_handler = None YDialogQt._open_dialogs.append(self) self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") @@ -224,11 +232,150 @@ def on_timeout(): timer.timeout.connect(on_timeout) timer.start(timeout_millisec) + # Robust SIGINT (Ctrl-C) handling: use wakeup fd + QSocketNotifier to exit loop + self._setup_sigint_notifier(loop) + # PySide6 / Qt6 uses exec() loop.exec() # cleanup if timer and timer.isActive(): timer.stop() + # teardown SIGINT notifier and restore previous wakeup fd + self._teardown_sigint_notifier() self._qt_event_loop = None return self._event_result if self._event_result is not None else YEvent() + + def _setup_sigint_notifier(self, loop): + """Install a wakeup fd and QSocketNotifier to gracefully quit on Ctrl-C.""" + try: + # create non-blocking pipe + rfd, wfd = os.pipe() + for fd in (rfd, wfd): + try: + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + except Exception: + pass + prev = None + try: + # store previous wakeup fd to restore later + prev = signal.set_wakeup_fd(wfd) + except Exception: + prev = None + self._sigint_r = rfd + self._sigint_w = wfd + self._prev_wakeup_fd = prev + # install a noop SIGINT handler to prevent KeyboardInterrupt + try: + self._prev_sigint_handler = signal.getsignal(signal.SIGINT) + except Exception: + self._prev_sigint_handler = None + try: + def _noop_sigint(_sig, _frm): + # no-op; wakeup fd + notifier will handle termination + pass + signal.signal(signal.SIGINT, _noop_sigint) + except Exception: + pass + # notifier to watch read end + notifier = QtCore.QSocketNotifier(self._sigint_r, QtCore.QSocketNotifier.Read) + def _on_readable(): + try: + # drain bytes written by signal module + while True: + try: + os.read(self._sigint_r, 1024) + except BlockingIOError: + break + except KeyboardInterrupt: + # suppress default Ctrl-C exception; we'll quit gracefully + break + except Exception: + break + except Exception: + pass + # Post cancel event and quit the loop + try: + self._post_event(YCancelEvent()) + except Exception: + pass + try: + if loop is not None and loop.isRunning(): + loop.quit() + except Exception: + pass + notifier.activated.connect(_on_readable) + self._sigint_notifier = notifier + except Exception: + # fall back: plain signal handler attempting to quit + def _on_sigint(_sig, _frm): + try: + self._post_event(YCancelEvent()) + except Exception: + pass + try: + if loop is not None and loop.isRunning(): + loop.quit() + except Exception: + pass + try: + signal.signal(signal.SIGINT, _on_sigint) + except Exception: + pass + + def _teardown_sigint_notifier(self): + """Remove SIGINT notifier and restore previous wakeup fd.""" + try: + if self._sigint_notifier is not None: + try: + self._sigint_notifier.setEnabled(False) + except Exception: + pass + self._sigint_notifier = None + except Exception: + pass + try: + if self._sigint_r is not None: + try: + os.close(self._sigint_r) + except Exception: + pass + self._sigint_r = None + if self._sigint_w is not None: + try: + os.close(self._sigint_w) + except Exception: + pass + self._sigint_w = None + except Exception: + pass + try: + # restore previous wakeup fd + if self._prev_wakeup_fd is not None: + try: + signal.set_wakeup_fd(self._prev_wakeup_fd) + except Exception: + pass + else: + # reset to default (no wakeup fd) + try: + signal.set_wakeup_fd(-1) + except Exception: + pass + except Exception: + pass + # restore previous SIGINT handler + try: + if self._prev_sigint_handler is not None: + try: + signal.signal(signal.SIGINT, self._prev_sigint_handler) + except Exception: + pass + else: + try: + signal.signal(signal.SIGINT, signal.default_int_handler) + except Exception: + pass + except Exception: + pass From 7d149e1c22e1d78e7742f01857a79e481e5fc758 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 14:50:12 +0100 Subject: [PATCH 224/523] Table example --- test/test_table.py | 171 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 test/test_table.py diff --git a/test/test_table.py b/test/test_table.py new file mode 100644 index 0000000..2211336 --- /dev/null +++ b/test/test_table.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 + +"""Example dialog to manually test YTable widgets. + +Layout: +- HBox with two tables (left checkboxed, right multi-selection). +- Labels show selected rows and checkbox states. +- OK button closes the dialog. + +Run with: `python -m pytest -q test/test_table.py::test_table_example -s` or run directly. +""" + +import os +import sys + +# allow running from repo root +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + +from manatools.aui.yui import YUI, YUI_ui +import manatools.aui.yui_common as yui + + +def build_left_checkbox_table(factory, table_widget): + items = [] + for i in range(1, 7): + itm = yui.YTableItem(f"Row {i}") + itm.addCell(str(i)) + itm.addCell(f"test {i}") + # third column is checkbox column + itm.addCell(False if i % 2 == 0 else True) + items.append(itm) + table_widget.addItem(itm) + return items + + +def build_right_multi_table(factory, table_widget): + items = [] + for i in range(1, 7): + itm = yui.YTableItem(f"Name {i}") + itm.addCells(f"Name {i}", f"Addr {i} Street", f"{10000+i}", f"Town {i}") + items.append(itm) + table_widget.addItem(itm) + return items + + +def test_table_example(backend_name=None): + if backend_name: + os.environ['YUI_BACKEND'] = backend_name + + # Ensure fresh YUI detection + YUI._instance = None + YUI._backend = None + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Log program name and detected backend + try: + backend = YUI.backend() + logging.getLogger().debug("test_table_example: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) + except Exception: + logging.getLogger().debug("test_table_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) + + dlg = factory.createMainDialog() + vbox = factory.createVBox(dlg) + factory.createHeading(vbox, "Table Example") + factory.createLabel(vbox, "Two tables side-by-side. Left has checkbox column.") + + hbox = factory.createHBox(vbox) + + # left: checkbox table header (third column is checkbox) with alignment: + # first column right aligned, checkbox column centered + left_header = yui.YTableHeader() + left_header.addColumn('num.', alignment=yui.YAlignmentType.YAlignEnd) + left_header.addColumn('info', alignment=yui.YAlignmentType.YAlignBegin) + left_header.addColumn('', checkBox=True, alignment=yui.YAlignmentType.YAlignCenter) + left_table = factory.createTable(hbox, left_header) + #left_table.setEnabled(False) + + # right: multi-selection table + right_header = yui.YTableHeader() + right_header.addColumn('name') + right_header.addColumn('address') + right_header.addColumn('zip code') + right_header.addColumn('town') + right_table = factory.createTable(hbox, right_header, True) + + # populate + left_items = build_left_checkbox_table(factory, left_table) + right_items = build_right_multi_table(factory, right_table) + + # status labels + sel_label = factory.createLabel(vbox, "Selected: None") + chk_label = factory.createLabel(vbox, "Checked rows: []") + + # ok/quit + ctrl_h = factory.createHBox(vbox) + ok_btn = factory.createPushButton(ctrl_h, "OK") + + def update_labels(checked_item=None): + try: + sel = right_table.selectedItems() + sel_text = [it.label() for it in sel] + sel_label.setText(f"Selected: {sel_text}") + # left table checked states + checked = [] + for it in left_items: + if it.cellCount() >= 3 and it.cell(2).checked(): + checked.append(it.label(0)) + if checked_item is not None: + chk_label.setText(f"Checked rows: {checked} | Selected{checked_item.label(0)}: checked:{checked_item.checked(2)}") + else: + chk_label.setText(f"Checked rows: {checked}") + except Exception: + pass + + update_labels() + + print("Opening Table example dialog...") + + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dlg.destroy() + break + if et == yui.YEventType.WidgetEvent: + w = ev.widget() + reason = ev.reason() + if w == right_table and reason == yui.YEventReason.SelectionChanged: + update_labels() + elif w == left_table and reason == yui.YEventReason.ValueChanged: + sel = left_table.changedItem() + #root_logger.debug(f"Left table checkbox changed, item: {sel.label(0) if sel else 'None'}") + update_labels(sel) + elif w == ok_btn and reason == yui.YEventReason.Activated: + dlg.destroy() + break + + print("Dialog closed") + + +if __name__ == '__main__': + if len(sys.argv) > 1: + test_table_example(sys.argv[1]) + else: + test_table_example() From 73aa1ac4cd6556f427e4ace2c8b305017911dba3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 15:22:47 +0100 Subject: [PATCH 225/523] Added YRichText for Qt --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/yui_qt.py | 4 + test/test_richtext.py | 124 ++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 test/test_richtext.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 226a16d..1feb8ef 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -14,6 +14,7 @@ from .progressbarqt import YProgressBarQt from .radiobuttonqt import YRadioButtonQt from .tableqt import YTableQt +from .richtextqt import YRichTextQt __all__ = [ @@ -33,5 +34,6 @@ "YProgressBarQt", "YRadioButtonQt", "YTableQt", + "YRichTextQt", # ... ] diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 0fcb859..1a56dc8 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -226,3 +226,7 @@ def createRadioButton(self, parent, label:str = "", isChecked:bool = False): def createTable(self, parent, header: YTableHeader, multiSelection: bool = False): """Create a Table widget.""" return YTableQt(parent, header, multiSelection) + + def createRichText(self, parent, text: str = "", plainTextMode: bool = False): + """Create a RichText widget (Qt backend).""" + return YRichTextQt(parent, text, plainTextMode) diff --git a/test/test_richtext.py b/test/test_richtext.py new file mode 100644 index 0000000..91251d9 --- /dev/null +++ b/test/test_richtext.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +"""Example dialog to manually test YRichText widget. + +Layout: +- VBox with heading and a RichText widget showing HTML. +- Label displays last activated link. +- OK button closes the dialog. + +Run with: `python -m pytest -q test/test_richtext.py::test_richtext_example -s` or run directly. +""" + +import os +import sys + +# allow running from repo root +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + +from manatools.aui.yui import YUI, YUI_ui +import manatools.aui.yui_common as yui + + +def test_richtext_example(backend_name=None): + if backend_name: + os.environ['YUI_BACKEND'] = backend_name + + # Ensure fresh YUI detection + YUI._instance = None + YUI._backend = None + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Log program name and detected backend + try: + backend = YUI.backend() + logging.getLogger().debug("test_richtext_example: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) + except Exception: + logging.getLogger().debug("test_richtext_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) + + dlg = factory.createMainDialog() + vbox = factory.createVBox(dlg) + factory.createHeading(vbox, "RichText Example") + factory.createLabel(vbox, "Rich text below with HTML formatting and a link.") + + # Rich text content (HTML) + html = ( + "

Welcome to RichText

" + "

This is bold, italic, and underlined text.

" + "

Click the example.com link to emit an activation event.

" + "

Lists:

" + "
  • Alpha
  • Beta
  • Gamma
" + ) + rich = factory.createRichText(vbox, html, False) + try: + # Enable auto scroll to demonstrate API + rich.setAutoScrollDown(True) + except Exception: + pass + + # Status label for last activated link + status_label = factory.createLabel(vbox, "Last link: (none)") + + # ok/quit + ctrl_h = factory.createHBox(vbox) + ok_btn = factory.createPushButton(ctrl_h, "OK") + + print("Opening RichText example dialog...") + + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dlg.destroy() + break + if et == yui.YEventType.WidgetEvent: + w = ev.widget() + reason = ev.reason() + if w == rich and reason == yui.YEventReason.Activated: + try: + # YRichTextQt exposes lastActivatedUrl(); fall back gracefully if not available + url = None + try: + url = rich.lastActivatedUrl() + except Exception: + url = None + status_label.setText(f"Last link: {url if url else '(unknown)'}") + except Exception: + pass + elif w == ok_btn and reason == yui.YEventReason.Activated: + dlg.destroy() + break + + print("Dialog closed") + + +if __name__ == '__main__': + if len(sys.argv) > 1: + test_richtext_example(sys.argv[1]) + else: + test_richtext_example() From 2f0cefbb671626e505c6aada7da52e6198688db2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 16:42:46 +0100 Subject: [PATCH 226/523] Added Qt RichText implementation and an example --- manatools/aui/backends/qt/richtextqt.py | 159 ++++++++++++++++++++++++ test/test_richtext.py | 62 +++++---- 2 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 manatools/aui/backends/qt/richtextqt.py diff --git a/manatools/aui/backends/qt/richtextqt.py b/manatools/aui/backends/qt/richtextqt.py new file mode 100644 index 0000000..a38b03a --- /dev/null +++ b/manatools/aui/backends/qt/richtextqt.py @@ -0,0 +1,159 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore, QtGui +import logging +from ...yui_common import * + +class YRichTextQt(YWidget): + """ + Rich text widget using Qt's QTextBrowser. + - Supports plain text and rich HTML-like content. + - Emits an Activated event on link clicks without navigating. + - Optional auto-scroll on updates. + """ + def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): + super().__init__(parent) + self._text = text or "" + self._plain = bool(plainTextMode) + self._auto_scroll = False + self._last_url = None + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + + def widgetClass(self): + return "YRichText" + + # Value API + def setValue(self, newValue: str): + try: + self._text = newValue or "" + except Exception: + self._text = "" + try: + if getattr(self, "_backend_widget", None) is not None: + if self._plain: + try: + self._backend_widget.setPlainText(self._text) + except Exception: + self._backend_widget.setText(self._text) + else: + try: + self._backend_widget.setHtml(self._text) + except Exception: + self._backend_widget.setText(self._text) + if self._auto_scroll: + try: + cursor = self._backend_widget.textCursor() + cursor.movePosition(QtGui.QTextCursor.End) + self._backend_widget.setTextCursor(cursor) + self._backend_widget.ensureCursorVisible() + except Exception: + pass + except Exception: + pass + + def value(self) -> str: + return self._text + + # Plain text mode + def plainTextMode(self) -> bool: + return bool(self._plain) + + def setPlainTextMode(self, on: bool = True): + self._plain = bool(on) + # refresh backend content to reflect mode + self.setValue(self._text) + + # Auto scroll + def autoScrollDown(self) -> bool: + return bool(self._auto_scroll) + + def setAutoScrollDown(self, on: bool = True): + self._auto_scroll = bool(on) + # optional immediate effect + if self._auto_scroll and getattr(self, "_backend_widget", None) is not None: + try: + cursor = self._backend_widget.textCursor() + cursor.movePosition(QtGui.QTextCursor.End) + self._backend_widget.setTextCursor(cursor) + self._backend_widget.ensureCursorVisible() + except Exception: + pass + + # Link activation info + def lastActivatedUrl(self): + return self._last_url + + def _create_backend_widget(self): + tb = QtWidgets.QTextBrowser() + # Prevent navigation; we only emit events on link clicks + try: + tb.setOpenExternalLinks(False) + except Exception: + pass + try: + tb.setOpenLinks(False) + except Exception: + pass + try: + tb.setReadOnly(True) + except Exception: + pass + # set initial content + try: + if self._plain: + tb.setPlainText(self._text) + else: + tb.setHtml(self._text) + except Exception: + try: + tb.setText(self._text) + except Exception: + pass + # connect link activation + def _on_anchor_clicked(url: QtCore.QUrl): + try: + self._last_url = url.toString() + except Exception: + self._last_url = None + try: + dlg = self.findDialog() + if dlg and self.notify(): + dlg._post_event(YMenuEvent( id=url.toString()) ) + self._logger.debug("Link activated: %s", url.toString()) + except Exception: + self._logger.error("Posting Link activated: %s", url.toString()) + pass + try: + tb.anchorClicked.connect(_on_anchor_clicked) + except Exception: + # fallback: try signal on QLabel-like + try: + tb.linkActivated.connect(lambda _u: _on_anchor_clicked(QtCore.QUrl(str(_u)))) + except Exception: + pass + self._backend_widget = tb + # respect initial enabled state + try: + self._backend_widget.setEnabled(bool(self._enabled)) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass diff --git a/test/test_richtext.py b/test/test_richtext.py index 91251d9..0e2d1a9 100644 --- a/test/test_richtext.py +++ b/test/test_richtext.py @@ -20,24 +20,19 @@ # Configure file logger for this test: write DEBUG logs to '.log' in cwd try: log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' - fh = logging.FileHandler(log_name, mode='w') - fh.setLevel(logging.DEBUG) - fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) - root_logger = logging.getLogger() + + logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') + root_logger = logging.getLogger() + fileHandler = logging.FileHandler(log_name, mode='w') + fileHandler.setFormatter(logFormatter) + root_logger.addHandler(fileHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + root_logger.addHandler(consoleHandler) + consoleHandler.setLevel(logging.INFO) root_logger.setLevel(logging.DEBUG) - existing = False - for h in list(root_logger.handlers): - try: - if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): - existing = True - break - except Exception: - pass - if not existing: - root_logger.addHandler(fh) - print(f"Logging test output to: {os.path.abspath(log_name)}") except Exception as _e: - print(f"Failed to configure file logger: {_e}") + logging.getLogger().exception("Failed to configure file logger: %s", _e) from manatools.aui.yui import YUI, YUI_ui import manatools.aui.yui_common as yui @@ -57,9 +52,9 @@ def test_richtext_example(backend_name=None): # Log program name and detected backend try: backend = YUI.backend() - logging.getLogger().debug("test_richtext_example: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) + root_logger.debug("test_richtext_example: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) except Exception: - logging.getLogger().debug("test_richtext_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) + root_logger.debug("test_richtext_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) dlg = factory.createMainDialog() vbox = factory.createVBox(dlg) @@ -68,8 +63,14 @@ def test_richtext_example(backend_name=None): # Rich text content (HTML) html = ( + "

Heading 1

" + "

Heading 2

" + "

Heading 3

" + "

Heading 4

" + "
Heading 5
" + "
Heading 6
" "

Welcome to RichText

" - "

This is bold, italic, and underlined text.

" + "

This is a paragraph with bold, italic, and underlined text.

" "

Click the example.com link to emit an activation event.

" "

Lists:

" "
  • Alpha
  • Beta
  • Gamma
" @@ -88,7 +89,7 @@ def test_richtext_example(backend_name=None): ctrl_h = factory.createHBox(vbox) ok_btn = factory.createPushButton(ctrl_h, "OK") - print("Opening RichText example dialog...") + root_logger.info("Opening RichText example dialog...") while True: ev = dlg.waitForEvent() @@ -96,25 +97,18 @@ def test_richtext_example(backend_name=None): if et == yui.YEventType.CancelEvent: dlg.destroy() break - if et == yui.YEventType.WidgetEvent: + elif et == yui.YEventType.WidgetEvent: w = ev.widget() reason = ev.reason() - if w == rich and reason == yui.YEventReason.Activated: - try: - # YRichTextQt exposes lastActivatedUrl(); fall back gracefully if not available - url = None - try: - url = rich.lastActivatedUrl() - except Exception: - url = None - status_label.setText(f"Last link: {url if url else '(unknown)'}") - except Exception: - pass - elif w == ok_btn and reason == yui.YEventReason.Activated: + if w == ok_btn and reason == yui.YEventReason.Activated: dlg.destroy() break + elif et == yui.YEventType.MenuEvent: + url = ev.id() if ev.id() else '(none)' + status_label.setValue(f"Last link: {url}") + root_logger.info("Link activated: %s", url) - print("Dialog closed") + root_logger.info("Dialog closed") if __name__ == '__main__': From 2df2858b00ebad195da37822634f64c48c81686a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 16:44:19 +0100 Subject: [PATCH 227/523] Added legacy set text variants --- manatools/aui/backends/curses/labelcurses.py | 6 ++++++ manatools/aui/backends/gtk/labelgtk.py | 6 ++++++ manatools/aui/backends/qt/labelqt.py | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index b4b247e..9004b81 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -51,6 +51,12 @@ def text(self): def setText(self, new_text): self._text = new_text + def setValue(self, newValue): + self.setText(newValue) + + def setLabel(self, newLabel): + self.setText(newLabel) + def _create_backend_widget(self): try: self._backend_widget = self diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py index 5dc4f30..ad58064 100644 --- a/manatools/aui/backends/gtk/labelgtk.py +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -43,6 +43,12 @@ def setText(self, new_text): except Exception: pass + def setValue(self, newValue): + self.setText(newValue) + + def setLabel(self, newLabel): + self.setText(newLabel) + def _create_backend_widget(self): self._backend_widget = Gtk.Label(label=self._text) try: diff --git a/manatools/aui/backends/qt/labelqt.py b/manatools/aui/backends/qt/labelqt.py index 908d752..d71dfa8 100644 --- a/manatools/aui/backends/qt/labelqt.py +++ b/manatools/aui/backends/qt/labelqt.py @@ -32,6 +32,12 @@ def setText(self, new_text): if self._backend_widget: self._backend_widget.setText(new_text) + def setValue(self, newValue): + self.setText(newValue) + + def setLabel(self, newLabel): + self.setText(newLabel) + def _create_backend_widget(self): self._backend_widget = QtWidgets.QLabel(self._text) if self._is_heading: From c907fb143ad5941172ad8a8aa7bc76c01a189855 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 17:02:22 +0100 Subject: [PATCH 228/523] Added Gtk Rich text --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/richtextgtk.py | 227 ++++++++++++++++++++++ manatools/aui/yui_gtk.py | 5 + 3 files changed, 234 insertions(+) create mode 100644 manatools/aui/backends/gtk/richtextgtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 381b90c..f835861 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -14,6 +14,7 @@ from .progressbargtk import YProgressBarGtk from .radiobuttongtk import YRadioButtonGtk from .tablegtk import YTableGtk +from .richtextgtk import YRichTextGtk __all__ = [ "YDialogGtk", @@ -32,5 +33,6 @@ "YProgressBarGtk", "YRadioButtonGtk", "YTableGtk", + "YRichTextGtk", # ... ] diff --git a/manatools/aui/backends/gtk/richtextgtk.py b/manatools/aui/backends/gtk/richtextgtk.py new file mode 100644 index 0000000..58d0847 --- /dev/null +++ b/manatools/aui/backends/gtk/richtextgtk.py @@ -0,0 +1,227 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +GTK4 backend RichText widget. + +- Plain text mode: Gtk.TextView (read-only) inside Gtk.ScrolledWindow +- Rich text mode: Gtk.Label with markup inside Gtk.ScrolledWindow +- Link activation: emits YMenuEvent with URL id +- Auto scroll down: applies to TextView; for Label, scrolled window shows full content +''' +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, GLib, Gdk +import logging +import re +from ...yui_common import * + + +class YRichTextGtk(YWidget): + def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): + super().__init__(parent) + self._text = text or "" + self._plain = bool(plainTextMode) + self._auto_scroll = False + self._last_url = None + self._backend_widget = None + self._content_widget = None # TextView or Label + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + + def widgetClass(self): + return "YRichText" + + def setValue(self, newValue: str): + self._text = newValue or "" + w = getattr(self, "_content_widget", None) + if w is None: + return + try: + if isinstance(w, Gtk.TextView): + buf = w.get_buffer() + if buf: + try: + buf.set_text(self._text) + except Exception: + pass + if self._auto_scroll: + try: + itr_end = buf.get_end_iter() + w.scroll_to_iter(itr_end, 0.0, True, 0.0, 1.0) + except Exception: + pass + elif isinstance(w, Gtk.Label): + try: + w.set_use_markup(True) + except Exception: + pass + # Convert common HTML tags to Pango markup supported by Gtk.Label + converted = self._html_to_pango_markup(self._text) + try: + w.set_markup(converted) + except Exception: + try: + w.set_text(converted) + except Exception: + pass + except Exception: + pass + + def value(self) -> str: + return self._text + + def plainTextMode(self) -> bool: + return bool(self._plain) + + def setPlainTextMode(self, on: bool = True): + self._plain = bool(on) + # rebuild content widget to reflect mode + if getattr(self, "_backend_widget", None) is not None: + self._create_content() + self.setValue(self._text) + + def autoScrollDown(self) -> bool: + return bool(self._auto_scroll) + + def setAutoScrollDown(self, on: bool = True): + self._auto_scroll = bool(on) + # apply immediately for TextView + if self._auto_scroll and isinstance(getattr(self, "_content_widget", None), Gtk.TextView): + try: + buf = self._content_widget.get_buffer() + itr_end = buf.get_end_iter() + self._content_widget.scroll_to_iter(itr_end, 0.0, True, 0.0, 1.0) + except Exception: + pass + + def lastActivatedUrl(self): + return self._last_url + + def _create_content(self): + # Create the content widget according to mode + try: + if self._plain: + tv = Gtk.TextView() + try: + tv.set_editable(False) + except Exception: + pass + try: + tv.set_cursor_visible(False) + except Exception: + pass + try: + tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + except Exception: + pass + self._content_widget = tv + # set initial text + try: + buf = tv.get_buffer() + buf.set_text(self._text) + except Exception: + pass + else: + lbl = Gtk.Label() + try: + lbl.set_use_markup(True) + except Exception: + pass + # Convert HTML to Pango markup for GTK Label + converted = self._html_to_pango_markup(self._text) + try: + lbl.set_markup(converted) + except Exception: + lbl.set_text(converted) + try: + lbl.set_selectable(False) + except Exception: + pass + try: + lbl.set_wrap(True) + lbl.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + lbl.set_xalign(0.0) + except Exception: + pass + # connect link activation + def _on_activate_link(label, uri): + try: + self._last_url = uri + dlg = self.findDialog() + if dlg and self.notify(): + # emit a MenuEvent for link activation (back-compat) + dlg._post_event(YMenuEvent(item=None, id=uri)) + except Exception: + pass + # return True to stop default handling + return True + try: + lbl.connect("activate-link", _on_activate_link) + except Exception: + pass + self._content_widget = lbl + except Exception: + # fallback to a simple label + self._content_widget = Gtk.Label(label=self._text) + + def _create_backend_widget(self): + sw = Gtk.ScrolledWindow() + try: + sw.set_hexpand(True) + sw.set_vexpand(True) + except Exception: + pass + + self._create_content() + try: + sw.set_child(self._content_widget) + except Exception: + try: + sw.add(self._content_widget) + except Exception: + pass + self._backend_widget = sw + # respect initial enabled state + try: + self._backend_widget.set_sensitive(bool(self._enabled)) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_sensitive(bool(enabled)) + except Exception: + pass + + def _html_to_pango_markup(self, s: str) -> str: + """Convert a limited subset of HTML into GTK/Pango markup. + Supports: h1-h6 (as bold spans with size), b/i/u, a href, p/br, ul/li. + Unknown tags are stripped. + """ + if not s: + return "" + t = s + # Normalize newlines for paragraphs and breaks + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + # Lists + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "• ", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + # Headings -> bold span with size + sizes = {1: "xx-large", 2: "x-large", 3: "large", 4: "medium", 5: "small", 6: "x-small"} + for n, sz in sizes.items(): + t = re.sub(fr"", f"", t, flags=re.IGNORECASE) + t = re.sub(fr"", "\n", t, flags=re.IGNORECASE) + # Allow basic formatting tags and ; strip all other tags + t = re.sub(r"]*>", "", t) + return t diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index ca3e010..3b4ea75 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -259,6 +259,11 @@ def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) + def createRichText(self, parent, text: str = "", plainTextMode: bool = False): + """Create a RichText widget (GTK backend).""" + from .backends.gtk.richtextgtk import YRichTextGtk + return YRichTextGtk(parent, text, plainTextMode) + def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): """Create a CheckBox Frame widget.""" return YCheckBoxFrameGtk(parent, label, checked) From 645f0f6da3ff8dc37cf03a1f95c203de0a4c8f3b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 17:57:04 +0100 Subject: [PATCH 229/523] Adding ncurses Rich Text --- manatools/aui/backends/curses/__init__.py | 2 + .../aui/backends/curses/richtextcurses.py | 442 ++++++++++++++++++ manatools/aui/yui_curses.py | 6 +- 3 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/richtextcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 38e5ed5..203306f 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -14,6 +14,7 @@ from .progressbarcurses import YProgressBarCurses from .radiobuttoncurses import YRadioButtonCurses from .tablecurses import YTableCurses +from .richtextcurses import YRichTextCurses __all__ = [ "YDialogCurses", @@ -32,5 +33,6 @@ "YProgressBarCurses", "YRadioButtonCurses", "YTableCurses", + "YRichTextCurses", # ... ] diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py new file mode 100644 index 0000000..dedad78 --- /dev/null +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -0,0 +1,442 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Curses backend RichText widget. +- Displays text content in a scrollable area. +- In plain text mode, shows the text as-is. +- In rich mode, strips simple HTML tags for display; detects URLs. +- Link activation: pressing Enter on a line with a URL posts a YMenuEvent with the URL. +''' +import curses +import re +import logging +from ...yui_common import * + +_mod_logger = logging.getLogger("manatools.aui.curses.richtext.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + + +class YRichTextCurses(YWidget): + def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): + super().__init__(parent) + self._text = text or "" + self._plain = bool(plainTextMode) + self._auto_scroll = False + self._last_url = None + self._height = 6 + self._can_focus = True + self._focused = False + self._scroll_offset = 0 # vertical offset + self._hscroll_offset = 0 # horizontal offset + self._hover_line = 0 + self._anchors = [] # list of dicts: {sline, scol, eline, ecol, target} + self._armed_index = -1 + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + + def widgetClass(self): + return "YRichText" + + def _create_backend_widget(self): + try: + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) + + def setValue(self, newValue: str): + self._text = newValue or "" + # re-parse anchors when in rich mode + if not self._plain: + try: + self._anchors = self._parse_anchors(self._text) + except Exception: + self._anchors = [] + # autoscroll: move hover to last line + if self._auto_scroll: + lines = self._lines() + self._hover_line = max(0, len(lines) - 1) + self._ensure_hover_visible() + + def value(self) -> str: + return self._text + + def plainTextMode(self) -> bool: + return bool(self._plain) + + def setPlainTextMode(self, on: bool = True): + self._plain = bool(on) + + def autoScrollDown(self) -> bool: + return bool(self._auto_scroll) + + def setAutoScrollDown(self, on: bool = True): + self._auto_scroll = bool(on) + if self._auto_scroll: + lines = self._lines() + self._hover_line = max(0, len(lines) - 1) + self._ensure_hover_visible() + + def lastActivatedUrl(self): + return self._last_url + + def _strip_tags(self, s: str) -> str: + # Convert minimal HTML into text breaks and bullets, then strip remaining tags + try: + t = s + # breaks and paragraphs + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + # lists + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + t = re.sub(r"", "• ", t, flags=re.IGNORECASE) + t = re.sub(r"", "\n", t, flags=re.IGNORECASE) + # headings -> uppercase with newline + for n in range(1,7): + t = re.sub(fr"", "", t, flags=re.IGNORECASE) + t = re.sub(fr"", "\n", t, flags=re.IGNORECASE) + # anchors: keep inner text and preserve URL text if no inner + t = re.sub(r"]*href=\"([^\"]+)\"[^>]*>(.*?)", r"\2 (\1)", t, flags=re.IGNORECASE|re.DOTALL) + t = re.sub(r"]*href='([^']+)'[^>]*>(.*?)", r"\2 (\1)", t, flags=re.IGNORECASE|re.DOTALL) + return re.sub(r"<[^>]+>", "", t) + except Exception: + return s + + def _lines(self): + content = self._text + if not self._plain: + content = self._strip_tags(content) + lines = content.splitlines() or [""] + return lines + + def _parse_anchors(self, s: str): + """Parse anchors into line/column positions and targets. + Returns a list of anchors with screen-relative positions based on current text conversion. + """ + anchors = [] + try: + # Build a text version while tracking anchors + text = s + # Normalize breaks the same way as in _strip_tags + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"", "• ", text, flags=re.IGNORECASE) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + # Find anchors and replace with inner text while recording positions + pos = 0 + lines = [] + current_line = "" + for m in re.finditer(r"]*href=\"([^\"]+)\"[^>]*>(.*?)|]*href='([^']+)'[^>]*>(.*?)|||||||", text, flags=re.IGNORECASE|re.DOTALL): + start, end = m.span() + # Add text before match + before = re.sub(r"<[^>]+>", "", text[pos:start]) + for ch in before: + if ch == '\n': + lines.append(current_line) + current_line = "" + else: + current_line += ch + if m.group(1) is not None: + url = m.group(1) + inner = m.group(2) or url + sline = len(lines) + scol = len(current_line) + current_line += inner + eline = len(lines) + ecol = len(current_line) + anchors.append({"sline": sline, "scol": scol, "eline": eline, "ecol": ecol, "target": url}) + elif m.group(3) is not None: + url = m.group(3) + inner = m.group(4) or url + sline = len(lines) + scol = len(current_line) + current_line += inner + eline = len(lines) + ecol = len(current_line) + anchors.append({"sline": sline, "scol": scol, "eline": eline, "ecol": ecol, "target": url}) + else: + tagtxt = text[start:end] + # handle structural tags resulting in newlines/bullets + if re.match(r"", tagtxt, flags=re.IGNORECASE): + lines.append(current_line) + current_line = "" + elif re.match(r"", tagtxt, flags=re.IGNORECASE): + lines.append(current_line) + lines.append("") + current_line = "" + elif re.match(r"", tagtxt, flags=re.IGNORECASE): + pass + elif re.match(r"|", tagtxt, flags=re.IGNORECASE): + lines.append(current_line) + current_line = "" + elif re.match(r"|", tagtxt, flags=re.IGNORECASE): + lines.append(current_line) + current_line = "" + elif re.match(r"", tagtxt, flags=re.IGNORECASE): + current_line += "• " + elif re.match(r"", tagtxt, flags=re.IGNORECASE): + lines.append(current_line) + current_line = "" + pos = end + # trailing text + trailing = re.sub(r"<[^>]+>", "", text[pos:]) + for ch in trailing: + if ch == '\n': + lines.append(current_line) + current_line = "" + else: + current_line += ch + lines.append(current_line) + # store parsed lines for rendering + self._parsed_lines = lines + except Exception: + self._parsed_lines = None + return anchors + + def _visible_row_count(self): + return max(1, getattr(self, "_preferred_rows", 6)) + + def _ensure_hover_visible(self): + visible = self._visible_row_count() + if self._hover_line < self._scroll_offset: + self._scroll_offset = self._hover_line + elif self._hover_line >= self._scroll_offset + visible: + self._scroll_offset = self._hover_line - visible + 1 + + def _draw(self, window, y, x, width, height): + try: + # draw border + try: + window.attrset(curses.A_NORMAL) + window.border() + except curses.error: + pass + + inner_x = x + 1 + inner_y = y + 1 + inner_w = max(1, width - 2) + inner_h = max(1, height - 2) + + # reserve rightmost column for vertical scrollbar, bottom row for horizontal scrollbar + bar_w = 1 if inner_w > 2 else 0 + content_w = inner_w - bar_w + bar_h_row = 1 if inner_h > 2 else 0 + content_h = inner_h - bar_h_row + + # obtain lines (prefer parsed rich lines if available) + lines = self._parsed_lines if (not self._plain and getattr(self, '_parsed_lines', None)) else self._lines() + total_rows = len(lines) + visible = min(total_rows, max(1, content_h)) + + # draw content with horizontal scrolling + for i in range(visible): + idx = self._scroll_offset + i + if idx >= total_rows: + break + txt = lines[idx] + # horizontal slice + start_col = self._hscroll_offset + end_col = self._hscroll_offset + content_w + segment = txt[start_col:end_col] + attr_default = curses.A_NORMAL + if not self.isEnabled(): + attr_default |= curses.A_DIM + # highlight hover line background + if self._focused and idx == self._hover_line and self.isEnabled(): + attr_default |= curses.A_REVERSE + # draw with anchor highlighting (underline) + try: + # build segments around anchors + line_x = inner_x + consumed = 0 + # anchors on this line + alist = [a for a in self._anchors if a['sline'] == idx] + if not alist: + window.addstr(inner_y + i, inner_x, segment.ljust(content_w), attr_default) + else: + # draw piecewise + cursor = start_col + for a in alist: + a_start = a['scol'] + a_end = a['ecol'] + # draw text before anchor + if a_start > cursor: + pre = txt[cursor:min(a_start, end_col)] + if pre: + window.addstr(inner_y + i, line_x, pre, attr_default) + line_x += len(pre) + cursor += len(pre) + # draw anchor segment + if a_end > cursor and cursor < end_col: + anc_seg = txt[max(cursor, a_start):min(a_end, end_col)] + if anc_seg: + attr_anchor = attr_default | curses.A_UNDERLINE + # extra highlight if armed + if self._armed_index != -1 and self._anchors[self._armed_index] is a: + attr_anchor |= curses.A_BOLD + window.addstr(inner_y + i, line_x, anc_seg, attr_anchor) + line_x += len(anc_seg) + cursor += len(anc_seg) + # draw trailing text + if cursor < end_col: + tail = txt[cursor:end_col] + if tail: + window.addstr(inner_y + i, line_x, tail, attr_default) + # pad remainder + rem = content_w - (line_x - inner_x) + if rem > 0: + window.addstr(inner_y + i, line_x, " " * rem, attr_default) + except curses.error: + pass + + # vertical scrollbar on right + if bar_w == 1 and content_h > 0 and total_rows > visible: + try: + # draw track + for r in range(content_h): + window.addch(inner_y + r, inner_x + content_w, '|') + # draw slider position + pos = 0 + if total_rows > 0: + pos = int((self._scroll_offset / max(1, total_rows)) * content_h) + pos = max(0, min(content_h - 1, pos)) + window.addch(inner_y + pos, inner_x + content_w, '#') + except curses.error: + pass + + # horizontal scrollbar on bottom + if bar_h_row == 1: + try: + # basic track + for c in range(content_w): + window.addch(inner_y + content_h, inner_x + c, '-') + # slider position relative to max line length + maxlen = max((len(l) for l in lines), default=0) + if maxlen > content_w: + hpos = int((self._hscroll_offset / max(1, maxlen - content_w)) * content_w) + hpos = max(0, min(content_w - 1, hpos)) + window.addch(inner_y + content_h, inner_x + hpos, '=') + except curses.error: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + handled = True + lines = self._lines() + if key == curses.KEY_UP: + if self._hover_line > 0: + self._hover_line -= 1 + self._ensure_hover_visible() + elif key == curses.KEY_DOWN: + if self._hover_line < max(0, len(lines) - 1): + self._hover_line += 1 + self._ensure_hover_visible() + elif key == curses.KEY_LEFT: + # horizontal scroll or move to previous anchor + if self._armed_index != -1: + if self._armed_index > 0: + self._armed_index -= 1 + a = self._anchors[self._armed_index] + self._hover_line = a['sline'] + self._ensure_hover_visible() + else: + if self._hscroll_offset > 0: + self._hscroll_offset = max(0, self._hscroll_offset - max(1, (self._visible_row_count() // 2))) + elif key == curses.KEY_RIGHT: + if self._armed_index != -1: + if self._armed_index + 1 < len(self._anchors): + self._armed_index += 1 + a = self._anchors[self._armed_index] + self._hover_line = a['sline'] + self._ensure_hover_visible() + else: + maxlen = max((len(l) for l in lines), default=0) + if maxlen > self._hscroll_offset: + self._hscroll_offset = min(maxlen, self._hscroll_offset + max(1, (self._visible_row_count() // 2))) + elif key == curses.KEY_PPAGE: + step = self._visible_row_count() or 1 + self._hover_line = max(0, self._hover_line - step) + self._ensure_hover_visible() + elif key == curses.KEY_NPAGE: + step = self._visible_row_count() or 1 + self._hover_line = min(max(0, len(lines) - 1), self._hover_line + step) + self._ensure_hover_visible() + elif key == curses.KEY_HOME: + self._hover_line = 0 + self._ensure_hover_visible() + elif key == curses.KEY_END: + self._hover_line = max(0, len(lines) - 1) + self._ensure_hover_visible() + elif key in (ord('\n'),): + # Try to detect a URL in the current line and emit a menu event + try: + if self._armed_index != -1 and 0 <= self._armed_index < len(self._anchors): + url = self._anchors[self._armed_index]['target'] + self._last_url = url + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YMenuEvent(item=None, id=url)) + else: + line = lines[self._hover_line] if 0 <= self._hover_line < len(lines) else "" + m = re.search(r"https?://\S+", line) + if m: + url = m.group(0) + self._last_url = url + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YMenuEvent(item=None, id=url)) + except Exception: + pass + else: + handled = False + return handled + + def _set_backend_enabled(self, enabled): + try: + self._can_focus = bool(enabled) + if not enabled: + self._focused = False + except Exception: + pass + + # Focus handling hooks (optional integration with parent) + def setFocus(self, on: bool = True): + self._focused = bool(on) and self._can_focus + # when focusing, if rich mode and anchors exist, arm first visible anchor + if self._focused and not self._plain and self._anchors: + # pick the first anchor within current page + visible_top = self._scroll_offset + visible_bottom = self._scroll_offset + (self._visible_row_count() or 1) + for i, a in enumerate(self._anchors): + if visible_top <= a['sline'] < visible_bottom: + self._armed_index = i + self._hover_line = a['sline'] + break + else: + self._armed_index = 0 + self._hover_line = self._anchors[0]['sline'] diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index b754055..4cba825 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -216,4 +216,8 @@ def createRadioButton(self, parent, label="", isChecked=False): def createTable(self, parent, header: YTableHeader, multiSelection=False): """Create a Table widget (curses backend).""" - return YTableCurses(parent, header, multiSelection) \ No newline at end of file + return YTableCurses(parent, header, multiSelection) + + def createRichText(self, parent, text: str = "", plainTextMode: bool = False): + """Create a RichText widget (curses backend).""" + return YRichTextCurses(parent, text, plainTextMode) \ No newline at end of file From 29de01f5e4e6f90360cec56e458f9b40a4520fa1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 18:56:12 +0100 Subject: [PATCH 230/523] managed link selection --- .../aui/backends/curses/richtextcurses.py | 137 +++++++++++++++--- 1 file changed, 115 insertions(+), 22 deletions(-) diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index dedad78..9047e95 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -35,6 +35,9 @@ def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): self._hover_line = 0 self._anchors = [] # list of dicts: {sline, scol, eline, ecol, target} self._armed_index = -1 + self._heading_lines = set() + self._color_link = None + self._color_link_armed = None self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") @@ -49,6 +52,13 @@ def _create_backend_widget(self): try: self._backend_widget = self self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + # initial parse for rich mode + if not self._plain: + try: + self._anchors = self._parse_anchors(self._text) + self._anchors.sort(key=lambda a: (a['sline'], a['scol'])) + except Exception: + self._anchors = [] except Exception as e: try: self._logger.error("_create_backend_widget error: %s", e, exc_info=True) @@ -61,6 +71,7 @@ def setValue(self, newValue: str): if not self._plain: try: self._anchors = self._parse_anchors(self._text) + self._anchors.sort(key=lambda a: (a['sline'], a['scol'])) except Exception: self._anchors = [] # autoscroll: move hover to last line @@ -110,9 +121,9 @@ def _strip_tags(self, s: str) -> str: for n in range(1,7): t = re.sub(fr"", "", t, flags=re.IGNORECASE) t = re.sub(fr"", "\n", t, flags=re.IGNORECASE) - # anchors: keep inner text and preserve URL text if no inner - t = re.sub(r"]*href=\"([^\"]+)\"[^>]*>(.*?)", r"\2 (\1)", t, flags=re.IGNORECASE|re.DOTALL) - t = re.sub(r"]*href='([^']+)'[^>]*>(.*?)", r"\2 (\1)", t, flags=re.IGNORECASE|re.DOTALL) + # anchors: keep inner text only; URLs handled via anchor list + t = re.sub(r"]*href=\"([^\"]+)\"[^>]*>(.*?)", r"\2", t, flags=re.IGNORECASE|re.DOTALL) + t = re.sub(r"]*href='([^']+)'[^>]*>(.*?)", r"\2", t, flags=re.IGNORECASE|re.DOTALL) return re.sub(r"<[^>]+>", "", t) except Exception: return s @@ -146,7 +157,8 @@ def _parse_anchors(self, s: str): pos = 0 lines = [] current_line = "" - for m in re.finditer(r"]*href=\"([^\"]+)\"[^>]*>(.*?)|]*href='([^']+)'[^>]*>(.*?)|||||||", text, flags=re.IGNORECASE|re.DOTALL): + pattern = re.compile(r"]*href=\"([^\"]+)\"[^>]*>(.*?)|]*href='([^']+)'[^>]*>(.*?)||||||||(.*?)", re.IGNORECASE|re.DOTALL) + for m in pattern.finditer(text): start, end = m.span() # Add text before match before = re.sub(r"<[^>]+>", "", text[pos:start]) @@ -174,6 +186,17 @@ def _parse_anchors(self, s: str): eline = len(lines) ecol = len(current_line) anchors.append({"sline": sline, "scol": scol, "eline": eline, "ecol": ecol, "target": url}) + elif m.group(5) is not None: + # heading level and inner text + level = int(m.group(5)) + inner = m.group(6) or "" + sline = len(lines) + scol = len(current_line) + current_line += inner + eline = len(lines) + ecol = len(current_line) + # mark whole line as heading for simple bold style + self._heading_lines.add(sline) else: tagtxt = text[start:end] # handle structural tags resulting in newlines/bullets @@ -247,6 +270,9 @@ def _draw(self, window, y, x, width, height): lines = self._parsed_lines if (not self._plain and getattr(self, '_parsed_lines', None)) else self._lines() total_rows = len(lines) visible = min(total_rows, max(1, content_h)) + # remember for visibility calculations + self._last_content_w = content_w + self._last_width = width # draw content with horizontal scrolling for i in range(visible): @@ -261,8 +287,11 @@ def _draw(self, window, y, x, width, height): attr_default = curses.A_NORMAL if not self.isEnabled(): attr_default |= curses.A_DIM - # highlight hover line background - if self._focused and idx == self._hover_line and self.isEnabled(): + # apply heading style when applicable + if (not self._plain) and (idx in self._heading_lines): + attr_default |= curses.A_BOLD + # highlight hover line only in plain mode or without anchors + if (self._plain or not self._anchors) and self._focused and idx == self._hover_line and self.isEnabled(): attr_default |= curses.A_REVERSE # draw with anchor highlighting (underline) try: @@ -291,9 +320,16 @@ def _draw(self, window, y, x, width, height): anc_seg = txt[max(cursor, a_start):min(a_end, end_col)] if anc_seg: attr_anchor = attr_default | curses.A_UNDERLINE + # colorize links + self._ensure_color_pairs() + if self._color_link is not None: + attr_anchor |= curses.color_pair(self._color_link) # extra highlight if armed if self._armed_index != -1 and self._anchors[self._armed_index] is a: - attr_anchor |= curses.A_BOLD + if self._color_link_armed is not None: + attr_anchor = (attr_anchor & ~curses.A_UNDERLINE) | curses.color_pair(self._color_link_armed) | curses.A_UNDERLINE | curses.A_BOLD + else: + attr_anchor |= curses.A_BOLD window.addstr(inner_y + i, line_x, anc_seg, attr_anchor) line_x += len(anc_seg) cursor += len(anc_seg) @@ -317,8 +353,8 @@ def _draw(self, window, y, x, width, height): window.addch(inner_y + r, inner_x + content_w, '|') # draw slider position pos = 0 - if total_rows > 0: - pos = int((self._scroll_offset / max(1, total_rows)) * content_h) + if total_rows > visible: + pos = int((self._scroll_offset / max(1, total_rows - visible)) * (content_h - 1)) pos = max(0, min(content_h - 1, pos)) window.addch(inner_y + pos, inner_x + content_w, '#') except curses.error: @@ -333,7 +369,7 @@ def _draw(self, window, y, x, width, height): # slider position relative to max line length maxlen = max((len(l) for l in lines), default=0) if maxlen > content_w: - hpos = int((self._hscroll_offset / max(1, maxlen - content_w)) * content_w) + hpos = int((self._hscroll_offset / max(1, maxlen - content_w)) * (content_w - 1)) hpos = max(0, min(content_w - 1, hpos)) window.addch(inner_y + content_h, inner_x + hpos, '=') except curses.error: @@ -345,33 +381,47 @@ def _handle_key(self, key): if not self._focused or not self.isEnabled(): return False handled = True - lines = self._lines() + lines = self._parsed_lines if (not self._plain and getattr(self, '_parsed_lines', None)) else self._lines() if key == curses.KEY_UP: - if self._hover_line > 0: - self._hover_line -= 1 - self._ensure_hover_visible() + if self._anchors: + if self._armed_index > 0: + self._armed_index -= 1 + a = self._anchors[self._armed_index] + self._hover_line = a['sline'] + self._ensure_anchor_visible(a) + else: + if self._hover_line > 0: + self._hover_line -= 1 + self._ensure_hover_visible() elif key == curses.KEY_DOWN: - if self._hover_line < max(0, len(lines) - 1): - self._hover_line += 1 - self._ensure_hover_visible() + if self._anchors: + if self._armed_index + 1 < len(self._anchors): + self._armed_index += 1 + a = self._anchors[self._armed_index] + self._hover_line = a['sline'] + self._ensure_anchor_visible(a) + else: + if self._hover_line < max(0, len(lines) - 1): + self._hover_line += 1 + self._ensure_hover_visible() elif key == curses.KEY_LEFT: # horizontal scroll or move to previous anchor - if self._armed_index != -1: + if self._anchors: if self._armed_index > 0: self._armed_index -= 1 a = self._anchors[self._armed_index] self._hover_line = a['sline'] - self._ensure_hover_visible() + self._ensure_anchor_visible(a) else: if self._hscroll_offset > 0: self._hscroll_offset = max(0, self._hscroll_offset - max(1, (self._visible_row_count() // 2))) elif key == curses.KEY_RIGHT: - if self._armed_index != -1: + if self._anchors: if self._armed_index + 1 < len(self._anchors): self._armed_index += 1 a = self._anchors[self._armed_index] self._hover_line = a['sline'] - self._ensure_hover_visible() + self._ensure_anchor_visible(a) else: maxlen = max((len(l) for l in lines), default=0) if maxlen > self._hscroll_offset: @@ -393,7 +443,7 @@ def _handle_key(self, key): elif key in (ord('\n'),): # Try to detect a URL in the current line and emit a menu event try: - if self._armed_index != -1 and 0 <= self._armed_index < len(self._anchors): + if self._anchors and self._armed_index != -1 and 0 <= self._armed_index < len(self._anchors): url = self._anchors[self._armed_index]['target'] self._last_url = url if self.notify(): @@ -440,3 +490,46 @@ def setFocus(self, on: bool = True): else: self._armed_index = 0 self._hover_line = self._anchors[0]['sline'] + self._ensure_anchor_visible(self._anchors[0]) + + def _ensure_anchor_visible(self, a): + try: + # vertical + self._hover_line = a['sline'] + self._ensure_hover_visible() + # horizontal: use last known content width + visible_w = max(1, getattr(self, '_last_content_w', 0)) + if visible_w <= 0: + visible_w = 40 + start_col = self._hscroll_offset + end_col = start_col + visible_w + if a['scol'] < start_col: + self._hscroll_offset = max(0, a['scol']) + elif a['ecol'] > end_col: + self._hscroll_offset = max(0, a['ecol'] - visible_w) + except Exception: + pass + + def _ensure_color_pairs(self): + try: + if self._color_link is not None and self._color_link_armed is not None: + return + if curses.has_colors(): + try: + curses.start_color() + except Exception: + pass + pid_link = 10 + pid_link_armed = 11 + try: + curses.init_pair(pid_link, curses.COLOR_BLUE, -1) + self._color_link = pid_link + except Exception: + self._color_link = None + try: + curses.init_pair(pid_link_armed, curses.COLOR_CYAN, -1) + self._color_link_armed = pid_link_armed + except Exception: + self._color_link_armed = None + except Exception: + pass From 50b787fcee47d82da9d4503d07864cf79d248b1d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 21:11:09 +0100 Subject: [PATCH 231/523] simplify html parsing to show something similar to richtext --- .../aui/backends/curses/richtextcurses.py | 443 ++++++++++++------ 1 file changed, 302 insertions(+), 141 deletions(-) diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index 9047e95..a2905f0 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -9,6 +9,7 @@ ''' import curses import re +from html.parser import HTMLParser import logging from ...yui_common import * @@ -38,6 +39,9 @@ def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): self._heading_lines = set() self._color_link = None self._color_link_armed = None + self._parsed_lines = None + self._named_color_pairs = {} + self._next_color_pid = 20 self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") @@ -136,104 +140,163 @@ def _lines(self): return lines def _parse_anchors(self, s: str): - """Parse anchors into line/column positions and targets. - Returns a list of anchors with screen-relative positions based on current text conversion. - """ + # New parser-based implementation anchors = [] try: - # Build a text version while tracking anchors - text = s - # Normalize breaks the same way as in _strip_tags - text = re.sub(r"", "\n", text, flags=re.IGNORECASE) - text = re.sub(r"", "\n\n", text, flags=re.IGNORECASE) - text = re.sub(r"", "", text, flags=re.IGNORECASE) - text = re.sub(r"", "\n", text, flags=re.IGNORECASE) - text = re.sub(r"", "\n", text, flags=re.IGNORECASE) - text = re.sub(r"", "\n", text, flags=re.IGNORECASE) - text = re.sub(r"", "\n", text, flags=re.IGNORECASE) - text = re.sub(r"", "• ", text, flags=re.IGNORECASE) - text = re.sub(r"", "\n", text, flags=re.IGNORECASE) - # Find anchors and replace with inner text while recording positions - pos = 0 - lines = [] - current_line = "" - pattern = re.compile(r"]*href=\"([^\"]+)\"[^>]*>(.*?)|]*href='([^']+)'[^>]*>(.*?)||||||||(.*?)", re.IGNORECASE|re.DOTALL) - for m in pattern.finditer(text): - start, end = m.span() - # Add text before match - before = re.sub(r"<[^>]+>", "", text[pos:start]) - for ch in before: - if ch == '\n': - lines.append(current_line) - current_line = "" - else: - current_line += ch - if m.group(1) is not None: - url = m.group(1) - inner = m.group(2) or url - sline = len(lines) - scol = len(current_line) - current_line += inner - eline = len(lines) - ecol = len(current_line) - anchors.append({"sline": sline, "scol": scol, "eline": eline, "ecol": ecol, "target": url}) - elif m.group(3) is not None: - url = m.group(3) - inner = m.group(4) or url - sline = len(lines) - scol = len(current_line) - current_line += inner - eline = len(lines) - ecol = len(current_line) - anchors.append({"sline": sline, "scol": scol, "eline": eline, "ecol": ecol, "target": url}) - elif m.group(5) is not None: - # heading level and inner text - level = int(m.group(5)) - inner = m.group(6) or "" - sline = len(lines) - scol = len(current_line) - current_line += inner - eline = len(lines) - ecol = len(current_line) - # mark whole line as heading for simple bold style - self._heading_lines.add(sline) - else: - tagtxt = text[start:end] - # handle structural tags resulting in newlines/bullets - if re.match(r"", tagtxt, flags=re.IGNORECASE): - lines.append(current_line) - current_line = "" - elif re.match(r"", tagtxt, flags=re.IGNORECASE): - lines.append(current_line) - lines.append("") - current_line = "" - elif re.match(r"", tagtxt, flags=re.IGNORECASE): - pass - elif re.match(r"|", tagtxt, flags=re.IGNORECASE): - lines.append(current_line) - current_line = "" - elif re.match(r"|", tagtxt, flags=re.IGNORECASE): - lines.append(current_line) - current_line = "" - elif re.match(r"", tagtxt, flags=re.IGNORECASE): - current_line += "• " - elif re.match(r"", tagtxt, flags=re.IGNORECASE): - lines.append(current_line) - current_line = "" - pos = end - # trailing text - trailing = re.sub(r"<[^>]+>", "", text[pos:]) - for ch in trailing: - if ch == '\n': - lines.append(current_line) - current_line = "" - else: - current_line += ch - lines.append(current_line) - # store parsed lines for rendering + class Parser(HTMLParser): + def __init__(self): + super().__init__() + self.lines = [[]] + self.styles = [] + self.anchor = None + self.buf = "" + self.heading_lines = set() + + def _flush(self): + if not self.buf: + return + seg = { + 'text': self.buf, + 'bold': any(s == 'b' for s in self.styles), + 'italic': any(s == 'i' for s in self.styles), + 'underline': any(s == 'u' for s in self.styles) or (self.anchor is not None), + 'color': None, + 'anchor': self.anchor + } + for s in self.styles: + if isinstance(s, dict) and 'color' in s: + seg['color'] = s['color'] + self.lines[-1].append(seg) + self.buf = "" + + def handle_starttag(self, tag, attrs): + at = dict(attrs) + if tag == 'br': + self._flush() + self.lines.append([]) + elif tag == 'p': + self._flush() + self.lines.append([]) + elif tag in ('ul','ol'): + self._flush() + self.lines.append([]) + elif tag == 'li': + self._flush() + self.buf += '• ' + elif tag in ('h1','h2','h3','h4','h5','h6'): + # flush previous text, then mark heading as bold + self._flush() + self.styles.append('b') + elif tag == 'a': + href = at.get('href') or at.get('HREF') + self._flush() + self.anchor = href + elif tag == 'span': + # flush previous text, then push color/style + self._flush() + color = at.get('foreground') or None + if not color and 'style' in at: + m = re.search(r'color:\s*([^;]+)', at.get('style')) + if m: + color = m.group(1) + if color: + self.styles.append({'color': color}) + else: + self.styles.append('span') + elif tag == 'b': + self._flush() + self.styles.append('b') + elif tag in ('i','em'): + self._flush() + self.styles.append('i') + elif tag == 'u': + self._flush() + self.styles.append('u') + + def handle_endtag(self, tag): + if tag == 'br': + self._flush() + self.lines.append([]) + elif tag == 'p': + self._flush() + self.lines.append([]) + elif tag in ('ul','ol'): + self._flush() + self.lines.append([]) + elif tag == 'li': + self._flush() + self.lines.append([]) + elif tag in ('h1','h2','h3','h4','h5','h6'): + try: + self.styles.remove('b') + except ValueError: + pass + self._flush() + # mark current line as heading + try: + self.heading_lines.add(len(self.lines)-1) + except Exception: + pass + self.lines.append([]) + elif tag == 'a': + self._flush() + self.anchor = None + elif tag == 'span': + # pop last color dict if present + for i in range(len(self.styles)-1, -1, -1): + if isinstance(self.styles[i], dict) and 'color' in self.styles[i]: + del self.styles[i] + break + else: + if self.styles: + self.styles.pop() + self._flush() + elif tag == 'b': + try: + self.styles.remove('b') + except ValueError: + pass + self._flush() + elif tag in ('i','em'): + try: + self.styles.remove('i') + except ValueError: + pass + self._flush() + elif tag == 'u': + try: + self.styles.remove('u') + except ValueError: + pass + self._flush() + + def handle_data(self, data): + parts = data.split('\n') + for idx, part in enumerate(parts): + if idx > 0: + self._flush() + self.lines.append([]) + self.buf += part + + p = Parser() + p.feed(s) + p._flush() + lines = p.lines + # collect anchors and heading lines from parser + anchors = [] + self._heading_lines = set(p.heading_lines) + for ln_idx, segs in enumerate(lines): + col = 0 + for seg_idx, seg in enumerate(segs): + text = seg.get('text','') + length = len(text) + if seg.get('anchor'): + anchors.append({'sline': ln_idx, 'scol': col, 'eline': ln_idx, 'ecol': col + length, 'target': seg.get('anchor'), 'seg_idx': seg_idx}) + col += length self._parsed_lines = lines except Exception: self._parsed_lines = None + anchors = [] return anchors def _visible_row_count(self): @@ -275,73 +338,118 @@ def _draw(self, window, y, x, width, height): self._last_width = width # draw content with horizontal scrolling + segmented = bool(lines and isinstance(lines[0], list)) for i in range(visible): idx = self._scroll_offset + i if idx >= total_rows: break - txt = lines[idx] - # horizontal slice - start_col = self._hscroll_offset - end_col = self._hscroll_offset + content_w - segment = txt[start_col:end_col] attr_default = curses.A_NORMAL if not self.isEnabled(): attr_default |= curses.A_DIM # apply heading style when applicable - if (not self._plain) and (idx in self._heading_lines): + is_heading = (not self._plain) and (idx in getattr(self, '_heading_lines', set())) + if is_heading: attr_default |= curses.A_BOLD # highlight hover line only in plain mode or without anchors if (self._plain or not self._anchors) and self._focused and idx == self._hover_line and self.isEnabled(): attr_default |= curses.A_REVERSE - # draw with anchor highlighting (underline) + + start_col = self._hscroll_offset + end_col = self._hscroll_offset + content_w + try: - # build segments around anchors line_x = inner_x - consumed = 0 - # anchors on this line - alist = [a for a in self._anchors if a['sline'] == idx] - if not alist: - window.addstr(inner_y + i, inner_x, segment.ljust(content_w), attr_default) + if not segmented: + txt = lines[idx] + segment = txt[start_col:end_col] + if is_heading: + # bold + red for headings + a = attr_default + red_pid = self._get_named_color_pair('red') + if red_pid is not None: + try: + a |= curses.color_pair(red_pid) + except Exception: + pass + window.addstr(inner_y + i, inner_x, segment.ljust(content_w), a) + else: + window.addstr(inner_y + i, inner_x, segment.ljust(content_w), attr_default) else: - # draw piecewise - cursor = start_col - for a in alist: - a_start = a['scol'] - a_end = a['ecol'] - # draw text before anchor - if a_start > cursor: - pre = txt[cursor:min(a_start, end_col)] - if pre: - window.addstr(inner_y + i, line_x, pre, attr_default) - line_x += len(pre) - cursor += len(pre) - # draw anchor segment - if a_end > cursor and cursor < end_col: - anc_seg = txt[max(cursor, a_start):min(a_end, end_col)] - if anc_seg: - attr_anchor = attr_default | curses.A_UNDERLINE - # colorize links + segs = lines[idx] + # iterate segments and draw visible portion + char_cursor = 0 + for seg in segs: + text = seg.get('text', '') + if not text: + continue + seg_len = len(text) + seg_start = char_cursor + seg_end = char_cursor + seg_len + # no overlap with visible window? + if seg_end <= start_col: + char_cursor += seg_len + continue + if seg_start >= end_col: + break + # compute visible slice within this segment + vis_lo = max(start_col, seg_start) - seg_start + vis_hi = min(end_col, seg_end) - seg_start + piece = text[vis_lo:vis_hi] + if not piece: + char_cursor += seg_len + continue + # determine attributes for this piece + a = attr_default + if is_heading: + # headings: bold + red + red_pid = self._get_named_color_pair('red') + if red_pid is not None: + try: + a |= curses.color_pair(red_pid) + except Exception: + pass + else: + # unify bold/italic/underline and anchors to bold gray + if seg.get('bold') or seg.get('italic') or seg.get('underline') or seg.get('anchor'): + a |= curses.A_BOLD + gray_pid = self._get_named_color_pair('gray') + if gray_pid is not None: + try: + a |= curses.color_pair(gray_pid) + except Exception: + pass + else: + # optional explicit color spans for normal text self._ensure_color_pairs() - if self._color_link is not None: - attr_anchor |= curses.color_pair(self._color_link) - # extra highlight if armed - if self._armed_index != -1 and self._anchors[self._armed_index] is a: - if self._color_link_armed is not None: - attr_anchor = (attr_anchor & ~curses.A_UNDERLINE) | curses.color_pair(self._color_link_armed) | curses.A_UNDERLINE | curses.A_BOLD - else: - attr_anchor |= curses.A_BOLD - window.addstr(inner_y + i, line_x, anc_seg, attr_anchor) - line_x += len(anc_seg) - cursor += len(anc_seg) - # draw trailing text - if cursor < end_col: - tail = txt[cursor:end_col] - if tail: - window.addstr(inner_y + i, line_x, tail, attr_default) + color_id = None + if isinstance(seg.get('color'), int): + color_id = seg.get('color') + elif seg.get('color'): + color_id = self._get_named_color_pair(seg.get('color')) + if color_id is not None: + try: + a |= curses.color_pair(color_id) + except Exception: + pass + # armed anchor: add reverse for visibility + if seg.get('anchor') and self._armed_index != -1 and 0 <= self._armed_index < len(self._anchors): + a_active = self._anchors[self._armed_index] + if a_active and a_active['sline'] == idx and a_active['scol'] <= seg_start and a_active['ecol'] >= seg_end: + a |= curses.A_REVERSE + + try: + window.addstr(inner_y + i, line_x, piece, a) + except curses.error: + pass + line_x += len(piece) + char_cursor += seg_len # pad remainder rem = content_w - (line_x - inner_x) if rem > 0: - window.addstr(inner_y + i, line_x, " " * rem, attr_default) + try: + window.addstr(inner_y + i, line_x, " " * rem, attr_default) + except curses.error: + pass except curses.error: pass @@ -517,6 +625,12 @@ def _ensure_color_pairs(self): if curses.has_colors(): try: curses.start_color() + # prefer default background where supported + if hasattr(curses, 'use_default_colors'): + try: + curses.use_default_colors() + except Exception: + pass except Exception: pass pid_link = 10 @@ -533,3 +647,50 @@ def _ensure_color_pairs(self): self._color_link_armed = None except Exception: pass + + def _get_named_color_pair(self, name: str): + try: + if not name: + return None + if not curses.has_colors(): + return None + # normalize name + nm = str(name).strip().lower() + # map common color names + cmap = { + 'black': curses.COLOR_BLACK, + 'red': curses.COLOR_RED, + 'green': curses.COLOR_GREEN, + 'yellow': curses.COLOR_YELLOW, + 'blue': curses.COLOR_BLUE, + 'magenta': curses.COLOR_MAGENTA, + 'purple': curses.COLOR_MAGENTA, + 'cyan': curses.COLOR_CYAN, + 'white': curses.COLOR_WHITE, + 'gray': curses.COLOR_WHITE, + 'grey': curses.COLOR_WHITE, + } + fg = cmap.get(nm) + if fg is None: + return None + # reuse if exists + if nm in self._named_color_pairs: + return self._named_color_pairs[nm] + # init colors if needed + try: + curses.start_color() + if hasattr(curses, 'use_default_colors'): + curses.use_default_colors() + except Exception: + pass + pid = self._next_color_pid + # advance, avoid collisions with link pairs + self._next_color_pid = pid + 1 + try: + curses.init_pair(pid, fg, -1) + self._named_color_pairs[nm] = pid + return pid + except Exception: + return None + except Exception: + return None From 9cba810c65847b8132f9df2fc4077a1931228c65 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 21:43:12 +0100 Subject: [PATCH 232/523] updated --- sow/TODO.md | 55 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 959504e..1f83d1f 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -6,30 +6,63 @@ As this is a non-profit project, we rely on developers who are contributing in t Next is the starting todo list. -Missing Widgets comparing libyui: +Missing Widgets comparing libyui original factory: [X] YComboBox - [X] YSelectionBox - [X] YMultiSelectionBox + [X] YSelectionBox + [X] YPushButton + [X] YLabel + [X] YInputField + [X] YCheckBox [X] YTree [X] YFrame [X] YTable [X] YProgressBar - [ ] YRichText + [X] YRichText [ ] YMultiLineEdit [ ] YIntField - [ ] YMenuBar (YMenuButton?) - [ ] YSpacing - [X] YAlignment + [ ] YMenuBar + [ ] YMenuButton + [ ] YSpacing (detailed variants: createHStretch/createVStretch/createHSpacing/createVSpacing/createSpacing) + [X] YAlignment helpers (createLeft/createRight/createTop/createBottom/createHCenter/createVCenter/createHVCenter) [ ] YReplacePoint - [X] YRadioButton, + [X] YRadioButton [ ] YWizard - [-] ~~YPackageSelector~~ - [-] ~~YRadioButtonGroup~~ + [ ] YBusyIndicator + [ ] YImage + [ ] YLogView + [ ] YItemSelector + [ ] YEmpty + [ ] YSquash / createSquash -To check how to manage YEvents [X] and YItems [ ] (verify selection attirbute). +Skipped widgets: + [-] YMultiSelectionBox (implemented as YSelectionBox + multiselection enabled) + [-] YPackageSelector (not ported) + [-] YRadioButtonGroup (not ported) + +Optional/special widgets (from `YOptionalWidgetFactory`): + + [ ] YWizard + [ ] YDumbTab + [ ] YSlider + [ ] YDateField + [ ] YTimeField + [ ] YBarGraph + [ ] YPatternSelector (createPatternSelector) + [ ] YSimplePatchSelector (createSimplePatchSelector) + [ ] YMultiProgressMeter + [ ] YPartitionSplitter + [ ] YDownloadProgress + [ ] YDummySpecialWidget + [ ] YTimezoneSelector + [ ] YGraph + [ ] Context menu support / hasContextMenu + +To check how to manage YEvents [X] and YItems [X] (verify selection attirbute). Nice to have: improvements outside YUI API [ ] window title [ ] window icons [ ] selected YItem(s) in event + [ ] Improving YEvents management (adding info on widget event containing data + such as item selection/s, checked item, rich text url, etc.) \ No newline at end of file From ee58da3142bda941aa86ba4ff33b7192df71ada6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 21:43:53 +0100 Subject: [PATCH 233/523] improved test case --- test/test_richtext.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/test/test_richtext.py b/test/test_richtext.py index 0e2d1a9..0760c7a 100644 --- a/test/test_richtext.py +++ b/test/test_richtext.py @@ -69,18 +69,31 @@ def test_richtext_example(backend_name=None): "

Heading 4

" "
Heading 5
" "
Heading 6
" + "
" "

Welcome to RichText

" + "
" "

This is a paragraph with bold, italic, and underlined text.

" - "

Click the example.com link to emit an activation event.

" + "

Click the example.com or go home link to emit an activation event.

" + "

Colored text:

" + "" "

Lists:

" "
  • Alpha
  • Beta
  • Gamma
" ) - rich = factory.createRichText(vbox, html, False) + # Two RichText widgets side-by-side: left=rich HTML, right=plain text (10 lines) + h = factory.createHBox(vbox) + rich_left = factory.createRichText(h, html, False) + #rich_left.setAutoScrollDown(True) + + # Plain text with 10 lines on the right + plain_lines = "\n".join([f"Line {i+1}: This is a sample plain text line." for i in range(10)]) + rich_right = factory.createRichText(h, plain_lines, True) try: - # Enable auto scroll to demonstrate API - rich.setAutoScrollDown(True) + rich_right.setValue(plain_lines) except Exception: pass + #rich_right.setEnabled(False) # Status label for last activated link status_label = factory.createLabel(vbox, "Last link: (none)") From a095e2b8d4cf5787c5f95e7fcd6ab927cff790d6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 23:03:52 +0100 Subject: [PATCH 234/523] Added autowrap mode and output field --- manatools/aui/backends/curses/labelcurses.py | 78 +++++++++++++++++++- manatools/aui/backends/curses/vboxcurses.py | 8 ++ manatools/aui/backends/gtk/labelgtk.py | 43 +++++++++++ manatools/aui/backends/qt/labelqt.py | 44 ++++++++++- 4 files changed, 169 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index 9004b81..5de159a 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -15,6 +15,7 @@ import os import time import logging +import textwrap from ...yui_common import * # Module-level logger for label curses backend @@ -32,6 +33,7 @@ def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): self._text = text self._is_heading = isHeading self._is_output_field = isOutputField + self._auto_wrap = False self._height = 1 self._focused = False self._can_focus = False # Labels don't get focus @@ -48,6 +50,18 @@ def widgetClass(self): def text(self): return self._text + def isHeading(self) -> bool: + return bool(self._is_heading) + + def isOutputField(self) -> bool: + return bool(self._is_output_field) + + def label(self): + return self.text() + + def value(self): + return self.text() + def setText(self, new_text): self._text = new_text @@ -56,6 +70,43 @@ def setValue(self, newValue): def setLabel(self, newLabel): self.setText(newLabel) + + def autoWrap(self) -> bool: + return bool(self._auto_wrap) + + def setAutoWrap(self, on: bool = True): + self._auto_wrap = bool(on) + # no backend widget; wrapping handled in _draw + # update cached minimal height for simple cases (explicit newlines) + try: + if not self._auto_wrap: + self._height = max(1, len(self._text.splitlines()) if "\n" in self._text else 1) + except Exception: + pass + + def _desired_height_for_width(self, width: int) -> int: + """Return desired height in rows for given width, considering wrapping/newlines.""" + try: + if width <= 1: + return 1 + if self._auto_wrap: + total = 0 + paragraphs = self._text.splitlines() if self._text is not None else [""] + if not paragraphs: + paragraphs = [""] + for para in paragraphs: + if para == "": + total += 1 + else: + wrapped = textwrap.wrap(para, width=max(1, width-1), break_long_words=True, break_on_hyphens=False) or [""] + total += len(wrapped) + return max(1, total) + else: + if "\n" in (self._text or ""): + return max(1, len(self._text.splitlines())) + return 1 + except Exception: + return max(1, getattr(self, "_height", 1)) def _create_backend_widget(self): try: @@ -84,10 +135,31 @@ def _draw(self, window, y, x, width, height): # dim if disabled if not self.isEnabled(): attr |= curses.A_DIM + if width > 1: + # Build lines to draw. Respect explicit newlines and wrap paragraphs. + lines = [] + if self._auto_wrap: + # Wrap each paragraph independently (preserve blank lines) + for para in self._text.splitlines(): + if para == "": + lines.append("") + else: + wrapped_para = textwrap.wrap(para, width=max(1, width-1), break_long_words=True, break_on_hyphens=False) or [""] + lines.extend(wrapped_para) + else: + # No auto-wrap: respect explicit newlines but do not reflow paragraphs + if "\n" in self._text: + lines = self._text.splitlines() + else: + lines = [self._text] - # Truncate text to fit available width - display_text = self._text[:max(0, width-1)] - window.addstr(y, x, display_text, attr) + # Draw only up to available height + max_lines = max(1, height) + for i, line in enumerate(lines[:max_lines]): + try: + window.addstr(y + i, x, line[:max(0, width-1)], attr) + except curses.error: + pass except curses.error as e: try: self._logger.error("_draw curses.error: %s", e, exc_info=True) diff --git a/manatools/aui/backends/curses/vboxcurses.py b/manatools/aui/backends/curses/vboxcurses.py index e974604..3818364 100644 --- a/manatools/aui/backends/curses/vboxcurses.py +++ b/manatools/aui/backends/curses/vboxcurses.py @@ -94,6 +94,14 @@ def _draw(self, window, y, x, width, height): for i, child in enumerate(self._children): # Use recursive min height for containers and frames child_min = max(1, _curses_recursive_min_height(child)) + # If child can compute desired height for the current width, honor it + try: + if hasattr(child, "_desired_height_for_width"): + dh = int(child._desired_height_for_width(width)) + if dh > child_min: + child_min = dh + except Exception: + pass child_min_heights.append(child_min) is_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py index ad58064..fe716b7 100644 --- a/manatools/aui/backends/gtk/labelgtk.py +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -27,6 +27,7 @@ def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): self._text = text self._is_heading = isHeading self._is_output_field = isOutputField + self._auto_wrap = False self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): @@ -35,6 +36,18 @@ def widgetClass(self): def text(self): return self._text + def isHeading(self) -> bool: + return bool(self._is_heading) + + def isOutputField(self) -> bool: + return bool(self._is_output_field) + + def label(self): + return self.text() + + def value(self): + return self.text() + def setText(self, new_text): self._text = new_text if self._backend_widget: @@ -49,6 +62,23 @@ def setValue(self, newValue): def setLabel(self, newLabel): self.setText(newLabel) + def autoWrap(self) -> bool: + return bool(self._auto_wrap) + + def setAutoWrap(self, on: bool = True): + self._auto_wrap = bool(on) + try: + if getattr(self, "_backend_widget", None) is not None: + if hasattr(self._backend_widget, "set_wrap"): + self._backend_widget.set_wrap(self._auto_wrap) + if hasattr(self._backend_widget, "set_wrap_mode"): + try: + self._backend_widget.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + except Exception: + pass + except Exception: + pass + def _create_backend_widget(self): self._backend_widget = Gtk.Label(label=self._text) try: @@ -57,6 +87,19 @@ def _create_backend_widget(self): self._backend_widget.set_xalign(0.0) except Exception: pass + try: + if hasattr(self._backend_widget, "set_wrap"): + self._backend_widget.set_wrap(bool(self._auto_wrap)) + if hasattr(self._backend_widget, "set_wrap_mode"): + self._backend_widget.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + except Exception: + pass + # Output field: allow text selection like a read-only input + try: + if hasattr(self._backend_widget, "set_selectable"): + self._backend_widget.set_selectable(bool(self._is_output_field)) + except Exception: + pass if self._is_heading: try: diff --git a/manatools/aui/backends/qt/labelqt.py b/manatools/aui/backends/qt/labelqt.py index d71dfa8..4c8fa99 100644 --- a/manatools/aui/backends/qt/labelqt.py +++ b/manatools/aui/backends/qt/labelqt.py @@ -9,7 +9,7 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtCore import logging from ...yui_common import * @@ -19,6 +19,7 @@ def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): self._text = text self._is_heading = isHeading self._is_output_field = isOutputField + self._auto_wrap = False self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): @@ -27,6 +28,18 @@ def widgetClass(self): def text(self): return self._text + def isHeading(self) -> bool: + return bool(self._is_heading) + + def isOutputField(self) -> bool: + return bool(self._is_output_field) + + def label(self): + return self.text() + + def value(self): + return self.text() + def setText(self, new_text): self._text = new_text if self._backend_widget: @@ -38,8 +51,37 @@ def setValue(self, newValue): def setLabel(self, newLabel): self.setText(newLabel) + def autoWrap(self) -> bool: + return bool(self._auto_wrap) + + def setAutoWrap(self, on: bool = True): + self._auto_wrap = bool(on) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setWordWrap(self._auto_wrap) + except Exception: + pass + def _create_backend_widget(self): self._backend_widget = QtWidgets.QLabel(self._text) + try: + self._backend_widget.setWordWrap(bool(self._auto_wrap)) + except Exception: + pass + # Output field: allow text selection like a read-only input + try: + if self._is_output_field: + flags = ( + QtCore.Qt.TextSelectableByMouse | + QtCore.Qt.TextSelectableByKeyboard + ) + self._backend_widget.setTextInteractionFlags(flags) + # Focus policy to allow keyboard selection + self._backend_widget.setFocusPolicy(QtCore.Qt.StrongFocus) + else: + self._backend_widget.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + except Exception: + pass if self._is_heading: font = self._backend_widget.font() font.setBold(True) From 0dade3c31038d73099691caabcff635d22380b5e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 23:05:26 +0100 Subject: [PATCH 235/523] Added an example of wrapping --- test/test_label_wrap.py | 103 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 test/test_label_wrap.py diff --git a/test/test_label_wrap.py b/test/test_label_wrap.py new file mode 100644 index 0000000..5151396 --- /dev/null +++ b/test/test_label_wrap.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Simple interactive test for YLabel auto-wrap and output field. + +Shows a dialog with three labels: +- Normal label +- Wrapped label (autoWrap=True) with 12-line long content +- Output-field label (isOutputField=True) + +Run this test manually to visually verify wrapping behavior in the available backend. +""" +import os +import sys +import logging + +# allow running from repo root +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') + root_logger = logging.getLogger() + fileHandler = logging.FileHandler(log_name, mode='w') + fileHandler.setFormatter(logFormatter) + root_logger.addHandler(fileHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + root_logger.addHandler(consoleHandler) + consoleHandler.setLevel(logging.INFO) + root_logger.setLevel(logging.DEBUG) +except Exception as _e: + logging.getLogger().exception("Failed to configure file logger: %s", _e) + +from manatools.aui.yui import YUI, YUI_ui +import manatools.aui.yui_common as yui + + +def make_long_text(lines=12): + parts = [f"Line {i+1}: The quick brown fox jumps over the lazy dog." for i in range(lines)] + return "\n".join(parts) + + +def test_label_wrap_example(backend_name=None): + if backend_name: + os.environ['YUI_BACKEND'] = backend_name + + # Ensure fresh YUI detection + YUI._instance = None + YUI._backend = None + + ui = YUI_ui() + factory = ui.widgetFactory() + + try: + backend = YUI.backend() + root_logger.debug("test_label_wrap_example: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) + except Exception: + root_logger.debug("test_label_wrap_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) + + dlg = factory.createMainDialog() + vbox = factory.createVBox(dlg) + + # Normal label + lab1 = factory.createLabel(vbox, "First label: normal text.") + + # Wrapped label with 12 lines + long_text = make_long_text(12) + lab2 = factory.createLabel(vbox, long_text) + try: + lab2.setAutoWrap(True) + except Exception: + pass + + # Output-field label + lab3 = factory.createLabel(vbox, "Output-field label: non-editable.", isOutputField=True) + + # ok/quit + ctrl_h = factory.createHBox(vbox) + ok_btn = factory.createPushButton(ctrl_h, "OK") + + root_logger.info("Opening Label wrap example dialog...") + + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dlg.destroy() + break + elif et == yui.YEventType.WidgetEvent: + w = ev.widget() + reason = ev.reason() + if w == ok_btn and reason == yui.YEventReason.Activated: + dlg.destroy() + break + + root_logger.info("Dialog closed") + + +if __name__ == '__main__': + if len(sys.argv) > 1: + test_label_wrap_example(sys.argv[1]) + else: + test_label_wrap_example() From 71eecb7443255264104e500a107e2331d9a51a07 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 27 Dec 2025 23:41:32 +0100 Subject: [PATCH 236/523] added YMenuItem --- manatools/aui/yui_common.py | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index c83ce8c..8b02267 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -399,6 +399,71 @@ def data(self): def setData(self, new_data): self._data = new_data +class YMenuItem: + """Lightweight menu item model for backend-agnostic use. + + Supports label, icon name, enabled state and hierarchical children + (submenus and items). Backends may attach platform-specific references + via `_backend_ref` for synchronization. + """ + def __init__(self, label: str, icon_name: str = "", enabled: bool = True, is_menu: bool = False): + self._label = str(label) + self._icon_name = str(icon_name) if icon_name else "" + self._enabled = bool(enabled) + self._is_menu = bool(is_menu) + self._children = [] # list of YMenuItem + self._parent = None + self._backend_ref = None # optional backend-specific handle + + def label(self) -> str: + return self._label + + def setLabel(self, new_label: str): + self._label = str(new_label) + + def iconName(self) -> str: + return self._icon_name + + def setIconName(self, new_icon_name: str): + self._icon_name = str(new_icon_name) if new_icon_name else "" + + def enabled(self) -> bool: + return bool(self._enabled) + + def setEnabled(self, on: bool = True): + self._enabled = bool(on) + + def isMenu(self) -> bool: + return bool(self._is_menu) + + def childrenBegin(self): + return iter(self._children) + + def childrenEnd(self): + return iter([]) + + def hasChildren(self) -> bool: + return len(self._children) > 0 + + def addItem(self, label: str, icon_name: str = ""): + child = YMenuItem(label, icon_name, enabled=True, is_menu=False) + child._parent = self + self._children.append(child) + return child + + def addMenu(self, label: str, icon_name: str = ""): + child = YMenuItem(label, icon_name, enabled=True, is_menu=True) + child._parent = self + self._children.append(child) + return child + + def addSeparator(self): + # Represent separator as a disabled item with label "-"; backends can special-case it. + sep = YMenuItem("-", "", enabled=False, is_menu=False) + sep._parent = self + self._children.append(sep) + return sep + class YTreeItem(YItem): def __init__(self, label: str, parent: Optional["YTreeItem"] = None, selected: Optional[bool] = False, is_open: bool = False, icon_name: str = ""): ''' YTreeItem represents an item in a tree structure. From 78321e5c373ac7a6786e4f61ac3bd997c18b3550 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 00:04:26 +0100 Subject: [PATCH 237/523] First Qt Menubar implementation --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/dialogqt.py | 2 + manatools/aui/backends/qt/menubarqt.py | 120 +++++++++++++++++++++++++ manatools/aui/yui_qt.py | 4 + 4 files changed, 128 insertions(+) create mode 100644 manatools/aui/backends/qt/menubarqt.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 1feb8ef..5b61fb3 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -15,6 +15,7 @@ from .radiobuttonqt import YRadioButtonQt from .tableqt import YTableQt from .richtextqt import YRichTextQt +from .menubarqt import YMenuBarQt __all__ = [ @@ -35,5 +36,6 @@ "YRadioButtonQt", "YTableQt", "YRichTextQt", + "YMenuBarQt", # ... ] diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index f62ea63..ba27eb1 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -152,6 +152,8 @@ def _create_backend_widget(self): if self.child(): layout = QtWidgets.QVBoxLayout(central_widget) layout.addWidget(self.child().get_backend_widget()) + # If the child is a layout box with a menubar as first child, Qt can display QMenuBar inline. + # Alternatively, backends may add YMenuBarQt directly to layout. self._backend_widget = self._qwidget self._qwidget.closeEvent = self._on_close_event diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py new file mode 100644 index 0000000..96fdc6a --- /dev/null +++ b/manatools/aui/backends/qt/menubarqt.py @@ -0,0 +1,120 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Qt backend: YMenuBar implementation using QMenuBar. +''' +from PySide6 import QtWidgets, QtCore, QtGui +import logging +from ...yui_common import YWidget, YMenuEvent, YMenuItem + + +class YMenuBarQt(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._menus = [] # list of YMenuItem (is_menu=True) + self._menu_to_qmenu = {} + self._item_to_qaction = {} + + def widgetClass(self): + return "YMenuBar" + + def addMenu(self, label: str, icon_name: str = "") -> YMenuItem: + m = YMenuItem(label, icon_name, enabled=True, is_menu=True) + self._menus.append(m) + if self._backend_widget: + self._ensure_menu_rendered(m) + return m + + def addItem(self, menu: YMenuItem, label: str, icon_name: str = "", enabled: bool = True) -> YMenuItem: + item = menu.addItem(label, icon_name) + item.setEnabled(enabled) + if self._backend_widget: + self._ensure_item_rendered(menu, item) + return item + + def setItemEnabled(self, item: YMenuItem, on: bool = True): + item.setEnabled(on) + act = self._item_to_qaction.get(item) + if act is not None: + try: + act.setEnabled(bool(on)) + except Exception: + pass + + def _path_for_item(self, item: YMenuItem) -> str: + labels = [] + cur = item + while cur is not None: + labels.append(cur.label()) + cur = getattr(cur, "_parent", None) + return "/".join(reversed(labels)) + + def _emit_activation(self, item: YMenuItem): + try: + dlg = self.findDialog() + if dlg and self.notify(): + dlg._post_event(YMenuEvent(item=item, id=self._path_for_item(item))) + except Exception: + pass + + def _ensure_menu_rendered(self, menu: YMenuItem): + qmenu = self._menu_to_qmenu.get(menu) + if qmenu is None: + qmenu = QtWidgets.QMenu(menu.label(), self._backend_widget) + # icon via theme + if menu.iconName(): + icon = QtGui.QIcon.fromTheme(menu.iconName()) + if not icon.isNull(): + qmenu.setIcon(icon) + self._backend_widget.addMenu(qmenu) + self._menu_to_qmenu[menu] = qmenu + # render children if any + for child in list(menu._children): + if child.isMenu(): + sub = qmenu.addMenu(child.label()) + if child.iconName(): + icon = QtGui.QIcon.fromTheme(child.iconName()) + if not icon.isNull(): + sub.setIcon(icon) + self._menu_to_qmenu[child] = sub + else: + self._ensure_item_rendered(menu, child) + + def _ensure_item_rendered(self, menu: YMenuItem, item: YMenuItem): + qmenu = self._menu_to_qmenu.get(menu) + if qmenu is None: + self._ensure_menu_rendered(menu) + qmenu = self._menu_to_qmenu.get(menu) + if item.label() == "-": + qmenu.addSeparator() + return + act = self._item_to_qaction.get(item) + if act is None: + #TODO use _resolve_icon from commonqt.py + icon = QtGui.QIcon.fromTheme(item.iconName()) if item.iconName() else QtGui.QIcon() + act = QtGui.QAction(icon, item.label(), self._backend_widget) + act.setEnabled(item.enabled()) + def on_triggered(): + self._emit_activation(item) + act.triggered.connect(on_triggered) + qmenu.addAction(act) + self._item_to_qaction[item] = act + + def _create_backend_widget(self): + mb = QtWidgets.QMenuBar() + self._backend_widget = mb + # render any menus added before creation + for m in self._menus: + self._ensure_menu_rendered(m) + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass \ No newline at end of file diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 1a56dc8..ad416f7 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -230,3 +230,7 @@ def createTable(self, parent, header: YTableHeader, multiSelection: bool = False def createRichText(self, parent, text: str = "", plainTextMode: bool = False): """Create a RichText widget (Qt backend).""" return YRichTextQt(parent, text, plainTextMode) + + def createMenuBar(self, parent): + """Create a MenuBar widget (Qt backend).""" + return YMenuBarQt(parent) From 434cbd984054880a6b705ff68efa67536c3eea7d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 00:08:45 +0100 Subject: [PATCH 238/523] Icon from commonqt._resolve_icon. Visible also sub-sub menu --- manatools/aui/backends/qt/menubarqt.py | 64 ++++++++++++++++++++------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index 96fdc6a..b24e613 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -6,6 +6,7 @@ from PySide6 import QtWidgets, QtCore, QtGui import logging from ...yui_common import YWidget, YMenuEvent, YMenuItem +from .commonqt import _resolve_icon class YMenuBarQt(YWidget): @@ -64,22 +65,40 @@ def _ensure_menu_rendered(self, menu: YMenuItem): qmenu = QtWidgets.QMenu(menu.label(), self._backend_widget) # icon via theme if menu.iconName(): - icon = QtGui.QIcon.fromTheme(menu.iconName()) - if not icon.isNull(): + try: + icon = _resolve_icon(menu.iconName()) + except Exception: + icon = QtGui.QIcon.fromTheme(menu.iconName()) + if icon and not icon.isNull(): qmenu.setIcon(icon) self._backend_widget.addMenu(qmenu) self._menu_to_qmenu[menu] = qmenu - # render children if any - for child in list(menu._children): - if child.isMenu(): - sub = qmenu.addMenu(child.label()) + # render children recursively + self._render_menu_children(menu) + + def _render_menu_children(self, menu: YMenuItem): + """Render all children (items and submenus) of a menu recursively.""" + parent_qmenu = self._menu_to_qmenu.get(menu) + if parent_qmenu is None: + return + for child in list(menu._children): + if child.isMenu(): + sub_qmenu = self._menu_to_qmenu.get(child) + if sub_qmenu is None: + sub_qmenu = parent_qmenu.addMenu(child.label()) + # icon for submenu if child.iconName(): - icon = QtGui.QIcon.fromTheme(child.iconName()) - if not icon.isNull(): - sub.setIcon(icon) - self._menu_to_qmenu[child] = sub - else: - self._ensure_item_rendered(menu, child) + try: + icon = _resolve_icon(child.iconName()) + except Exception: + icon = QtGui.QIcon.fromTheme(child.iconName()) + if icon and not icon.isNull(): + sub_qmenu.setIcon(icon) + self._menu_to_qmenu[child] = sub_qmenu + # recurse into submenu + self._render_menu_children(child) + else: + self._ensure_item_rendered(menu, child) def _ensure_item_rendered(self, menu: YMenuItem, item: YMenuItem): qmenu = self._menu_to_qmenu.get(menu) @@ -91,8 +110,13 @@ def _ensure_item_rendered(self, menu: YMenuItem, item: YMenuItem): return act = self._item_to_qaction.get(item) if act is None: - #TODO use _resolve_icon from commonqt.py - icon = QtGui.QIcon.fromTheme(item.iconName()) if item.iconName() else QtGui.QIcon() + # Resolve icon using common helper (theme or path) + icon = QtGui.QIcon() + if item.iconName(): + try: + icon = _resolve_icon(item.iconName()) or QtGui.QIcon() + except Exception: + icon = QtGui.QIcon.fromTheme(item.iconName()) act = QtGui.QAction(icon, item.label(), self._backend_widget) act.setEnabled(item.enabled()) def on_triggered(): @@ -104,6 +128,18 @@ def on_triggered(): def _create_backend_widget(self): mb = QtWidgets.QMenuBar() self._backend_widget = mb + try: + # Prevent vertical stretching: fix height to size hint and set fixed vertical size policy + h = mb.sizeHint().height() + if h and h > 0: + mb.setMinimumHeight(h) + mb.setMaximumHeight(h) + sp = mb.sizePolicy() + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Fixed) + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + mb.setSizePolicy(sp) + except Exception: + pass # render any menus added before creation for m in self._menus: self._ensure_menu_rendered(m) From bdc0e5f7f8772cc76223f2c6ef5100c933451769 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 00:12:47 +0100 Subject: [PATCH 239/523] added more test cases --- test/test_menubar.py | 132 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/test_menubar.py diff --git a/test/test_menubar.py b/test/test_menubar.py new file mode 100644 index 0000000..d749c0e --- /dev/null +++ b/test/test_menubar.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Interactive test for YMenuBar across backends. + +- Creates a dialog with a menu bar and a status label. +- File menu: Open (enabled), Close (disabled), Exit. +- Selecting Open disables Open and enables Close; selecting Close reverses. +- Exit closes the dialog. +- Edit menu: Copy, Paste, Cut. +- More menu: submenus to verify nested menus. +- OK button exits. +""" +import os +import sys +import logging + +# allow running from repo root +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') + root_logger = logging.getLogger() + fileHandler = logging.FileHandler(log_name, mode='w') + fileHandler.setFormatter(logFormatter) + root_logger.addHandler(fileHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + root_logger.addHandler(consoleHandler) + consoleHandler.setLevel(logging.INFO) + root_logger.setLevel(logging.DEBUG) +except Exception as _e: + logging.getLogger().exception("Failed to configure file logger: %s", _e) + +from manatools.aui.yui import YUI, YUI_ui +import manatools.aui.yui_common as yui + + +def test_menubar_example(backend_name=None): + if backend_name: + os.environ['YUI_BACKEND'] = backend_name + + # Ensure fresh YUI detection + YUI._instance = None + YUI._backend = None + + ui = YUI_ui() + factory = ui.widgetFactory() + + # Log program name and detected backend + try: + backend = YUI.backend() + root_logger.debug("test_menubar_example: program=%s backend=%s", os.path.basename(sys.argv[0]), getattr(backend, 'value', str(backend))) + except Exception: + root_logger.debug("test_menubar_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) + + dlg = factory.createMainDialog() + vbox = factory.createVBox(dlg) + + # Menu bar + menubar = factory.createMenuBar(vbox) + + # File menu + file_menu = menubar.addMenu("File", icon_name="application-menu") + item_open = menubar.addItem(file_menu, "Open", icon_name="document-open", enabled=True) + item_close = menubar.addItem(file_menu, "Close", icon_name="window-close", enabled=False) + #menubar.addItem(file_menu, "-") + item_exit = menubar.addItem(file_menu, "Exit", icon_name="application-exit", enabled=True) + + # Edit menu + edit_menu = menubar.addMenu("Edit") + menubar.addItem(edit_menu, "Copy", icon_name="edit-copy") + menubar.addItem(edit_menu, "Paste", icon_name="edit-paste") + menubar.addItem(edit_menu, "Cut", icon_name="edit-cut") + + # More menu with submenus + more_menu = menubar.addMenu("More") + sub1 = more_menu.addMenu("Submenu 1") + sub1.addItem("One") + sub1.addItem("Two") + sub2 = more_menu.addMenu("Submenu 2") + sub2.addItem("Alpha") + sub2.addItem("Beta") + sub3 = sub2.addMenu("Submenu 2.1") + sub3.addItem("Info") + + # Status label + status_label = factory.createLabel(vbox, "Selected: (none)") + + # OK button + ctrl_h = factory.createHBox(vbox) + ok_btn = factory.createPushButton(ctrl_h, "OK") + + root_logger.info("Opening MenuBar example dialog...") + + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dlg.destroy() + break + elif et == yui.YEventType.WidgetEvent: + w = ev.widget() + reason = ev.reason() + if w == ok_btn and reason == yui.YEventReason.Activated: + dlg.destroy() + break + elif et == yui.YEventType.MenuEvent: + path = ev.id() if ev.id() else '(none)' + status_label.setValue(f"Selected: {path}") + item = ev.item() + if item is not None: + if item == item_open: + root_logger.debug("Menu item selected: File/Open") + menubar.setItemEnabled(item_open, False) + menubar.setItemEnabled(item_close, True) + elif item == item_close: + root_logger.debug("Menu item selected: File/Close") + menubar.setItemEnabled(item_open, True) + menubar.setItemEnabled(item_close, False) + elif item == item_exit: + root_logger.info("Menu item selected: File/Exit") + dlg.destroy() + break + + root_logger.info("Dialog closed") + + +if __name__ == '__main__': + if len(sys.argv) > 1: + test_menubar_example(sys.argv[1]) + else: + test_menubar_example() From bca523895bcb5f9eedad0e008732f2aa7822c74e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 14:30:16 +0100 Subject: [PATCH 240/523] First attempt to have gtk menubar --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/menubargtk.py | 184 +++++++++++++++++++++++ manatools/aui/yui_gtk.py | 5 +- 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 manatools/aui/backends/gtk/menubargtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index f835861..8856f16 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -15,6 +15,7 @@ from .radiobuttongtk import YRadioButtonGtk from .tablegtk import YTableGtk from .richtextgtk import YRichTextGtk +from .menubargtk import YMenuBarGtk __all__ = [ "YDialogGtk", @@ -34,5 +35,6 @@ "YRadioButtonGtk", "YTableGtk", "YRichTextGtk", + "YMenuBarGtk", # ... ] diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py new file mode 100644 index 0000000..205fde3 --- /dev/null +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -0,0 +1,184 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +GTK backend: YMenuBar implementation using a horizontal box of MenuButtons with Popovers. +GTK4 lacks traditional Gtk.MenuBar; this simulates a menubar. +''' +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gio', '2.0') +from gi.repository import Gtk, Gio, GLib +import logging +from ...yui_common import YWidget, YMenuEvent, YMenuItem +from .commongtk import _resolve_gicon, _resolve_icon + + +class YMenuBarGtk(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._menus = [] # top-level YMenuItem menus + self._menu_to_model = {} + self._item_to_action = {} + self._action_group = Gio.SimpleActionGroup() + + def widgetClass(self): + return "YMenuBar" + + def addMenu(self, label: str, icon_name: str = "") -> YMenuItem: + m = YMenuItem(label, icon_name, enabled=True, is_menu=True) + self._menus.append(m) + if self._backend_widget: + self._ensure_menu_rendered(m) + self._rebuild_root_model() + return m + + def addItem(self, menu: YMenuItem, label: str, icon_name: str = "", enabled: bool = True) -> YMenuItem: + item = menu.addItem(label, icon_name) + item.setEnabled(enabled) + if self._backend_widget: + # update model for this menu and rebuild root + self._ensure_menu_rendered(menu) + self._rebuild_root_model() + return item + + def setItemEnabled(self, item: YMenuItem, on: bool = True): + item.setEnabled(on) + act = self._item_to_action.get(item) + if act is not None: + try: + act.set_enabled(bool(on)) + except Exception: + pass + + def _path_for_item(self, item: YMenuItem) -> str: + labels = [] + cur = item + while cur is not None: + labels.append(cur.label()) + cur = getattr(cur, "_parent", None) + return "/".join(reversed(labels)) + + def _emit_activation(self, item: YMenuItem): + try: + dlg = self.findDialog() + if dlg and self.notify(): + dlg._post_event(YMenuEvent(item=item, id=self._path_for_item(item))) + except Exception: + pass + + def _ensure_menu_rendered(self, menu: YMenuItem): + # Build Gio.Menu model for this menu + model = Gio.Menu() + self._menu_to_model[menu] = model + self._populate_menu_model(model, menu) + + def _action_name_for_item(self, item: YMenuItem) -> str: + # derive action id from path, sanitized + path = self._path_for_item(item) + return path.replace('/', '_').replace(' ', '_').lower() + + def _populate_menu_model(self, model: Gio.Menu, menu: YMenuItem): + for child in list(menu._children): + if child.isMenu(): + sub_model = Gio.Menu() + self._populate_menu_model(sub_model, child) + model.append_submenu(child.label(), sub_model) + else: + if child.label() == "-": + # separator + model.append_section(None, Gio.Menu()) + continue + item = Gio.MenuItem.new(child.label(), None) + image = _resolve_icon(child.iconName()) if child.iconName() else None + if image is None and child.iconName(): + self._logger.error("Failed to resolve icon for menu item <%s> <%s>", child.label(), child.iconName()) + gicon = image.get_gicon() if image else None + if gicon is not None: + try: + item.set_icon(gicon) + except Exception: + self._logger.error("Failed to set icon for menu item <%s>", child.label(), exc_info=True) + pass + elif child.iconName(): + self._logger.error("No icon for menu item <%s> <%s>", child.label(), child.iconName()) + act_name = self._action_name_for_item(child) + action = Gio.SimpleAction.new(act_name, None) + def on_activate(_action, _param=None, _child=child): + if not _child.enabled(): + return + self._emit_activation(_child) + action.connect("activate", on_activate) + try: + self._action_group.add_action(action) + except Exception: + pass + self._item_to_action[child] = action + try: + item.set_attribute_value("action", GLib.Variant.new_string(f"menubar.{act_name}")) + except Exception: + pass + model.append_item(item) + + # no per-item rendering in PopoverMenuBar approach + + def _create_backend_widget(self): + # Combine all top-level menus into a single model + root = Gio.Menu() + for m in self._menus: + self._ensure_menu_rendered(m) + model = self._menu_to_model.get(m) + if model is not None: + root.append_submenu(m.label(), model) + mb = Gtk.PopoverMenuBar.new_from_model(root) + try: + mb.insert_action_group("menubar", self._action_group) + except Exception: + pass + # Limit vertical expansion + #try: + # mb.set_vexpand(False) + # mb.set_hexpand(True) + #except Exception: + # pass + self._backend_widget = mb + + def _rebuild_root_model(self): + try: + mb = self._backend_widget + if mb is None: + return + # Reset actions to keep state in sync + self._action_group = Gio.SimpleActionGroup() + self._item_to_action.clear() + root = Gio.Menu() + for m in self._menus: + # Ensure individual menu model exists and is populated + self._ensure_menu_rendered(m) + model = self._menu_to_model.get(m) + if model is not None: + # refresh model contents + refreshed = Gio.Menu() + self._populate_menu_model(refreshed, m) + self._menu_to_model[m] = refreshed + root.append_submenu(m.label(), refreshed) + try: + mb.set_menu_model(root) + except Exception: + # Fallback: recreate component if setter not available + pass + try: + mb.insert_action_group("menubar", self._action_group) + except Exception: + pass + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_sensitive(bool(enabled)) + except Exception: + pass + + # No ListBox activation in PopoverMenuBar implementation diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 3b4ea75..fe9c1bf 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -217,8 +217,9 @@ def createPasswordField(self, parent, label): def createComboBox(self, parent, label, editable=False): return YComboBoxGtk(parent, label, editable) - def createSelectionBox(self, parent, label): - return YSelectionBoxGtk(parent, label) + def createMenuBar(self, parent): + """Create a MenuBar widget (GTK backend).""" + return YMenuBarGtk(parent) # Alignment helpers def createLeft(self, parent): From dea5ce2a61f0375c951b8ef5351f5ec8a1f2230d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 14:42:35 +0100 Subject: [PATCH 241/523] updated --- sow/TODO.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 1f83d1f..c701bc5 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -58,11 +58,14 @@ Optional/special widgets (from `YOptionalWidgetFactory`): [ ] YGraph [ ] Context menu support / hasContextMenu -To check how to manage YEvents [X] and YItems [X] (verify selection attirbute). +To check/review: + how to manage YEvents [X] and YItems [X] (verify selection attirbute). + [ ] YInputField password mode + [ ] adding factory create alternative methods (e.g. createMultiSelectionBox) Nice to have: improvements outside YUI API [ ] window title [ ] window icons [ ] selected YItem(s) in event [ ] Improving YEvents management (adding info on widget event containing data - such as item selection/s, checked item, rich text url, etc.) \ No newline at end of file + such as item selection/s, checked item, rich text url, etc.) From c415c9eda91b627b923ddd5419ed581fe110744a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 14:45:44 +0100 Subject: [PATCH 242/523] added _resolve_gicon --- manatools/aui/backends/gtk/commongtk.py | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/commongtk.py b/manatools/aui/backends/gtk/commongtk.py index 6b11c13..de083ac 100644 --- a/manatools/aui/backends/gtk/commongtk.py +++ b/manatools/aui/backends/gtk/commongtk.py @@ -13,7 +13,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Gdk', '4.0') -from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib, Gio import cairo import threading import os @@ -21,7 +21,7 @@ from ...yui_common import * -__all__ = ["_resolve_icon"] +__all__ = ["_resolve_icon", "_resolve_gicon"] def _resolve_icon(icon_name, size=16): @@ -118,4 +118,31 @@ def load_from_file(filename): except Exception: pass + return None + + +def _resolve_gicon(icon_spec): + """Resolve icon specification to a Gio.Icon. + + Supports theme icon names (Gio.ThemedIcon) and absolute file paths + (Gio.FileIcon). Returns None if it cannot be resolved. + """ + if not icon_spec: + return None + try: + # absolute file path + if os.path.isabs(icon_spec) and os.path.exists(icon_spec): + try: + gfile = Gio.File.new_for_path(icon_spec) + return Gio.FileIcon.new(gfile) + except Exception: + pass + # theme icon + base_name = os.path.splitext(icon_spec)[0] if '.' in icon_spec else icon_spec + try: + return Gio.ThemedIcon.new(base_name) + except Exception: + pass + except Exception: + pass return None \ No newline at end of file From 299118a0549ce9846432fc9d9a15272e243436c0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 14:46:54 +0100 Subject: [PATCH 243/523] Added back createSelectionBox --- manatools/aui/yui_gtk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index fe9c1bf..a220b5c 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -217,6 +217,9 @@ def createPasswordField(self, parent, label): def createComboBox(self, parent, label, editable=False): return YComboBoxGtk(parent, label, editable) + def createSelectionBox(self, parent, label): + return YSelectionBoxGtk(parent, label) + def createMenuBar(self, parent): """Create a MenuBar widget (GTK backend).""" return YMenuBarGtk(parent) From 79d06a4834ab016300b8861c51332629f780c208 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 14:47:22 +0100 Subject: [PATCH 244/523] Respect vertical stretching --- manatools/aui/backends/gtk/vboxgtk.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index 055a869..7d2bcce 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -43,13 +43,22 @@ def _create_backend_widget(self): for child in self._children: widget = child.get_backend_widget() try: - widget.set_vexpand(True) - widget.set_hexpand(True) - #widget.set_valign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_VERT) else Gtk.Align.START) - #widget.set_halign(Gtk.Align.FILL if self.stretchable(YUIDimension.YD_HORIZ) else Gtk.Align.START) + # Respect the child's stretchable/weight hints instead of forcing expansion + vert_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) or bool(child.weight(YUIDimension.YD_VERT)) + horiz_stretch = bool(child.stretchable(YUIDimension.YD_HORIZ)) or bool(child.weight(YUIDimension.YD_HORIZ)) + widget.set_vexpand(bool(vert_stretch)) + widget.set_hexpand(bool(horiz_stretch)) + try: + widget.set_valign(Gtk.Align.FILL if vert_stretch else Gtk.Align.START) + except Exception: + pass + try: + widget.set_halign(Gtk.Align.FILL if horiz_stretch else Gtk.Align.START) + except Exception: + pass except Exception: pass - + # Gtk4: use append instead of pack_start try: self._backend_widget.append(widget) From cfa4b3a42e88ea8e0644f409372880d7d07a4548 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 14:48:14 +0100 Subject: [PATCH 245/523] Recall to use the separator when api is ready --- test/test_menubar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_menubar.py b/test/test_menubar.py index d749c0e..0e94085 100644 --- a/test/test_menubar.py +++ b/test/test_menubar.py @@ -63,7 +63,8 @@ def test_menubar_example(backend_name=None): file_menu = menubar.addMenu("File", icon_name="application-menu") item_open = menubar.addItem(file_menu, "Open", icon_name="document-open", enabled=True) item_close = menubar.addItem(file_menu, "Close", icon_name="window-close", enabled=False) - #menubar.addItem(file_menu, "-") + # TODO add separator to hide this trick + menubar.addItem(file_menu, "-") item_exit = menubar.addItem(file_menu, "Exit", icon_name="application-exit", enabled=True) # Edit menu From f6d23cbb0ed2268f8bc53ed72798799a1ca6c439 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 15:12:32 +0100 Subject: [PATCH 246/523] using a standard icon to test --- test/test_selectionbox2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_selectionbox2.py b/test/test_selectionbox2.py index b7e7ad6..e8ccc4a 100644 --- a/test/test_selectionbox2.py +++ b/test/test_selectionbox2.py @@ -67,7 +67,7 @@ def test_two_selectionbox(backend_name=None): items2 = [ yui.YItem("Red"), - yui.YItem("Green", icon_name="protected"), + yui.YItem("Green", icon_name="system-search"), yui.YItem("Blue") ] items2[2].setSelected(True) From 6ace0531b4ac760eb5afef8caac648c716d4c868 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 15:28:00 +0100 Subject: [PATCH 247/523] fixed menu item disabled at menubar creation --- manatools/aui/backends/gtk/menubargtk.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 205fde3..34abc2b 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -104,6 +104,11 @@ def _populate_menu_model(self, model: Gio.Menu, menu: YMenuItem): self._logger.error("No icon for menu item <%s> <%s>", child.label(), child.iconName()) act_name = self._action_name_for_item(child) action = Gio.SimpleAction.new(act_name, None) + # honor initial enabled state from the YMenuItem model + try: + action.set_enabled(bool(child.enabled())) + except Exception: + pass def on_activate(_action, _param=None, _child=child): if not _child.enabled(): return @@ -116,6 +121,11 @@ def on_activate(_action, _param=None, _child=child): self._item_to_action[child] = action try: item.set_attribute_value("action", GLib.Variant.new_string(f"menubar.{act_name}")) + # also set enabled attribute to guide rendering + try: + item.set_attribute_value("enabled", GLib.Variant.new_boolean(bool(child.enabled()))) + except Exception: + pass except Exception: pass model.append_item(item) From 32cfd403190ff527b7068b9bbbfc255046579e5d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 16:47:06 +0100 Subject: [PATCH 248/523] updated --- sow/TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sow/TODO.md b/sow/TODO.md index c701bc5..78d6472 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -62,6 +62,7 @@ To check/review: how to manage YEvents [X] and YItems [X] (verify selection attirbute). [ ] YInputField password mode [ ] adding factory create alternative methods (e.g. createMultiSelectionBox) + [ ] managing shortcuts Nice to have: improvements outside YUI API [ ] window title From e10dfd7f7d51c3ea124c9dba2873839b9aac619f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 16:47:24 +0100 Subject: [PATCH 249/523] Revert to Popover and menubutton to get rid of unmanaged icons --- manatools/aui/backends/gtk/menubargtk.py | 281 +++++++++++++++-------- 1 file changed, 184 insertions(+), 97 deletions(-) diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 34abc2b..e5e4b4e 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -9,6 +9,7 @@ gi.require_version('Gio', '2.0') from gi.repository import Gtk, Gio, GLib import logging +import os from ...yui_common import YWidget, YMenuEvent, YMenuItem from .commongtk import _resolve_gicon, _resolve_icon @@ -19,8 +20,10 @@ def __init__(self, parent=None): self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") self._menus = [] # top-level YMenuItem menus self._menu_to_model = {} - self._item_to_action = {} - self._action_group = Gio.SimpleActionGroup() + # For MenuButton+Popover approach + self._menu_to_button = {} + self._item_to_row = {} + self._row_to_item = {} def widgetClass(self): return "YMenuBar" @@ -44,12 +47,24 @@ def addItem(self, menu: YMenuItem, label: str, icon_name: str = "", enabled: boo def setItemEnabled(self, item: YMenuItem, on: bool = True): item.setEnabled(on) - act = self._item_to_action.get(item) - if act is not None: - try: - act.set_enabled(bool(on)) - except Exception: - pass + # Update rendered row or button sensitivity if present + try: + row = self._item_to_row.get(item) + if row is not None: + try: + row.set_sensitive(bool(on)) + except Exception: + pass + return + pair = self._menu_to_button.get(item) + if pair is not None: + try: + btn, _ = pair + btn.set_sensitive(bool(on)) + except Exception: + pass + except Exception: + self._logger.exception("Error updating enabled state for menu item '%s'", getattr(item, 'label', lambda: 'unknown')()) def _path_for_item(self, item: YMenuItem) -> str: labels = [] @@ -68,10 +83,11 @@ def _emit_activation(self, item: YMenuItem): pass def _ensure_menu_rendered(self, menu: YMenuItem): - # Build Gio.Menu model for this menu - model = Gio.Menu() - self._menu_to_model[menu] = model - self._populate_menu_model(model, menu) + # For backwards compatibility call-through to button renderer + try: + self._ensure_menu_rendered_button(menu) + except Exception: + self._logger.exception("Failed to ensure menu rendered for '%s'", menu.label()) def _action_name_for_item(self, item: YMenuItem) -> str: # derive action id from path, sanitized @@ -79,110 +95,173 @@ def _action_name_for_item(self, item: YMenuItem) -> str: return path.replace('/', '_').replace(' ', '_').lower() def _populate_menu_model(self, model: Gio.Menu, menu: YMenuItem): - for child in list(menu._children): - if child.isMenu(): - sub_model = Gio.Menu() - self._populate_menu_model(sub_model, child) - model.append_submenu(child.label(), sub_model) - else: - if child.label() == "-": - # separator - model.append_section(None, Gio.Menu()) - continue - item = Gio.MenuItem.new(child.label(), None) - image = _resolve_icon(child.iconName()) if child.iconName() else None - if image is None and child.iconName(): - self._logger.error("Failed to resolve icon for menu item <%s> <%s>", child.label(), child.iconName()) - gicon = image.get_gicon() if image else None - if gicon is not None: + # This implementation builds per-menu Gtk.Popover/ListBox widgets instead of a Gio.Menu model. + # Population is handled by `_render_menu_children` when menus are rendered. + return + + def _ensure_menu_rendered_button(self, menu: YMenuItem): + # Create a MenuButton with a Popover containing a ListBox for `menu` children. + if menu in self._menu_to_button: + return + hb = self._backend_widget + if hb is None: + return + btn = Gtk.MenuButton() + try: + btn.set_label(menu.label()) + except Exception: + pass + + # optional icon on the button + if menu.iconName(): + try: + img = _resolve_icon(menu.iconName()) + if img is not None: try: - item.set_icon(gicon) + btn.set_icon(img.get_paintable()) except Exception: - self._logger.error("Failed to set icon for menu item <%s>", child.label(), exc_info=True) - pass - elif child.iconName(): - self._logger.error("No icon for menu item <%s> <%s>", child.label(), child.iconName()) - act_name = self._action_name_for_item(child) - action = Gio.SimpleAction.new(act_name, None) - # honor initial enabled state from the YMenuItem model + # Some Gtk versions may not support set_icon; try set_child with a box + try: + hb_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + hb_box.append(img) + btn.set_child(hb_box) + except Exception: + self._logger.exception("Failed to set icon on MenuButton for '%s'", menu.label()) + except Exception: + self._logger.exception("Error resolving icon for MenuButton '%s'", menu.label()) + + pop = Gtk.Popover() + listbox = Gtk.ListBox() + listbox.set_selection_mode(Gtk.SelectionMode.NONE) + listbox.connect("row-activated", self._on_row_activated) + pop.set_child(listbox) + btn.set_popover(pop) + # ensure button doesn't expand vertically + try: + btn.set_vexpand(False) + btn.set_hexpand(False) + except Exception: + pass + try: + hb.append(btn) + except Exception: + try: + hb.add(btn) + except Exception: + pass + + self._menu_to_button[menu] = (btn, listbox) + # populate listbox rows + self._render_menu_children(menu) + + def _render_menu_children(self, menu: YMenuItem): + pair = self._menu_to_button.get(menu) + if not pair: + return + btn, listbox = pair + # clear existing + try: + for row in list(listbox.get_children() or []): try: - action.set_enabled(bool(child.enabled())) + listbox.remove(row) except Exception: pass - def on_activate(_action, _param=None, _child=child): - if not _child.enabled(): - return - self._emit_activation(_child) - action.connect("activate", on_activate) + except Exception: + pass + + for child in list(menu._children): + if child.isMenu(): + # submenu: create a nested MenuButton inside the row + sub_btn = Gtk.MenuButton() try: - self._action_group.add_action(action) + sub_btn.set_label(child.label()) except Exception: pass - self._item_to_action[child] = action - try: - item.set_attribute_value("action", GLib.Variant.new_string(f"menubar.{act_name}")) - # also set enabled attribute to guide rendering + sub_pop = Gtk.Popover() + sub_lb = Gtk.ListBox() + sub_lb.set_selection_mode(Gtk.SelectionMode.NONE) + sub_lb.connect("row-activated", self._on_row_activated) + sub_pop.set_child(sub_lb) + sub_btn.set_popover(sub_pop) + # optional icon + if child.iconName(): try: - item.set_attribute_value("enabled", GLib.Variant.new_boolean(bool(child.enabled()))) + img = _resolve_icon(child.iconName()) + if img is not None: + try: + sub_btn.set_icon(img.get_paintable()) + except Exception: + try: + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + box.append(img) + sub_btn.set_child(box) + except Exception: + self._logger.exception("Failed to set icon on submenu button '%s'", child.label()) except Exception: - pass - except Exception: - pass - model.append_item(item) + self._logger.exception("Error resolving icon for submenu '%s'", child.label()) - # no per-item rendering in PopoverMenuBar approach + row = Gtk.ListBoxRow() + row_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + row_box.append(sub_btn) + row.set_child(row_box) + row.set_sensitive(child.enabled()) + listbox.append(row) + # store mapping and recurse + self._menu_to_button[child] = (sub_btn, sub_lb) + self._render_menu_children(child) + else: + if child.label() == "-": + sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + listbox.append(sep) + continue + row = Gtk.ListBoxRow() + row_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + # optional icon + if child.iconName(): + try: + img = _resolve_icon(child.iconName()) + if img is not None: + row_box.append(img) + except Exception: + self._logger.exception("Failed to resolve icon for menu item '%s'", child.label()) + lbl = Gtk.Label(label=child.label()) + lbl.set_xalign(0.0) + row_box.append(lbl) + row.set_child(row_box) + row.set_sensitive(child.enabled()) + listbox.append(row) + self._item_to_row[child] = row + self._row_to_item[row] = child def _create_backend_widget(self): - # Combine all top-level menus into a single model - root = Gio.Menu() - for m in self._menus: - self._ensure_menu_rendered(m) - model = self._menu_to_model.get(m) - if model is not None: - root.append_submenu(m.label(), model) - mb = Gtk.PopoverMenuBar.new_from_model(root) + hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + # Do not allow the menubar box to expand vertically try: - mb.insert_action_group("menubar", self._action_group) + hb.set_vexpand(False) + hb.set_hexpand(True) except Exception: pass - # Limit vertical expansion - #try: - # mb.set_vexpand(False) - # mb.set_hexpand(True) - #except Exception: - # pass - self._backend_widget = mb + self._backend_widget = hb + # render any menus added before creation + for m in self._menus: + try: + self._ensure_menu_rendered_button(m) + except Exception: + self._logger.exception("Failed to render menu '%s'", m.label()) def _rebuild_root_model(self): + # Re-render all menus and their popovers to reflect model changes try: - mb = self._backend_widget - if mb is None: - return - # Reset actions to keep state in sync - self._action_group = Gio.SimpleActionGroup() - self._item_to_action.clear() - root = Gio.Menu() - for m in self._menus: - # Ensure individual menu model exists and is populated - self._ensure_menu_rendered(m) - model = self._menu_to_model.get(m) - if model is not None: - # refresh model contents - refreshed = Gio.Menu() - self._populate_menu_model(refreshed, m) - self._menu_to_model[m] = refreshed - root.append_submenu(m.label(), refreshed) - try: - mb.set_menu_model(root) - except Exception: - # Fallback: recreate component if setter not available - pass - try: - mb.insert_action_group("menubar", self._action_group) - except Exception: - pass + for m in list(self._menus): + try: + self._ensure_menu_rendered_button(m) + self._render_menu_children(m) + except Exception: + self._logger.exception("Failed rebuilding menu '%s'", m.label()) except Exception: - pass + self._logger.exception("Unexpected error in _rebuild_root_model") + + def _set_backend_enabled(self, enabled): try: @@ -191,4 +270,12 @@ def _set_backend_enabled(self, enabled): except Exception: pass - # No ListBox activation in PopoverMenuBar implementation + # ListBox activation handled by `_on_row_activated` + + def _on_row_activated(self, listbox: Gtk.ListBox, row: Gtk.ListBoxRow): + try: + item = self._row_to_item.get(row) + if item and item.enabled(): + self._emit_activation(item) + except Exception: + self._logger.exception("Error handling row activation") From a396f2044eccc49e8b80091123484bebd2e809db Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 17:11:52 +0100 Subject: [PATCH 250/523] Removed button frame --- manatools/aui/backends/gtk/menubargtk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index e5e4b4e..7446589 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -109,6 +109,7 @@ def _ensure_menu_rendered_button(self, menu: YMenuItem): btn = Gtk.MenuButton() try: btn.set_label(menu.label()) + btn.set_has_frame(False) except Exception: pass @@ -175,6 +176,7 @@ def _render_menu_children(self, menu: YMenuItem): sub_btn = Gtk.MenuButton() try: sub_btn.set_label(child.label()) + sub_btn.set_has_frame(False) except Exception: pass sub_pop = Gtk.Popover() From 350823139b0003301ba4fd90d656f5cf402d9680 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 17:15:20 +0100 Subject: [PATCH 251/523] Closing menu when the item is selected --- manatools/aui/backends/gtk/menubargtk.py | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 7446589..7907a56 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -24,6 +24,7 @@ def __init__(self, parent=None): self._menu_to_button = {} self._item_to_row = {} self._row_to_item = {} + self._row_to_popover = {} def widgetClass(self): return "YMenuBar" @@ -210,6 +211,11 @@ def _render_menu_children(self, menu: YMenuItem): listbox.append(row) # store mapping and recurse self._menu_to_button[child] = (sub_btn, sub_lb) + # map rows in submenu to its popover so we can close it on activation + try: + self._row_to_popover[row] = sub_btn.get_popover() + except Exception: + pass self._render_menu_children(child) else: if child.label() == "-": @@ -234,6 +240,11 @@ def _render_menu_children(self, menu: YMenuItem): listbox.append(row) self._item_to_row[child] = row self._row_to_item[row] = child + # remember the popover that contains this row (top-level menu) + try: + self._row_to_popover[row] = btn.get_popover() + except Exception: + pass def _create_backend_widget(self): hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) @@ -279,5 +290,31 @@ def _on_row_activated(self, listbox: Gtk.ListBox, row: Gtk.ListBoxRow): item = self._row_to_item.get(row) if item and item.enabled(): self._emit_activation(item) + # Close any popovers related to the menubar to mimic standard menu behavior + try: + for btn, _lb in list(self._menu_to_button.values()): + try: + pop = None + try: + pop = btn.get_popover() + except Exception: + pop = None + if pop is None: + continue + # Try popdown(), else hide/set_visible(False) + try: + pop.popdown() + except Exception: + try: + pop.set_visible(False) + except Exception: + try: + pop.hide() + except Exception: + pass + except Exception: + pass + except Exception: + self._logger.exception("Error closing popovers after activation") except Exception: self._logger.exception("Error handling row activation") From fd06c684882971995f1015bac34b8a037b7e0fdc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 17:52:29 +0100 Subject: [PATCH 252/523] Menubar on curses --- manatools/aui/backends/curses/__init__.py | 2 + .../aui/backends/curses/menubarcurses.py | 316 ++++++++++++++++++ manatools/aui/yui_curses.py | 6 +- 3 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/menubarcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 203306f..31f3ff1 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -15,6 +15,7 @@ from .radiobuttoncurses import YRadioButtonCurses from .tablecurses import YTableCurses from .richtextcurses import YRichTextCurses +from .menubarcurses import YMenuBarCurses __all__ = [ "YDialogCurses", @@ -34,5 +35,6 @@ "YRadioButtonCurses", "YTableCurses", "YRichTextCurses", + "YMenuBarCurses", # ... ] diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py new file mode 100644 index 0000000..fe265b4 --- /dev/null +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -0,0 +1,316 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +NCurses backend: YMenuBar implementation with keyboard navigation. +- Left/Right: switch top-level menus when focused +- Space: expand/collapse current menu +- Up/Down: navigate items when expanded +- Enter: activate item and emit YMenuEvent +''' +import curses +import logging +from ...yui_common import YWidget, YMenuEvent, YMenuItem + + +class YMenuBarCurses(YWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + self._menus = [] # list of YMenuItem (is_menu=True) + self._focused = False + self._can_focus = True + self._expanded = False + self._current_menu_index = 0 + self._current_item_index = 0 + self._height = 2 # one row for bar, one for dropdown baseline + # For popup menu navigation: stack of menus and indices per level + self._menu_path = [] # list of YMenuItem for current path (top menu -> submenus) + self._menu_indices = [] # list of int current index per level + # positions of top-level menu labels on the bar (start x) + self._menu_positions = [] + # scrolling support: offsets per level and max visible rows + self._scroll_offsets = [] # list of int per level for first visible item + self._visible_rows_max = 8 + # remember last drawn bar geometry for overlay drawing + self._bar_y = 0 + self._bar_x = 0 + self._bar_width = 0 + + def widgetClass(self): + return "YMenuBar" + + def addMenu(self, label: str, icon_name: str = "") -> YMenuItem: + m = YMenuItem(label, icon_name, enabled=True, is_menu=True) + self._menus.append(m) + return m + + def addItem(self, menu: YMenuItem, label: str, icon_name: str = "", enabled: bool = True) -> YMenuItem: + item = menu.addItem(label, icon_name) + item.setEnabled(enabled) + return item + + def setItemEnabled(self, item: YMenuItem, on: bool = True): + item.setEnabled(on) + + def _path_for_item(self, item: YMenuItem) -> str: + labels = [] + cur = item + while cur is not None: + labels.append(cur.label()) + cur = getattr(cur, "_parent", None) + return "/".join(reversed(labels)) + + def _emit_activation(self, item: YMenuItem): + try: + dlg = self.findDialog() + if dlg and self.notify(): + dlg._post_event(YMenuEvent(item=item, id=self._path_for_item(item))) + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + # remember bar area + self._bar_y = y + self._bar_x = x + self._bar_width = width + # draw menubar on first line + bar_attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + cx = x + # reset menu positions + self._menu_positions = [] + for idx, menu in enumerate(self._menus): + label = f" {menu.label()} " + attr = bar_attr + if idx == self._current_menu_index: + attr |= curses.A_BOLD + try: + # record start position for this menu label + self._menu_positions.append(cx) + window.addstr(y, cx, label[:max(0, width - (cx - x))], attr) + except curses.error: + self._menu_positions.append(cx) + pass + cx += len(label) + if cx >= x + width: + break + # dropdown area: drawn in _draw_expanded_list by dialog to ensure overlay on top + except curses.error: + pass + + def _draw_expanded_list(self, window): + """Draw expanded popups on top of other widgets (overlay), similar to combobox.""" + if not self._expanded or not (0 <= self._current_menu_index < len(self._menus)): + return + try: + # Start with top-level menu position, relative to dialog + try: + start_x = self._menu_positions[self._current_menu_index] if self._current_menu_index < len(self._menu_positions) else self._bar_x + except Exception: + start_x = self._bar_x + popup_x = start_x + popup_y = self._bar_y + 1 + # ensure menu_path initialized + if not self._menu_path: + self._menu_path = [self._menus[self._current_menu_index]] + self._menu_indices = [0] + self._scroll_offsets = [0] + for level, menu in enumerate(self._menu_path): + items = list(menu._children) + # compute width for this popup + max_label = 0 + for it in items: + txt = it.label() + if it.isMenu(): + txt = txt + " ►" + if len(txt) > max_label: + max_label = len(txt) + popup_width = max(10, max_label + 4) + # check screen bounds + screen_h, screen_w = window.getmaxyx() + if popup_x + popup_width >= screen_w: + popup_x = max(0, start_x - popup_width) + # compute visible rows; draw above if not enough space below + available_rows_below = max(1, screen_h - (popup_y) - 1) + visible_rows = min(len(items), min(self._visible_rows_max, available_rows_below)) + if popup_y + visible_rows >= screen_h: + popup_y = max(1, self._bar_y - visible_rows) + visible_rows = min(len(items), min(self._visible_rows_max, popup_y)) + # ensure scroll_offsets entry + if level >= len(self._scroll_offsets): + self._scroll_offsets.append(0) + # keep selection visible + sel_idx = self._menu_indices[level] if level < len(self._menu_indices) else 0 + offset = self._scroll_offsets[level] + if sel_idx < offset: + offset = sel_idx + if sel_idx >= offset + visible_rows: + offset = max(0, sel_idx - visible_rows + 1) + self._scroll_offsets[level] = offset + # opaque background + try: + for i in range(visible_rows): + bg = " " * popup_width + window.addstr(popup_y + i, popup_x, bg[:popup_width], curses.A_NORMAL) + # optional separator line above + except curses.error: + pass + # visible items slice + vis_items = items[offset:offset + visible_rows] + for i, item in enumerate(vis_items): + real_i = offset + i + sel = (self._menu_indices[level] == real_i) if level < len(self._menu_indices) else (i == 0) + prefix = "* " if sel else " " + label_text = item.label() + marker = " ►" if item.isMenu() else "" + text = prefix + label_text + marker + attr = curses.A_REVERSE if sel else curses.A_NORMAL + if not item.enabled(): + attr |= curses.A_DIM + try: + window.addstr(popup_y + i, popup_x, text.ljust(popup_width)[:popup_width], attr) + except curses.error: + pass + # scroll indicators + try: + if self._scroll_offsets[level] > 0 and visible_rows > 0: + window.addstr(popup_y, popup_x + popup_width - 1, "▲") + except curses.error: + pass + try: + if self._scroll_offsets[level] + visible_rows < len(items) and visible_rows > 0: + window.addstr(popup_y + visible_rows - 1, popup_x + popup_width - 1, "▼") + except curses.error: + pass + # next level to right + popup_x = popup_x + popup_width + 1 + if level + 1 >= len(self._menu_path): + break + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + handled = True + if key in (curses.KEY_LEFT, ord('h')): + if self._expanded: + # go up one level if possible, otherwise move to previous top-level menu + if len(self._menu_path) > 1: + self._menu_path.pop() + self._menu_indices.pop() + else: + if self._menus: + self._current_menu_index = max(0, self._current_menu_index - 1) + # reset path to new top menu + self._menu_path = [self._menus[self._current_menu_index]] + self._menu_indices = [0] + self._scroll_offsets = [0] + else: + if self._menus: + self._current_menu_index = max(0, self._current_menu_index - 1) + elif key in (curses.KEY_RIGHT, ord('l')): + if self._expanded: + # try to descend into submenu if selected item is a menu + cur_menu = self._menu_path[-1] + idx = self._menu_indices[-1] if self._menu_indices else 0 + items = list(cur_menu._children) + if 0 <= idx < len(items) and items[idx].isMenu(): + self._menu_path.append(items[idx]) + self._menu_indices.append(0) + else: + # otherwise move to next top-level menu + if self._menus: + self._current_menu_index = min(len(self._menus) - 1, self._current_menu_index + 1) + self._menu_path = [self._menus[self._current_menu_index]] + self._menu_indices = [0] + self._scroll_offsets = [0] + else: + if self._menus: + self._current_menu_index = min(len(self._menus) - 1, self._current_menu_index + 1) + elif key in (ord(' '), curses.KEY_DOWN): + # expand + if not self._expanded: + self._expanded = True + # initialize path to current top menu + if 0 <= self._current_menu_index < len(self._menus): + self._menu_path = [self._menus[self._current_menu_index]] + self._menu_indices = [0] + self._scroll_offsets = [0] + else: + # move down in current popup + cur_idx = self._menu_indices[-1] + cur_menu = self._menu_path[-1] + if cur_menu._children: + new_idx = min(len(cur_menu._children) - 1, cur_idx + 1) + self._menu_indices[-1] = new_idx + # adjust scroll offset + level = len(self._menu_indices) - 1 + # compute visible rows similarly to draw + # assume default max + visible_rows = self._visible_rows_max + offset = self._scroll_offsets[level] if level < len(self._scroll_offsets) else 0 + if new_idx >= offset + visible_rows: + self._scroll_offsets[level] = max(0, new_idx - visible_rows + 1) + elif key == curses.KEY_UP: + if self._expanded: + cur_idx = self._menu_indices[-1] + new_idx = max(0, cur_idx - 1) + self._menu_indices[-1] = new_idx + level = len(self._menu_indices) - 1 + visible_rows = self._visible_rows_max + offset = self._scroll_offsets[level] if level < len(self._scroll_offsets) else 0 + if new_idx < offset: + self._scroll_offsets[level] = new_idx + elif key == curses.KEY_NPAGE: + # page down + if self._expanded and self._menu_path: + level = len(self._menu_indices) - 1 + cur_menu = self._menu_path[-1] + total = len(cur_menu._children) + visible_rows = self._visible_rows_max + idx = self._menu_indices[-1] + idx = min(total - 1, idx + visible_rows) + self._menu_indices[-1] = idx + offset = self._scroll_offsets[level] + self._scroll_offsets[level] = min(max(0, total - visible_rows), offset + visible_rows) + elif key == curses.KEY_PPAGE: + # page up + if self._expanded and self._menu_path: + level = len(self._menu_indices) - 1 + cur_menu = self._menu_path[-1] + visible_rows = self._visible_rows_max + idx = self._menu_indices[-1] + idx = max(0, idx - visible_rows) + self._menu_indices[-1] = idx + offset = self._scroll_offsets[level] + self._scroll_offsets[level] = max(0, offset - visible_rows) + elif key in (curses.KEY_ENTER, 10, 13): + if self._expanded: + cur_menu = self._menu_path[-1] + idx = self._menu_indices[-1] + items = list(cur_menu._children) + if 0 <= idx < len(items): + item = items[idx] + if item.isMenu(): + # descend + self._menu_path.append(item) + self._menu_indices.append(0) + self._scroll_offsets.append(0) + else: + if item.enabled(): + self._emit_activation(item) + # collapse after activation + self._expanded = False + self._menu_path = [] + self._menu_indices = [] + self._scroll_offsets = [] + elif key in (27, ord('q')): + # collapse menus + self._expanded = False + self._menu_path = [] + self._menu_indices = [] + self._scroll_offsets = [] + else: + handled = False + return handled diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 4cba825..7f3b980 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -220,4 +220,8 @@ def createTable(self, parent, header: YTableHeader, multiSelection=False): def createRichText(self, parent, text: str = "", plainTextMode: bool = False): """Create a RichText widget (curses backend).""" - return YRichTextCurses(parent, text, plainTextMode) \ No newline at end of file + return YRichTextCurses(parent, text, plainTextMode) + + def createMenuBar(self, parent): + """Create a MenuBar widget (curses backend).""" + return YMenuBarCurses(parent) \ No newline at end of file From ad7f41a0580295eb04c5e6c62b3896106ed27eea Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 18:08:57 +0100 Subject: [PATCH 253/523] skipped separator and disabled items --- .../aui/backends/curses/menubarcurses.py | 133 ++++++++++++++---- 1 file changed, 109 insertions(+), 24 deletions(-) diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index fe265b4..b1b1d57 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -51,6 +51,20 @@ def addItem(self, menu: YMenuItem, label: str, icon_name: str = "", enabled: boo def setItemEnabled(self, item: YMenuItem, on: bool = True): item.setEnabled(on) + # If current selection becomes disabled while expanded, move to next selectable + try: + if self._expanded and self._menu_path: + cur_menu = self._menu_path[-1] + idx = self._menu_indices[-1] if self._menu_indices else 0 + items = list(cur_menu._children) + if 0 <= idx < len(items) and items[idx] == item and not item.enabled(): + nxt = self._next_selectable_index(items, idx, +1) + if nxt is None: + nxt = self._next_selectable_index(items, idx, -1) + if nxt is not None: + self._menu_indices[-1] = nxt + except Exception: + pass def _path_for_item(self, item: YMenuItem) -> str: labels = [] @@ -68,6 +82,40 @@ def _emit_activation(self, item: YMenuItem): except Exception: pass + def _is_separator(self, item: YMenuItem) -> bool: + try: + return item.label() == "-" + except Exception: + return False + + def _is_selectable(self, item: YMenuItem) -> bool: + try: + if self._is_separator(item): + return False + return bool(item.enabled()) + except Exception: + return False + + def _first_selectable_index(self, items): + for i, it in enumerate(items): + if self._is_selectable(it): + return i + return None + + def _next_selectable_index(self, items, start_idx, direction): + """Return next selectable index from start_idx stepping by direction; None if none.""" + if not items: + return None + if direction > 0: + rng = range(start_idx + 1, len(items)) + else: + rng = range(start_idx - 1, -1, -1) + for i in rng: + it = items[i] + if self._is_selectable(it): + return i + return None + def _draw(self, window, y, x, width, height): try: # remember bar area @@ -235,33 +283,55 @@ def _handle_key(self, key): # initialize path to current top menu if 0 <= self._current_menu_index < len(self._menus): self._menu_path = [self._menus[self._current_menu_index]] - self._menu_indices = [0] + # start at first selectable item + items = list(self._menu_path[0]._children) + first = self._first_selectable_index(items) + if first is None: + # no selectable items -> keep expanded false + self._expanded = False + self._menu_path = [] + self._menu_indices = [] + self._scroll_offsets = [] + return True + self._menu_indices = [first] self._scroll_offsets = [0] else: # move down in current popup cur_idx = self._menu_indices[-1] cur_menu = self._menu_path[-1] - if cur_menu._children: - new_idx = min(len(cur_menu._children) - 1, cur_idx + 1) - self._menu_indices[-1] = new_idx - # adjust scroll offset - level = len(self._menu_indices) - 1 - # compute visible rows similarly to draw - # assume default max - visible_rows = self._visible_rows_max - offset = self._scroll_offsets[level] if level < len(self._scroll_offsets) else 0 - if new_idx >= offset + visible_rows: - self._scroll_offsets[level] = max(0, new_idx - visible_rows + 1) + items = list(cur_menu._children) + if items: + nxt = self._next_selectable_index(items, cur_idx, +1) + if nxt is not None: + self._menu_indices[-1] = nxt + # adjust scroll offset + level = len(self._menu_indices) - 1 + visible_rows = self._visible_rows_max + offset = self._scroll_offsets[level] if level < len(self._scroll_offsets) else 0 + if nxt >= offset + visible_rows: + self._scroll_offsets[level] = max(0, nxt - visible_rows + 1) elif key == curses.KEY_UP: if self._expanded: cur_idx = self._menu_indices[-1] - new_idx = max(0, cur_idx - 1) - self._menu_indices[-1] = new_idx - level = len(self._menu_indices) - 1 - visible_rows = self._visible_rows_max - offset = self._scroll_offsets[level] if level < len(self._scroll_offsets) else 0 - if new_idx < offset: - self._scroll_offsets[level] = new_idx + cur_menu = self._menu_path[-1] + items = list(cur_menu._children) + if items: + # If at first selectable item on top-level, collapse to menubar + first = self._first_selectable_index(items) + level = len(self._menu_indices) - 1 + if level == 0 and first is not None and cur_idx == first: + self._expanded = False + self._menu_path = [] + self._menu_indices = [] + self._scroll_offsets = [] + return True + prev = self._next_selectable_index(items, cur_idx, -1) + if prev is not None: + self._menu_indices[-1] = prev + visible_rows = self._visible_rows_max + offset = self._scroll_offsets[level] if level < len(self._scroll_offsets) else 0 + if prev < offset: + self._scroll_offsets[level] = prev elif key == curses.KEY_NPAGE: # page down if self._expanded and self._menu_path: @@ -269,11 +339,19 @@ def _handle_key(self, key): cur_menu = self._menu_path[-1] total = len(cur_menu._children) visible_rows = self._visible_rows_max + # advance through selectable items idx = self._menu_indices[-1] - idx = min(total - 1, idx + visible_rows) + items = list(cur_menu._children) + steps = visible_rows + while steps > 0: + nxt = self._next_selectable_index(items, idx, +1) + if nxt is None: + break + idx = nxt + steps -= 1 self._menu_indices[-1] = idx offset = self._scroll_offsets[level] - self._scroll_offsets[level] = min(max(0, total - visible_rows), offset + visible_rows) + self._scroll_offsets[level] = min(max(0, total - visible_rows), max(offset, idx - visible_rows + 1)) elif key == curses.KEY_PPAGE: # page up if self._expanded and self._menu_path: @@ -281,10 +359,17 @@ def _handle_key(self, key): cur_menu = self._menu_path[-1] visible_rows = self._visible_rows_max idx = self._menu_indices[-1] - idx = max(0, idx - visible_rows) + items = list(cur_menu._children) + steps = visible_rows + while steps > 0: + prv = self._next_selectable_index(items, idx, -1) + if prv is None: + break + idx = prv + steps -= 1 self._menu_indices[-1] = idx offset = self._scroll_offsets[level] - self._scroll_offsets[level] = max(0, offset - visible_rows) + self._scroll_offsets[level] = min(offset, idx) elif key in (curses.KEY_ENTER, 10, 13): if self._expanded: cur_menu = self._menu_path[-1] @@ -292,7 +377,7 @@ def _handle_key(self, key): items = list(cur_menu._children) if 0 <= idx < len(items): item = items[idx] - if item.isMenu(): + if item.isMenu() and item.enabled(): # descend self._menu_path.append(item) self._menu_indices.append(0) From 4a6cec8f4fd75227efdd1f260de359b8c93d97ed Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 18:09:54 +0100 Subject: [PATCH 254/523] added title --- test/test_menubar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_menubar.py b/test/test_menubar.py index 0e94085..88d87bc 100644 --- a/test/test_menubar.py +++ b/test/test_menubar.py @@ -53,6 +53,7 @@ def test_menubar_example(backend_name=None): except Exception: root_logger.debug("test_menubar_example: program=%s backend=unknown", os.path.basename(sys.argv[0])) + ui.app().setApplicationTitle(f"Menu Bar {backend.value} Test") dlg = factory.createMainDialog() vbox = factory.createVBox(dlg) From 76f62abc531fd8a44cea7cbc53e74aed1a2fb540 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 18:44:52 +0100 Subject: [PATCH 255/523] Managed separator as separator not as a given label --- .../aui/backends/curses/menubarcurses.py | 13 +++++++++++- manatools/aui/backends/gtk/menubargtk.py | 2 +- manatools/aui/backends/qt/menubarqt.py | 2 +- manatools/aui/yui_common.py | 21 ++++++++++++++----- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index b1b1d57..01e4326 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -84,7 +84,7 @@ def _emit_activation(self, item: YMenuItem): def _is_separator(self, item: YMenuItem) -> bool: try: - return item.label() == "-" + return item.isSeparator() except Exception: return False @@ -207,6 +207,17 @@ def _draw_expanded_list(self, window): vis_items = items[offset:offset + visible_rows] for i, item in enumerate(vis_items): real_i = offset + i + # separators are drawn as a centered line of underscores + if self._is_separator(item): + try: + us_count = max(1, popup_width - 2) + pad = max(0, (popup_width - us_count) // 2) + sep_text = (" " * pad) + ("–" * us_count) + sep_text = sep_text.ljust(popup_width)[:popup_width] + window.addstr(popup_y + i, popup_x, sep_text, curses.A_NORMAL) + except curses.error: + pass + continue sel = (self._menu_indices[level] == real_i) if level < len(self._menu_indices) else (i == 0) prefix = "* " if sel else " " label_text = item.label() diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 7907a56..21209bf 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -218,7 +218,7 @@ def _render_menu_children(self, menu: YMenuItem): pass self._render_menu_children(child) else: - if child.label() == "-": + if child.isSeparator(): sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) listbox.append(sep) continue diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index b24e613..88d1fb3 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -105,7 +105,7 @@ def _ensure_item_rendered(self, menu: YMenuItem, item: YMenuItem): if qmenu is None: self._ensure_menu_rendered(menu) qmenu = self._menu_to_qmenu.get(menu) - if item.label() == "-": + if item.isSeparator(): qmenu.addSeparator() return act = self._item_to_qaction.get(item) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 8b02267..2b7c01f 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -406,11 +406,13 @@ class YMenuItem: (submenus and items). Backends may attach platform-specific references via `_backend_ref` for synchronization. """ - def __init__(self, label: str, icon_name: str = "", enabled: bool = True, is_menu: bool = False): - self._label = str(label) + def __init__(self, label: str, icon_name: str = "", enabled: bool = True, is_menu: bool = False, is_separator: bool = False): + self._is_separator = bool(is_separator) + self._label = "-" if self._is_separator else str(label) self._icon_name = str(icon_name) if icon_name else "" - self._enabled = bool(enabled) - self._is_menu = bool(is_menu) + self._enabled = False if self._is_separator else bool(enabled) + self._is_menu = False if self._is_separator else bool(is_menu) + self._visbile = True self._children = [] # list of YMenuItem self._parent = None self._backend_ref = None # optional backend-specific handle @@ -433,9 +435,18 @@ def enabled(self) -> bool: def setEnabled(self, on: bool = True): self._enabled = bool(on) + def visible(self) -> bool: + return bool(self._visible) + + def setVisible(self, on: bool = True): + self._visible = bool(on) + def isMenu(self) -> bool: return bool(self._is_menu) + def isSeparator(self) -> bool: + return bool(self._is_separator) + def childrenBegin(self): return iter(self._children) @@ -459,7 +470,7 @@ def addMenu(self, label: str, icon_name: str = ""): def addSeparator(self): # Represent separator as a disabled item with label "-"; backends can special-case it. - sep = YMenuItem("-", "", enabled=False, is_menu=False) + sep = YMenuItem("-", is_menu=False, enabled=False, is_separator=True) sep._parent = self self._children.append(sep) return sep From 8cf25ebc54820e74e81b471a582b9fe1a170ff2e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 19:09:25 +0100 Subject: [PATCH 256/523] Add managing visible property --- .../aui/backends/curses/menubarcurses.py | 74 +++++++++++++++---- manatools/aui/backends/gtk/menubargtk.py | 24 +++++- manatools/aui/backends/qt/menubarqt.py | 16 ++++ manatools/aui/yui_common.py | 7 +- test/test_menubar.py | 12 ++- 5 files changed, 115 insertions(+), 18 deletions(-) diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index 01e4326..f6a58ce 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -133,7 +133,17 @@ def _draw(self, window, y, x, width, height): if idx == self._current_menu_index: attr |= curses.A_BOLD try: - # record start position for this menu label + # record start position for this menu label (keep index alignment) + try: + if not menu.visible(): + # keep alignment with None placeholder + self._menu_positions.append(None) + cx += len(label) + if cx >= x + width: + break + continue + except Exception: + pass self._menu_positions.append(cx) window.addstr(y, cx, label[:max(0, width - (cx - x))], attr) except curses.error: @@ -153,7 +163,11 @@ def _draw_expanded_list(self, window): try: # Start with top-level menu position, relative to dialog try: - start_x = self._menu_positions[self._current_menu_index] if self._current_menu_index < len(self._menu_positions) else self._bar_x + if self._current_menu_index < len(self._menu_positions): + pos = self._menu_positions[self._current_menu_index] + start_x = pos if pos is not None else self._bar_x + else: + start_x = self._bar_x except Exception: start_x = self._bar_x popup_x = start_x @@ -164,7 +178,8 @@ def _draw_expanded_list(self, window): self._menu_indices = [0] self._scroll_offsets = [0] for level, menu in enumerate(self._menu_path): - items = list(menu._children) + # only visible children + items = [it for it in list(menu._children) if getattr(it, 'visible', lambda: True)()] # compute width for this popup max_label = 0 for it in items: @@ -248,6 +263,25 @@ def _draw_expanded_list(self, window): except curses.error: pass + def _prev_visible_menu_index(self, start_idx): + for i in range(start_idx - 1, -1, -1): + try: + if self._menus[i].visible(): + return i + except Exception: + # assume visible if error + return i + return None + + def _next_visible_menu_index(self, start_idx): + for i in range(start_idx + 1, len(self._menus)): + try: + if self._menus[i].visible(): + return i + except Exception: + return i + return None + def _handle_key(self, key): if not self._focused or not self.isEnabled(): return False @@ -259,40 +293,52 @@ def _handle_key(self, key): self._menu_path.pop() self._menu_indices.pop() else: - if self._menus: - self._current_menu_index = max(0, self._current_menu_index - 1) + prev = self._prev_visible_menu_index(self._current_menu_index) + if prev is not None: + self._current_menu_index = prev # reset path to new top menu self._menu_path = [self._menus[self._current_menu_index]] self._menu_indices = [0] self._scroll_offsets = [0] else: - if self._menus: - self._current_menu_index = max(0, self._current_menu_index - 1) + prev = self._prev_visible_menu_index(self._current_menu_index) + if prev is not None: + self._current_menu_index = prev elif key in (curses.KEY_RIGHT, ord('l')): if self._expanded: # try to descend into submenu if selected item is a menu cur_menu = self._menu_path[-1] idx = self._menu_indices[-1] if self._menu_indices else 0 - items = list(cur_menu._children) + items = [it for it in list(cur_menu._children) if getattr(it, 'visible', lambda: True)()] if 0 <= idx < len(items) and items[idx].isMenu(): self._menu_path.append(items[idx]) self._menu_indices.append(0) else: # otherwise move to next top-level menu - if self._menus: - self._current_menu_index = min(len(self._menus) - 1, self._current_menu_index + 1) + nxt = self._next_visible_menu_index(self._current_menu_index) + if nxt is not None: + self._current_menu_index = nxt self._menu_path = [self._menus[self._current_menu_index]] self._menu_indices = [0] self._scroll_offsets = [0] else: - if self._menus: - self._current_menu_index = min(len(self._menus) - 1, self._current_menu_index + 1) + nxt = self._next_visible_menu_index(self._current_menu_index) + if nxt is not None: + self._current_menu_index = nxt elif key in (ord(' '), curses.KEY_DOWN): # expand if not self._expanded: self._expanded = True # initialize path to current top menu if 0 <= self._current_menu_index < len(self._menus): + # ensure current top menu is visible; if not find next visible + try: + if not self._menus[self._current_menu_index].visible(): + nxt = self._next_visible_menu_index(self._current_menu_index) + if nxt is not None: + self._current_menu_index = nxt + except Exception: + pass self._menu_path = [self._menus[self._current_menu_index]] # start at first selectable item items = list(self._menu_path[0]._children) @@ -310,7 +356,7 @@ def _handle_key(self, key): # move down in current popup cur_idx = self._menu_indices[-1] cur_menu = self._menu_path[-1] - items = list(cur_menu._children) + items = [it for it in list(cur_menu._children) if getattr(it, 'visible', lambda: True)()] if items: nxt = self._next_selectable_index(items, cur_idx, +1) if nxt is not None: @@ -385,7 +431,7 @@ def _handle_key(self, key): if self._expanded: cur_menu = self._menu_path[-1] idx = self._menu_indices[-1] - items = list(cur_menu._children) + items = [it for it in list(cur_menu._children) if getattr(it, 'visible', lambda: True)()] if 0 <= idx < len(items): item = items[idx] if item.isMenu() and item.enabled(): diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 21209bf..d49aa97 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -172,6 +172,12 @@ def _render_menu_children(self, menu: YMenuItem): pass for child in list(menu._children): + # skip invisible children + try: + if not child.visible(): + continue + except Exception: + pass if child.isMenu(): # submenu: create a nested MenuButton inside the row sub_btn = Gtk.MenuButton() @@ -218,6 +224,12 @@ def _render_menu_children(self, menu: YMenuItem): pass self._render_menu_children(child) else: + # skip invisible children + try: + if not child.visible(): + continue + except Exception: + pass if child.isSeparator(): sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) listbox.append(sep) @@ -255,9 +267,14 @@ def _create_backend_widget(self): except Exception: pass self._backend_widget = hb - # render any menus added before creation + # render any visible menus added before creation for m in self._menus: try: + try: + if not m.visible(): + continue + except Exception: + pass self._ensure_menu_rendered_button(m) except Exception: self._logger.exception("Failed to render menu '%s'", m.label()) @@ -267,6 +284,11 @@ def _rebuild_root_model(self): try: for m in list(self._menus): try: + try: + if not m.visible(): + continue + except Exception: + pass self._ensure_menu_rendered_button(m) self._render_menu_children(m) except Exception: diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index 88d1fb3..8b7a5fa 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -60,6 +60,12 @@ def _emit_activation(self, item: YMenuItem): pass def _ensure_menu_rendered(self, menu: YMenuItem): + # skip invisible top-level menus + try: + if not menu.visible(): + return + except Exception: + pass qmenu = self._menu_to_qmenu.get(menu) if qmenu is None: qmenu = QtWidgets.QMenu(menu.label(), self._backend_widget) @@ -82,6 +88,11 @@ def _render_menu_children(self, menu: YMenuItem): if parent_qmenu is None: return for child in list(menu._children): + try: + if not child.visible(): + continue + except Exception: + pass if child.isMenu(): sub_qmenu = self._menu_to_qmenu.get(child) if sub_qmenu is None: @@ -105,6 +116,11 @@ def _ensure_item_rendered(self, menu: YMenuItem, item: YMenuItem): if qmenu is None: self._ensure_menu_rendered(menu) qmenu = self._menu_to_qmenu.get(menu) + try: + if not item.visible(): + return + except Exception: + pass if item.isSeparator(): qmenu.addSeparator() return diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 2b7c01f..82a8fec 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -412,7 +412,8 @@ def __init__(self, label: str, icon_name: str = "", enabled: bool = True, is_men self._icon_name = str(icon_name) if icon_name else "" self._enabled = False if self._is_separator else bool(enabled) self._is_menu = False if self._is_separator else bool(is_menu) - self._visbile = True + # visible flag (backends should respect this when rendering) + self._visible = True self._children = [] # list of YMenuItem self._parent = None self._backend_ref = None # optional backend-specific handle @@ -440,6 +441,8 @@ def visible(self) -> bool: def setVisible(self, on: bool = True): self._visible = bool(on) + for child in self._children: + child.setVisible(on) def isMenu(self) -> bool: return bool(self._is_menu) @@ -459,12 +462,14 @@ def hasChildren(self) -> bool: def addItem(self, label: str, icon_name: str = ""): child = YMenuItem(label, icon_name, enabled=True, is_menu=False) child._parent = self + child.setVisible(self.visible()) self._children.append(child) return child def addMenu(self, label: str, icon_name: str = ""): child = YMenuItem(label, icon_name, enabled=True, is_menu=True) child._parent = self + child.setVisible(self.visible()) self._children.append(child) return child diff --git a/test/test_menubar.py b/test/test_menubar.py index 88d87bc..d722bea 100644 --- a/test/test_menubar.py +++ b/test/test_menubar.py @@ -64,8 +64,7 @@ def test_menubar_example(backend_name=None): file_menu = menubar.addMenu("File", icon_name="application-menu") item_open = menubar.addItem(file_menu, "Open", icon_name="document-open", enabled=True) item_close = menubar.addItem(file_menu, "Close", icon_name="window-close", enabled=False) - # TODO add separator to hide this trick - menubar.addItem(file_menu, "-") + file_menu.addSeparator() item_exit = menubar.addItem(file_menu, "Exit", icon_name="application-exit", enabled=True) # Edit menu @@ -84,6 +83,11 @@ def test_menubar_example(backend_name=None): sub2.addItem("Beta") sub3 = sub2.addMenu("Submenu 2.1") sub3.addItem("Info") + enableSubMenu3 = sub3.addItem("Enable Submenu 3") + # Hidden submenu for testing visibility + sub4 = more_menu.addMenu("Submenu 3") + sub4.addItem("Was Hidden") + sub4.setVisible(False) # Status label status_label = factory.createLabel(vbox, "Selected: (none)") @@ -123,6 +127,10 @@ def test_menubar_example(backend_name=None): root_logger.info("Menu item selected: File/Exit") dlg.destroy() break + elif item == enableSubMenu3: + root_logger.info(f"Menu item selected: {item.label()}") + sub4.setVisible(not sub4.visible()) + enableSubMenu3.setLabel("Disable Submenu 3" if sub4.visible() else "Enable Submenu 3") root_logger.info("Dialog closed") From 2372b2aa98a3f2a79e6140c24bd8e0d45731553b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 28 Dec 2025 19:25:20 +0100 Subject: [PATCH 257/523] Added RebuildMenu --- .../aui/backends/curses/menubarcurses.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index f6a58ce..efdc2ed 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -282,6 +282,50 @@ def _next_visible_menu_index(self, start_idx): return i return None + def rebuildMenu(self, menu: YMenuItem = None): + """Rebuild menu(s) for curses backend. + + For curses there is no native widget per-menu, so we force a redraw + of the dialog and adjust current selection if the affected menu + becomes invisible. + """ + try: + # If a specific menu was passed and it's invisible, ensure current + # index points to a visible menu. + try: + if menu is not None: + if not menu.isMenu(): + return + if not menu.visible(): + # if the hidden menu is the current one try to move + if menu in self._menus and self._menus.index(menu) == self._current_menu_index: + nxt = self._next_visible_menu_index(self._current_menu_index) + if nxt is None: + prv = self._prev_visible_menu_index(self._current_menu_index) + if prv is not None: + self._current_menu_index = prv + else: + self._current_menu_index = nxt + except Exception: + pass + # reset transient layout caches + self._menu_positions = [] + self._menu_path = [] + self._menu_indices = [] + self._scroll_offsets = [] + # request dialog redraw + try: + dlg = self.findDialog() + if dlg is not None: + dlg._last_draw_time = 0 + except Exception: + pass + except Exception: + try: + self._logger.exception("rebuildMenu failed for curses backend") + except Exception: + pass + def _handle_key(self, key): if not self._focused or not self.isEnabled(): return False From 1ab3703672ff5c96321173e67fc9d7fc5d210224 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 17:18:52 +0100 Subject: [PATCH 258/523] added parent and hasChildren as YTreeItem --- manatools/aui/yui_common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 82a8fec..ed352f9 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -449,6 +449,12 @@ def isMenu(self) -> bool: def isSeparator(self) -> bool: return bool(self._is_separator) + + def parentItem(self): + return self._parent + + def hasChildren(self): + return len(self._children) > 0 def childrenBegin(self): return iter(self._children) From 4d78686a7378aea3e5b577d00892d12de10b4dca Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 17:19:50 +0100 Subject: [PATCH 259/523] Added addMenu receiving a menuItem, rebuildMenus and deleteMenus to manage changes both on YMenuItem or menu model --- manatools/aui/backends/qt/menubarqt.py | 154 ++++++++++++++++++++++--- 1 file changed, 141 insertions(+), 13 deletions(-) diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index 8b7a5fa..cac3622 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -20,8 +20,17 @@ def __init__(self, parent=None): def widgetClass(self): return "YMenuBar" - def addMenu(self, label: str, icon_name: str = "") -> YMenuItem: - m = YMenuItem(label, icon_name, enabled=True, is_menu=True) + def addMenu(self, label: str="", icon_name: str = "", menu: YMenuItem = None) -> YMenuItem: + """Add a menu by lable or by an existing YMenuItem (is_menu=True) to the menubar.""" + m = None + if menu is not None: + if not menu.isMenu(): + raise ValueError("Provided YMenuItem is not a menu (is_menu=True)") + m = menu + else: + if not label: + raise ValueError("Menu label must be provided when no YMenuItem is given") + m = YMenuItem(label, icon_name, enabled=True, is_menu=True) self._menus.append(m) if self._backend_widget: self._ensure_menu_rendered(m) @@ -43,6 +52,10 @@ def setItemEnabled(self, item: YMenuItem, on: bool = True): except Exception: pass + def setItemVisible(self, item: YMenuItem, visible: bool = True): + item.setVisible(visible) + self.rebuildMenu() + def _path_for_item(self, item: YMenuItem) -> str: labels = [] cur = item @@ -61,11 +74,9 @@ def _emit_activation(self, item: YMenuItem): def _ensure_menu_rendered(self, menu: YMenuItem): # skip invisible top-level menus - try: - if not menu.visible(): - return - except Exception: - pass + if not menu.visible(): + return + qmenu = self._menu_to_qmenu.get(menu) if qmenu is None: qmenu = QtWidgets.QMenu(menu.label(), self._backend_widget) @@ -81,6 +92,15 @@ def _ensure_menu_rendered(self, menu: YMenuItem): self._menu_to_qmenu[menu] = qmenu # render children recursively self._render_menu_children(menu) + # ensure the top-level action visibility matches model + try: + act = qmenu.menuAction() + try: + act.setVisible(bool(menu.visible())) + except Exception: + pass + except Exception: + pass def _render_menu_children(self, menu: YMenuItem): """Render all children (items and submenus) of a menu recursively.""" @@ -106,6 +126,15 @@ def _render_menu_children(self, menu: YMenuItem): if icon and not icon.isNull(): sub_qmenu.setIcon(icon) self._menu_to_qmenu[child] = sub_qmenu + # ensure submenu action visibility + try: + sa = sub_qmenu.menuAction() + try: + sa.setVisible(bool(child.visible())) + except Exception: + pass + except Exception: + pass # recurse into submenu self._render_menu_children(child) else: @@ -116,11 +145,8 @@ def _ensure_item_rendered(self, menu: YMenuItem, item: YMenuItem): if qmenu is None: self._ensure_menu_rendered(menu) qmenu = self._menu_to_qmenu.get(menu) - try: - if not item.visible(): - return - except Exception: - pass + if not item.visible(): + return if item.isSeparator(): qmenu.addSeparator() return @@ -135,11 +161,16 @@ def _ensure_item_rendered(self, menu: YMenuItem, item: YMenuItem): icon = QtGui.QIcon.fromTheme(item.iconName()) act = QtGui.QAction(icon, item.label(), self._backend_widget) act.setEnabled(item.enabled()) + try: + act.setVisible(bool(item.visible())) + except Exception: + pass def on_triggered(): self._emit_activation(item) act.triggered.connect(on_triggered) qmenu.addAction(act) self._item_to_qaction[item] = act + self._logger.debug("Rendered menu item: %s", self._path_for_item(item)) def _create_backend_widget(self): mb = QtWidgets.QMenuBar() @@ -169,4 +200,101 @@ def _set_backend_enabled(self, enabled): if getattr(self, "_backend_widget", None) is not None: self._backend_widget.setEnabled(bool(enabled)) except Exception: - pass \ No newline at end of file + pass + + def rebuildMenus(self): + """Rebuild all the menus. + + Useful when menu model changes at runtime. + + This action must be perforemed to reflect any direct changes to + YMenuItem data (e.g., label, enabled, visible) without passing + through the menubar. + """ + # Rebuild all menus: clear existing QMenu/QAction structures + try: + if getattr(self, "_backend_widget", None) is not None: + try: + # remove all actions from the menubar + for a in list(self._backend_widget.actions()): + try: + self._backend_widget.removeAction(a) + except Exception: + self._logger.exception("Failed removing action from menubar") + try: + a.setVisible(False) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + # clear internal mappings so we rebuild from scratch + try: + self._menu_to_qmenu.clear() + except Exception: + self._menu_to_qmenu = {} + try: + self._item_to_qaction.clear() + except Exception: + self._item_to_qaction = {} + + # recreate menus as done in _create_backend_widget + for m in self._menus: + try: + self._ensure_menu_rendered(m) + except Exception: + self._logger.exception("Failed ensuring menu rendered for '%s'", getattr(m, 'label', lambda: 'unknown')()) + try: + self._logger.debug("rebuildMenu: recreated menubar <%s>", self.debugLabel()) + except Exception: + pass + + def deleteMenus(self): + """Remove all menus/items and their backend QMenu/QAction mappings. + + After clearing model and mappings, call `rebuildMenus()` so the + backend menubar is emptied. + """ + try: + # clear the model (top-level menus) + try: + self._menus.clear() + except Exception: + self._menus = [] + + # remove all actions from the backend widget + try: + if getattr(self, "_backend_widget", None) is not None: + try: + for a in list(self._backend_widget.actions()): + try: + self._backend_widget.removeAction(a) + except Exception: + try: + a.setVisible(False) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + # clear internal mappings + try: + self._menu_to_qmenu.clear() + except Exception: + self._menu_to_qmenu = {} + try: + self._item_to_qaction.clear() + except Exception: + self._item_to_qaction = {} + + # Ensure UI reflects cleared state + try: + self.rebuildMenu() + except Exception: + pass + except Exception: + self._logger.exception("deleteAllItems failed") From 1692368893438a59d60cdab95b4bfbc0da86c98a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 17:48:44 +0100 Subject: [PATCH 260/523] Improving MenuBar --- manatools/aui/backends/gtk/menubargtk.py | 176 ++++++++++++++++++++++- 1 file changed, 169 insertions(+), 7 deletions(-) diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index d49aa97..904e28f 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -29,8 +29,17 @@ def __init__(self, parent=None): def widgetClass(self): return "YMenuBar" - def addMenu(self, label: str, icon_name: str = "") -> YMenuItem: - m = YMenuItem(label, icon_name, enabled=True, is_menu=True) + def addMenu(self, label: str="", icon_name: str = "", menu: YMenuItem = None) -> YMenuItem: + """Add a menu by lable or by an existing YMenuItem (is_menu=True) to the menubar.""" + m = None + if menu is not None: + if not menu.isMenu(): + raise ValueError("Provided YMenuItem is not a menu (is_menu=True)") + m = menu + else: + if not label: + raise ValueError("Menu label must be provided when no YMenuItem is given") + m = YMenuItem(label, icon_name, enabled=True, is_menu=True) self._menus.append(m) if self._backend_widget: self._ensure_menu_rendered(m) @@ -67,6 +76,10 @@ def setItemEnabled(self, item: YMenuItem, on: bool = True): except Exception: self._logger.exception("Error updating enabled state for menu item '%s'", getattr(item, 'label', lambda: 'unknown')()) + def setItemVisible(self, item: YMenuItem, visible: bool = True): + item.setVisible(visible) + self.rebuildMenus() + def _path_for_item(self, item: YMenuItem) -> str: labels = [] cur = item @@ -270,11 +283,8 @@ def _create_backend_widget(self): # render any visible menus added before creation for m in self._menus: try: - try: - if not m.visible(): - continue - except Exception: - pass + if not m.visible(): + continue self._ensure_menu_rendered_button(m) except Exception: self._logger.exception("Failed to render menu '%s'", m.label()) @@ -296,6 +306,158 @@ def _rebuild_root_model(self): except Exception: self._logger.exception("Unexpected error in _rebuild_root_model") + def rebuildMenus(self): + """Rebuild all the menus. + + Useful when menu model changes at runtime. + + This action must be perforemed to reflect any direct changes to + YMenuItem data (e.g., label, enabled, visible) without passing + through the menubar. + """ + try: + hb = self._backend_widget + # clear all existing rendered buttons and rows + try: + for m, pair in list(self._menu_to_button.items()): + try: + btn, listbox = pair + # clear listbox rows + try: + for row in list(listbox.get_children() or []): + try: + listbox.remove(row) + except Exception: + pass + except Exception: + pass + # remove button from container + try: + if hb is not None: + try: + hb.remove(btn) + except Exception: + self._logger.exception("Failed to remove MenuButton for '%s' from container", m.label()) + try: + btn.set_visible(False) + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + pass + + # clear mappings + try: + self._menu_to_button.clear() + except Exception: + self._menu_to_button = {} + try: + self._item_to_row.clear() + except Exception: + self._item_to_row = {} + try: + self._row_to_item.clear() + except Exception: + self._row_to_item = {} + try: + self._row_to_popover.clear() + except Exception: + self._row_to_popover = {} + + # recreate top-level buttons like in _create_backend_widget + for m in self._menus: + try: + self._ensure_menu_rendered_button(m) + try: + pair = self._menu_to_button.get(m) + if pair is not None: + try: + btn, _ = pair + btn.set_visible(bool(m.visible())) + except Exception: + pass + except Exception: + pass + except Exception: + self._logger.exception("Failed to ensure menu rendered for '%s'", m.label()) + + except Exception: + self._logger.exception("rebuildMenus failed") + + def deleteMenus(self): + """Remove all top-level menus and their backend widgets/mappings. + + Clears the `self._menus` model list and removes any created + MenuButton widgets from the backend container, then invokes + `rebuildMenus()` so the UI is updated. + """ + try: + # clear model + try: + self._menus.clear() + except Exception: + self._menus = [] + + # remove buttons from container and clear listboxes + try: + hb = self._backend_widget + for m, pair in list(self._menu_to_button.items()): + try: + btn, listbox = pair + try: + for row in list(listbox.get_children() or []): + try: + listbox.remove(row) + except Exception: + pass + except Exception: + pass + try: + if hb is not None: + try: + hb.remove(btn) + except Exception: + self._logger.exception("Failed to remove MenuButton for '%s' from container", m.label()) + try: + btn.set_visible(False) + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + pass + + # clear mappings + try: + self._menu_to_button.clear() + except Exception: + self._menu_to_button = {} + try: + self._item_to_row.clear() + except Exception: + self._item_to_row = {} + try: + self._row_to_item.clear() + except Exception: + self._row_to_item = {} + try: + self._row_to_popover.clear() + except Exception: + self._row_to_popover = {} + + # reflect empty state + try: + self.rebuildMenus() + except Exception: + pass + except Exception: + self._logger.exception("deleteMenus failed") + def _set_backend_enabled(self, enabled): From cdd6cbd71ed63bb4940399efa86e87ab77a15206 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 17:49:35 +0100 Subject: [PATCH 261/523] Typos on RebuildMenus --- manatools/aui/backends/qt/menubarqt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index cac3622..1b2ef83 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -54,7 +54,7 @@ def setItemEnabled(self, item: YMenuItem, on: bool = True): def setItemVisible(self, item: YMenuItem, visible: bool = True): item.setVisible(visible) - self.rebuildMenu() + self.rebuildMenus() def _path_for_item(self, item: YMenuItem) -> str: labels = [] @@ -247,7 +247,7 @@ def rebuildMenus(self): except Exception: self._logger.exception("Failed ensuring menu rendered for '%s'", getattr(m, 'label', lambda: 'unknown')()) try: - self._logger.debug("rebuildMenu: recreated menubar <%s>", self.debugLabel()) + self._logger.debug("rebuildMenus: recreated menubar <%s>", self.debugLabel()) except Exception: pass @@ -293,7 +293,7 @@ def deleteMenus(self): # Ensure UI reflects cleared state try: - self.rebuildMenu() + self.rebuildMenus() except Exception: pass except Exception: From 2189574f8fb142b80ade12f16c8eb454e9f237cb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 19:22:49 +0100 Subject: [PATCH 262/523] Fixed deleting and adding new menus --- manatools/aui/backends/gtk/menubargtk.py | 146 ++++++++++++++++------- 1 file changed, 102 insertions(+), 44 deletions(-) diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 904e28f..64dcd01 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -25,12 +25,13 @@ def __init__(self, parent=None): self._item_to_row = {} self._row_to_item = {} self._row_to_popover = {} + self._pending_rebuild = False def widgetClass(self): return "YMenuBar" def addMenu(self, label: str="", icon_name: str = "", menu: YMenuItem = None) -> YMenuItem: - """Add a menu by lable or by an existing YMenuItem (is_menu=True) to the menubar.""" + """Add a menu by label or by an existing YMenuItem (is_menu=True) to the menubar.""" m = None if menu is not None: if not menu.isMenu(): @@ -78,7 +79,32 @@ def setItemEnabled(self, item: YMenuItem, on: bool = True): def setItemVisible(self, item: YMenuItem, visible: bool = True): item.setVisible(visible) - self.rebuildMenus() + # Defer rebuild to idle to avoid clearing widgets during signal emission + self._queue_rebuild() + + def _queue_rebuild(self): + try: + if self._pending_rebuild: + return + self._pending_rebuild = True + def do_rebuild(): + try: + self.rebuildMenus() + except Exception: + self._logger.exception("Deferred rebuildMenus failed") + finally: + self._pending_rebuild = False + return False # remove idle source + try: + GLib.idle_add(do_rebuild, priority=GLib.PRIORITY_DEFAULT_IDLE) + except Exception: + # Fallback: rebuild synchronously if idle_add not available + try: + self.rebuildMenus() + finally: + self._pending_rebuild = False + except Exception: + self._logger.exception("_queue_rebuild failed") def _path_for_item(self, item: YMenuItem) -> str: labels = [] @@ -168,19 +194,20 @@ def _ensure_menu_rendered_button(self, menu: YMenuItem): self._menu_to_button[menu] = (btn, listbox) # populate listbox rows self._render_menu_children(menu) + # reflect initial visibility + try: + btn.set_visible(bool(menu.visible())) + except Exception: + pass def _render_menu_children(self, menu: YMenuItem): pair = self._menu_to_button.get(menu) if not pair: return btn, listbox = pair - # clear existing + # clear existing rows reliably on GTK4 try: - for row in list(listbox.get_children() or []): - try: - listbox.remove(row) - except Exception: - pass + self.__clear_widget(listbox) except Exception: pass @@ -306,48 +333,74 @@ def _rebuild_root_model(self): except Exception: self._logger.exception("Unexpected error in _rebuild_root_model") + def __clear_widget(self, widget: Gtk.Widget): + """Remove all children from a GTK4 widget. + + Recursively unparents/removes children to help rebuild container + widgets safely. + """ + if not widget: + return + + child = widget.get_first_child() + while child is not None: + next_child = child.get_next_sibling() + self.__clear_widget(child) + + if hasattr(widget, 'remove'): + widget.remove(child) + #elif hasattr(child, 'unparent'): + # child.unparent() + + child = next_child + def rebuildMenus(self): """Rebuild all the menus. - Useful when menu model changes at runtime. - - This action must be perforemed to reflect any direct changes to - YMenuItem data (e.g., label, enabled, visible) without passing - through the menubar. + Useful when the menu model changes at runtime. + + This action must be performed to reflect any direct changes to + `YMenuItem` data (e.g., label, enabled, visible) without going + through higher-level menubar helpers. """ try: hb = self._backend_widget - # clear all existing rendered buttons and rows + # proactively disconnect signals and close popovers before clearing children try: - for m, pair in list(self._menu_to_button.items()): + for _m, pair in list(self._menu_to_button.items()): try: btn, listbox = pair - # clear listbox rows + # disconnect row-activated if available try: - for row in list(listbox.get_children() or []): - try: - listbox.remove(row) - except Exception: - pass + if hasattr(listbox, 'disconnect_by_func'): + listbox.disconnect_by_func(self._on_row_activated) except Exception: pass - # remove button from container + # close/detach popover try: - if hb is not None: + pop = btn.get_popover() + if pop is not None: try: - hb.remove(btn) + pop.popdown() except Exception: - self._logger.exception("Failed to remove MenuButton for '%s' from container", m.label()) try: - btn.set_visible(False) + pop.set_visible(False) except Exception: - pass + try: + pop.hide() + except Exception: + pass + try: + pop.set_child(None) + except Exception: + pass except Exception: pass except Exception: pass except Exception: pass + self.__clear_widget(hb) # clear mappings try: @@ -371,6 +424,7 @@ def rebuildMenus(self): for m in self._menus: try: self._ensure_menu_rendered_button(m) + # set button visibility according to model try: pair = self._menu_to_button.get(m) if pair is not None: @@ -401,34 +455,38 @@ def deleteMenus(self): except Exception: self._menus = [] - # remove buttons from container and clear listboxes + # remove buttons from container, disconnect signals and clear listboxes try: hb = self._backend_widget for m, pair in list(self._menu_to_button.items()): + btn, listbox = pair + # disconnect row-activated if available try: - btn, listbox = pair - try: - for row in list(listbox.get_children() or []): - try: - listbox.remove(row) - except Exception: - pass - except Exception: - pass - try: - if hb is not None: + if hasattr(listbox, 'disconnect_by_func'): + listbox.disconnect_by_func(self._on_row_activated) + except Exception: + pass + # close/detach popover + try: + pop = btn.get_popover() + if pop is not None: + try: + pop.popdown() + except Exception: try: - hb.remove(btn) + pop.set_visible(False) except Exception: - self._logger.exception("Failed to remove MenuButton for '%s' from container", m.label()) try: - btn.set_visible(False) + pop.hide() except Exception: pass - except Exception: - pass + try: + pop.set_child(None) + except Exception: + pass except Exception: pass + self.__clear_widget(hb) except Exception: pass From 7ef8e7fdd3a45b4467bc4cface74b9c193c8ec48 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 19:23:09 +0100 Subject: [PATCH 263/523] typos --- manatools/aui/backends/qt/menubarqt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index 1b2ef83..828b30d 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -21,7 +21,7 @@ def widgetClass(self): return "YMenuBar" def addMenu(self, label: str="", icon_name: str = "", menu: YMenuItem = None) -> YMenuItem: - """Add a menu by lable or by an existing YMenuItem (is_menu=True) to the menubar.""" + """Add a menu by label or by an existing YMenuItem (is_menu=True) to the menubar.""" m = None if menu is not None: if not menu.isMenu(): @@ -207,7 +207,7 @@ def rebuildMenus(self): Useful when menu model changes at runtime. - This action must be perforemed to reflect any direct changes to + This action must be performed to reflect any direct changes to YMenuItem data (e.g., label, enabled, visible) without passing through the menubar. """ @@ -297,4 +297,4 @@ def deleteMenus(self): except Exception: pass except Exception: - self._logger.exception("deleteAllItems failed") + self._logger.exception("deleteMenus failed") From 23871052caee8a771e00e721188643619a88625c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 19:35:53 +0100 Subject: [PATCH 264/523] Added deletemenus and fixed addMenu as in other be --- .../aui/backends/curses/menubarcurses.py | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index efdc2ed..66ad5a6 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -39,14 +39,51 @@ def __init__(self, parent=None): def widgetClass(self): return "YMenuBar" - def addMenu(self, label: str, icon_name: str = "") -> YMenuItem: - m = YMenuItem(label, icon_name, enabled=True, is_menu=True) + def addMenu(self, label: str = "", icon_name: str = "", menu: YMenuItem = None) -> YMenuItem: + """Add a top-level menu by label or attach an existing YMenuItem. + + Mirrors the Qt/GTK signatures for parameter consistency. + """ + m=None + if menu is not None: + if not menu.isMenu(): + raise ValueError("Provided YMenuItem is not a menu (is_menu=True)") + m = menu + else: + if not label: + raise ValueError("Menu label must be provided when no YMenuItem is given") + m = YMenuItem(label, icon_name, enabled=True, is_menu=True) self._menus.append(m) return m - def addItem(self, menu: YMenuItem, label: str, icon_name: str = "", enabled: bool = True) -> YMenuItem: - item = menu.addItem(label, icon_name) - item.setEnabled(enabled) + def addItem(self, menu: YMenuItem, label: object, icon_name: str = "", enabled: bool = True) -> YMenuItem: + """Add an item to a menu, accepting either a label or an existing YMenuItem. + + If `label` is a YMenuItem instance, attach it to `menu`. Otherwise, + create a new item using the label and optional icon. + """ + if isinstance(label, YMenuItem): + item = label + # attach to parent if not already + try: + setattr(item, "_parent", menu) + except Exception: + pass + try: + menu._children.append(item) + except Exception: + # fallback: use provided API if available + try: + menu.addItem(item.label(), item.iconName()) + item = menu._children[-1] + except Exception: + raise + else: + item = menu.addItem(label, icon_name) + try: + item.setEnabled(enabled) + except Exception: + pass return item def setItemEnabled(self, item: YMenuItem, on: bool = True): @@ -282,22 +319,38 @@ def _next_visible_menu_index(self, start_idx): return i return None - def rebuildMenu(self, menu: YMenuItem = None): - """Rebuild menu(s) for curses backend. + def rebuildMenus(self): + """Rebuild all menus for curses backend. - For curses there is no native widget per-menu, so we force a redraw - of the dialog and adjust current selection if the affected menu - becomes invisible. + There are no native widgets per-menu; reset transient caches and + request a dialog redraw to reflect model changes. """ try: - # If a specific menu was passed and it's invisible, ensure current - # index points to a visible menu. + # reset transient layout caches + self._menu_positions = [] + self._menu_path = [] + self._menu_indices = [] + self._scroll_offsets = [] + # request dialog redraw try: - if menu is not None: - if not menu.isMenu(): - return + dlg = self.findDialog() + if dlg is not None: + dlg._last_draw_time = 0 + except Exception: + pass + except Exception: + try: + self._logger.exception("rebuildMenus failed for curses backend") + except Exception: + pass + + # Backward compatibility shim + def rebuildMenu(self, menu: YMenuItem = None): + # adjust current menu index if a specific menu becomes invisible + try: + if menu is not None and hasattr(menu, 'isMenu') and menu.isMenu(): + try: if not menu.visible(): - # if the hidden menu is the current one try to move if menu in self._menus and self._menus.index(menu) == self._current_menu_index: nxt = self._next_visible_menu_index(self._current_menu_index) if nxt is None: @@ -306,14 +359,28 @@ def rebuildMenu(self, menu: YMenuItem = None): self._current_menu_index = prv else: self._current_menu_index = nxt + except Exception: + pass + except Exception: + pass + self.rebuildMenus() + + def deleteMenus(self): + """Clear all menus and reset transient state, then request redraw.""" + try: + try: + self._menus.clear() except Exception: - pass - # reset transient layout caches + self._menus = [] + # reset indices and caches + self._expanded = False + self._current_menu_index = 0 + self._current_item_index = 0 self._menu_positions = [] self._menu_path = [] self._menu_indices = [] self._scroll_offsets = [] - # request dialog redraw + # request redraw try: dlg = self.findDialog() if dlg is not None: @@ -322,7 +389,7 @@ def rebuildMenu(self, menu: YMenuItem = None): pass except Exception: try: - self._logger.exception("rebuildMenu failed for curses backend") + self._logger.exception("deleteMenus failed for curses backend") except Exception: pass From 10c308567bb4532d9e1d3fce2f8559f2c04e7511 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 19:36:56 +0100 Subject: [PATCH 265/523] Improved menubar example --- test/test_menubar.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/test_menubar.py b/test/test_menubar.py index d722bea..e9158ff 100644 --- a/test/test_menubar.py +++ b/test/test_menubar.py @@ -83,12 +83,21 @@ def test_menubar_example(backend_name=None): sub2.addItem("Beta") sub3 = sub2.addMenu("Submenu 2.1") sub3.addItem("Info") - enableSubMenu3 = sub3.addItem("Enable Submenu 3") + sub2.addSeparator() + enableSubMenu3 = sub2.addItem("Enable Submenu 3") + sub2.addSeparator() + to_change_menu = sub2.addItem("To Change menu") # Hidden submenu for testing visibility sub4 = more_menu.addMenu("Submenu 3") sub4.addItem("Was Hidden") +# menubar.setItemVisible(sub4 ,False) sub4.setVisible(False) + change_menu = yui.YMenuItem("Change", is_menu = True) + to_more_menu = change_menu.addItem("To More menu") + + menu1 = [file_menu, edit_menu, more_menu] + menu2 = [file_menu, edit_menu, change_menu] # Status label status_label = factory.createLabel(vbox, "Selected: (none)") @@ -128,9 +137,21 @@ def test_menubar_example(backend_name=None): dlg.destroy() break elif item == enableSubMenu3: - root_logger.info(f"Menu item selected: {item.label()}") + root_logger.info(f"Menu item selected: {item.label()}") + #menubar.setItemVisible(sub4, not sub4.visible()) sub4.setVisible(not sub4.visible()) enableSubMenu3.setLabel("Disable Submenu 3" if sub4.visible() else "Enable Submenu 3") + menubar.rebuildMenus() # more_menu + elif item == to_change_menu: + root_logger.info(f"Menu item selected: {item.label()}") + menubar.deleteMenus() + for m in menu2: + menubar.addMenu(menu=m) + elif item == to_more_menu: + root_logger.info(f"Menu item selected: {item.label()}") + menubar.deleteMenus() + for m in menu1: + menubar.addMenu(menu=m) root_logger.info("Dialog closed") From 5cc7f942a7a43f14284e95f6b685a747b224ef40 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 19:37:28 +0100 Subject: [PATCH 266/523] Updated --- sow/TODO.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 78d6472..8eb9b74 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -21,8 +21,7 @@ Missing Widgets comparing libyui original factory: [X] YRichText [ ] YMultiLineEdit [ ] YIntField - [ ] YMenuBar - [ ] YMenuButton + [X] YMenuBar [ ] YSpacing (detailed variants: createHStretch/createVStretch/createHSpacing/createVSpacing/createSpacing) [X] YAlignment helpers (createLeft/createRight/createTop/createBottom/createHCenter/createVCenter/createHVCenter) [ ] YReplacePoint @@ -39,6 +38,7 @@ Skipped widgets: [-] YMultiSelectionBox (implemented as YSelectionBox + multiselection enabled) [-] YPackageSelector (not ported) [-] YRadioButtonGroup (not ported) + [-] YMenuButton (legacy menus) Optional/special widgets (from `YOptionalWidgetFactory`): From 4f6c17dfc1ce28d519b87ea018155ce45ec36492 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 21:33:08 +0100 Subject: [PATCH 267/523] Forgotten logging --- manatools/aui/backends/gtk/checkboxframegtk.py | 1 + manatools/aui/backends/gtk/framegtk.py | 1 + 2 files changed, 2 insertions(+) diff --git a/manatools/aui/backends/gtk/checkboxframegtk.py b/manatools/aui/backends/gtk/checkboxframegtk.py index 027459e..2b84d3a 100644 --- a/manatools/aui/backends/gtk/checkboxframegtk.py +++ b/manatools/aui/backends/gtk/checkboxframegtk.py @@ -13,6 +13,7 @@ gi.require_version('Gtk', '4.0') gi.require_version('Gdk', '4.0') from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib +import logging import cairo import threading import os diff --git a/manatools/aui/backends/gtk/framegtk.py b/manatools/aui/backends/gtk/framegtk.py index c879538..6d81afd 100644 --- a/manatools/aui/backends/gtk/framegtk.py +++ b/manatools/aui/backends/gtk/framegtk.py @@ -13,6 +13,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Gdk', '4.0') +import logging from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib import cairo import threading From ecdb0b3bbb4b01a2e495726e97089ed1d114423e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 29 Dec 2025 21:35:53 +0100 Subject: [PATCH 268/523] First ncurses ReplacePoint implementation --- manatools/aui/backends/curses/__init__.py | 2 + manatools/aui/backends/curses/commoncurses.py | 4 + .../aui/backends/curses/replacepointcurses.py | 144 ++++++++++++++++++ manatools/aui/yui_common.py | 23 +++ manatools/aui/yui_curses.py | 6 +- test/test_replacePoint.py | 101 ++++++++++++ 6 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/replacepointcurses.py create mode 100644 test/test_replacePoint.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 31f3ff1..ce273e5 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -16,6 +16,7 @@ from .tablecurses import YTableCurses from .richtextcurses import YRichTextCurses from .menubarcurses import YMenuBarCurses +from .replacepointcurses import YReplacePointCurses __all__ = [ "YDialogCurses", @@ -36,5 +37,6 @@ "YTableCurses", "YRichTextCurses", "YMenuBarCurses", + "YReplacePointCurses", # ... ] diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py index 4dee676..abeaba9 100644 --- a/manatools/aui/backends/curses/commoncurses.py +++ b/manatools/aui/backends/curses/commoncurses.py @@ -53,6 +53,10 @@ def _curses_recursive_min_height(widget): elif cls == "YAlignment": child = widget.child() return max(1, _curses_recursive_min_height(child)) + elif cls == "YReplacePoint": + # Treat ReplacePoint as a transparent single-child container + child = widget.child() + return max(1, _curses_recursive_min_height(child)) elif cls == "YFrame" or cls == "YCheckBoxFrame": child = widget.child() inner_top = max(0, getattr(widget, "_inner_top_padding", 1)) diff --git a/manatools/aui/backends/curses/replacepointcurses.py b/manatools/aui/backends/curses/replacepointcurses.py new file mode 100644 index 0000000..995e888 --- /dev/null +++ b/manatools/aui/backends/curses/replacepointcurses.py @@ -0,0 +1,144 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import logging +from ...yui_common import * + +# Module-level logger for curses replace point backend +_mod_logger = logging.getLogger("manatools.aui.curses.replacepoint.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + +class YReplacePointCurses(YSingleChildContainerWidget): + """ + NCurses backend implementation of YReplacePoint. + + A single-child placeholder; showChild() ensures the current child will + be drawn in the curses UI, and deleteChildren() clears the logical model + so a new child can be added later. + """ + def __init__(self, parent=None): + super().__init__(parent) + self._backend_widget = self # curses backends often use self as the drawable + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + try: + self._logger.debug("%s.__init__", self.__class__.__name__) + except Exception: + pass + + def widgetClass(self): + return "YReplacePoint" + + def _create_backend_widget(self): + # No separate widget for curses: keep self as the drawable entity + try: + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + pass + + def stretchable(self, dim: YUIDimension): + """Propagate stretchability from the single child to the container.""" + try: + ch = self.child() + if ch is None: + return False + try: + if bool(ch.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(ch.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def _set_backend_enabled(self, enabled): + # Propagate enabled state to child if present + try: + ch = self.child() + if ch is not None and hasattr(ch, "setEnabled"): + ch.setEnabled(enabled) + except Exception as e: + try: + self._logger.error("_set_backend_enabled error: %s", e, exc_info=True) + except Exception: + pass + + def addChild(self, child): + super().addChild(child) + try: + self.showChild() + except Exception: + pass + + def showChild(self): + """ + For curses, showing the child means ensuring the child will be drawn + when this container's _draw is invoked. No further action is needed + beyond the logical association, but this method exists for API parity + and debugging. + """ + try: + self._logger.debug("showChild: child=%s", getattr(self.child(), "widgetClass", lambda: "?")()) + # Force dialog redraw on next loop iteration (similar to recalcLayout) + try: + dlg = self.findDialog() + if dlg is not None: + try: + dlg._last_draw_time = 0 + except Exception: + pass + # Update minimal height to reflect child's needs + try: + from .commoncurses import _curses_recursive_min_height + ch = self.child() + inner_min = _curses_recursive_min_height(ch) if ch is not None else 1 + self._height = max(1, inner_min) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def deleteChildren(self): + try: + super().deleteChildren() + except Exception as e: + try: + self._logger.error("deleteChildren error: %s", e, exc_info=True) + except Exception: + pass + + def _draw(self, window, y, x, width, height): + """Delegate drawing to the single child if present.""" + try: + ch = self.child() + if ch is None: + return + if hasattr(ch, "_draw"): + ch._draw(window, y, x, width, height) + except Exception: + pass diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index ed352f9..8ffa04a 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -187,6 +187,29 @@ def removeChild(self, child): if child in self._children: self._children.remove(child) child._parent = None + + def deleteChildren(self): + """ + Remove all logical children from this widget. + + This clears the internal children list and detaches each child's + parent reference. Backend containers that render children should + override this method to also clear any backend layout or content + area. For generic widgets, this only affects the logical model. + """ + try: + for ch in list(self._children): + try: + ch._parent = None + except Exception: + pass + self._children.clear() + except Exception: + # Best-effort: ignore failures and keep model consistent + try: + self._children = [] + except Exception: + pass def parent(self): return self._parent diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 7f3b980..fedf7e4 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -224,4 +224,8 @@ def createRichText(self, parent, text: str = "", plainTextMode: bool = False): def createMenuBar(self, parent): """Create a MenuBar widget (curses backend).""" - return YMenuBarCurses(parent) \ No newline at end of file + return YMenuBarCurses(parent) + + def createReplacePoint(self, parent): + """Create a ReplacePoint widget (curses backend).""" + return YReplacePointCurses(parent) \ No newline at end of file diff --git a/test/test_replacePoint.py b/test/test_replacePoint.py new file mode 100644 index 0000000..538ebf4 --- /dev/null +++ b/test/test_replacePoint.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +import os +import sys +import logging + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +# Basic logging setup +logger = logging.getLogger("manatools.test.replacepoint") +if not logging.getLogger().handlers: + h = logging.StreamHandler() + h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + logging.getLogger().addHandler(h) + logging.getLogger().setLevel(logging.INFO) + logger.setLevel(logging.INFO) + +def test_replace_point(backend_name=None): + """Interactive test for ReplacePoint widget across backends. + + - Creates a dialog with a ReplacePoint + - Adds an initial child layout and shows it + - Replaces it at runtime using deleteChildren() and showChild() + """ + if backend_name: + logger.info("Setting backend to: %s", backend_name) + os.environ['YUI_BACKEND'] = backend_name + else: + logger.info("Using auto-detection") + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + logger.info("Using backend: %s", backend.value) + + ui = YUI_ui() + factory = ui.widgetFactory() + + ui.application().setApplicationTitle("Test ReplacePoint") + dialog = factory.createPopupDialog() + mainVbox = factory.createVBox(dialog) + frame = factory.createFrame(mainVbox, "Replace Point here") + + rp = factory.createReplacePoint(frame) + + # Initial child layout + vbox1 = factory.createVBox(rp) + label1 = factory.createLabel(vbox1, "Initial child layout") + value_btn1 = factory.createPushButton(vbox1, "Value 1") + rp.showChild() + + hbox = factory.createHBox(mainVbox) + replace_button = factory.createPushButton(hbox, "Replace child") + close_button = factory.createPushButton(hbox, "Close") + n_call = 0 + while True: + event = dialog.waitForEvent() + if not event: + continue + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + try: + logger.info("WidgetEvent from: %s", getattr(wdg, "widgetClass", lambda: "?")()) + except Exception: + pass + if wdg == close_button: + dialog.destroy() + break + elif wdg == replace_button: + # Replace the child content dynamically + n_call = (n_call + 1) % 100 + logger.info("Replacing child layout iteration=%d", n_call) + rp.deleteChildren() + vbox2 = factory.createVBox(rp) + label2 = factory.createLabel(vbox2, f"Replaced child layout ({n_call})") + value_btn2 = factory.createPushButton(vbox2, f"Value ({n_call})") + rp.showChild() + elif wdg == value_btn1: + # no-op; just ensure button works + logger.info("Value 1 clicked") + else: + # Handle events from new child too + logger.debug("Unhandled widget event") + except Exception as e: + logger.error("Error testing ReplacePoint with backend %s: %s", backend_name, e, exc_info=True) + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_replace_point(sys.argv[1]) + else: + test_replace_point() From 2c474fbf0bb660ac692dce1b83d0f5491aaa89ed Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 30 Dec 2025 19:34:12 +0100 Subject: [PATCH 269/523] first attempt to add RP for qt --- manatools/aui/backends/qt/replacepointqt.py | 238 ++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 manatools/aui/backends/qt/replacepointqt.py diff --git a/manatools/aui/backends/qt/replacepointqt.py b/manatools/aui/backends/qt/replacepointqt.py new file mode 100644 index 0000000..3ad73af --- /dev/null +++ b/manatools/aui/backends/qt/replacepointqt.py @@ -0,0 +1,238 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets +import logging +from ...yui_common import * + +class YReplacePointQt(YSingleChildContainerWidget): + """ + Qt backend implementation of YReplacePoint. + + A placeholder container that hosts exactly one child. The child can be + removed and replaced at runtime, and showChild() should be called after + creating/adding a new child to attach and present it in the backend view. + """ + def __init__(self, parent=None): + super().__init__(parent) + self._backend_widget = None + self._layout = None + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + try: + self._logger.debug("%s.__init__", self.__class__.__name__) + except Exception: + pass + + def widgetClass(self): + return "YReplacePoint" + + def _create_backend_widget(self): + """Create a QWidget container with a vertical layout to host the single child.""" + try: + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) + self._backend_widget = container + self._layout = layout + # Default to expanding so parents like YFrame can allocate space + try: + sp = container.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + container.setSizePolicy(sp) + except Exception: + pass + self._backend_widget.setEnabled(bool(self._enabled)) + # If a child already exists, attach it now + try: + if self.child(): + cw = self.child().get_backend_widget() + self._logger.debug("Attaching existing child %s", self.child().widgetClass()) + if cw: + self._layout.addWidget(cw) + else: + self._logger.debug("No existing child to attach") + except Exception as e: + try: + self._logger.error("_create_backend_widget attach child error: %s", e, exc_info=True) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + pass + self._backend_widget = None + self._layout = None + + def _set_backend_enabled(self, enabled): + """Enable/disable the container and propagate to its logical child.""" + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass + try: + ch = self.child() + if ch is not None: + ch.setEnabled(enabled) + except Exception: + pass + + def stretchable(self, dim: YUIDimension): + """Propagate stretchability from the single child to the container.""" + try: + ch = self.child() + if ch is None: + return False + try: + if bool(ch.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(ch.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def _attach_child_backend(self): + """Attach the current child's backend into this container layout (no redraw).""" + if not (self._backend_widget and self._layout and self.child()): + return + try: + if self._layout.count() > 0: + self._logger.warning("_attach_child_backend: layout is not empty") + # Clear previous layout widgets + try: + while self._layout.count(): + it = self._layout.takeAt(0) + w = it.widget() if it else None + if w: + w.setParent(None) + except Exception: + pass + # Ensure ReplacePoint expands to show content even if child isn't stretchable + try: + sp = self._backend_widget.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + self._backend_widget.setSizePolicy(sp) + except Exception: + self._logger.exception("_attach_child_backend: sizePolicy failed") + pass + ch = self.child() + cw = ch.get_backend_widget() + if cw: + self._layout.addWidget(cw) + except Exception: + self._logger.exception("_attach_child_backend failed") + pass + + def addChild(self, child): + """Add logical child and attach backend if possible (no forced redraw).""" + super().addChild(child) + self._logger.debug("addChild: %s", child.debugLabel()) + self._attach_child_backend() + + def showChild(self): + """ + Attach and show the newly added child in the backend view. + This removes any previous widget from the layout and inserts + the current child's backend widget. + """ + if not (self._backend_widget and self._layout and self.child()): + self._logger.debug("showChild: no backend/layout/child to show") + return + try: + # Reuse the attach helper + #self._attach_child_backend() + # Force a dialog layout recalculation and redraw, similar to libyui's recalcLayout. + try: + dlg = self.findDialog() + if dlg is not None: + qwin = getattr(dlg, "_qwidget", None) + if qwin: + try: + qwin.update() + self.child().get_backend_widget().show() + + #qwin.repaint() + except Exception: + self._logger.exception("showChild: repaint failed") + pass + try: + qwin.adjustSize() + except Exception: + self._logger.exception("showChild: adjustSize failed") + pass + try: + app = QtWidgets.QApplication.instance() + if app: + app.processEvents() + except Exception: + self._logger.exception("showChild: processEvents failed") + pass + else: + self._logger.debug("showChild: dialog has no _qwidget") + except Exception: + pass + except Exception as e: + try: + self._logger.error("showChild error: %s", e, exc_info=True) + except Exception: + pass + + def deleteChildren(self): + """ + Remove the logical child and clear the backend layout so new children + can be added and shown afterwards. + """ + try: + # Clear backend layout + if self._layout is not None: + try: + while self._layout.count(): + it = self._layout.takeAt(0) + w = it.widget() if it else None + if w: + w.setParent(None) + except Exception: + pass + # Clear model children + super().deleteChildren() + except Exception as e: + try: + self._logger.error("deleteChildren error: %s", e, exc_info=True) + except Exception: + pass From b916bd3b1d5ddd627ad39f747e1aad9d5a44d1d0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 30 Dec 2025 19:49:36 +0100 Subject: [PATCH 270/523] RP for Qt --- manatools/aui/backends/qt/__init__.py | 2 ++ manatools/aui/yui_qt.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 5b61fb3..03fc27b 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -16,6 +16,7 @@ from .tableqt import YTableQt from .richtextqt import YRichTextQt from .menubarqt import YMenuBarQt +from .replacepointqt import YReplacePointQt __all__ = [ @@ -37,5 +38,6 @@ "YTableQt", "YRichTextQt", "YMenuBarQt", + "YReplacePointQt", # ... ] diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index ad416f7..f14bc33 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -234,3 +234,7 @@ def createRichText(self, parent, text: str = "", plainTextMode: bool = False): def createMenuBar(self, parent): """Create a MenuBar widget (Qt backend).""" return YMenuBarQt(parent) + + def createReplacePoint(self, parent): + """Create a ReplacePoint widget (Qt backend).""" + return YReplacePointQt(parent) From 264af468a3810560f91e3d615d161f547bbc1e7b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 30 Dec 2025 22:31:15 +0100 Subject: [PATCH 271/523] Fixed RP behavior when deleting and adding new widgets --- manatools/aui/backends/qt/hboxqt.py | 55 +++++- manatools/aui/backends/qt/replacepointqt.py | 194 ++++++++++++++++++-- manatools/aui/backends/qt/vboxqt.py | 57 +++++- 3 files changed, 293 insertions(+), 13 deletions(-) diff --git a/manatools/aui/backends/qt/hboxqt.py b/manatools/aui/backends/qt/hboxqt.py index 8a9e8f5..3ed8941 100644 --- a/manatools/aui/backends/qt/hboxqt.py +++ b/manatools/aui/backends/qt/hboxqt.py @@ -9,7 +9,7 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtCore import logging from ...yui_common import * @@ -88,3 +88,56 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def addChild(self, child): + """Attach child's backend widget to this HBox when added at runtime.""" + super().addChild(child) + try: + if getattr(self, "_backend_widget", None) is None: + return + + def _deferred_attach(): + try: + widget = child.get_backend_widget() + expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 + try: + if expand == 1: + sp = widget.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + widget.setSizePolicy(sp) + except Exception: + pass + try: + lay = self._backend_widget.layout() + if lay is None: + lay = QtWidgets.QHBoxLayout(self._backend_widget) + self._backend_widget.setLayout(lay) + lay.addWidget(widget, stretch=expand) + try: + widget.show() + except Exception: + pass + try: + widget.updateGeometry() + except Exception: + pass + except Exception: + try: + self._logger.exception("YHBoxQt.addChild: failed to attach child") + except Exception: + pass + except Exception: + pass + + try: + QtCore.QTimer.singleShot(0, _deferred_attach) + except Exception: + _deferred_attach() + except Exception: + pass diff --git a/manatools/aui/backends/qt/replacepointqt.py b/manatools/aui/backends/qt/replacepointqt.py index 3ad73af..5317b0b 100644 --- a/manatools/aui/backends/qt/replacepointqt.py +++ b/manatools/aui/backends/qt/replacepointqt.py @@ -9,7 +9,7 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtCore import logging from ...yui_common import * @@ -38,9 +38,11 @@ def _create_backend_widget(self): """Create a QWidget container with a vertical layout to host the single child.""" try: container = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout(container) + # Use a QStackedLayout so the single active child is shown reliably + layout = QtWidgets.QStackedLayout() layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(5) + container.setLayout(layout) self._backend_widget = container self._layout = layout # Default to expanding so parents like YFrame can allocate space @@ -65,7 +67,21 @@ def _create_backend_widget(self): cw = self.child().get_backend_widget() self._logger.debug("Attaching existing child %s", self.child().widgetClass()) if cw: - self._layout.addWidget(cw) + try: + cw.setParent(self._backend_widget) + except Exception: + pass + try: + self._layout.addWidget(cw) + try: + self._layout.setCurrentWidget(cw) + except Exception: + pass + except Exception: + try: + self._layout.addWidget(cw) + except Exception: + pass else: self._logger.debug("No existing child to attach") except Exception as e: @@ -121,12 +137,14 @@ def stretchable(self, dim: YUIDimension): def _attach_child_backend(self): """Attach the current child's backend into this container layout (no redraw).""" + # Ensure the backend container exists if not (self._backend_widget and self._layout and self.child()): + self._logger.debug("_attach_child_backend called but no backend or no child to attach") return try: if self._layout.count() > 0: self._logger.warning("_attach_child_backend: layout is not empty") - # Clear previous layout widgets + # Clear previous layout widgets try: while self._layout.count(): it = self._layout.takeAt(0) @@ -154,7 +172,105 @@ def _attach_child_backend(self): ch = self.child() cw = ch.get_backend_widget() if cw: - self._layout.addWidget(cw) + try: + self._logger.debug("_attach_child_backend: layout.count before add = %s", self._layout.count()) + except Exception: + pass + try: + # Debug info to help diagnose invisible children + try: + self._logger.debug("_attach_child_backend: attaching widget type=%s parent=%s visible=%s sizeHint=%s", + type(cw), getattr(cw, 'parent', lambda: None)(), getattr(cw, 'isVisible', lambda: False)(), getattr(cw, 'sizeHint', lambda: None)()) + except Exception: + pass + except Exception: + pass + try: + # Ensure the widget is parented to our container before adding + try: + cw.setParent(self._backend_widget) + except Exception: + pass + self._layout.addWidget(cw) + try: + # If using QStackedLayout, show the new widget as current + try: + self._layout.setCurrentWidget(cw) + except Exception: + try: + # fallback to setCurrentIndex if available + idx = self._layout.indexOf(cw) + if idx is not None and idx >= 0: + try: + self._layout.setCurrentIndex(idx) + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + # fallback: try remove any previous parent then add + try: + try: + cw.setParent(None) + except Exception: + pass + self._layout.addWidget(cw) + except Exception: + self._logger.exception("_attach_child_backend: addWidget fallback failed") + # Encourage the child to expand so content becomes visible + # Encourage the child to expand so content becomes visible + try: + sp_cw = cw.sizePolicy() + try: + sp_cw.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + sp_cw.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp_cw.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + sp_cw.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + cw.setSizePolicy(sp_cw) + except Exception: + pass + try: + # Ensure the single child gets stretch in the layout (stacked ignores stretch but keep for safety) + try: + self._layout.setStretch(0, 1) + except Exception: + pass + try: + self._logger.debug("_attach_child_backend: layout.count after add = %s currentWidget=%s", self._layout.count(), getattr(self._layout, 'currentWidget', lambda: None)()) + except Exception: + pass + except Exception: + pass + try: + cw.show() + try: + cw.raise_() + except Exception: + pass + try: + cw.updateGeometry() + except Exception: + pass + # If sizeHint is empty, give a small visible minimum so layout doesn't collapse + try: + sh = cw.sizeHint() + if sh is not None and (sh.width() == 0 or sh.height() == 0): + try: + mh = max(24, sh.height()) if hasattr(sh, 'height') else 24 + mw = max(24, sh.width()) if hasattr(sh, 'width') else 24 + cw.setMinimumSize(mw, mh) + except Exception: + pass + except Exception: + pass + except Exception: + pass except Exception: self._logger.exception("_attach_child_backend failed") pass @@ -172,25 +288,59 @@ def showChild(self): the current child's backend widget. """ if not (self._backend_widget and self._layout and self.child()): - self._logger.debug("showChild: no backend/layout/child to show") - return + self._logger.debug("showChild called but no backend or no child to attach") + return try: # Reuse the attach helper - #self._attach_child_backend() # Force a dialog layout recalculation and redraw, similar to libyui's recalcLayout. try: dlg = self.findDialog() if dlg is not None: qwin = getattr(dlg, "_qwidget", None) if qwin: + # Trigger local layout updates first + try: + if self._layout is not None: + self._layout.invalidate() + try: + self._layout.activate() + except Exception: + pass + if self._backend_widget is not None: + self._backend_widget.updateGeometry() + self._backend_widget.setVisible(True) + self._backend_widget.update() + except Exception: + pass try: qwin.update() - self.child().get_backend_widget().show() + try: + ch = self.child() + if ch is not None: + cw = ch.get_backend_widget() + if cw: + cw.show() + except Exception: + pass #qwin.repaint() except Exception: self._logger.exception("showChild: repaint failed") pass + # Activate the dialog's layout if present + try: + lay = qwin.layout() + if lay is not None: + try: + lay.invalidate() + except Exception: + pass + try: + lay.activate() + except Exception: + pass + except Exception: + pass try: qwin.adjustSize() except Exception: @@ -199,7 +349,10 @@ def showChild(self): try: app = QtWidgets.QApplication.instance() if app: - app.processEvents() + try: + app.processEvents(QtCore.QEventLoop.AllEvents) + except Exception: + app.processEvents() except Exception: self._logger.exception("showChild: processEvents failed") pass @@ -222,11 +375,30 @@ def deleteChildren(self): # Clear backend layout if self._layout is not None: try: + # Properly remove and hide widgets so layout recalculates while self._layout.count(): it = self._layout.takeAt(0) w = it.widget() if it else None if w: - w.setParent(None) + try: + try: + self._layout.removeWidget(w) + except Exception: + pass + try: + w.hide() + except Exception: + pass + try: + w.setParent(None) + except Exception: + pass + try: + w.update() + except Exception: + pass + except Exception: + pass except Exception: pass # Clear model children diff --git a/manatools/aui/backends/qt/vboxqt.py b/manatools/aui/backends/qt/vboxqt.py index 30d012b..52728c4 100644 --- a/manatools/aui/backends/qt/vboxqt.py +++ b/manatools/aui/backends/qt/vboxqt.py @@ -9,7 +9,7 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtCore import logging from ...yui_common import * @@ -89,3 +89,58 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def addChild(self, child): + """Attach child's backend widget to this VBox when added at runtime.""" + super().addChild(child) + try: + if getattr(self, "_backend_widget", None) is None: + return + + def _deferred_attach(): + try: + widget = child.get_backend_widget() + expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 + try: + if expand == 1: + sp = widget.sizePolicy() + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + widget.setSizePolicy(sp) + except Exception: + pass + try: + lay = self._backend_widget.layout() + if lay is None: + lay = QtWidgets.QVBoxLayout(self._backend_widget) + self._backend_widget.setLayout(lay) + lay.addWidget(widget, stretch=expand) + try: + widget.show() + except Exception: + pass + try: + widget.updateGeometry() + except Exception: + pass + except Exception: + try: + self._logger.exception("YVBoxQt.addChild: failed to attach child") + except Exception: + pass + except Exception: + pass + + # Defer attach to next event loop iteration so child's __init__ can complete + try: + QtCore.QTimer.singleShot(0, _deferred_attach) + except Exception: + # fallback: attach immediately + _deferred_attach() + except Exception: + pass From 7cd59950fa0ec53aa7e058380ecd989e356c0cea Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 30 Dec 2025 23:07:38 +0100 Subject: [PATCH 272/523] Replace Point for gtk --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/replacepointgtk.py | 396 ++++++++++++++++++ manatools/aui/yui_gtk.py | 4 + 3 files changed, 402 insertions(+) create mode 100644 manatools/aui/backends/gtk/replacepointgtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 8856f16..ffd8e8d 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -16,6 +16,7 @@ from .tablegtk import YTableGtk from .richtextgtk import YRichTextGtk from .menubargtk import YMenuBarGtk +from .replacepointgtk import YReplacePointGtk __all__ = [ "YDialogGtk", @@ -36,5 +37,6 @@ "YTableGtk", "YRichTextGtk", "YMenuBarGtk", + "YReplacePointGtk", # ... ] diff --git a/manatools/aui/backends/gtk/replacepointgtk.py b/manatools/aui/backends/gtk/replacepointgtk.py new file mode 100644 index 0000000..4b91eea --- /dev/null +++ b/manatools/aui/backends/gtk/replacepointgtk.py @@ -0,0 +1,396 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GLib +import logging +from ...yui_common import * + +class YReplacePointGtk(YSingleChildContainerWidget): + """ + GTK backend implementation of YReplacePoint. + + A single-child placeholder container; call showChild() after adding a new + child to attach its backend widget inside a Gtk.Box container. deleteChildren() + clears both the logical model and the Gtk content. + """ + def __init__(self, parent=None): + super().__init__(parent) + self._backend_widget = None + self._content = None + # counter to generate stable unique page names for Gtk.Stack + self._stack_page_counter = 0 + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + try: + self._logger.debug("%s.__init__", self.__class__.__name__) + except Exception: + pass + + def widgetClass(self): + return "YReplacePoint" + + def _create_backend_widget(self): + """Create a container that hosts a Gtk.Stack so only the active child is visible. + + Using a Gtk.Stack mirrors the Qt stacked layout approach and makes + showing/hiding the single child more reliable across backends. + """ + try: + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + outer.set_hexpand(True) + outer.set_vexpand(True) + try: + outer.set_halign(Gtk.Align.FILL) + outer.set_valign(Gtk.Align.FILL) + except Exception: + pass + # Try to give a reasonable default minimum size so the area is visible + try: + # GTK3/4 compatibility: set_size_request may exist + outer.set_size_request(200, 140) + except Exception: + try: + # Fallback: set a minimum content height if available + outer.set_minimum_size = getattr(outer, 'set_minimum_size', None) + if callable(outer.set_minimum_size): + try: + outer.set_minimum_size(200, 140) + except Exception: + pass + except Exception: + pass + stack = Gtk.Stack() + try: + stack.set_hexpand(True) + stack.set_vexpand(True) + try: + stack.set_halign(Gtk.Align.FILL) + stack.set_valign(Gtk.Align.FILL) + except Exception: + pass + try: + stack.set_size_request(200, 140) + except Exception: + pass + except Exception: + pass + outer.append(stack) + self._backend_widget = outer + self._content = stack + # Attach child if already present + try: + ch = self.child() + if ch is not None: + cw = ch.get_backend_widget() + if cw: + try: + # add_titled requires a name; use debugLabel to produce a unique id + name = f"child_{id(cw)}" + stack.add_titled(cw, name, name) + try: + stack.set_visible_child(cw) + except Exception: + pass + except Exception: + try: + stack.add(cw) + except Exception: + pass + except Exception as e: + try: + self._logger.error("_create_backend_widget attach child error: %s", e, exc_info=True) + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + except Exception as e: + try: + self._logger.error("_create_backend_widget error: %s", e, exc_info=True) + except Exception: + pass + self._backend_widget = None + self._content = None + + def _set_backend_enabled(self, enabled): + try: + if self._backend_widget is not None: + self._backend_widget.set_sensitive(bool(enabled)) + except Exception: + pass + try: + ch = self.child() + if ch is not None: + ch.setEnabled(enabled) + except Exception: + pass + + def stretchable(self, dim: YUIDimension): + """Propagate stretchability from the single child to the container.""" + try: + ch = self.child() + if ch is None: + return False + try: + if bool(ch.stretchable(dim)): + return True + except Exception: + pass + try: + if bool(ch.weight(dim)): + return True + except Exception: + pass + except Exception: + pass + return False + + def addChild(self, child): + super().addChild(child) + # Best-effort attach without forcing a full dialog relayout + self._attach_child_backend() + + def _clear_content(self): + try: + if self._content is None: + return + while True: + first = self._content.get_first_child() + if first is None: + break + try: + self._content.remove(first) + except Exception: + break + except Exception: + pass + + def _attach_child_backend(self): + """Attach the current child's backend into the content box (no redraw).""" + if not (self._backend_widget and self._content and self.child()): + self._logger.debug("_attach_child_backend called but no backend or no child to attach") + return + try: + if self._content is None: + self.get_backend_widget() + if self._content is None: + return + # Use idle_add to defer the attach until the child's backend is fully ready + def _do_attach(): + try: + # Clear previous stack children + try: + # Gtk.Stack doesn't provide a direct clear; remove children one by one + for c in list(self._content.get_children()): + try: + self._content.remove(c) + except Exception: + pass + except Exception: + pass + ch = self.child() + if ch is None: + return False + cw = ch.get_backend_widget() + if cw: + try: + # Wrap the child in a vertical Box so it fills and aligns like other backends + wrapper = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + try: + wrapper.set_hexpand(True) + wrapper.set_vexpand(True) + try: + wrapper.set_halign(Gtk.Align.FILL) + wrapper.set_valign(Gtk.Align.FILL) + except Exception: + pass + except Exception: + pass + + # If cw has an existing parent, try to unparent it first + try: + parent = cw.get_parent() + if parent is not None: + try: + parent.remove(cw) + except Exception: + try: + cw.unparent() + except Exception: + pass + except Exception: + pass + + # append cw into wrapper (Gtk4 uses append) + try: + wrapper.append(cw) + except Exception: + try: + wrapper.add(cw) + except Exception: + pass + + # Encourage cw and its children to expand/fill so layout behaves like Qt + try: + if hasattr(cw, 'set_hexpand'): + cw.set_hexpand(True) + if hasattr(cw, 'set_vexpand'): + cw.set_vexpand(True) + if hasattr(cw, 'set_halign'): + cw.set_halign(Gtk.Align.FILL) + if hasattr(cw, 'set_valign'): + cw.set_valign(Gtk.Align.FILL) + except Exception: + pass + try: + for inner in list(getattr(cw, 'get_children', lambda: [])()): + try: + if hasattr(inner, 'set_hexpand'): + inner.set_hexpand(True) + except Exception: + pass + try: + if hasattr(inner, 'set_vexpand'): + inner.set_vexpand(True) + except Exception: + pass + try: + if hasattr(inner, 'set_halign'): + inner.set_halign(Gtk.Align.FILL) + except Exception: + pass + try: + if hasattr(inner, 'set_valign'): + inner.set_valign(Gtk.Align.FILL) + except Exception: + pass + except Exception: + pass + + # Remove previous stack children + try: + for c in list(self._content.get_children()): + try: + self._content.remove(c) + except Exception: + pass + except Exception: + pass + + # Add wrapper to stack and show it + name = f"child_{self._stack_page_counter}" + self._stack_page_counter += 1 + try: + self._content.add_titled(wrapper, name, name) + except Exception: + try: + self._content.add(wrapper) + except Exception: + pass + try: + self._content.set_visible_child(wrapper) + except Exception: + pass + try: + wrapper.set_visible(True) + except Exception: + pass + except Exception: + pass + except Exception: + pass + # return False to run only once + return False + + try: + GLib.idle_add(_do_attach) + except Exception: + _do_attach() + except Exception: + pass + + def showChild(self): + """Attach child backend and then force a dialog relayout/redraw.""" + if not (self._backend_widget and self._content and self.child()): + self._logger.debug("showChild called but no backend or no child to attach") + return + # Force a dialog layout/reallocation and redraw (GTK4 best-effort) + try: + dlg = self.findDialog() + win = getattr(dlg, "_window", None) if dlg is not None else None + if win is not None: + try: + if hasattr(win, "queue_resize"): + win.queue_resize() + except Exception: + pass + try: + if hasattr(win, "queue_allocate"): + win.queue_allocate() + except Exception: + pass + try: + ctx = GLib.MainContext.default() + if ctx is not None: + while ctx.pending(): + ctx.iteration(False) + except Exception: + pass + try: + def _idle_relayout(): + try: + if hasattr(win, "queue_resize"): + win.queue_resize() + except Exception: + pass + return False + GLib.idle_add(_idle_relayout) + except Exception: + pass + # Also ensure stack shows the current child and the window updates + try: + if self._content is not None: + try: + cw = None + # get visible child if possible + try: + cw = self._content.get_visible_child() + except Exception: + # fallback: use first child + try: + children = self._content.get_children() + cw = children[0] if children else None + except Exception: + cw = None + if cw is not None: + try: + cw.set_visible(True) + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def deleteChildren(self): + """Clear logical children and content UI for replacement.""" + try: + self._clear_content() + super().deleteChildren() + except Exception as e: + try: + self._logger.error("deleteChildren error: %s", e, exc_info=True) + except Exception: + pass diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index a220b5c..f968a96 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -259,6 +259,10 @@ def createTable(self, parent, header: YTableHeader, multiSelection: bool = False from .backends.gtk.tablegtk import YTableGtk return YTableGtk(parent, header, multiSelection) + def createReplacePoint(self, parent): + """Create a ReplacePoint widget (GTK backend).""" + return YReplacePointGtk(parent) + def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) From b25c3dcf8146c7d724b371a62d6d7ac8240c1e9d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Tue, 30 Dec 2025 23:08:36 +0100 Subject: [PATCH 273/523] improved test on RP --- test/test_replacePoint.py | 52 +++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/test/test_replacePoint.py b/test/test_replacePoint.py index 538ebf4..b5e6ad6 100644 --- a/test/test_replacePoint.py +++ b/test/test_replacePoint.py @@ -7,14 +7,20 @@ # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -# Basic logging setup -logger = logging.getLogger("manatools.test.replacepoint") -if not logging.getLogger().handlers: - h = logging.StreamHandler() - h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) - logging.getLogger().addHandler(h) - logging.getLogger().setLevel(logging.INFO) - logger.setLevel(logging.INFO) +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') + root_logger = logging.getLogger() + fileHandler = logging.FileHandler(log_name, mode='w') + fileHandler.setFormatter(logFormatter) + root_logger.addHandler(fileHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + root_logger.addHandler(consoleHandler) + consoleHandler.setLevel(logging.INFO) + root_logger.setLevel(logging.DEBUG) +except Exception as _e: + logging.getLogger().exception("Failed to configure file logger: %s", _e) def test_replace_point(backend_name=None): """Interactive test for ReplacePoint widget across backends. @@ -24,10 +30,10 @@ def test_replace_point(backend_name=None): - Replaces it at runtime using deleteChildren() and showChild() """ if backend_name: - logger.info("Setting backend to: %s", backend_name) + root_logger.info("Setting backend to: %s", backend_name) os.environ['YUI_BACKEND'] = backend_name else: - logger.info("Using auto-detection") + root_logger.info("Using auto-detection") try: from manatools.aui.yui import YUI, YUI_ui import manatools.aui.yui_common as yui @@ -37,12 +43,12 @@ def test_replace_point(backend_name=None): YUI._backend = None backend = YUI.backend() - logger.info("Using backend: %s", backend.value) + root_logger.info("Using backend: %s", backend.value) ui = YUI_ui() factory = ui.widgetFactory() - ui.application().setApplicationTitle("Test ReplacePoint") + ui.application().setApplicationTitle(f"Test {backend.value} ReplacePoint") dialog = factory.createPopupDialog() mainVbox = factory.createVBox(dialog) frame = factory.createFrame(mainVbox, "Replace Point here") @@ -51,9 +57,15 @@ def test_replace_point(backend_name=None): # Initial child layout vbox1 = factory.createVBox(rp) + # hint: allow vertical stretch to avoid collapsed area in some backends + import manatools.aui.yui_common as yui + try: + vbox1.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass label1 = factory.createLabel(vbox1, "Initial child layout") value_btn1 = factory.createPushButton(vbox1, "Value 1") - rp.showChild() + #rp.showChild() hbox = factory.createHBox(mainVbox) replace_button = factory.createPushButton(hbox, "Replace child") @@ -70,7 +82,7 @@ def test_replace_point(backend_name=None): elif typ == yui.YEventType.WidgetEvent: wdg = event.widget() try: - logger.info("WidgetEvent from: %s", getattr(wdg, "widgetClass", lambda: "?")()) + root_logger.info("WidgetEvent from: %s", getattr(wdg, "widgetClass", lambda: "?")()) except Exception: pass if wdg == close_button: @@ -79,20 +91,24 @@ def test_replace_point(backend_name=None): elif wdg == replace_button: # Replace the child content dynamically n_call = (n_call + 1) % 100 - logger.info("Replacing child layout iteration=%d", n_call) + root_logger.info("Replacing child layout iteration=%d", n_call) rp.deleteChildren() vbox2 = factory.createVBox(rp) + try: + vbox2.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass label2 = factory.createLabel(vbox2, f"Replaced child layout ({n_call})") value_btn2 = factory.createPushButton(vbox2, f"Value ({n_call})") rp.showChild() elif wdg == value_btn1: # no-op; just ensure button works - logger.info("Value 1 clicked") + root_logger.info("Value 1 clicked") else: # Handle events from new child too - logger.debug("Unhandled widget event") + root_logger.debug("Unhandled widget event") except Exception as e: - logger.error("Error testing ReplacePoint with backend %s: %s", backend_name, e, exc_info=True) + root_logger.error("Error testing ReplacePoint with backend %s: %s", backend_name, e, exc_info=True) if __name__ == "__main__": if len(sys.argv) > 1: From e222ca43c381e58135c602e4242c3c84db0a062d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 14:50:14 +0100 Subject: [PATCH 274/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 8eb9b74..648ec08 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -24,7 +24,7 @@ Missing Widgets comparing libyui original factory: [X] YMenuBar [ ] YSpacing (detailed variants: createHStretch/createVStretch/createHSpacing/createVSpacing/createSpacing) [X] YAlignment helpers (createLeft/createRight/createTop/createBottom/createHCenter/createVCenter/createHVCenter) - [ ] YReplacePoint + [X] YReplacePoint [X] YRadioButton [ ] YWizard [ ] YBusyIndicator From 58a897ae3e7602a4886c892b2c55899b102c0f1a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 15:51:41 +0100 Subject: [PATCH 275/523] Qt IntField start implementation --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/intfieldqt.py | 133 ++++++++++++++++++++++++ manatools/aui/yui_qt.py | 3 + test/test_intfield.py | 115 ++++++++++++++++++++ 4 files changed, 253 insertions(+) create mode 100644 manatools/aui/backends/qt/intfieldqt.py create mode 100644 test/test_intfield.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 03fc27b..33cd557 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -17,6 +17,7 @@ from .richtextqt import YRichTextQt from .menubarqt import YMenuBarQt from .replacepointqt import YReplacePointQt +from .intfieldqt import YIntFieldQt __all__ = [ @@ -39,5 +40,6 @@ "YRichTextQt", "YMenuBarQt", "YReplacePointQt", + "YIntFieldQt", # ... ] diff --git a/manatools/aui/backends/qt/intfieldqt.py b/manatools/aui/backends/qt/intfieldqt.py new file mode 100644 index 0000000..eb7eb29 --- /dev/null +++ b/manatools/aui/backends/qt/intfieldqt.py @@ -0,0 +1,133 @@ +# vim: set fileencoding=utf-8 : +from PySide6 import QtWidgets +import logging +from ...yui_common import * + + +class YIntFieldQt(YWidget): + def __init__(self, parent=None, label="", minValue=0, maxValue=100, initialValue=0): + super().__init__(parent) + self._label = label + self._min = int(minValue) + self._max = int(maxValue) + self._value = int(initialValue) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + + def widgetClass(self): + return "YIntField" + + def value(self): + return int(self._value) + + def setValue(self, val): + try: + v = int(val) + except Exception: + try: + self._logger.debug("setValue: invalid int %r", val) + except Exception: + pass + return + if v < self._min: + v = self._min + if v > self._max: + v = self._max + self._value = v + try: + if getattr(self, '_spinbox', None) is not None: + try: + self._spinbox.setValue(self._value) + except Exception: + pass + except Exception: + pass + + def minValue(self): + return int(self._min) + + def maxValue(self): + return int(self._max) + + def setMinValue(self, val): + try: + self._min = int(val) + except Exception: + return + try: + if getattr(self, '_spinbox', None) is not None: + try: + self._spinbox.setMinimum(self._min) + except Exception: + pass + except Exception: + pass + + def setMaxValue(self, val): + try: + self._max = int(val) + except Exception: + return + try: + if getattr(self, '_spinbox', None) is not None: + try: + self._spinbox.setMaximum(self._max) + except Exception: + pass + except Exception: + pass + + def label(self): + return self._label + + def _create_backend_widget(self): + try: + container = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(container) + layout.setContentsMargins(0,0,0,0) + if self._label: + lbl = QtWidgets.QLabel(self._label) + layout.addWidget(lbl) + + spin = QtWidgets.QSpinBox() + try: + spin.setRange(self._min, self._max) + spin.setValue(self._value) + except Exception: + pass + try: + spin.valueChanged.connect(self._on_value_changed) + except Exception: + pass + layout.addWidget(spin) + + self._backend_widget = container + self._spinbox = spin + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + except Exception as e: + try: + logging.getLogger("manatools.aui.qt.intfield").exception("Error creating Qt IntField backend: %s", e) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, '_spinbox', None) is not None: + try: + self._spinbox.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_value_changed(self, val): + try: + self._value = int(val) + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception : + pass diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index f14bc33..a4f96bd 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -162,6 +162,9 @@ def createHeading(self, parent, label): def createInputField(self, parent, label, password_mode=False): return YInputFieldQt(parent, label, password_mode) + + def createIntField(self, parent, label, minVal, maxVal, initialVal): + return YIntFieldQt(parent, label, minVal, maxVal, initialVal) def createCheckBox(self, parent, label, is_checked=False): return YCheckBoxQt(parent, label, is_checked) diff --git a/test/test_intfield.py b/test/test_intfield.py new file mode 100644 index 0000000..6f3980d --- /dev/null +++ b/test/test_intfield.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Interactive test for YIntField across all backends. + +- Creates two int fields and two labels that display their values. +- Has an OK button to exit. +- Logs exceptions and backend creation issues. +""" +import os +import sys +import logging + +# Ensure project root on PYTHONPATH +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') + root_logger = logging.getLogger() + fileHandler = logging.FileHandler(log_name, mode='w') + fileHandler.setFormatter(logFormatter) + root_logger.addHandler(fileHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + root_logger.addHandler(consoleHandler) + consoleHandler.setLevel(logging.INFO) + root_logger.setLevel(logging.DEBUG) +except Exception as _e: + logging.getLogger().exception("Failed to configure file logger: %s", _e) + + +def test_intfield(backend_name=None): + if backend_name: + os.environ['YUI_BACKEND'] = backend_name + logging.getLogger().info("Set backend to %s", backend_name) + else: + root_logger.info("Using auto-detection") + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + root_logger.info("Using backend: %s", backend.value) + + ui = YUI_ui() + factory = ui.widgetFactory() + + ui.application().setApplicationTitle(f"Test {backend.value} IntField") + dlg = factory.createPopupDialog() + v = factory.createVBox(dlg) + + # Left column with intfields + h = factory.createHBox(v) + col1 = factory.createVBox(h) + col2 = factory.createVBox(h) + + int1 = factory.createIntField(col1, "First", 0, 100, 10) + int2 = factory.createIntField(col1, "Second", -50, 50, 0) + + lab1 = factory.createLabel(col2, "Value 1: 10") + lab2 = factory.createLabel(col2, "Value 2: 0") + + ok = factory.createPushButton(v, "OK") + + try: + # make int fields vertically stretchable if supported + int1.setStretchable(yui.YUIDimension.YD_VERT, True) + int2.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + + while True: + ev = dlg.waitForEvent() + if not ev: + continue + if ev.eventType() == yui.YEventType.CancelEvent: + dlg.destroy() + break + if ev.eventType() == yui.YEventType.WidgetEvent: + w = ev.widget() + try: + logging.getLogger().debug("WidgetEvent from %s", getattr(w, 'widgetClass', lambda: '?')()) + except Exception: + pass + if w == ok: + dlg.destroy() + break + # Update labels if values change; some backends post events differently + try: + v1 = int1.value() + v2 = int2.value() + try: + lab1.setValue(f"Value 1: {v1}") + except Exception: + pass + try: + lab2.setValue(f"Value 2: {v2}") + except Exception: + pass + logging.getLogger().debug("Int values: %s, %s", v1, v2) + except Exception: + logging.getLogger().exception("Failed to read intfield values") + + except Exception as e: + logging.getLogger().exception("Error in IntField test: %s", e) + + +if __name__ == '__main__': + if len(sys.argv) > 1: + test_intfield(sys.argv[1]) + else: + test_intfield() From 10cdbb0a53fc5e02f0fd169e61382fdd465b0d44 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 16:25:19 +0100 Subject: [PATCH 276/523] Honored stretching and label up --- manatools/aui/backends/qt/intfieldqt.py | 135 ++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/manatools/aui/backends/qt/intfieldqt.py b/manatools/aui/backends/qt/intfieldqt.py index eb7eb29..7acb81f 100644 --- a/manatools/aui/backends/qt/intfieldqt.py +++ b/manatools/aui/backends/qt/intfieldqt.py @@ -1,5 +1,15 @@ # vim: set fileencoding=utf-8 : -from PySide6 import QtWidgets +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore import logging from ...yui_common import * @@ -82,11 +92,35 @@ def label(self): def _create_backend_widget(self): try: container = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(container) - layout.setContentsMargins(0,0,0,0) + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) if self._label: lbl = QtWidgets.QLabel(self._label) + try: + lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) + except Exception: + try: + lbl.setAlignment(QtCore.Qt.AlignLeft) + except Exception: + pass + # keep label from expanding vertically + try: + sp_lbl = lbl.sizePolicy() + try: + sp_lbl.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Preferred) + sp_lbl.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Fixed) + except Exception: + try: + sp_lbl.setHorizontalPolicy(QtWidgets.QSizePolicy.Preferred) + sp_lbl.setVerticalPolicy(QtWidgets.QSizePolicy.Fixed) + except Exception: + pass + lbl.setSizePolicy(sp_lbl) + except Exception: + pass layout.addWidget(lbl) + self._label_widget = lbl spin = QtWidgets.QSpinBox() try: @@ -106,6 +140,11 @@ def _create_backend_widget(self): self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass + # apply initial size policy from stretchable hints + try: + self._apply_size_policy() + except Exception: + pass except Exception as e: try: logging.getLogger("manatools.aui.qt.intfield").exception("Error creating Qt IntField backend: %s", e) @@ -121,13 +160,95 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + try: + if getattr(self, '_label_widget', None) is not None: + try: + self._label_widget.setEnabled(bool(enabled)) + except Exception: + pass + except Exception: + pass def _on_value_changed(self, val): try: self._value = int(val) + except Exception: + return + # If notify is enabled, post a ValueChanged widget event + try: if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - except Exception : + dlg = self.findDialog() + if dlg is not None: + try: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + try: + self._logger.debug("Failed to post ValueChanged event") + except Exception: + pass + except Exception: + pass + + def _apply_size_policy(self): + """Apply size policy to container and spinbox according to stretchable flags.""" + try: + # horizontal policy + try: + horiz = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Fixed + except Exception: + self._logger.debug("Failed to get horizontal stretchable policy") + try: + horiz = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Fixed + except Exception: + self._logger.debug("Failed to get horizontal stretchable policy fallback") + horiz = QtWidgets.QSizePolicy.Preferred + # vertical policy + try: + vert = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed + except Exception: + try: + vert = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed + except Exception: + vert = QtWidgets.QSizePolicy.Preferred + + if getattr(self, '_backend_widget', None) is not None: + try: + sp = self._backend_widget.sizePolicy() + try: + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + except Exception: + pass + try: + self._backend_widget.setSizePolicy(sp) + except Exception: + pass + except Exception: + pass + + if getattr(self, '_spinbox', None) is not None: + try: + sp2 = self._spinbox.sizePolicy() + try: + sp2.setHorizontalPolicy(horiz) + sp2.setVerticalPolicy(vert) + except Exception: + pass + try: + self._spinbox.setSizePolicy(sp2) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_size_policy() + except Exception: pass From bcc8d1f94655635b1e0149c85e8d346a2cb84d1a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 16:57:54 +0100 Subject: [PATCH 277/523] First gtk IntField implementation --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/intfieldgtk.py | 216 ++++++++++++++++++++++ manatools/aui/yui_gtk.py | 2 + 3 files changed, 220 insertions(+) create mode 100644 manatools/aui/backends/gtk/intfieldgtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index ffd8e8d..498228f 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -17,6 +17,7 @@ from .richtextgtk import YRichTextGtk from .menubargtk import YMenuBarGtk from .replacepointgtk import YReplacePointGtk +from .intfieldgtk import YIntFieldGtk __all__ = [ "YDialogGtk", @@ -38,5 +39,6 @@ "YRichTextGtk", "YMenuBarGtk", "YReplacePointGtk", + "YIntFieldGtk", # ... ] diff --git a/manatools/aui/backends/gtk/intfieldgtk.py b/manatools/aui/backends/gtk/intfieldgtk.py new file mode 100644 index 0000000..9a7b090 --- /dev/null +++ b/manatools/aui/backends/gtk/intfieldgtk.py @@ -0,0 +1,216 @@ +import logging +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GLib +from ...yui_common import * + + +class YIntFieldGtk(YWidget): + def __init__(self, parent=None, label="", minValue=0, maxValue=100, initialValue=0): + super().__init__(parent) + self._label = label + self._min = int(minValue) + self._max = int(maxValue) + self._value = int(initialValue) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + + def widgetClass(self): + return "YIntField" + + def value(self): + return int(self._value) + + def setValue(self, val): + try: + v = int(val) + except Exception: + try: + self._logger.debug("setValue: invalid int %r", val) + except Exception: + pass + return + if v < self._min: + v = self._min + if v > self._max: + v = self._max + self._value = v + try: + if getattr(self, '_spin', None) is not None: + try: + self._spin.set_value(self._value) + except Exception: + pass + except Exception: + pass + + def minValue(self): + return int(self._min) + + def maxValue(self): + return int(self._max) + + def setMinValue(self, val): + try: + self._min = int(val) + except Exception: + return + try: + if getattr(self, '_adj', None) is not None: + try: + self._adj.set_lower(self._min) + except Exception: + pass + except Exception: + pass + + def setMaxValue(self, val): + try: + self._max = int(val) + except Exception: + return + try: + if getattr(self, '_adj', None) is not None: + try: + self._adj.set_upper(self._max) + except Exception: + pass + except Exception: + pass + + def label(self): + return self._label + + def _create_backend_widget(self): + try: + # vertical layout: label above spin so they're attached + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) + if self._label: + lbl = Gtk.Label(label=self._label) + try: + lbl.set_halign(Gtk.Align.START) + except Exception: + try: + lbl.set_xalign(0.0) + except Exception: + pass + # keep label from expanding vertically + try: + lbl.set_hexpand(False) + lbl.set_vexpand(False) + except Exception: + pass + box.append(lbl) + + # Create adjustment and spinbutton + adj = Gtk.Adjustment(value=self._value, lower=self._min, upper=self._max, step_increment=1, page_increment=10) + spin = Gtk.SpinButton(adjustment=adj) + try: + spin.set_value(self._value) + except Exception: + pass + try: + spin.connect('value-changed', self._on_value_changed) + except Exception: + pass + # Attach spin below the label and let _apply_size_policy control expansion + box.append(spin) + + self._backend_widget = box + self._spin = spin + self._adj = adj + try: + self._label_widget = lbl + except Exception: + self._label_widget = None + # apply size policy according to stretchable hints (do not expand by default) + try: + self._apply_size_policy() + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + except Exception as e: + try: + logging.getLogger('manatools.aui.gtk.intfield').exception("Error creating GTK IntField backend: %s", e) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, '_spin', None) is not None: + try: + self._spin.set_sensitive(bool(enabled)) + except Exception: + pass + except Exception: + pass + try: + if getattr(self, '_label_widget', None) is not None: + try: + self._label_widget.set_sensitive(bool(enabled)) + except Exception: + pass + except Exception: + pass + + def _on_value_changed(self, spin): + try: + v = int(spin.get_value()) + self._value = v + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + + def _apply_size_policy(self): + """Apply hexpand/vexpand flags according to `stretchable` hints. + + Default: do not expand (respect user's setStretchable).""" + try: + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + horiz = False + try: + vert = bool(self.stretchable(YUIDimension.YD_VERT)) + except Exception: + vert = False + + try: + if getattr(self, '_backend_widget', None) is not None: + try: + self._backend_widget.set_hexpand(horiz) + except Exception: + pass + try: + self._backend_widget.set_vexpand(vert) + except Exception: + pass + except Exception: + pass + + try: + if getattr(self, '_spin', None) is not None: + try: + self._spin.set_hexpand(horiz) + except Exception: + pass + try: + self._spin.set_vexpand(vert) + except Exception: + pass + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_size_policy() + except Exception: + pass diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index f968a96..30d3cc6 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -207,6 +207,8 @@ def createHeading(self, parent, label): def createInputField(self, parent, label, password_mode=False): return YInputFieldGtk(parent, label, password_mode) + def createIntField(self, parent, label, minVal, maxVal, initialVal): + return YIntFieldGtk(parent, label, minVal, maxVal, initialVal) def createCheckBox(self, parent, label, is_checked=False): return YCheckBoxGtk(parent, label, is_checked) From 5ffbe20f960ca682e794349b438a34761a1e7ff5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 16:59:11 +0100 Subject: [PATCH 278/523] header --- manatools/aui/backends/gtk/intfieldgtk.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/manatools/aui/backends/gtk/intfieldgtk.py b/manatools/aui/backends/gtk/intfieldgtk.py index 9a7b090..68e6387 100644 --- a/manatools/aui/backends/gtk/intfieldgtk.py +++ b/manatools/aui/backends/gtk/intfieldgtk.py @@ -1,3 +1,14 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' import logging import gi gi.require_version('Gtk', '4.0') From 205a096996efe8e2b85cbeb037a658e7a15a0048 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 17:02:37 +0100 Subject: [PATCH 279/523] added size policy --- manatools/aui/backends/gtk/labelgtk.py | 40 +++++++++++++++++++++ manatools/aui/backends/qt/labelqt.py | 50 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py index fe716b7..37dc067 100644 --- a/manatools/aui/backends/gtk/labelgtk.py +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -112,6 +112,11 @@ def _create_backend_widget(self): self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass + try: + # apply initial size policy according to any stretch hints + self._apply_size_policy() + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the label widget backend.""" @@ -123,3 +128,38 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def _apply_size_policy(self): + """Apply `hexpand`/`vexpand` on the Gtk.Label according to model stretchable hints.""" + try: + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + horiz = False + try: + vert = bool(self.stretchable(YUIDimension.YD_VERT)) + except Exception: + vert = False + try: + if getattr(self, '_backend_widget', None) is not None: + try: + if hasattr(self._backend_widget, 'set_hexpand'): + self._backend_widget.set_hexpand(horiz) + except Exception: + pass + try: + if hasattr(self._backend_widget, 'set_vexpand'): + self._backend_widget.set_vexpand(vert) + except Exception: + pass + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_size_policy() + except Exception: + pass diff --git a/manatools/aui/backends/qt/labelqt.py b/manatools/aui/backends/qt/labelqt.py index 4c8fa99..f5400f2 100644 --- a/manatools/aui/backends/qt/labelqt.py +++ b/manatools/aui/backends/qt/labelqt.py @@ -92,6 +92,11 @@ def _create_backend_widget(self): self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass + try: + # apply initial size policy according to any stretch hints + self._apply_size_policy() + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the QLabel backend.""" @@ -103,3 +108,48 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def _apply_size_policy(self): + """Apply Qt size policy based on `stretchable` hints.""" + try: + try: + horiz = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Preferred + except Exception: + try: + horiz = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Preferred + except Exception: + horiz = QtWidgets.QSizePolicy.Preferred + try: + vert = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Preferred + except Exception: + try: + vert = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Preferred + except Exception: + vert = QtWidgets.QSizePolicy.Preferred + + if getattr(self, '_backend_widget', None) is not None: + try: + sp = self._backend_widget.sizePolicy() + try: + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + except Exception: + pass + try: + self._backend_widget.setSizePolicy(sp) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_size_policy() + except Exception: + pass From 5e0042c941f6e8cb5e9c346b11c96759a9ab0b3d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 17:15:39 +0100 Subject: [PATCH 280/523] ncurses intfield --- manatools/aui/backends/curses/__init__.py | 2 + .../aui/backends/curses/intfieldcurses.py | 166 ++++++++++++++++++ manatools/aui/yui_curses.py | 2 + 3 files changed, 170 insertions(+) create mode 100644 manatools/aui/backends/curses/intfieldcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index ce273e5..6332897 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -17,6 +17,7 @@ from .richtextcurses import YRichTextCurses from .menubarcurses import YMenuBarCurses from .replacepointcurses import YReplacePointCurses +from .intfieldcurses import YIntFieldCurses __all__ = [ "YDialogCurses", @@ -38,5 +39,6 @@ "YRichTextCurses", "YMenuBarCurses", "YReplacePointCurses", + "YIntFieldCurses", # ... ] diff --git a/manatools/aui/backends/curses/intfieldcurses.py b/manatools/aui/backends/curses/intfieldcurses.py new file mode 100644 index 0000000..80be2df --- /dev/null +++ b/manatools/aui/backends/curses/intfieldcurses.py @@ -0,0 +1,166 @@ +import curses +import logging +from ...yui_common import * + +_mod_logger = logging.getLogger("manatools.aui.curses.intfield.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + + +class YIntFieldCurses(YWidget): + def __init__(self, parent=None, label="", minValue=0, maxValue=100, initialValue=0): + super().__init__(parent) + self._label = label + self._min = int(minValue) + self._max = int(maxValue) + self._value = int(initialValue) + # height: 2 lines if label present (label above control), else 1 + self._height = 2 if bool(self._label) else 1 + self._focused = False + self._can_focus = True + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and _mod_logger.handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + + def widgetClass(self): + return "YIntField" + + def value(self): + return int(self._value) + + def setValue(self, val): + try: + v = int(val) + except Exception: + return + if v < self._min: + v = self._min + if v > self._max: + v = self._max + self._value = v + + def label(self): + return self._label + + def setLabel(self, label): + self._label = label + try: + self._height = 2 if bool(self._label) else 1 + except Exception: + pass + + def _create_backend_widget(self): + try: + self._backend_widget = self + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception as e: + try: + self._logger.exception("Error creating curses IntField backend: %s", e) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + # curses backend: affect focusability if needed + if not enabled: + self._can_focus = False + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + # Draw label on the first line, control on the second line (if available) + line_label = y + line_ctrl = y if height <= 1 else y + 1 + + # Draw label (if present) on its line + if self._label: + try: + lbl_txt = str(self._label) + ':' + # truncate if necessary + lbl_out = lbl_txt[:max(0, width)] + window.addstr(line_label, x, lbl_out) + except Exception: + pass + + # Remaining width for the control + ctrl_x = x + ctrl_width = max(1, width) + + # Format control as: ↓ ↑ + try: + up_ch = '↑' + down_ch = '↓' + except Exception: + up_ch = '^' + down_ch = 'v' + + num_s = str(self._value) + inner_width = max(1, ctrl_width - 4) + if len(num_s) > inner_width: + num_s = num_s[-inner_width:] + + pad_left = max(0, (inner_width - len(num_s)) // 2) + pad_right = inner_width - len(num_s) - pad_left + display = f"{down_ch} " + (' ' * pad_left) + num_s + (' ' * pad_right) + f" {up_ch}" + + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if getattr(self, '_focused', False) else curses.A_NORMAL + + try: + window.addstr(line_ctrl, ctrl_x, display[:ctrl_width], attr) + except Exception: + try: + window.addstr(line_ctrl, ctrl_x, num_s[:ctrl_width], attr) + except Exception: + pass + except curses.error as e: + try: + self._logger.error("_draw curses.error: %s", e, exc_info=True) + except Exception: + pass + + def _handle_key(self, key): + """Handle keys for focusable spin-like behaviour: up/down to change value.""" + if not getattr(self, '_focused', False) or not self.isEnabled(): + return False + + changed = False + try: + if key == curses.KEY_UP: + if self._value < self._max: + self._value += 1 + changed = True + elif key == curses.KEY_DOWN: + if self._value > self._min: + self._value -= 1 + changed = True + else: + return False + except Exception: + return False + + if changed: + # Force dialog redraw next loop + dlg = self.findDialog() + if dlg is not None: + try: + dlg._last_draw_time = 0 + except Exception: + pass + # Emit value changed event if notify enabled + try: + if self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + try: + self._logger.debug("Failed to post ValueChanged event") + except Exception: + pass + return True diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index fedf7e4..0ed76db 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -155,6 +155,8 @@ def createHeading(self, parent, label): def createInputField(self, parent, label, password_mode=False): return YInputFieldCurses(parent, label, password_mode) + def createIntField(self, parent, label, minVal, maxVal, initialVal): + return YIntFieldCurses(parent, label, minVal, maxVal, initialVal) def createPushButton(self, parent, label): return YPushButtonCurses(parent, label) From f51396353d5d0a4984327c83a8671de2f27ecf88 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 17:20:05 +0100 Subject: [PATCH 281/523] avoid trunking last char --- manatools/aui/backends/curses/labelcurses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index 5de159a..da52a93 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -98,7 +98,7 @@ def _desired_height_for_width(self, width: int) -> int: if para == "": total += 1 else: - wrapped = textwrap.wrap(para, width=max(1, width-1), break_long_words=True, break_on_hyphens=False) or [""] + wrapped = textwrap.wrap(para, width=max(1, width), break_long_words=True, break_on_hyphens=False) or [""] total += len(wrapped) return max(1, total) else: @@ -144,7 +144,7 @@ def _draw(self, window, y, x, width, height): if para == "": lines.append("") else: - wrapped_para = textwrap.wrap(para, width=max(1, width-1), break_long_words=True, break_on_hyphens=False) or [""] + wrapped_para = textwrap.wrap(para, width=max(1, width), break_long_words=True, break_on_hyphens=False) or [""] lines.extend(wrapped_para) else: # No auto-wrap: respect explicit newlines but do not reflow paragraphs @@ -157,7 +157,7 @@ def _draw(self, window, y, x, width, height): max_lines = max(1, height) for i, line in enumerate(lines[:max_lines]): try: - window.addstr(y + i, x, line[:max(0, width-1)], attr) + window.addstr(y + i, x, line[:max(0, width)], attr) except curses.error: pass except curses.error as e: From af61bb2116e1885fdd9329ee2089eb3345515814 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 17:29:00 +0100 Subject: [PATCH 282/523] improved to insert also the value directly --- .../aui/backends/curses/intfieldcurses.py | 159 +++++++++++++++--- 1 file changed, 133 insertions(+), 26 deletions(-) diff --git a/manatools/aui/backends/curses/intfieldcurses.py b/manatools/aui/backends/curses/intfieldcurses.py index 80be2df..b1c7925 100644 --- a/manatools/aui/backends/curses/intfieldcurses.py +++ b/manatools/aui/backends/curses/intfieldcurses.py @@ -1,3 +1,14 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' import curses import logging from ...yui_common import * @@ -21,6 +32,9 @@ def __init__(self, parent=None, label="", minValue=0, maxValue=100, initialValue self._height = 2 if bool(self._label) else 1 self._focused = False self._can_focus = True + # editing buffer when user types numbers + self._editing = False + self._edit_buffer = "" self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") if not self._logger.handlers and _mod_logger.handlers: for h in _mod_logger.handlers: @@ -42,6 +56,12 @@ def setValue(self, val): if v > self._max: v = self._max self._value = v + # reset edit buffer when value programmatically changed + try: + self._editing = False + self._edit_buffer = "" + except Exception: + pass def label(self): return self._label @@ -99,7 +119,17 @@ def _draw(self, window, y, x, width, height): up_ch = '^' down_ch = 'v' - num_s = str(self._value) + # hide arrows when at limits: show space to preserve layout + try: + if self._value >= self._max: + up_ch = ' ' + if self._value <= self._min: + down_ch = ' ' + except Exception: + pass + + # if editing, show the edit buffer; otherwise show committed value + num_s = self._edit_buffer if getattr(self, '_editing', False) and self._edit_buffer != None and self._edit_buffer != "" else str(self._value) inner_width = max(1, ctrl_width - 4) if len(num_s) > inner_width: num_s = num_s[-inner_width:] @@ -130,37 +160,114 @@ def _handle_key(self, key): """Handle keys for focusable spin-like behaviour: up/down to change value.""" if not getattr(self, '_focused', False) or not self.isEnabled(): return False - - changed = False try: + # If currently editing, handle editing keys + if getattr(self, '_editing', False): + # Enter -> commit + if key == ord('\n') or key == 10 or key == 13: + try: + val = int(self._edit_buffer) + # clamp + if val < self._min: + val = self._min + if val > self._max: + val = self._max + old = self._value + self._value = val + self._editing = False + self._edit_buffer = "" + # redraw + dlg = self.findDialog() + if dlg is not None: + try: + dlg._last_draw_time = 0 + except Exception: + pass + try: + if self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + except Exception: + # invalid buffer: cancel edit and keep previous value + self._editing = False + self._edit_buffer = "" + return True + # ESC -> cancel edit + if key == 27: + self._editing = False + self._edit_buffer = "" + return True + # Backspace/delete + if key in (curses.KEY_BACKSPACE, 127, 8): + try: + if self._edit_buffer: + self._edit_buffer = self._edit_buffer[:-1] + except Exception: + pass + return True + # digits or leading minus + if 48 <= key <= 57 or key == ord('-'): + ch = chr(key) + try: + # prevent multiple leading zeros weirdness; accept minus only at start + if ch == '-' and self._edit_buffer == "": + self._edit_buffer = ch + elif ch.isdigit(): + self._edit_buffer += ch + # else ignore + except Exception: + pass + return True + # ignore other keys while editing + return False + + # Not editing: handle navigation and start-edit keys + # Arrow up/down adjust value if key == curses.KEY_UP: if self._value < self._max: self._value += 1 - changed = True - elif key == curses.KEY_DOWN: + # notify and redraw + dlg = self.findDialog() + if dlg is not None: + try: + dlg._last_draw_time = 0 + except Exception: + pass + try: + if self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + return True + if key == curses.KEY_DOWN: if self._value > self._min: self._value -= 1 - changed = True - else: - return False - except Exception: - return False + dlg = self.findDialog() + if dlg is not None: + try: + dlg._last_draw_time = 0 + except Exception: + pass + try: + if self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + return True - if changed: - # Force dialog redraw next loop - dlg = self.findDialog() - if dlg is not None: + # Start editing on digit or minus + if 48 <= key <= 57 or key == ord('-'): try: - dlg._last_draw_time = 0 + ch = chr(key) + self._editing = True + self._edit_buffer = ch if ch != '+' else '' except Exception: - pass - # Emit value changed event if notify enabled - try: - if self.notify(): - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - except Exception: - try: - self._logger.debug("Failed to post ValueChanged event") - except Exception: - pass - return True + self._editing = True + self._edit_buffer = '' + return True + + except Exception: + return False + + return False From 6f89c44ccfdcddb051ad6cac808dd7d5412833d0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 17:44:26 +0100 Subject: [PATCH 283/523] Updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 648ec08..3de3641 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -20,7 +20,7 @@ Missing Widgets comparing libyui original factory: [X] YProgressBar [X] YRichText [ ] YMultiLineEdit - [ ] YIntField + [X] YIntField [X] YMenuBar [ ] YSpacing (detailed variants: createHStretch/createVStretch/createHSpacing/createVSpacing/createSpacing) [X] YAlignment helpers (createLeft/createRight/createTop/createBottom/createHCenter/createVCenter/createHVCenter) From bed4c4a1ec84ce23faa293d1095aaef9ee7ee02f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 17:52:13 +0100 Subject: [PATCH 284/523] Multi Line Edit for QT --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/multilineeditqt.py | 127 +++++++++++++++++++ manatools/aui/yui_qt.py | 3 + test/test_multilineedit.py | 97 ++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 manatools/aui/backends/qt/multilineeditqt.py create mode 100644 test/test_multilineedit.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 33cd557..588b7bf 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -18,6 +18,7 @@ from .menubarqt import YMenuBarQt from .replacepointqt import YReplacePointQt from .intfieldqt import YIntFieldQt +from .multilineeditqt import YMultiLineEditQt __all__ = [ @@ -41,5 +42,6 @@ "YMenuBarQt", "YReplacePointQt", "YIntFieldQt", + "YMultiLineEditQt", # ... ] diff --git a/manatools/aui/backends/qt/multilineeditqt.py b/manatools/aui/backends/qt/multilineeditqt.py new file mode 100644 index 0000000..8b48de5 --- /dev/null +++ b/manatools/aui/backends/qt/multilineeditqt.py @@ -0,0 +1,127 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains Qt backend for YMultiLineEdit + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +import logging +from ...yui_common import * +try: + from PySide6 import QtWidgets, QtCore +except Exception: + QtWidgets = None + QtCore = None + +_mod_logger = logging.getLogger("manatools.aui.qt.multiline.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + + +class YMultiLineEditQt(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + self._height = 4 + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + if not self._logger.handlers and _mod_logger.handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + + self._qwidget = None + + def widgetClass(self): + return "YMultiLineEdit" + + def value(self): + return str(self._value) + + def setValue(self, text): + try: + s = str(text) if text is not None else "" + except Exception: + s = "" + self._value = s + try: + if self._qwidget is not None: + try: + self._qtext.setPlainText(self._value) + except Exception: + pass + except Exception: + pass + + def label(self): + return self._label + + def setLabel(self, label): + try: + self._label = label + if self._qwidget is not None: + try: + self._qlbl.setText(str(label)) + except Exception: + pass + except Exception: + pass + + def _create_backend_widget(self): + try: + if QtWidgets is None: + raise ImportError("PySide6 not available") + self._qwidget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(self._qwidget) + layout.setContentsMargins(0, 0, 0, 0) + self._qlbl = QtWidgets.QLabel(str(self._label)) + self._qtext = QtWidgets.QPlainTextEdit() + self._qtext.setPlainText(self._value) + layout.addWidget(self._qlbl) + layout.addWidget(self._qtext) + try: + self._qtext.textChanged.connect(self._on_text_changed) + except Exception: + pass + self._backend_widget = self._qwidget + except Exception as e: + try: + self._logger.exception("Error creating Qt MultiLineEdit backend: %s", e) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if self._qwidget is not None: + try: + self._qtext.setReadOnly(not enabled) + except Exception: + pass + except Exception: + pass + + def _on_text_changed(self): + try: + text = self._qtext.toPlainText() + self._value = text + dlg = self.findDialog() + if dlg is not None: + try: + if self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + try: + self._logger.exception("Failed to post ValueChanged event") + except Exception: + pass + except Exception: + try: + self._logger.exception("_on_text_changed error") + except Exception: + pass diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index a4f96bd..fb4eff0 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -163,6 +163,9 @@ def createHeading(self, parent, label): def createInputField(self, parent, label, password_mode=False): return YInputFieldQt(parent, label, password_mode) + def createMultiLineEdit(self, parent, label): + return YMultiLineEditQt(parent, label) + def createIntField(self, parent, label, minVal, maxVal, initialVal): return YIntFieldQt(parent, label, minVal, maxVal, initialVal) diff --git a/test/test_multilineedit.py b/test/test_multilineedit.py new file mode 100644 index 0000000..f40b24f --- /dev/null +++ b/test/test_multilineedit.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Interactive test for YMultiLineEdit across backends. + +- Creates a multiline edit and an OK button to exit. +- Logs value-changed notifications and final value. +""" +import os +import sys +import logging + +# Ensure project root on PYTHONPATH +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') + root_logger = logging.getLogger() + fileHandler = logging.FileHandler(log_name, mode='w') + fileHandler.setFormatter(logFormatter) + root_logger.addHandler(fileHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + root_logger.addHandler(consoleHandler) + consoleHandler.setLevel(logging.INFO) + root_logger.setLevel(logging.DEBUG) +except Exception as _e: + logging.getLogger().exception("Failed to configure file logger: %s", _e) + + +def test_multilineedit(backend_name=None): + if backend_name: + os.environ['YUI_BACKEND'] = backend_name + logging.getLogger().info("Set backend to %s", backend_name) + else: + logging.getLogger().info("Using auto-detection") + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + logging.getLogger().info("Using backend: %s", backend.value) + + ui = YUI_ui() + factory = ui.widgetFactory() + + ui.application().setApplicationTitle(f"Test {backend.value} MultiLineEdit") + dlg = factory.createPopupDialog() + v = factory.createVBox(dlg) + + mled = factory.createMultiLineEdit(v, "Notes") + mled.setStretchable(yui.YUIDimension.YD_VERT, True) + mled.setStretchable(yui.YUIDimension.YD_HORIZ, True) + ok = factory.createPushButton(v, "OK") + + while True: + ev = dlg.waitForEvent() + if not ev: + continue + if ev.eventType() == yui.YEventType.CancelEvent: + dlg.destroy() + break + if ev.eventType() == yui.YEventType.WidgetEvent: + w = ev.widget() + try: + logging.getLogger().debug("WidgetEvent from %s", getattr(w, 'widgetClass', lambda: '?')()) + except Exception: + pass + if w == ok: + try: + logging.getLogger().info("Final value:\n%s", mled.value()) + except Exception: + logging.getLogger().exception("Failed to read final value") + dlg.destroy() + break + # Log value-changed notifications + try: + if hasattr(w, 'widgetClass') and w.widgetClass() == 'YMultiLineEdit': + try: + logging.getLogger().info("ValueChanged notification: %s", w.value()) + except Exception: + logging.getLogger().exception("Failed to read multiline value on notification") + except Exception: + logging.getLogger().exception("Error handling widget event") + + except Exception as e: + logging.getLogger().exception("Error in MultiLineEdit test: %s", e) + + +if __name__ == '__main__': + if len(sys.argv) > 1: + test_multilineedit(sys.argv[1]) + else: + test_multilineedit() From e61c112abd97edf2d5b0f5b8a7a74394fc837ed7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 17:53:42 +0100 Subject: [PATCH 285/523] use a better layout to test intfield --- test/test_intfield.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test/test_intfield.py b/test/test_intfield.py index 6f3980d..6a7739c 100644 --- a/test/test_intfield.py +++ b/test/test_intfield.py @@ -53,22 +53,27 @@ def test_intfield(backend_name=None): v = factory.createVBox(dlg) # Left column with intfields - h = factory.createHBox(v) - col1 = factory.createVBox(h) - col2 = factory.createVBox(h) + row1 = factory.createHBox(v) + row2 = factory.createHBox(v) + + int1 = factory.createIntField(row1, "First", 0, 100, 10) + int2 = factory.createIntField(row2, "Second", -50, 50, 0) - int1 = factory.createIntField(col1, "First", 0, 100, 10) - int2 = factory.createIntField(col1, "Second", -50, 50, 0) - - lab1 = factory.createLabel(col2, "Value 1: 10") - lab2 = factory.createLabel(col2, "Value 2: 0") + lab1 = factory.createLabel(factory.createVCenter(row1), "Value 1: 10") + lab2 = factory.createLabel(factory.createVCenter(row2), "Value 2: 0") ok = factory.createPushButton(v, "OK") try: - # make int fields vertically stretchable if supported - int1.setStretchable(yui.YUIDimension.YD_VERT, True) - int2.setStretchable(yui.YUIDimension.YD_VERT, True) + # make int fields not vertically stretchable + int1.setStretchable(yui.YUIDimension.YD_VERT, False) + int2.setStretchable(yui.YUIDimension.YD_VERT, False) + int1.setStretchable(yui.YUIDimension.YD_HORIZ, False) + int2.setStretchable(yui.YUIDimension.YD_HORIZ, False) + #lab1.setStretchable(yui.YUIDimension.YD_VERT, False) + #lab2.setStretchable(yui.YUIDimension.YD_VERT, False) + #lab1.setStretchable(yui.YUIDimension.YD_HORIZ, False) + #lab2.setStretchable(yui.YUIDimension.YD_HORIZ, False) except Exception: pass From ce630a5b1744f1a9f258edc21d69bd4faa51d795 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 18:18:34 +0100 Subject: [PATCH 286/523] Improved stretching properties --- manatools/aui/backends/qt/multilineeditqt.py | 172 ++++++++++++++++++- 1 file changed, 170 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/qt/multilineeditqt.py b/manatools/aui/backends/qt/multilineeditqt.py index 8b48de5..3e6255e 100644 --- a/manatools/aui/backends/qt/multilineeditqt.py +++ b/manatools/aui/backends/qt/multilineeditqt.py @@ -30,7 +30,12 @@ def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label self._value = "" - self._height = 4 + # default visible content lines (consistent across backends) + self._default_visible_lines = 3 + # -1 means no input length limit + self._input_max_length = -1 + # reported minimal height: content lines + label row (if present) + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") if not self._logger.handlers and _mod_logger.handlers: for h in _mod_logger.handlers: @@ -44,11 +49,40 @@ def widgetClass(self): def value(self): return str(self._value) + def inputMaxLength(self): + return int(getattr(self, '_input_max_length', -1)) + + def setInputMaxLength(self, numberOfChars): + try: + self._input_max_length = int(numberOfChars) + except Exception: + self._input_max_length = -1 + # enforce immediately on widget + try: + if self._qwidget is not None and self._input_max_length >= 0: + txt = self._qtext.toPlainText() + if len(txt) > self._input_max_length: + try: + self._qtext.blockSignals(True) + self._qtext.setPlainText(txt[:self._input_max_length]) + self._qtext.blockSignals(False) + self._value = self._qtext.toPlainText() + except Exception: + pass + except Exception: + pass + def setValue(self, text): try: s = str(text) if text is not None else "" except Exception: s = "" + # enforce input max length if set + try: + if getattr(self, '_input_max_length', -1) >= 0 and len(s) > self._input_max_length: + s = s[: self._input_max_length] + except Exception: + pass self._value = s try: if self._qwidget is not None: @@ -59,6 +93,16 @@ def setValue(self, text): except Exception: pass + def defaultVisibleLines(self): + return int(getattr(self, '_default_visible_lines', 3)) + + def setDefaultVisibleLines(self, newVisibleLines): + try: + self._default_visible_lines = int(newVisibleLines) + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + except Exception: + pass + def label(self): return self._label @@ -73,6 +117,107 @@ def setLabel(self, label): except Exception: pass + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + # If vertical stretch disabled, ensure minimal height equals default visible lines + if dim == YUIDimension.YD_VERT: + if not self.stretchable(YUIDimension.YD_VERT): + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + # apply fixed widget size (approximate width for 20 chars) + try: + if self._qwidget is not None: + fm = self._qtext.fontMetrics() + char_w = fm.horizontalAdvance('M') if fm is not None else 8 + line_h = fm.lineSpacing() if fm is not None else 16 + desired_chars = 20 + w_px = int(char_w * desired_chars) + 12 + h_px = int(line_h * self._default_visible_lines) + self._qlbl.sizeHint().height() + 8 + self._qwidget.setFixedSize(w_px, h_px) + self._qtext.setFixedHeight(int(line_h * self._default_visible_lines)) + self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + except Exception: + pass + else: + # make widget expandable + try: + if self._qwidget is not None: + try: + self._qwidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) + except Exception: + pass + try: + self._qwidget.setMinimumSize(QtCore.QSize(0, 0)) + except Exception: + pass + self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + except Exception: + pass + + def _apply_stretch_policy(self): + """Apply current stretchable flags to the created Qt widgets. + + When vertical/horizontal stretch is disabled we enforce a fixed size + approximating 3 lines x 20 characters; when enabled we allow expanding. + """ + try: + if self._qwidget is None: + return + # horizontal and vertical checks + horiz = self.stretchable(YUIDimension.YD_HORIZ) + vert = self.stretchable(YUIDimension.YD_VERT) + if not vert or not horiz: + # compute approximate pixel sizes + try: + fm = self._qtext.fontMetrics() + char_w = fm.horizontalAdvance('M') if fm is not None else 8 + line_h = fm.lineSpacing() if fm is not None else 16 + except Exception: + char_w = 8 + line_h = 16 + desired_chars = 20 + w_px = int(char_w * desired_chars) + 12 + h_px = int(line_h * self._default_visible_lines) + (self._qlbl.sizeHint().height() if hasattr(self, '_qlbl') else 0) + 8 + try: + self._qwidget.setFixedSize(w_px, h_px) + except Exception: + try: + self._qwidget.setMaximumSize(QtCore.QSize(w_px, h_px)) + except Exception: + pass + try: + self._qtext.setFixedHeight(int(line_h * self._default_visible_lines)) + except Exception: + pass + try: + self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + except Exception: + pass + else: + try: + self._qwidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) + except Exception: + pass + try: + self._qwidget.setMinimumSize(QtCore.QSize(0, 0)) + except Exception: + pass + try: + self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding if horiz else QtWidgets.QSizePolicy.Fixed, + QtWidgets.QSizePolicy.Expanding if vert else QtWidgets.QSizePolicy.Fixed) + except Exception: + pass + except Exception: + try: + self._logger.exception("_apply_stretch_policy failed") + except Exception: + pass + def _create_backend_widget(self): try: if QtWidgets is None: @@ -85,6 +230,11 @@ def _create_backend_widget(self): self._qtext.setPlainText(self._value) layout.addWidget(self._qlbl) layout.addWidget(self._qtext) + # apply current stretchable policy state + try: + self._apply_stretch_policy() + except Exception: + pass try: self._qtext.textChanged.connect(self._on_text_changed) except Exception: @@ -109,7 +259,25 @@ def _set_backend_enabled(self, enabled): def _on_text_changed(self): try: text = self._qtext.toPlainText() - self._value = text + # enforce input max length if set + try: + if getattr(self, '_input_max_length', -1) >= 0 and len(text) > self._input_max_length: + # truncate and restore cursor + cur = self._qtext.textCursor() + pos = cur.position() + self._qtext.blockSignals(True) + self._qtext.setPlainText(text[:self._input_max_length]) + self._qtext.blockSignals(False) + self._value = self._qtext.toPlainText() + try: + cur.setPosition(min(pos, self._input_max_length)) + self._qtext.setTextCursor(cur) + except Exception: + pass + else: + self._value = text + except Exception: + self._value = text dlg = self.findDialog() if dlg is not None: try: From 3ef926a0884d6f018733a6aab35555d67e17cfd3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 18:52:56 +0100 Subject: [PATCH 287/523] fixed stretching properties --- manatools/aui/backends/qt/multilineeditqt.py | 134 +++++++++---------- 1 file changed, 63 insertions(+), 71 deletions(-) diff --git a/manatools/aui/backends/qt/multilineeditqt.py b/manatools/aui/backends/qt/multilineeditqt.py index 3e6255e..e029e60 100644 --- a/manatools/aui/backends/qt/multilineeditqt.py +++ b/manatools/aui/backends/qt/multilineeditqt.py @@ -122,101 +122,93 @@ def setStretchable(self, dim, new_stretch): super().setStretchable(dim, new_stretch) except Exception: pass + # Re-apply policy so changes take effect immediately try: - # If vertical stretch disabled, ensure minimal height equals default visible lines - if dim == YUIDimension.YD_VERT: - if not self.stretchable(YUIDimension.YD_VERT): - self._height = self._default_visible_lines + (1 if bool(self._label) else 0) - # apply fixed widget size (approximate width for 20 chars) - try: - if self._qwidget is not None: - fm = self._qtext.fontMetrics() - char_w = fm.horizontalAdvance('M') if fm is not None else 8 - line_h = fm.lineSpacing() if fm is not None else 16 - desired_chars = 20 - w_px = int(char_w * desired_chars) + 12 - h_px = int(line_h * self._default_visible_lines) + self._qlbl.sizeHint().height() + 8 - self._qwidget.setFixedSize(w_px, h_px) - self._qtext.setFixedHeight(int(line_h * self._default_visible_lines)) - self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - except Exception: - pass - else: - # make widget expandable - try: - if self._qwidget is not None: - try: - self._qwidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) - except Exception: - pass - try: - self._qwidget.setMinimumSize(QtCore.QSize(0, 0)) - except Exception: - pass - self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - except Exception: - pass + self._apply_stretch_policy() except Exception: pass def _apply_stretch_policy(self): - """Apply current stretchable flags to the created Qt widgets. + """Apply current stretchable flags per axis without locking both dimensions.""" + if self._qwidget is None: + return + + # Current flags + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + vert = bool(self.stretchable(YUIDimension.YD_VERT)) - When vertical/horizontal stretch is disabled we enforce a fixed size - approximating 3 lines x 20 characters; when enabled we allow expanding. - """ + # Compute approximate pixel sizes (fallback to constants) try: - if self._qwidget is None: - return - # horizontal and vertical checks - horiz = self.stretchable(YUIDimension.YD_HORIZ) - vert = self.stretchable(YUIDimension.YD_VERT) - if not vert or not horiz: - # compute approximate pixel sizes - try: - fm = self._qtext.fontMetrics() - char_w = fm.horizontalAdvance('M') if fm is not None else 8 - line_h = fm.lineSpacing() if fm is not None else 16 - except Exception: - char_w = 8 - line_h = 16 - desired_chars = 20 - w_px = int(char_w * desired_chars) + 12 - h_px = int(line_h * self._default_visible_lines) + (self._qlbl.sizeHint().height() if hasattr(self, '_qlbl') else 0) + 8 + fm = self._qtext.fontMetrics() if hasattr(self, '_qtext') and self._qtext is not None else None + char_w = fm.horizontalAdvance('M') if fm is not None else 8 + line_h = fm.lineSpacing() if fm is not None else 16 + except Exception: + char_w = 8 + line_h = 16 + + desired_chars = 20 + try: + qlabel_h = self._qlbl.sizeHint().height() if hasattr(self, '_qlbl') and self._qlbl is not None else 0 + except Exception: + qlabel_h = 0 + + w_px = int(char_w * desired_chars) + 12 + h_px = int(line_h * max(1, self._default_visible_lines)) + qlabel_h + 8 + + # Set per-axis size policy + try: + self._qwidget.setSizePolicy( + QtWidgets.QSizePolicy.Expanding if horiz else QtWidgets.QSizePolicy.Fixed, + QtWidgets.QSizePolicy.Expanding if vert else QtWidgets.QSizePolicy.Fixed, + ) + except Exception: + pass + + # Apply horizontal constraint independently + try: + if not horiz: try: - self._qwidget.setFixedSize(w_px, h_px) + self._qwidget.setFixedWidth(w_px) except Exception: try: - self._qwidget.setMaximumSize(QtCore.QSize(w_px, h_px)) + self._qwidget.setMaximumWidth(w_px) except Exception: pass + else: try: - self._qtext.setFixedHeight(int(line_h * self._default_visible_lines)) - except Exception: - pass - try: - self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + self._qwidget.setMinimumWidth(0) + self._qwidget.setMaximumWidth(16777215) except Exception: pass - else: + except Exception: + pass + + # Apply vertical constraint independently + try: + if not vert: try: - self._qwidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self._qwidget.setFixedHeight(h_px) except Exception: - pass + try: + self._qwidget.setMaximumHeight(h_px) + except Exception: + pass try: - self._qwidget.setMinimumSize(QtCore.QSize(0, 0)) + if hasattr(self, '_qtext') and self._qtext is not None: + self._qtext.setFixedHeight(int(line_h * max(1, self._default_visible_lines))) except Exception: pass + else: try: - self._qwidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding if horiz else QtWidgets.QSizePolicy.Fixed, - QtWidgets.QSizePolicy.Expanding if vert else QtWidgets.QSizePolicy.Fixed) + self._qwidget.setMinimumHeight(0) + self._qwidget.setMaximumHeight(16777215) + if hasattr(self, '_qtext') and self._qtext is not None: + self._qtext.setMinimumHeight(0) + self._qtext.setMaximumHeight(16777215) except Exception: pass except Exception: - try: - self._logger.exception("_apply_stretch_policy failed") - except Exception: - pass + pass def _create_backend_widget(self): try: From cf8e6aaef8c4d058ee471b903c2c3d4430dbb4bc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 19:01:16 +0100 Subject: [PATCH 288/523] Multi Line Edit for Gtk --- manatools/aui/backends/gtk/__init__.py | 2 + .../aui/backends/gtk/multilineeditgtk.py | 331 ++++++++++++++++++ manatools/aui/yui_gtk.py | 2 + 3 files changed, 335 insertions(+) create mode 100644 manatools/aui/backends/gtk/multilineeditgtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 498228f..ec95f37 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -18,6 +18,7 @@ from .menubargtk import YMenuBarGtk from .replacepointgtk import YReplacePointGtk from .intfieldgtk import YIntFieldGtk +from .multilineeditgtk import YMultiLineEditGtk __all__ = [ "YDialogGtk", @@ -40,5 +41,6 @@ "YMenuBarGtk", "YReplacePointGtk", "YIntFieldGtk", + "YMultiLineEditGtk", # ... ] diff --git a/manatools/aui/backends/gtk/multilineeditgtk.py b/manatools/aui/backends/gtk/multilineeditgtk.py new file mode 100644 index 0000000..4e28e66 --- /dev/null +++ b/manatools/aui/backends/gtk/multilineeditgtk.py @@ -0,0 +1,331 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains GTK backend for YMultiLineEdit + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +import logging +import gi +try: + gi.require_version('Gtk', '4.0') + from gi.repository import Gtk, GLib +except Exception: + Gtk = None + GLib = None + +from ...yui_common import * + +_mod_logger = logging.getLogger("manatools.aui.gtk.multiline.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + + +class YMultiLineEditGtk(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._value = "" + # default visible content lines (consistent across backends) + self._default_visible_lines = 3 + # -1 means no input length limit + self._input_max_length = -1 + # reported minimal height: content lines + label row (if present) + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + if not self._logger.handlers and _mod_logger.handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + + self._widget = None + + def widgetClass(self): + return "YMultiLineEdit" + + def value(self): + return str(self._value) + + def inputMaxLength(self): + return int(getattr(self, '_input_max_length', -1)) + + def setInputMaxLength(self, numberOfChars): + try: + self._input_max_length = int(numberOfChars) + except Exception: + self._input_max_length = -1 + try: + if self._widget is not None and self._input_max_length >= 0: + buf = self._textview.get_buffer() + start = buf.get_start_iter() + end = buf.get_end_iter() + try: + text = buf.get_text(start, end, True) + except Exception: + text = "" + if len(text) > self._input_max_length: + try: + buf.set_text(text[:self._input_max_length]) + self._value = text[:self._input_max_length] + except Exception: + pass + except Exception: + pass + + def setValue(self, text): + try: + s = str(text) if text is not None else "" + except Exception: + s = "" + self._value = s + try: + if self._widget is not None: + buf = self._textview.get_buffer() + try: + buf.set_text(self._value) + except Exception: + pass + except Exception: + pass + + def label(self): + return self._label + + def setLabel(self, label): + try: + self._label = label + if self._widget is not None: + try: + self._lbl.set_text(str(label)) + except Exception: + pass + except Exception: + pass + + def defaultVisibleLines(self): + return int(getattr(self, '_default_visible_lines', 3)) + + def setDefaultVisibleLines(self, newVisibleLines): + try: + self._default_visible_lines = int(newVisibleLines) + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + try: + # Re-apply sizing based on new visible lines + self._apply_stretch_policy() + except Exception: + pass + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + # Re-apply full policy so axes are handled independently + self._apply_stretch_policy() + except Exception: + pass + + def _create_backend_widget(self): + try: + if Gtk is None: + raise ImportError("GTK not available") + box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + self._lbl = Gtk.Label.new(str(self._label)) + # Use scrolled window so when expanded beyond screen scrollbars appear + try: + self._scrolled = Gtk.ScrolledWindow.new() + self._textview = Gtk.TextView.new() + self._scrolled.set_child(self._textview) + except Exception: + self._scrolled = None + self._textview = Gtk.TextView.new() + try: + buf = self._textview.get_buffer() + buf.set_text(self._value) + try: + self._buf_handler_id = buf.connect('changed', self._on_buffer_changed) + except Exception: + self._buf_handler_id = None + except Exception: + pass + box.append(self._lbl) + if getattr(self, '_scrolled', None) is not None: + box.append(self._scrolled) + else: + box.append(self._textview) + self._widget = box + self._backend_widget = self._widget + try: + # Apply initial stretch/min size policy + self._apply_stretch_policy() + except Exception: + pass + except Exception as e: + try: + self._logger.exception("Error creating GTK MultiLineEdit backend: %s", e) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if self._widget is not None: + try: + self._textview.set_sensitive(enabled) + except Exception: + pass + except Exception: + pass + + def _on_buffer_changed(self, buf): + try: + start = buf.get_start_iter() + end = buf.get_end_iter() + try: + text = buf.get_text(start, end, True) + except Exception: + text = "" + # enforce input max length + try: + if getattr(self, '_input_max_length', -1) >= 0 and len(text) > self._input_max_length: + try: + if getattr(self, '_buf_handler_id', None) is not None: + buf.handler_block(self._buf_handler_id) + except Exception: + pass + try: + buf.set_text(text[:self._input_max_length]) + text = text[:self._input_max_length] + except Exception: + pass + try: + if getattr(self, '_buf_handler_id', None) is not None: + buf.handler_unblock(self._buf_handler_id) + except Exception: + pass + except Exception: + pass + self._value = text + dlg = self.findDialog() + if dlg is not None: + try: + if self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + try: + self._logger.exception("Failed to post ValueChanged event") + except Exception: + pass + except Exception: + try: + self._logger.exception("_on_buffer_changed error") + except Exception: + pass + + def _apply_stretch_policy(self): + """Apply horizontal/vertical stretch independently and set min pixel sizes. + + - When an axis is not stretchable, set a minimum pixel size: + width ~= 20 chars; height ~= `defaultVisibleLines` lines + label. + - When stretchable, clear size requests and set expand flags. + """ + try: + if self._widget is None: + return + target = self._scrolled if getattr(self, '_scrolled', None) is not None else self._textview + + # Determine current stretch flags + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + vert = bool(self.stretchable(YUIDimension.YD_VERT)) + + # Compute approximate character width and line height via Pango + try: + layout = self._textview.create_pango_layout("M") if self._textview is not None else None + if layout is not None: + char_w, line_h = layout.get_pixel_size() + else: + char_w, line_h = 8, 16 + # Fallback if layout reports zeros + if not char_w: + char_w = 8 + if not line_h: + line_h = 16 + except Exception: + char_w, line_h = 8, 16 + + # Label height approximation + try: + lbl_layout = self._lbl.create_pango_layout("M") if self._lbl is not None else None + qlabel_w, qlabel_h = lbl_layout.get_pixel_size() if lbl_layout is not None else (0, 20) + if not qlabel_h: + qlabel_h = 20 + except Exception: + qlabel_h = 20 + + desired_chars = 20 + w_px = int(char_w * desired_chars) + 12 + text_h_px = int(line_h * max(1, self._default_visible_lines)) + box_h_px = text_h_px + qlabel_h + 8 + + # Expansion flags + try: + if target is not None: + target.set_hexpand(horiz) + target.set_vexpand(vert) + self._widget.set_hexpand(horiz) + self._widget.set_vexpand(vert) + except Exception: + pass + + # Horizontal constraint + try: + if not horiz: + if target is not None and hasattr(target, 'set_min_content_width'): + target.set_min_content_width(w_px) + else: + # Fallback + if target is not None: + target.set_size_request(w_px, -1) + else: + self._widget.set_size_request(w_px, -1) + else: + if target is not None: + target.set_size_request(-1, -1) + except Exception: + pass + + # Vertical constraint + try: + if not vert: + if target is not None and hasattr(target, 'set_min_content_height'): + target.set_min_content_height(text_h_px) + else: + if target is not None: + target.set_size_request(-1, text_h_px) + # Ensure overall box accounts for label rows + try: + self._widget.set_size_request(-1, box_h_px) + except Exception: + pass + else: + if target is not None: + target.set_size_request(-1, -1) + try: + self._widget.set_size_request(-1, -1) + except Exception: + pass + except Exception: + pass + except Exception: + try: + self._logger.exception("_apply_stretch_policy failed") + except Exception: + pass diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 30d3cc6..8a2d7c1 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -207,6 +207,8 @@ def createHeading(self, parent, label): def createInputField(self, parent, label, password_mode=False): return YInputFieldGtk(parent, label, password_mode) + def createMultiLineEdit(self, parent, label): + return YMultiLineEditGtk(parent, label) def createIntField(self, parent, label, minVal, maxVal, initialVal): return YIntFieldGtk(parent, label, minVal, maxVal, initialVal) From 9ec60c3184db42f73af49a98ae417e300c941121 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 19:18:34 +0100 Subject: [PATCH 289/523] Added Multi Line Edit for ncurses --- manatools/aui/backends/curses/__init__.py | 2 + .../backends/curses/multilineeditcurses.py | 358 ++++++++++++++++++ manatools/aui/yui_curses.py | 2 + 3 files changed, 362 insertions(+) create mode 100644 manatools/aui/backends/curses/multilineeditcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 6332897..2ba1c3b 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -18,6 +18,7 @@ from .menubarcurses import YMenuBarCurses from .replacepointcurses import YReplacePointCurses from .intfieldcurses import YIntFieldCurses +from .multilineeditcurses import YMultiLineEditCurses __all__ = [ "YDialogCurses", @@ -40,5 +41,6 @@ "YMenuBarCurses", "YReplacePointCurses", "YIntFieldCurses", + "YMultiLineEditCurses", # ... ] diff --git a/manatools/aui/backends/curses/multilineeditcurses.py b/manatools/aui/backends/curses/multilineeditcurses.py new file mode 100644 index 0000000..656ab6f --- /dev/null +++ b/manatools/aui/backends/curses/multilineeditcurses.py @@ -0,0 +1,358 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains curses backend for YMultiLineEdit + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import logging +from ...yui_common import * + +_mod_logger = logging.getLogger("manatools.aui.curses.multiline.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + + +class YMultiLineEditCurses(YWidget): + def __init__(self, parent=None, label=""): + super().__init__(parent) + self._label = label + self._lines = [""] + self._editing = False + self._focused = False + self._can_focus = True + self._cursor_row = 0 + self._cursor_col = 0 + self._scroll_offset = 0 # topmost visible line index + # default visible content lines (consistent across backends) + self._default_visible_lines = 3 + # -1 means no input length limit + self._input_max_length = -1 + # desired height (content lines + optional label row) + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and _mod_logger.handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + + def widgetClass(self): + return "YMultiLineEdit" + + def value(self): + try: + return "\n".join(self._lines) + except Exception: + return "" + + def setValue(self, text): + try: + s = str(text) if text is not None else "" + except Exception: + s = "" + try: + self._lines = s.split('\n') + except Exception: + self._lines = [s] + self._editing = False + + def label(self): + return self._label + + def setLabel(self, label): + try: + self._label = label + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + if dim == YUIDimension.YD_VERT and not self.stretchable(YUIDimension.YD_VERT): + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + except Exception: + pass + + def _create_backend_widget(self): + try: + self._backend_widget = self + except Exception as e: + try: + self._logger.exception("Error creating curses MultiLineEdit backend: %s", e) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + # preserve/restore focusability similar to other input widgets + if not hasattr(self, "_saved_can_focus"): + self._saved_can_focus = getattr(self, "_can_focus", True) + if not enabled: + try: + self._saved_can_focus = self._can_focus + except Exception: + self._saved_can_focus = False + self._can_focus = False + self._editing = False + self._focused = False + else: + try: + self._can_focus = bool(getattr(self, "_saved_can_focus", True)) + except Exception: + self._can_focus = True + except Exception: + pass + + def inputMaxLength(self): + return int(getattr(self, '_input_max_length', -1)) + + def setInputMaxLength(self, numberOfChars): + try: + self._input_max_length = int(numberOfChars) + except Exception: + self._input_max_length = -1 + + def defaultVisibleLines(self): + return int(getattr(self, '_default_visible_lines', 3)) + + def setDefaultVisibleLines(self, newVisibleLines): + try: + self._default_visible_lines = int(newVisibleLines) + self._height = self._default_visible_lines + (1 if bool(self._label) else 0) + except Exception: + pass + + def _desired_height_for_width(self, width: int) -> int: + try: + # minimal height independent from width: default visible lines + label row + return max(1, self._default_visible_lines + (1 if bool(self._label) else 0)) + except Exception: + return max(1, getattr(self, '_height', 1)) + + def _draw(self, window, y, x, width, height): + try: + # Draw label (if present) and up to available lines + line = y + if self._label: + try: + lbl_txt = str(self._label) + ':' + lbl_out = lbl_txt[:max(0, width)] + window.addstr(line, x, lbl_out) + except Exception: + pass + line += 1 + + # content area excluding label + content_h = max(1, height - (1 if self._label else 0)) + if content_h <= 0: + return + + # Reserve 1 column for scrollbar if needed + bar_w = 1 if len(self._lines) > content_h else 0 + content_w = max(1, width - bar_w) + + # Ensure scroll offset keeps cursor visible + try: + if self._cursor_row < self._scroll_offset: + self._scroll_offset = self._cursor_row + elif self._cursor_row >= self._scroll_offset + content_h: + self._scroll_offset = self._cursor_row - content_h + 1 + self._scroll_offset = max(0, min(self._scroll_offset, max(0, len(self._lines) - content_h))) + except Exception: + pass + + # Draw visible lines + for i in range(content_h): + idx = self._scroll_offset + i + out = self._lines[idx] if 0 <= idx < len(self._lines) else "" + # clip horizontally + clipped = out[:max(0, content_w)] + try: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + window.addstr(line + i, x, clipped.ljust(content_w), attr) + except Exception: + pass + + # Draw cursor if focused and enabled + try: + if self._focused and self.isEnabled(): + vis_row = self._cursor_row - self._scroll_offset + if 0 <= vis_row < content_h: + vis_col = min(max(0, self._cursor_col), content_w - 1) + try: + window.chgat(line + vis_row, x + vis_col, 1, curses.A_REVERSE | curses.A_BOLD) + except Exception: + pass + except Exception: + pass + + # Draw vertical scrollbar + if bar_w == 1: + try: + for r in range(content_h): + window.addch(line + r, x + content_w, '|') + if len(self._lines) > content_h: + pos = int((self._scroll_offset / max(1, len(self._lines) - content_h)) * (content_h - 1)) + pos = max(0, min(content_h - 1, pos)) + window.addch(line + pos, x + content_w, '#') + except curses.error: + pass + except curses.error as e: + try: + self._logger.error("_draw curses.error: %s", e, exc_info=True) + except Exception: + pass + + def _handle_key(self, key): + """Multiline edit with cursor navigation and scrolling. + + - Arrow keys move the cursor; Home/End to line ends. + - Enter inserts a new line; Backspace/Delete remove chars across lines. + - Printable characters insert at cursor. + - PageUp/PageDown scroll the view. + - Ctrl-S posts ValueChanged (explicit commit), but we also post on each edit. + """ + if not getattr(self, '_focused', False) or not self.isEnabled(): + return False + + try: + total_lines = len(self._lines) if self._lines else 1 + if self._cursor_row >= total_lines: + self._cursor_row = max(0, total_lines - 1) + if self._cursor_col > len(self._lines[self._cursor_row]): + self._cursor_col = len(self._lines[self._cursor_row]) + + edited = False + + # Navigation + if key == curses.KEY_UP: + if self._cursor_row > 0: + self._cursor_row -= 1 + self._cursor_col = min(self._cursor_col, len(self._lines[self._cursor_row])) + return True + if key == curses.KEY_DOWN: + if self._cursor_row + 1 < len(self._lines): + self._cursor_row += 1 + self._cursor_col = min(self._cursor_col, len(self._lines[self._cursor_row])) + return True + if key == curses.KEY_LEFT: + if self._cursor_col > 0: + self._cursor_col -= 1 + elif self._cursor_row > 0: + self._cursor_row -= 1 + self._cursor_col = len(self._lines[self._cursor_row]) + return True + if key == curses.KEY_RIGHT: + line_len = len(self._lines[self._cursor_row]) + if self._cursor_col < line_len: + self._cursor_col += 1 + elif self._cursor_row + 1 < len(self._lines): + self._cursor_row += 1 + self._cursor_col = 0 + return True + if key == curses.KEY_HOME: + self._cursor_col = 0 + return True + if key == curses.KEY_END: + self._cursor_col = len(self._lines[self._cursor_row]) + return True + if key == curses.KEY_PPAGE: # PageUp + self._scroll_offset = max(0, self._scroll_offset - max(1, self._default_visible_lines)) + return True + if key == curses.KEY_NPAGE: # PageDown + max_off = max(0, len(self._lines) - max(1, self._default_visible_lines)) + self._scroll_offset = min(max_off, self._scroll_offset + max(1, self._default_visible_lines)) + return True + + # Commit (explicit) + if key == 19: # Ctrl-S + dlg = self.findDialog() + if dlg is not None and self.notify(): + try: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + return True + + # Cancel editing (ESC) - keep content + if key == 27: + self._editing = False + return True + + # Backspace + if key in (curses.KEY_BACKSPACE, 127, 8): + if self._cursor_col > 0: + line = self._lines[self._cursor_row] + self._lines[self._cursor_row] = line[:self._cursor_col - 1] + line[self._cursor_col:] + self._cursor_col -= 1 + edited = True + else: + if self._cursor_row > 0: + # merge with previous line + prev_len = len(self._lines[self._cursor_row - 1]) + self._lines[self._cursor_row - 1] += self._lines[self._cursor_row] + self._lines.pop(self._cursor_row) + self._cursor_row -= 1 + self._cursor_col = prev_len + edited = True + + # Delete + elif key == curses.KEY_DC: + line = self._lines[self._cursor_row] + if self._cursor_col < len(line): + self._lines[self._cursor_row] = line[:self._cursor_col] + line[self._cursor_col + 1:] + edited = True + else: + # merge with next line + if self._cursor_row + 1 < len(self._lines): + self._lines[self._cursor_row] += self._lines[self._cursor_row + 1] + self._lines.pop(self._cursor_row + 1) + edited = True + + # Enter -> new line + elif key in (10, ord('\n')): + line = self._lines[self._cursor_row] + left, right = line[:self._cursor_col], line[self._cursor_col:] + self._lines[self._cursor_row] = left + self._lines.insert(self._cursor_row + 1, right) + self._cursor_row += 1 + self._cursor_col = 0 + edited = True + + # Printable char + elif 32 <= key <= 126: + ch = chr(key) + line = self._lines[self._cursor_row] + # enforce per-line input max length + if getattr(self, '_input_max_length', -1) >= 0 and len(line) >= self._input_max_length: + return True + self._lines[self._cursor_row] = line[:self._cursor_col] + ch + line[self._cursor_col:] + self._cursor_col += 1 + edited = True + else: + return False + + if edited: + # post value-changed on each edit + dlg = self.findDialog() + if dlg is not None and self.notify(): + try: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + return True + + return True + except Exception: + return False diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 0ed76db..b1a50fd 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -157,6 +157,8 @@ def createInputField(self, parent, label, password_mode=False): return YInputFieldCurses(parent, label, password_mode) def createIntField(self, parent, label, minVal, maxVal, initialVal): return YIntFieldCurses(parent, label, minVal, maxVal, initialVal) + def createMultiLineEdit(self, parent, label): + return YMultiLineEditCurses(parent, label) def createPushButton(self, parent, label): return YPushButtonCurses(parent, label) From 0a5d720a9d495962041fa1e9669dce45d0a5591a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 19:23:30 +0100 Subject: [PATCH 290/523] Removed Q and q to exit, to avoid conflicts with text input fields --- manatools/aui/backends/curses/dialogcurses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index 69c0231..b4fcc02 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -190,7 +190,7 @@ def _draw_dialog(self): self._draw_child_content(content_y, content_x, content_width, content_height) # Draw footer with instructions - footer_text = " TAB=Navigate | SPACE=Expand | ENTER=Select | F10/Q=Quit " + footer_text = " TAB=Navigate | SPACE=Expand | ENTER=Select | F10=Quit " footer_x = max(0, (width - len(footer_text)) // 2) if footer_x + len(footer_text) < width: self._backend_widget.addstr(height - 1, footer_x, footer_text, curses.A_DIM) @@ -340,7 +340,7 @@ def waitForEvent(self, timeout_millisec=0): continue # Global keys - if key == curses.KEY_F10 or key == ord('q') or key == ord('Q'): + if key == curses.KEY_F10: self._post_event(YCancelEvent()) break elif key == curses.KEY_RESIZE: From 9684e02c665d071f7666538a0f629ba6a6e974a0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 19:27:20 +0100 Subject: [PATCH 291/523] Fixed horizontal stretching --- .../backends/curses/multilineeditcurses.py | 20 ++++++++++++++++++- sow/TODO.md | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/multilineeditcurses.py b/manatools/aui/backends/curses/multilineeditcurses.py index 656ab6f..7632ea8 100644 --- a/manatools/aui/backends/curses/multilineeditcurses.py +++ b/manatools/aui/backends/curses/multilineeditcurses.py @@ -46,6 +46,20 @@ def __init__(self, parent=None, label=""): def widgetClass(self): return "YMultiLineEdit" + def minWidth(self): + """Return minimal preferred width in columns when not horizontally stretchable. + + Heuristic: about 20 characters plus 1 column for a scrollbar when needed. + Containers like `YHBoxCurses` use this to allocate space for non-stretchable + children. + """ + try: + desired_chars = 20 + # reserve one column for potential scrollbar + return int(desired_chars + 1) + except Exception: + return 21 + def value(self): try: return "\n".join(self._lines) @@ -158,9 +172,13 @@ def _draw(self, window, y, x, width, height): if content_h <= 0: return + # Compute effective width respecting horizontal stretch + desired_w = self.minWidth() + eff_width = width if self.stretchable(YUIDimension.YD_HORIZ) else min(width, desired_w) + # Reserve 1 column for scrollbar if needed bar_w = 1 if len(self._lines) > content_h else 0 - content_w = max(1, width - bar_w) + content_w = max(1, eff_width - bar_w) # Ensure scroll offset keeps cursor visible try: diff --git a/sow/TODO.md b/sow/TODO.md index 3de3641..995131f 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -19,7 +19,7 @@ Missing Widgets comparing libyui original factory: [X] YTable [X] YProgressBar [X] YRichText - [ ] YMultiLineEdit + [X] YMultiLineEdit [X] YIntField [X] YMenuBar [ ] YSpacing (detailed variants: createHStretch/createVStretch/createHSpacing/createVSpacing/createSpacing) From 223f0c7d57b9d5249d5aab358e61cf2101f7f0a0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 20:13:45 +0100 Subject: [PATCH 292/523] Fixed Input field with label above --- .../aui/backends/curses/inputfieldcurses.py | 69 ++++++--- manatools/aui/backends/gtk/inputfieldgtk.py | 134 +++++++++++++++--- manatools/aui/backends/qt/inputfieldqt.py | 106 ++++++++++++-- 3 files changed, 255 insertions(+), 54 deletions(-) diff --git a/manatools/aui/backends/curses/inputfieldcurses.py b/manatools/aui/backends/curses/inputfieldcurses.py index 33bc535..d2f7247 100644 --- a/manatools/aui/backends/curses/inputfieldcurses.py +++ b/manatools/aui/backends/curses/inputfieldcurses.py @@ -35,7 +35,8 @@ def __init__(self, parent=None, label="", password_mode=False): self._cursor_pos = 0 self._focused = False self._can_focus = True - self._height = 1 + # one row for field + optional label row on top + self._height = 1 + (1 if bool(self._label) else 0) # per-instance logger self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") if not self._logger.handlers and not logging.getLogger().handlers: @@ -56,6 +57,13 @@ def setValue(self, text): def label(self): return self._label + def setLabel(self, label): + self._label = label + try: + self._height = 1 + (1 if bool(self._label) else 0) + except Exception: + pass + def _create_backend_widget(self): try: self._backend_widget = self @@ -93,19 +101,20 @@ def _set_backend_enabled(self, enabled): def _draw(self, window, y, x, width, height): try: - # Draw label + line = y + # Draw label on its own row above field if self._label: - label_text = self._label - if len(label_text) > width // 3: - label_text = label_text[:width // 3] - lbl_attr = curses.A_BOLD if self._is_heading else curses.A_NORMAL + label_text = str(self._label) + lbl_attr = curses.A_BOLD if getattr(self, '_is_heading', False) else curses.A_NORMAL if not self.isEnabled(): lbl_attr |= curses.A_DIM - window.addstr(y, x, label_text, lbl_attr) - x += len(label_text) + 1 - width -= len(label_text) + 1 + window.addstr(line, x, label_text[:max(0, width)], lbl_attr) + line += 1 - if width <= 0: + # Effective width respecting horizontal stretch + desired_w = self.minWidth() + eff_width = width if self.stretchable(YUIDimension.YD_HORIZ) else min(width, desired_w) + if eff_width <= 0: return # Prepare display value @@ -115,12 +124,12 @@ def _draw(self, window, y, x, width, height): display_value = self._value # Handle scrolling for long values - if len(display_value) > width: - if self._cursor_pos >= width: - start_pos = self._cursor_pos - width + 1 - display_value = display_value[start_pos:start_pos + width] + if len(display_value) > eff_width: + if self._cursor_pos >= eff_width: + start_pos = self._cursor_pos - eff_width + 1 + display_value = display_value[start_pos:start_pos + eff_width] else: - display_value = display_value[:width] + display_value = display_value[:eff_width] # Draw input field background if not self.isEnabled(): @@ -128,18 +137,18 @@ def _draw(self, window, y, x, width, height): else: attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - field_bg = ' ' * width - window.addstr(y, x, field_bg, attr) + field_bg = ' ' * eff_width + window.addstr(line, x, field_bg, attr) # Draw text if display_value: - window.addstr(y, x, display_value, attr) + window.addstr(line, x, display_value, attr) # Show cursor if focused and enabled if self._focused and self.isEnabled(): - cursor_display_pos = min(self._cursor_pos, width - 1) + cursor_display_pos = min(self._cursor_pos, eff_width - 1) if cursor_display_pos < len(display_value): - window.chgat(y, x + cursor_display_pos, 1, curses.A_REVERSE | curses.A_BOLD) + window.chgat(line, x + cursor_display_pos, 1, curses.A_REVERSE | curses.A_BOLD) except curses.error as e: try: self._logger.error("_draw curses.error: %s", e, exc_info=True) @@ -172,7 +181,27 @@ def _handle_key(self, key): elif 32 <= key <= 126: # Printable characters self._value = self._value[:self._cursor_pos] + chr(key) + self._value[self._cursor_pos:] self._cursor_pos += 1 + # Post ValueChanged immediately + try: + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass else: handled = False return handled + + def minWidth(self): + """Preferred minimal width in columns when not horizontally stretchable.""" + try: + return 20 + except Exception: + return 20 + + def _desired_height_for_width(self, width: int) -> int: + try: + return max(1, 1 + (1 if bool(self._label) else 0)) + except Exception: + return 1 diff --git a/manatools/aui/backends/gtk/inputfieldgtk.py b/manatools/aui/backends/gtk/inputfieldgtk.py index cbb1406..e7b959c 100644 --- a/manatools/aui/backends/gtk/inputfieldgtk.py +++ b/manatools/aui/backends/gtk/inputfieldgtk.py @@ -47,43 +47,45 @@ def label(self): return self._label def _create_backend_widget(self): - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - - if self._label: - label = Gtk.Label(label=self._label) - try: - if hasattr(label, "set_xalign"): - label.set_xalign(0.0) - except Exception: - pass - try: - hbox.append(label) - except Exception: - hbox.add(label) - + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + self._lbl = Gtk.Label(label=self._label) + try: + if hasattr(self._lbl, "set_xalign"): + self._lbl.set_xalign(0.0) + except Exception: + pass + try: + vbox.append(self._lbl) + except Exception: + vbox.add(self._lbl) + + entry = Gtk.Entry() if self._password_mode: - entry = Gtk.Entry() try: entry.set_visibility(False) except Exception: pass - else: - entry = Gtk.Entry() - + try: entry.set_text(self._value) entry.connect("changed", self._on_changed) except Exception: pass - + try: - hbox.append(entry) + vbox.append(entry) except Exception: - hbox.add(entry) + vbox.add(entry) - self._backend_widget = hbox + self._backend_widget = vbox self._entry_widget = entry self._backend_widget.set_sensitive(self._enabled) + try: + # initial stretch policy + self._apply_stretch_policy() + except Exception: + pass try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -94,6 +96,13 @@ def _on_changed(self, entry): self._value = entry.get_text() except Exception: self._value = "" + # Post ValueChanged + try: + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass def _set_backend_enabled(self, enabled): """Enable/disable the input field (entry and container).""" @@ -113,3 +122,84 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def setLabel(self, label): + self._label = label + try: + if hasattr(self, '_lbl') and self._lbl is not None: + self._lbl.set_text(str(label)) + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_stretch_policy() + except Exception: + pass + + def _apply_stretch_policy(self): + """Apply independent hexpand/vexpand and size requests for single-line input.""" + try: + if self._backend_widget is None: + return + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + vert = bool(self.stretchable(YUIDimension.YD_VERT)) + # Expansion flags + try: + self._backend_widget.set_hexpand(horiz) + self._backend_widget.set_vexpand(vert) + self._entry_widget.set_hexpand(horiz) + self._entry_widget.set_vexpand(vert) + except Exception: + pass + + # Compute char/label sizes via Pango + try: + layout = self._entry_widget.create_pango_layout("M") + char_w, line_h = layout.get_pixel_size() + if not char_w: + char_w = 8 + if not line_h: + line_h = 18 + except Exception: + char_w, line_h = 8, 18 + try: + lbl_layout = self._lbl.create_pango_layout("M") + _, lbl_h = lbl_layout.get_pixel_size() + if not lbl_h: + lbl_h = 20 + except Exception: + lbl_h = 20 + desired_chars = 20 + w_px = int(char_w * desired_chars) + 12 + h_px = int(line_h) + lbl_h + 8 + + # Horizontal constraint + try: + if not horiz: + # Prefer width in characters when available + if hasattr(self._entry_widget, 'set_width_chars'): + self._entry_widget.set_width_chars(desired_chars) + else: + self._entry_widget.set_size_request(w_px, -1) + self._backend_widget.set_size_request(w_px, -1) + else: + self._backend_widget.set_size_request(-1, -1) + self._entry_widget.set_size_request(-1, -1) + except Exception: + pass + + # Vertical constraint + try: + if not vert: + self._backend_widget.set_size_request(-1, h_px) + else: + self._backend_widget.set_size_request(-1, -1) + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/qt/inputfieldqt.py b/manatools/aui/backends/qt/inputfieldqt.py index 4d79863..26f5e5a 100644 --- a/manatools/aui/backends/qt/inputfieldqt.py +++ b/manatools/aui/backends/qt/inputfieldqt.py @@ -9,7 +9,7 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtCore import logging from ...yui_common import * @@ -37,26 +37,29 @@ def label(self): def _create_backend_widget(self): container = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(container) + layout = QtWidgets.QVBoxLayout(container) layout.setContentsMargins(0, 0, 0, 0) - - if self._label: - label = QtWidgets.QLabel(self._label) - layout.addWidget(label) - + + self._qlbl = QtWidgets.QLabel(self._label) + layout.addWidget(self._qlbl) + + entry = QtWidgets.QLineEdit() if self._password_mode: - entry = QtWidgets.QLineEdit() entry.setEchoMode(QtWidgets.QLineEdit.Password) - else: - entry = QtWidgets.QLineEdit() - + entry.setText(self._value) entry.textChanged.connect(self._on_text_changed) layout.addWidget(entry) - + self._backend_widget = container self._entry_widget = entry self._backend_widget.setEnabled(bool(self._enabled)) + + # Apply initial stretch policy + try: + self._apply_stretch_policy() + except Exception: + pass try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -83,3 +86,82 @@ def _set_backend_enabled(self, enabled): def _on_text_changed(self, text): self._value = text + # Post ValueChanged when notify is enabled + try: + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + pass + + def setLabel(self, label): + self._label = label + try: + if hasattr(self, '_qlbl') and self._qlbl is not None: + self._qlbl.setText(str(label)) + except Exception: + pass + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_stretch_policy() + except Exception: + pass + + def _apply_stretch_policy(self): + """Apply independent horizontal/vertical stretch policies for single-line input.""" + if not hasattr(self, '_backend_widget') or self._backend_widget is None: + return + + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + vert = bool(self.stretchable(YUIDimension.YD_VERT)) + + # Compute approximate metrics + try: + fm = self._entry_widget.fontMetrics() if hasattr(self, '_entry_widget') else None + char_w = fm.horizontalAdvance('M') if fm is not None else 8 + line_h = fm.lineSpacing() if fm is not None else 18 + except Exception: + char_w, line_h = 8, 18 + + desired_chars = 20 + try: + qlabel_h = self._qlbl.sizeHint().height() if hasattr(self, '_qlbl') and self._qlbl is not None else 0 + except Exception: + qlabel_h = 0 + + w_px = int(char_w * desired_chars) + 12 + h_px = int(line_h) + qlabel_h + 8 + + # Policy per axis + try: + self._backend_widget.setSizePolicy( + QtWidgets.QSizePolicy.Expanding if horiz else QtWidgets.QSizePolicy.Fixed, + QtWidgets.QSizePolicy.Expanding if vert else QtWidgets.QSizePolicy.Fixed, + ) + except Exception: + pass + + # Width constraint + try: + if not horiz: + self._backend_widget.setFixedWidth(w_px) + else: + self._backend_widget.setMinimumWidth(0) + self._backend_widget.setMaximumWidth(16777215) + except Exception: + pass + + # Height constraint + try: + if not vert: + self._backend_widget.setFixedHeight(h_px) + else: + self._backend_widget.setMinimumHeight(0) + self._backend_widget.setMaximumHeight(16777215) + except Exception: + pass From 7754acadb816c7fd844f019e0af62334eccc0850 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 31 Dec 2025 20:14:40 +0100 Subject: [PATCH 293/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 995131f..0e90303 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -60,7 +60,7 @@ Optional/special widgets (from `YOptionalWidgetFactory`): To check/review: how to manage YEvents [X] and YItems [X] (verify selection attirbute). - [ ] YInputField password mode + [X] YInputField password mode [ ] adding factory create alternative methods (e.g. createMultiSelectionBox) [ ] managing shortcuts From fd222bcddc8ef8ac439017430d4807971f82731c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 1 Jan 2026 13:24:01 +0100 Subject: [PATCH 294/523] Update TODO list with skipped widgets information --- sow/TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sow/TODO.md b/sow/TODO.md index 0e90303..881ef68 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -35,6 +35,7 @@ Missing Widgets comparing libyui original factory: [ ] YSquash / createSquash Skipped widgets: + [-] YMultiSelectionBox (implemented as YSelectionBox + multiselection enabled) [-] YPackageSelector (not ported) [-] YRadioButtonGroup (not ported) From fe3445cd7c5bcb9ccb86da479a62223379c37040 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 1 Jan 2026 23:19:52 +0100 Subject: [PATCH 295/523] Added setIcon --- .../aui/backends/curses/pushbuttoncurses.py | 8 ++ manatools/aui/backends/gtk/pushbuttongtk.py | 77 +++++++++++++++++++ manatools/aui/backends/qt/pushbuttonqt.py | 39 +++++++++- 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index 4f9f56c..bd1f2c5 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -32,6 +32,7 @@ def __init__(self, parent=None, label=""): self._label = label self._focused = False self._can_focus = True + self._icon_name = None self._height = 1 # Fixed height - buttons are always one line self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") if not self._logger.handlers and not logging.getLogger().handlers: @@ -118,3 +119,10 @@ def _handle_key(self, key): _mod_logger.error("_handle_key post event error", exc_info=True) return True return False + + def setIcon(self, icon_name: str): + """Store icon name for curses backend (no graphical icon support).""" + try: + self._icon_name = icon_name + except Exception: + pass diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index 1212e7f..3f9674f 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -19,12 +19,14 @@ import os import logging from ...yui_common import * +from .commongtk import _resolve_icon, _resolve_gicon class YPushButtonGtk(YWidget): def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label + self._icon_name = None self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): @@ -43,6 +45,32 @@ def setLabel(self, label): def _create_backend_widget(self): self._backend_widget = Gtk.Button(label=self._label) + # apply icon if previously set + try: + if getattr(self, "_icon_name", None): + try: + img = _resolve_icon(self._icon_name) + except Exception: + img = None + if img is not None: + try: + # Prefer set_icon if available + try: + self._backend_widget.set_icon(img.get_paintable()) + except Exception: + # Fallback: set a composite child with image + label + try: + hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + hb.append(img) + lbl = Gtk.Label(label=self._label) + hb.append(lbl) + self._backend_widget.set_child(hb) + except Exception: + pass + except Exception: + pass + except Exception: + pass # Prevent button from being stretched horizontally by default. try: self._backend_widget.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) @@ -84,3 +112,52 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def setIcon(self, icon_name: str): + """Set/clear the icon for this pushbutton (icon_name may be theme name or path).""" + try: + self._icon_name = icon_name + if getattr(self, "_backend_widget", None) is None: + return + img = None + try: + img = _resolve_icon(icon_name) + except Exception: + img = None + if img is not None: + try: + try: + self._backend_widget.set_icon(img.get_paintable()) + return + except Exception: + # Fallback: set composite child with image + label + try: + hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + hb.append(img) + lbl = Gtk.Label(label=self._label) + hb.append(lbl) + self._backend_widget.set_child(hb) + return + except Exception: + pass + except Exception: + pass + # If we reach here, clear any icon and ensure label is present + try: + # Reset to simple label-only button + try: + self._backend_widget.set_label(self._label) + except Exception: + try: + # If set_label not available, set child to a label + lbl = Gtk.Label(label=self._label) + self._backend_widget.set_child(lbl) + except Exception: + pass + except Exception: + pass + except Exception: + try: + self._logger.exception("setIcon failed") + except Exception: + pass diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index 1862670..b182845 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -9,15 +9,17 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtGui import logging from ...yui_common import * +from .commonqt import _resolve_icon class YPushButtonQt(YWidget): def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label + self._icon_name = None self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): @@ -33,6 +35,17 @@ def setLabel(self, label): def _create_backend_widget(self): self._backend_widget = QtWidgets.QPushButton(self._label) + # apply icon if previously set + try: + if getattr(self, "_icon_name", None): + ico = _resolve_icon(self._icon_name) + if ico is not None and not ico.isNull(): + try: + self._backend_widget.setIcon(ico) + except Exception: + pass + except Exception: + pass # Set size policy to prevent unwanted expansion try: try: @@ -84,3 +97,27 @@ def _on_clicked(self): self._logger.warning("Button clicked (no dialog found): %s", self._label) except Exception: pass + + def setIcon(self, icon_name: str): + """Set/clear the icon for this pushbutton (icon_name may be theme name or path).""" + try: + self._icon_name = icon_name + if getattr(self, "_backend_widget", None) is None: + return + ico = _resolve_icon(icon_name) + if ico is not None and not ico.isNull(): + try: + self._backend_widget.setIcon(ico) + return + except Exception: + pass + # Clear icon if resolution failed + try: + self._backend_widget.setIcon(QtGui.QIcon()) + except Exception: + pass + except Exception: + try: + self._logger.exception("setIcon failed") + except Exception: + pass From e6146b9c50cf0d7abbed142d4c9c65ff82608099 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 14:22:09 +0100 Subject: [PATCH 296/523] default app icon "manatools"! --- manatools/aui/backends/qt/commonqt.py | 13 +++++++++---- manatools/aui/yui_gtk.py | 2 +- manatools/aui/yui_qt.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/manatools/aui/backends/qt/commonqt.py b/manatools/aui/backends/qt/commonqt.py index a48429c..45cb5d2 100644 --- a/manatools/aui/backends/qt/commonqt.py +++ b/manatools/aui/backends/qt/commonqt.py @@ -27,25 +27,27 @@ def _resolve_icon(icon_name): try: if not icon_name: return None + logger = logging.getLogger(f"manatools.aui.qt.common") # If icon_name looks like a filesystem path (absolute or contains a # path separator), prefer loading from disk. If it has no # extension, also try the same path with a .png suffix to help # debugging/test cases where a directory+basename is provided. try: if os.path.isabs(icon_name) or os.path.sep in icon_name: - # exact file - if os.path.exists(icon_name): + if os.path.isfile(icon_name): return QtGui.QIcon(icon_name) # if there's no extension, try .png base, ext = os.path.splitext(icon_name) if not ext: png_candidate = icon_name + '.png' - if os.path.exists(png_candidate): + if os.path.isfile(png_candidate): + logger.debug("Resolved icon %r to %r", icon_name, png_candidate) return QtGui.QIcon(png_candidate) # not found on filesystem: fall through to theme/name else: # non-path might still be a relative file name - if os.path.exists(icon_name): + if os.path.isfile(icon_name): + logger.debug("Resolved icon %r to filesystem path", icon_name) return QtGui.QIcon(icon_name) except Exception: pass @@ -56,14 +58,17 @@ def _resolve_icon(icon_name): except Exception: pass try: + logger.debug("Trying to resolve icon %r via QIcon.fromTheme(%r)", icon_name, base) ico = QtGui.QIcon.fromTheme(base) if ico and not ico.isNull(): + logger.debug("Resolved icon %r to theme icon %r", icon_name, base) return ico except Exception: pass try: ico = QtGui.QIcon(icon_name) if ico and not ico.isNull(): + logger.debug("Resolved icon %r to QIcon(%r)", icon_name, icon_name) return ico except Exception: pass diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 8a2d7c1..b56273e 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -37,7 +37,7 @@ def __init__(self): self._application_title = "manatools GTK Application" self._product_name = "manatools AUI Gtk" self._icon_base_path = None - self._icon = "" + self._icon = "manatools" # default icon name # cached resolved GdkPixbuf.Pixbuf (or None) self._gtk_icon_pixbuf = None diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index fb4eff0..725c1a7 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -39,7 +39,7 @@ def __init__(self): self._application_title = "manatools Qt Application" self._product_name = "manatools AUI Qt" self._icon_base_path = None - self._icon = "" + self._icon = "manatools" # default icon name # cached QIcon resolved from _icon (None if not resolved) self._qt_icon = None From 1590a64e4ef8b2b7480d60e548c519d8210a9ce7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 19:02:17 +0100 Subject: [PATCH 297/523] Label aligned to center --- manatools/aui/backends/gtk/pushbuttongtk.py | 23 ++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index 3f9674f..5048636 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -63,6 +63,17 @@ def _create_backend_widget(self): hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) hb.append(img) lbl = Gtk.Label(label=self._label) + # center contents inside the box so button label appears centered + try: + hb.set_halign(Gtk.Align.CENTER) + hb.set_valign(Gtk.Align.CENTER) + hb.set_hexpand(False) + except Exception: + pass + try: + lbl.set_halign(Gtk.Align.CENTER) + except Exception: + pass hb.append(lbl) self._backend_widget.set_child(hb) except Exception: @@ -130,11 +141,21 @@ def setIcon(self, icon_name: str): self._backend_widget.set_icon(img.get_paintable()) return except Exception: - # Fallback: set composite child with image + label + # Fallback: set composite child with image + label (centered) try: hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) hb.append(img) lbl = Gtk.Label(label=self._label) + try: + hb.set_halign(Gtk.Align.CENTER) + hb.set_valign(Gtk.Align.CENTER) + hb.set_hexpand(False) + except Exception: + pass + try: + lbl.set_halign(Gtk.Align.CENTER) + except Exception: + pass hb.append(lbl) self._backend_widget.set_child(hb) return From cbf817a40d78d1150b673285414fe6704ea283d1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 19:08:08 +0100 Subject: [PATCH 298/523] Wrong HV alignment and exit icon --- test/test_aligment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_aligment.py b/test/test_aligment.py index a9bc35c..f488c4b 100644 --- a/test/test_aligment.py +++ b/test/test_aligment.py @@ -61,10 +61,11 @@ def test_Alignment(backend_name=None): factory.createLabel(vbox, "Testing aligment HVCenter into HBox") hbox = factory.createHBox( vbox ) - align = factory.createVCenter( hbox ) + align = factory.createHVCenter( hbox ) factory.createPushButton( align, "HVCenter" ) b = factory.createPushButton( vbox, "OK" ) + b.setIcon("application-exit") b.setStretchable(yui.YUIDimension.YD_HORIZ, True ) b.setStretchable(yui.YUIDimension.YD_VERT, True ) dialog.open() From bd979451736a9a895946c5af45c58fae54d08836 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 19:47:04 +0100 Subject: [PATCH 299/523] Added test spacing --- test/test_spacing.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/test_spacing.py diff --git a/test/test_spacing.py b/test/test_spacing.py new file mode 100644 index 0000000..80d1806 --- /dev/null +++ b/test/test_spacing.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_Spacing(backend_name=None): + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + ui.yApp().setApplicationTitle("Spacing Demo") + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox(dialog) + factory.createLabel(vbox, "Spacing demo: pixels as unit; curses converts to chars.") + + # Horizontal spacing line: label - spacing(50px, fixed) - button - spacing(80px, stretch min) + hbox = factory.createHBox(vbox) + factory.createLabel(hbox, "LeftLabel") + factory.createSpacing(hbox, yui.YUIDimension.YD_HORIZ, False, 50.0) + factory.createPushButton(hbox, "Click Me") + factory.createSpacing(hbox, yui.YUIDimension.YD_HORIZ, True, 80.0) + factory.createCheckBox(hbox, "Check", False) + + # Vertical spacing between rows (24px ~= 1 char in curses) + factory.createSpacing(vbox, yui.YUIDimension.YD_VERT, False, 24.0) + + # Another row with stretchable vertical spacing on both sides + hbox2 = factory.createHBox(vbox) + factory.createSpacing(hbox2, yui.YUIDimension.YD_HORIZ, True, 20.0) + factory.createLabel(hbox2, "Centered by spacers") + factory.createSpacing(hbox2, yui.YUIDimension.YD_HORIZ, True, 20.0) + + # OK button + ok = factory.createPushButton(vbox, "OK") + ok.setStretchable(yui.YUIDimension.YD_HORIZ, True) + ok.setStretchable(yui.YUIDimension.YD_VERT, False) + + dialog.open() + event = dialog.waitForEvent() + dialog.destroy() + + except Exception as e: + print(f"Error testing Spacing with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_Spacing(sys.argv[1]) + else: + test_Spacing() From 87c327817d296ae745c736be74687174cbc284d0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 19:48:05 +0100 Subject: [PATCH 300/523] First attempt to produce YSpacing for Qt --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/spacingqt.py | 108 +++++++++++++++++++++++++ manatools/aui/yui_qt.py | 9 +++ 3 files changed, 119 insertions(+) create mode 100644 manatools/aui/backends/qt/spacingqt.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 588b7bf..d0e5c4a 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -19,6 +19,7 @@ from .replacepointqt import YReplacePointQt from .intfieldqt import YIntFieldQt from .multilineeditqt import YMultiLineEditQt +from .spacingqt import YSpacingQt __all__ = [ @@ -43,5 +44,6 @@ "YReplacePointQt", "YIntFieldQt", "YMultiLineEditQt", + "YSpacingQt", # ... ] diff --git a/manatools/aui/backends/qt/spacingqt.py b/manatools/aui/backends/qt/spacingqt.py new file mode 100644 index 0000000..6b31a76 --- /dev/null +++ b/manatools/aui/backends/qt/spacingqt.py @@ -0,0 +1,108 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Qt backend: YSpacing implementation using a lightweight QWidget. + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore +import logging +from ...yui_common import * +from .commonqt import _resolve_icon + + +class YSpacingQt(YWidget): + """Spacing/Stretch widget for Qt. + + Constructor arguments: + - parent: containing widget + - dim: `YUIDimension` — primary dimension where spacing applies + - stretchable: bool — if True, the spacing expands in its primary dimension + - size: float — spacing size in pixels (device units). When stretchable is True, + this acts as a minimum size in the primary dimension. When False, the size is + fixed in the primary dimension. + + Notes: + - This widget is visually empty; it only reserves space. + - Pixels are used as the unit to avoid ambiguity. Other backends convert as + appropriate (e.g., curses maps pixels to character cells using a fixed ratio). + """ + def __init__(self, parent=None, dim: YUIDimension = YUIDimension.YD_HORIZ, stretchable: bool = False, size: float = 0.0): + super().__init__(parent) + self._dim = dim + self._stretchable = bool(stretchable) + try: + self._size_px = max(0, int(round(float(size)))) + except Exception: + self._size_px = 0 + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + try: + self._logger.debug("%s.__init__(dim=%s, stretchable=%s, size_px=%d)", self.__class__.__name__, self._dim, self._stretchable, self._size_px) + except Exception: + pass + + def widgetClass(self): + return "YSpacing" + + def dimension(self): + return self._dim + + def size(self): + return self._size_px + + def sizeDim(self, dim: YUIDimension): + return self._size_px if dim == self._dim else 0 + + def _create_backend_widget(self): + try: + w = QtWidgets.QWidget() + sp = w.sizePolicy() + if self._dim == YUIDimension.YD_HORIZ: + # horizontal spacing + try: + if self._stretchable: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding) + w.setMinimumWidth(self._size_px) + else: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Fixed) + w.setFixedWidth(self._size_px) + # vertical should not force expansion + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Preferred) + except Exception: + pass + else: + # vertical spacing + try: + if self._stretchable: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding) + w.setMinimumHeight(self._size_px) + else: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Fixed) + w.setFixedHeight(self._size_px) + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Preferred) + except Exception: + pass + try: + w.setSizePolicy(sp) + except Exception: + pass + self._backend_widget = w + self._backend_widget.setEnabled(bool(self._enabled)) + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + try: + self._logger.exception("Failed to create YSpacingQt backend") + except Exception: + pass + self._backend_widget = QtWidgets.QWidget() + + def _set_backend_enabled(self, enabled): + try: + if self._backend_widget is not None: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 725c1a7..75cf5ef 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -244,3 +244,12 @@ def createMenuBar(self, parent): def createReplacePoint(self, parent): """Create a ReplacePoint widget (Qt backend).""" return YReplacePointQt(parent) + + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size: float = 0.0): + """Create a Spacing/Stretch widget. + + - `dim`: primary dimension for spacing (YUIDimension) + - `stretchable`: expand in primary dimension when True (minimum size = `size`) + - `size`: spacing size in pixels (device units) + """ + return YSpacingQt(parent, dim, stretchable, size) From a3e483366bf35e1c6ac54ce22c9f58ad3e16027b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 19:49:02 +0100 Subject: [PATCH 301/523] First attempt to produce YSpacing for Gtk --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/spacinggtk.py | 101 +++++++++++++++++++++++ manatools/aui/yui_gtk.py | 9 ++ 3 files changed, 112 insertions(+) create mode 100644 manatools/aui/backends/gtk/spacinggtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index ec95f37..3b9f2ac 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -19,6 +19,7 @@ from .replacepointgtk import YReplacePointGtk from .intfieldgtk import YIntFieldGtk from .multilineeditgtk import YMultiLineEditGtk +from .spacinggtk import YSpacingGtk __all__ = [ "YDialogGtk", @@ -42,5 +43,6 @@ "YReplacePointGtk", "YIntFieldGtk", "YMultiLineEditGtk", + "YSpacingGtk", # ... ] diff --git a/manatools/aui/backends/gtk/spacinggtk.py b/manatools/aui/backends/gtk/spacinggtk.py new file mode 100644 index 0000000..a5570e4 --- /dev/null +++ b/manatools/aui/backends/gtk/spacinggtk.py @@ -0,0 +1,101 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +GTK backend: YSpacing implementation using an empty Gtk.Box with size requests. + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk +import logging +from ...yui_common import * + + +class YSpacingGtk(YWidget): + """Spacing/Stretch widget for GTK. + + - `dim`: primary dimension (horizontal or vertical) where spacing applies + - `stretchable`: whether the spacing expands in its primary dimension + - `size`: spacing size in pixels (device units). Pixels are used for clarity + and uniformity across backends; GTK honors `set_size_request` in pixels. + """ + def __init__(self, parent=None, dim: YUIDimension = YUIDimension.YD_HORIZ, stretchable: bool = False, size: float = 0.0): + super().__init__(parent) + self._dim = dim + self._stretchable = bool(stretchable) + try: + self._size_px = max(0, int(round(float(size)))) + except Exception: + self._size_px = 0 + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + try: + self._logger.debug("%s.__init__(dim=%s, stretchable=%s, size_px=%d)", self.__class__.__name__, self._dim, self._stretchable, self._size_px) + except Exception: + pass + + def widgetClass(self): + return "YSpacing" + + def dimension(self): + return self._dim + + def size(self): + return self._size_px + + def sizeDim(self, dim: YUIDimension): + return self._size_px if dim == self._dim else 0 + + def _create_backend_widget(self): + try: + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + # size request in the primary dimension + if self._dim == YUIDimension.YD_HORIZ: + try: + if self._stretchable: + box.set_hexpand(True) + box.set_halign(Gtk.Align.FILL) + box.set_size_request(self._size_px, -1) + else: + box.set_hexpand(False) + box.set_halign(Gtk.Align.CENTER) + box.set_size_request(self._size_px, -1) + box.set_vexpand(False) + box.set_valign(Gtk.Align.CENTER) + except Exception: + pass + else: + try: + if self._stretchable: + box.set_vexpand(True) + box.set_valign(Gtk.Align.FILL) + box.set_size_request(-1, self._size_px) + else: + box.set_vexpand(False) + box.set_valign(Gtk.Align.CENTER) + box.set_size_request(-1, self._size_px) + box.set_hexpand(False) + box.set_halign(Gtk.Align.CENTER) + except Exception: + pass + self._backend_widget = box + self._backend_widget.set_sensitive(self._enabled) + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + try: + self._logger.exception("Failed to create YSpacingGtk backend") + except Exception: + pass + self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + def _set_backend_enabled(self, enabled): + try: + if self._backend_widget is not None: + self._backend_widget.set_sensitive(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index b56273e..b26dfca 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -267,6 +267,15 @@ def createReplacePoint(self, parent): """Create a ReplacePoint widget (GTK backend).""" return YReplacePointGtk(parent) + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size: float = 0.0): + """Create a Spacing/Stretch widget. + + - `dim`: primary dimension for spacing (YUIDimension) + - `stretchable`: expand in primary dimension when True (minimum size = `size`) + - `size`: spacing size in pixels (device units) + """ + return YSpacingGtk(parent, dim, stretchable, size) + def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) From 6335b06ef3f0a4cefdba665f4e6908b6ad754712 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 19:49:39 +0100 Subject: [PATCH 302/523] First attempt to produce YSpacing for ncurses --- manatools/aui/backends/curses/__init__.py | 2 ++ manatools/aui/backends/curses/commoncurses.py | 36 +++++++++++++++++++ manatools/aui/yui_curses.py | 11 +++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 2ba1c3b..2ae6a9a 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -19,6 +19,7 @@ from .replacepointcurses import YReplacePointCurses from .intfieldcurses import YIntFieldCurses from .multilineeditcurses import YMultiLineEditCurses +from .spacingcurses import YSpacingCurses __all__ = [ "YDialogCurses", @@ -42,5 +43,6 @@ "YReplacePointCurses", "YIntFieldCurses", "YMultiLineEditCurses", + "YSpacingCurses", # ... ] diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py index abeaba9..7225c7a 100644 --- a/manatools/aui/backends/curses/commoncurses.py +++ b/manatools/aui/backends/curses/commoncurses.py @@ -1,6 +1,42 @@ # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: ''' +Common helpers for the curses backend. + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import logging +from ...yui_common import YUIDimension + +__all__ = ["pixels_to_chars"] + +_logger = logging.getLogger("manatools.aui.curses.common") + +def pixels_to_chars(size_px: float, dim: YUIDimension) -> int: + """Convert a pixel size into character cells for curses. + + Mapping rationale: libyui uses an abstract unit where a main window of + 800x600 pixels corresponds to an 80x25 character window. We adopt the same + ratio: 10 px per column horizontally, 24 px per row vertically. + + This conversion provides a uniform interpretation of the `size` argument + across Qt/GTK/curses backends. + """ + try: + px = max(0.0, float(size_px)) + except Exception: + px = 0.0 + if dim == YUIDimension.YD_HORIZ: + return max(0, int(round(px / 10.0))) + else: + return max(0, int(round(px / 24.0))) +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' Python manatools.aui.backends.curses contains all curses backend classes License: LGPLv2+ diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index b1a50fd..92d2c55 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -232,4 +232,13 @@ def createMenuBar(self, parent): def createReplacePoint(self, parent): """Create a ReplacePoint widget (curses backend).""" - return YReplacePointCurses(parent) \ No newline at end of file + return YReplacePointCurses(parent) + + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size: float = 0.0): + """Create a Spacing/Stretch widget. + + - `dim`: primary dimension for spacing (YUIDimension) + - `stretchable`: expand in primary dimension when True (minimum size = `size`) + - `size`: spacing size in pixels, converted to character cells using 800x600→80x25 mapping + """ + return YSpacingCurses(parent, dim, stretchable, size) \ No newline at end of file From 9758bb9c933ed79ffdc76259f2c3bca9a5f9d51f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 20:26:15 +0100 Subject: [PATCH 303/523] Forgot --- .../aui/backends/curses/spacingcurses.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 manatools/aui/backends/curses/spacingcurses.py diff --git a/manatools/aui/backends/curses/spacingcurses.py b/manatools/aui/backends/curses/spacingcurses.py new file mode 100644 index 0000000..c93cbf8 --- /dev/null +++ b/manatools/aui/backends/curses/spacingcurses.py @@ -0,0 +1,83 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Curses backend: YSpacing implementation as a non-drawing spacer. + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import logging +from ...yui_common import * +from .commoncurses import pixels_to_chars + + +class YSpacingCurses(YWidget): + """Spacing/Stretch widget for curses. + + - `dim`: primary dimension where spacing applies + - `stretchable`: if True, the spacing expands in its primary dimension + - `size`: spacing size expressed in pixels; converted to character cells + using the libyui 800x600→80x25 mapping (10 px/col, 24 px/row). + """ + def __init__(self, parent=None, dim: YUIDimension = YUIDimension.YD_HORIZ, stretchable: bool = False, size_px: int = 0): + super().__init__(parent) + self._dim = dim + self._stretchable = bool(stretchable) + try: + spx = int(size_px) + self._size_px = 0 if spx <= 0 else max(1, spx) + except Exception: + self._size_px = 0 + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + # Sync base stretch flags so containers can query stretchable() + try: + self.setStretchable(self._dim, self._stretchable) + except Exception: + pass + try: + self._logger.debug("%s.__init__(dim=%s, stretchable=%s, size_px=%d)", self.__class__.__name__, self._dim, self._stretchable, self._size_px) + except Exception: + pass + + def widgetClass(self): + return "YSpacing" + + def dimension(self): + return self._dim + + def size(self): + return self._size_px + + def sizeDim(self, dim: YUIDimension): + return self._size_px if dim == self._dim else 0 + + def _create_backend_widget(self): + # curses uses logical widget; no separate backend structure required + self._backend_widget = self + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _desired_height_for_width(self, width): + try: + if self._dim == YUIDimension.YD_VERT: + return pixels_to_chars(self._size_px, YUIDimension.YD_VERT) + except Exception: + pass + return 0 + + def minWidth(self): + try: + if self._dim == YUIDimension.YD_HORIZ: + return pixels_to_chars(self._size_px, YUIDimension.YD_HORIZ) + except Exception: + pass + return 0 + + def _draw(self, window, y, x, width, height): + # spacing draws nothing; reserved area remains blank + return From a674420e89f16347a5337e95ccb16ea9e9bd2a06 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 20:27:38 +0100 Subject: [PATCH 304/523] Pixels --- manatools/aui/backends/qt/spacingqt.py | 11 +++++++++-- manatools/aui/yui_qt.py | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/qt/spacingqt.py b/manatools/aui/backends/qt/spacingqt.py index 6b31a76..1225ff8 100644 --- a/manatools/aui/backends/qt/spacingqt.py +++ b/manatools/aui/backends/qt/spacingqt.py @@ -31,15 +31,22 @@ class YSpacingQt(YWidget): - Pixels are used as the unit to avoid ambiguity. Other backends convert as appropriate (e.g., curses maps pixels to character cells using a fixed ratio). """ - def __init__(self, parent=None, dim: YUIDimension = YUIDimension.YD_HORIZ, stretchable: bool = False, size: float = 0.0): + def __init__(self, parent=None, dim: YUIDimension = YUIDimension.YD_HORIZ, stretchable: bool = False, size_px: int = 0): super().__init__(parent) self._dim = dim self._stretchable = bool(stretchable) try: - self._size_px = max(0, int(round(float(size)))) + # integer pixel size; minimum granularity is 1 px when non-zero + spx = int(size_px) + self._size_px = 0 if spx <= 0 else max(1, spx) except Exception: self._size_px = 0 self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + # Sync base stretch flags so containers can query stretchable() + try: + self.setStretchable(self._dim, self._stretchable) + except Exception: + pass try: self._logger.debug("%s.__init__(dim=%s, stretchable=%s, size_px=%d)", self.__class__.__name__, self._dim, self._stretchable, self._size_px) except Exception: diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 75cf5ef..2852a2a 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -245,11 +245,11 @@ def createReplacePoint(self, parent): """Create a ReplacePoint widget (Qt backend).""" return YReplacePointQt(parent) - def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size: float = 0.0): + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size_px: int = 0): """Create a Spacing/Stretch widget. - `dim`: primary dimension for spacing (YUIDimension) - `stretchable`: expand in primary dimension when True (minimum size = `size`) - - `size`: spacing size in pixels (device units) + - `size_px`: spacing size in pixels (device units, integer) """ - return YSpacingQt(parent, dim, stretchable, size) + return YSpacingQt(parent, dim, stretchable, size_px) From d94527ea49cda8aa7019ff20896c12d74c1154a6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 20:28:45 +0100 Subject: [PATCH 305/523] In Pixels and fixed Horizontal stretching --- manatools/aui/backends/gtk/hboxgtk.py | 19 ++++++++++++++----- manatools/aui/backends/gtk/spacinggtk.py | 10 ++++++++-- manatools/aui/yui_gtk.py | 6 +++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py index 483003c..a11f692 100644 --- a/manatools/aui/backends/gtk/hboxgtk.py +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -42,15 +42,24 @@ def _create_backend_widget(self): for child in self._children: try: - self._logger.debug("HBox child: %s", child.widgetClass()) + self._logger.debug("HBox child: %s stretch(H)=%s weight(H)=%s stretch(V)=%s", child.widgetClass(), child.stretchable(YUIDimension.YD_HORIZ), child.weight(YUIDimension.YD_HORIZ), child.stretchable(YUIDimension.YD_VERT)) except Exception: pass widget = child.get_backend_widget() try: - widget.set_hexpand(True) - widget.set_vexpand(True) - #if hasattr(widget, "set_halign"): - # widget.set_halign(Gtk.Align.FILL) + # Respect per-axis stretch flags + hexp = bool(child.stretchable(YUIDimension.YD_HORIZ) or child.weight(YUIDimension.YD_HORIZ)) + vexp = bool(child.stretchable(YUIDimension.YD_VERT) or child.weight(YUIDimension.YD_VERT)) + widget.set_hexpand(hexp) + widget.set_vexpand(vexp) + try: + widget.set_halign(Gtk.Align.FILL if hexp else Gtk.Align.CENTER) + except Exception: + pass + try: + widget.set_valign(Gtk.Align.FILL if vexp else Gtk.Align.CENTER) + except Exception: + pass except Exception: pass diff --git a/manatools/aui/backends/gtk/spacinggtk.py b/manatools/aui/backends/gtk/spacinggtk.py index a5570e4..dfbd7c8 100644 --- a/manatools/aui/backends/gtk/spacinggtk.py +++ b/manatools/aui/backends/gtk/spacinggtk.py @@ -25,15 +25,21 @@ class YSpacingGtk(YWidget): - `size`: spacing size in pixels (device units). Pixels are used for clarity and uniformity across backends; GTK honors `set_size_request` in pixels. """ - def __init__(self, parent=None, dim: YUIDimension = YUIDimension.YD_HORIZ, stretchable: bool = False, size: float = 0.0): + def __init__(self, parent=None, dim: YUIDimension = YUIDimension.YD_HORIZ, stretchable: bool = False, size_px: int = 0): super().__init__(parent) self._dim = dim self._stretchable = bool(stretchable) try: - self._size_px = max(0, int(round(float(size)))) + spx = int(size_px) + self._size_px = 0 if spx <= 0 else max(1, spx) except Exception: self._size_px = 0 self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + # Keep base stretch flags in sync for layout containers + try: + self.setStretchable(self._dim, self._stretchable) + except Exception: + pass try: self._logger.debug("%s.__init__(dim=%s, stretchable=%s, size_px=%d)", self.__class__.__name__, self._dim, self._stretchable, self._size_px) except Exception: diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index b26dfca..a5eb73b 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -267,14 +267,14 @@ def createReplacePoint(self, parent): """Create a ReplacePoint widget (GTK backend).""" return YReplacePointGtk(parent) - def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size: float = 0.0): + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size_px: int = 0): """Create a Spacing/Stretch widget. - `dim`: primary dimension for spacing (YUIDimension) - `stretchable`: expand in primary dimension when True (minimum size = `size`) - - `size`: spacing size in pixels (device units) + - `size_px`: spacing size in pixels (device units, integer) """ - return YSpacingGtk(parent, dim, stretchable, size) + return YSpacingGtk(parent, dim, stretchable, size_px) def createFrame(self, parent, label: str=""): """Create a Frame widget.""" From c652b28b81ba87a7e517a23997a8e1d62625e832 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 21:44:16 +0100 Subject: [PATCH 306/523] pixel as 0.125 chars --- manatools/aui/backends/curses/commoncurses.py | 21 +++++++++---------- manatools/aui/yui_curses.py | 7 ++++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py index 7225c7a..a375fff 100644 --- a/manatools/aui/backends/curses/commoncurses.py +++ b/manatools/aui/backends/curses/commoncurses.py @@ -16,24 +16,23 @@ _logger = logging.getLogger("manatools.aui.curses.common") -def pixels_to_chars(size_px: float, dim: YUIDimension) -> int: - """Convert a pixel size into character cells for curses. +def pixels_to_chars(size_px: int, dim: YUIDimension) -> int: + """Convert a pixel size into terminal character cells. - Mapping rationale: libyui uses an abstract unit where a main window of - 800x600 pixels corresponds to an 80x25 character window. We adopt the same - ratio: 10 px per column horizontally, 24 px per row vertically. + Horizontal (X): 1 character = 8 pixels (i.e., 1 pixel = 0.125 char). + Vertical (Y): assumed 1 character row ≈ 16 pixels (typical terminal font). - This conversion provides a uniform interpretation of the `size` argument - across Qt/GTK/curses backends. + The vertical ratio can be adjusted later if needed; this maps pixel sizes + to curses units uniformly with Qt/GTK which operate in pixels. """ try: - px = max(0.0, float(size_px)) + px = max(0, int(size_px)) except Exception: - px = 0.0 + px = 0 if dim == YUIDimension.YD_HORIZ: - return max(0, int(round(px / 10.0))) + return max(0, int(round(px / 8.0))) else: - return max(0, int(round(px / 24.0))) + return max(0, int(round(px / 16.0))) # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: ''' diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 92d2c55..5218a9c 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -234,11 +234,12 @@ def createReplacePoint(self, parent): """Create a ReplacePoint widget (curses backend).""" return YReplacePointCurses(parent) - def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size: float = 0.0): + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size_px: int = 0): """Create a Spacing/Stretch widget. - `dim`: primary dimension for spacing (YUIDimension) - `stretchable`: expand in primary dimension when True (minimum size = `size`) - - `size`: spacing size in pixels, converted to character cells using 800x600→80x25 mapping + - `size_px`: spacing size in pixels (integer), converted to character cells + using 8 px/char horizontally and ~16 px/row vertically. """ - return YSpacingCurses(parent, dim, stretchable, size) \ No newline at end of file + return YSpacingCurses(parent, dim, stretchable, size_px) \ No newline at end of file From 13d59d36115f3fe718b28d62b900dc682824e682 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 22:53:40 +0100 Subject: [PATCH 307/523] fixed last right widget with a stretchable spacyng behind --- manatools/aui/backends/curses/commoncurses.py | 126 +++++++++++++----- manatools/aui/backends/curses/hboxcurses.py | 77 ++++++++--- 2 files changed, 156 insertions(+), 47 deletions(-) diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py index a375fff..8078f28 100644 --- a/manatools/aui/backends/curses/commoncurses.py +++ b/manatools/aui/backends/curses/commoncurses.py @@ -9,12 +9,23 @@ @package manatools.aui.backends.curses ''' +import curses +import curses.ascii +import sys +import os +import time import logging -from ...yui_common import YUIDimension +from ...yui_common import * -__all__ = ["pixels_to_chars"] +__all__ = ["pixels_to_chars", "_curses_recursive_min_height", "_curses_recursive_min_width"] -_logger = logging.getLogger("manatools.aui.curses.common") +# Module-level logger for common curses helpers +_mod_logger = logging.getLogger("manatools.aui.curses.common") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) def pixels_to_chars(size_px: int, dim: YUIDimension) -> int: """Convert a pixel size into terminal character cells. @@ -33,35 +44,6 @@ def pixels_to_chars(size_px: int, dim: YUIDimension) -> int: return max(0, int(round(px / 8.0))) else: return max(0, int(round(px / 16.0))) -# vim: set fileencoding=utf-8 : -# vim: set et ts=4 sw=4: -''' -Python manatools.aui.backends.curses contains all curses backend classes - -License: LGPLv2+ - -Author: Angelo Naselli - -@package manatools.aui.backends.curses -''' -import curses -import curses.ascii -import sys -import os -import time -import logging -from ...yui_common import * - -# Module-level logger for common curses helpers -_mod_logger = logging.getLogger("manatools.aui.curses.common.module") -if not logging.getLogger().handlers: - _h = logging.StreamHandler() - _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) - _mod_logger.addHandler(_h) - _mod_logger.setLevel(logging.INFO) - -__all__ = ["_curses_recursive_min_height"] - def _curses_recursive_min_height(widget): """Compute minimal height for a widget, recursively considering container children.""" @@ -105,3 +87,83 @@ def _curses_recursive_min_height(widget): except Exception: pass return max(1, getattr(widget, "_height", 1)) + + +def _curses_recursive_min_width(widget): + """Compute minimal width for a widget, recursively considering container children. + + Heuristics: + - YHBox: sum of children's minimal widths plus 1 char spacing between them + - YVBox: maximal minimal width among children + - Frames (YFrame/YCheckBoxFrame): border (2) + inner minimal width + - Alignment/ReplacePoint: pass-through to child + - Basic widgets: use `minWidth()` if available, otherwise infer from text/label + """ + try: + if widget is None: + return 1 + cls = widget.widgetClass() if hasattr(widget, "widgetClass") else "" + # Helper to infer min width of leaf/basic widgets + def _leaf_min_width(w): + try: + if hasattr(w, "minWidth"): + m = int(w.minWidth()) + return max(1, m) + except Exception: + pass + try: + c = w.widgetClass() if hasattr(w, "widgetClass") else "" + if c in ("YLabel", "YPushButton", "YCheckBox"): + text = getattr(w, "_text", None) + if text is None: + text = getattr(w, "_label", "") + pad = 4 if c == "YPushButton" else 0 + # Checkbox includes symbol like "[ ] " + if c == "YCheckBox": + pad = max(pad, 4) + return max(1, len(str(text)) + pad) + except Exception: + pass + try: + return max(1, int(getattr(w, "_width", 10))) + except Exception: + return 10 + + if cls == "YHBox": + chs = list(getattr(widget, "_children", []) or []) + if not chs: + return 1 + spacing = max(0, len(chs) - 1) + total = 0 + for c in chs: + total += _curses_recursive_min_width(c) + return max(1, total + spacing) + elif cls == "YVBox": + chs = list(getattr(widget, "_children", []) or []) + if not chs: + return 1 + widest = 1 + for c in chs: + widest = max(widest, _curses_recursive_min_width(c)) + return max(1, widest) + elif cls == "YAlignment": + child = widget.child() + return max(1, _curses_recursive_min_width(child)) + elif cls == "YReplacePoint": + child = widget.child() + return max(1, _curses_recursive_min_width(child)) + elif cls == "YFrame" or cls == "YCheckBoxFrame": + child = widget.child() + inner_min = _curses_recursive_min_width(child) if child is not None else 1 + return max(3, 2 + inner_min) # borders(2) + inner + else: + return _leaf_min_width(widget) + except Exception as e: + try: + _mod_logger.error("_curses_recursive_min_width error: %s", e, exc_info=True) + except Exception: + pass + try: + return max(1, int(getattr(widget, "_width", 10))) + except Exception: + return 10 diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py index fe893d2..963b9d4 100644 --- a/manatools/aui/backends/curses/hboxcurses.py +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -115,7 +115,7 @@ def _child_min_width(self, child, max_width): text = getattr(child, "_text", None) if text is None: text = getattr(child, "_label", "") - pad = 4 if cls == "YPushButton" else 0 + pad = 4 if cls in ("YPushButton", "YCheckBox") else 0 return min(max_width, max(1, len(str(text)) + pad)) except Exception: pass @@ -131,31 +131,78 @@ def _draw(self, window, y, x, width, height): spacing = max(0, num_children - 1) available = max(0, width - spacing) + # Compute a safe minimal reservation for every child so that + # stretchable spacers cannot steal the minimum space required + # by subsequent fixed widgets (e.g. the final checkbox). widths = [0] * num_children stretchables = [] - fixed_total = 0 + min_reserved = [0] * num_children for i, child in enumerate(self._children): + # compute each child's minimal width (best-effort) + m = self._child_min_width(child, available) + min_reserved[i] = max(1, m) if child.stretchable(YUIDimension.YD_HORIZ): stretchables.append(i) - else: - w = self._child_min_width(child, available) - widths[i] = w - fixed_total += w - remaining = max(0, available - fixed_total) - if stretchables: + # Sum fixed (non-stretchable) minimal widths and minimal total for stretchables + fixed_total = sum(min_reserved[i] for i, c in enumerate(self._children) if not c.stretchable(YUIDimension.YD_HORIZ)) + min_stretch_total = sum(min_reserved[i] for i, c in enumerate(self._children) if c.stretchable(YUIDimension.YD_HORIZ)) + + # Available space already accounts for gaps + remaining = available - fixed_total - min_stretch_total + + if stretchables and remaining > 0: + # Start from each stretchable's minimum, then distribute leftover per = remaining // len(stretchables) extra = remaining % len(stretchables) for k, idx in enumerate(stretchables): - widths[idx] = max(1, per + (1 if k < extra else 0)) + widths[idx] = min_reserved[idx] + per + (1 if k < extra else 0) + # Fixed children get their reserved minima + for i, child in enumerate(self._children): + if not child.stretchable(YUIDimension.YD_HORIZ): + widths[i] = min_reserved[i] else: - if fixed_total < available: - leftover = available - fixed_total - per = leftover // num_children - extra = leftover % num_children + # Either no stretchables, or not enough space to give extra. + # In this case, try to honor minimal reservations as much as possible. + # If total minima exceed available, shrink proportionally but keep at least 1. + total_min = fixed_total + min_stretch_total + if total_min <= available: + # we have some leftover but no stretchables to expand; distribute among all + leftover = available - total_min + per = leftover // num_children if num_children else 0 + extra = leftover % num_children if num_children else 0 for i in range(num_children): - base = widths[i] if widths[i] else 1 - widths[i] = base + per + (1 if i < extra else 0) + widths[i] = min_reserved[i] + per + (1 if i < extra else 0) + else: + # Need to shrink some minima to fit; compute shrink ratio + # Start from minima and reduce from largest items first to preserve small widgets + widths = list(min_reserved) + overflow = total_min - available + # Sort indices by current width descending + order = sorted(range(num_children), key=lambda ii: widths[ii], reverse=True) + for idx in order: + if overflow <= 0: + break + can_reduce = widths[idx] - 1 + if can_reduce <= 0: + continue + take = min(can_reduce, overflow) + widths[idx] -= take + overflow -= take + # If still overflow (shouldn't happen), clamp all to 1 + if overflow > 0: + for i in range(num_children): + widths[i] = 1 + + # Debug: log final allocation before drawing children + try: + total_assigned = sum(widths) + self._logger.info("HBox final widths=%s total=%d (available=%d)", widths, total_assigned, available) + self._logger.debug("HBox internal min_reserved=%s fixed_total=%d min_stretch_total=%d num_stretch=%d", + min_reserved, fixed_total, min_stretch_total if 'min_stretch_total' in locals() else 0, + len(stretchables)) + except Exception: + pass # Draw children and pass full container height to stretchable children cx = x From 80936c05b20321a6a671aaf47b08445c82810fbf Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 22:56:10 +0100 Subject: [PATCH 308/523] Improved test spacing --- test/test_spacing.py | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/test/test_spacing.py b/test/test_spacing.py index 80d1806..87c37c7 100644 --- a/test/test_spacing.py +++ b/test/test_spacing.py @@ -2,9 +2,24 @@ import os import sys +import logging # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s') + root_logger = logging.getLogger() + fileHandler = logging.FileHandler(log_name, mode='w') + fileHandler.setFormatter(logFormatter) + root_logger.addHandler(fileHandler) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + root_logger.addHandler(consoleHandler) + consoleHandler.setLevel(logging.INFO) + root_logger.setLevel(logging.DEBUG) +except Exception as _e: + logging.getLogger().exception("Failed to configure file logger: %s", _e) def test_Spacing(backend_name=None): if backend_name: @@ -24,30 +39,42 @@ def test_Spacing(backend_name=None): backend = YUI.backend() print(f"Using backend: {backend.value}") + # Basic logging setup + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s: %(message)s") + ui = YUI_ui() ui.yApp().setApplicationTitle("Spacing Demo") factory = ui.widgetFactory() dialog = factory.createMainDialog() vbox = factory.createVBox(dialog) - factory.createLabel(vbox, "Spacing demo: pixels as unit; curses converts to chars.") + factory.createHeading(vbox, "Spacing demo: pixels as unit; curses converts to chars.") + factory.createLabel(vbox, "Next row is [LeftLabel, spacing 50px, button, spacing 80px (stretchable), checkbox]") # Horizontal spacing line: label - spacing(50px, fixed) - button - spacing(80px, stretch min) hbox = factory.createHBox(vbox) factory.createLabel(hbox, "LeftLabel") - factory.createSpacing(hbox, yui.YUIDimension.YD_HORIZ, False, 50.0) + factory.createSpacing(hbox, yui.YUIDimension.YD_HORIZ, False, 50) factory.createPushButton(hbox, "Click Me") - factory.createSpacing(hbox, yui.YUIDimension.YD_HORIZ, True, 80.0) + # stretchable spacing should take remaining width between button and checkbox + s = factory.createSpacing(hbox, yui.YUIDimension.YD_HORIZ, True, 80) factory.createCheckBox(hbox, "Check", False) + factory.createLabel(vbox, "Next vertical spacing 24px (fixed) between rows") # Vertical spacing between rows (24px ~= 1 char in curses) - factory.createSpacing(vbox, yui.YUIDimension.YD_VERT, False, 24.0) + factory.createSpacing(vbox, yui.YUIDimension.YD_VERT, False, 24) # Another row with stretchable vertical spacing on both sides + factory.createLabel(vbox, "Next row is [spacing 20px (stretchable), centered button, spacing 20px (stretchable)]") hbox2 = factory.createHBox(vbox) - factory.createSpacing(hbox2, yui.YUIDimension.YD_HORIZ, True, 20.0) - factory.createLabel(hbox2, "Centered by spacers") - factory.createSpacing(hbox2, yui.YUIDimension.YD_HORIZ, True, 20.0) + factory.createSpacing(hbox2, yui.YUIDimension.YD_HORIZ, True, 20) + btn = factory.createPushButton(hbox2, "Centered by spacers") + btn.setStretchable(yui.YUIDimension.YD_HORIZ, False) + btn.setStretchable(yui.YUIDimension.YD_VERT, False) + factory.createSpacing(hbox2, yui.YUIDimension.YD_HORIZ, True, 20) + factory.createLabel(vbox, "Next vertical spacing 20px (stretchable) between rows") + # add a vertical stretch spacer below the row to demonstrate vertical expansion + factory.createSpacing(vbox, yui.YUIDimension.YD_VERT, True, 20) # OK button ok = factory.createPushButton(vbox, "OK") From 174743a96c60940dc8c0ba51e1dd1be22d251535 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 2 Jan 2026 22:56:55 +0100 Subject: [PATCH 309/523] Updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 881ef68..850e1f8 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -22,7 +22,7 @@ Missing Widgets comparing libyui original factory: [X] YMultiLineEdit [X] YIntField [X] YMenuBar - [ ] YSpacing (detailed variants: createHStretch/createVStretch/createHSpacing/createVSpacing/createSpacing) + [X] YSpacing (detailed variants: createHStretch/createVStretch/createHSpacing/createVSpacing/createSpacing) [X] YAlignment helpers (createLeft/createRight/createTop/createBottom/createHCenter/createVCenter/createHVCenter) [X] YReplacePoint [X] YRadioButton From de82f8b2c72ce130e0e8684cf65a45ebbb1c7bf1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 12:35:25 +0100 Subject: [PATCH 310/523] Added create Spacing variants and MultiSelectionBox by using selection box with multi-selection enabled --- .../aui/backends/curses/selectionboxcurses.py | 5 +- manatools/aui/backends/gtk/selectionboxgtk.py | 5 +- manatools/aui/backends/qt/selectionboxqt.py | 5 +- manatools/aui/yui_common.py | 2 +- manatools/aui/yui_curses.py | 24 ++++++++- manatools/aui/yui_gtk.py | 54 +++++++++++++------ manatools/aui/yui_qt.py | 23 +++++++- 7 files changed, 93 insertions(+), 25 deletions(-) diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index aa7eff2..cc99255 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -14,6 +14,7 @@ import sys import os import time +from typing import Optional import logging from ...yui_common import * @@ -28,7 +29,7 @@ class YSelectionBoxCurses(YSelectionWidget): - def __init__(self, parent=None, label=""): + def __init__(self, parent=None, label="", multi_selection: Optional[bool] = False): super().__init__(parent) # per-instance logger named by package/backend/class self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") @@ -40,7 +41,7 @@ def __init__(self, parent=None, label=""): self._label = label self._value = "" self._selected_items = [] - self._multi_selection = False + self._multi_selection = multi_selection # UI state for drawing/navigation # actual minimal height for layout (keep small so parent can expand it) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 6d06be4..3a0dc67 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -17,18 +17,19 @@ import cairo import threading import os +from typing import Optional import logging from ...yui_common import * from .commongtk import _resolve_icon class YSelectionBoxGtk(YSelectionWidget): - def __init__(self, parent=None, label=""): + def __init__(self, parent=None, label="", multi_selection: Optional[bool] = False): super().__init__(parent) self._label = label self._value = "" self._old_selected_items = [] # for change detection - self._multi_selection = False + self._multi_selection = bool(multi_selection) self._listbox = None self._backend_widget = None # keep a stable list of rows we create so we don't rely on ListBox container APIs diff --git a/manatools/aui/backends/qt/selectionboxqt.py b/manatools/aui/backends/qt/selectionboxqt.py index 066b254..753e70f 100644 --- a/manatools/aui/backends/qt/selectionboxqt.py +++ b/manatools/aui/backends/qt/selectionboxqt.py @@ -12,16 +12,17 @@ from PySide6 import QtWidgets import logging import os +from typing import Optional from ...yui_common import * from .commonqt import _resolve_icon class YSelectionBoxQt(YSelectionWidget): - def __init__(self, parent=None, label=""): + def __init__(self, parent=None, label="", multi_selection: Optional[bool] = False): super().__init__(parent) self._label = label self._value = "" self._selected_items = [] - self._multi_selection = False + self._multi_selection = multi_selection self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) self._list_widget = None diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 8ffa04a..289622c 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -150,7 +150,7 @@ def widgetClass(self): return self.__class__.__name__ def debugLabel(self): - return f"{self.widgetClass()}({self._id})" + return f"{self._parent.debugLabel()}/{self.widgetClass()}({self._id})" if self._parent else f"{self.widgetClass()}({self._id})" def helpText(self): return self._help_text diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 5218a9c..4199df8 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -172,6 +172,10 @@ def createComboBox(self, parent, label, editable=False): def createSelectionBox(self, parent, label): return YSelectionBoxCurses(parent, label) + #Multi-selection box variant + def createMultiSelectionBox(self, parent, label): + return YSelectionBoxCurses(parent, label, multi_selection=True) + # Alignment helpers def createLeft(self, parent): return YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignBegin, vertAlign=YAlignmentType.YAlignUnchanged) @@ -242,4 +246,22 @@ def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, si - `size_px`: spacing size in pixels (integer), converted to character cells using 8 px/char horizontally and ~16 px/row vertically. """ - return YSpacingCurses(parent, dim, stretchable, size_px) \ No newline at end of file + return YSpacingCurses(parent, dim, stretchable, size_px) + + # Create a Spacing widget variant + def createHStretch(self, parent): + """Create a Horizontal Stretch widget.""" + return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=True) + + def createVStretch(self, parent): + """Create a Vertical Stretch widget.""" + return self.createSpacing(parent, YUIDimension.Vertical, stretchable=True) + + def createHSpacing(self, parent, size_px: int = 8): + """Create a Horizontal Spacing widget.""" + return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=False, size_px=size_px) + + def createVSpacing(self, parent, size_px: int = 16): + """Create a Vertical Spacing widget.""" + return self.createSpacing(parent, YUIDimension.Vertical, stretchable=False, size_px=size_px) + diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index a5eb73b..955ddb0 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -223,7 +223,11 @@ def createComboBox(self, parent, label, editable=False): def createSelectionBox(self, parent, label): return YSelectionBoxGtk(parent, label) - + + #Multi-selection box variant + def createMultiSelectionBox(self, parent, label): + return YSelectionBoxGtk(parent, label, multi_selection=True) + def createMenuBar(self, parent): """Create a MenuBar widget (GTK backend).""" return YMenuBarGtk(parent) @@ -263,6 +267,22 @@ def createTable(self, parent, header: YTableHeader, multiSelection: bool = False from .backends.gtk.tablegtk import YTableGtk return YTableGtk(parent, header, multiSelection) + def createRichText(self, parent, text: str = "", plainTextMode: bool = False): + """Create a RichText widget (GTK backend).""" + from .backends.gtk.richtextgtk import YRichTextGtk + return YRichTextGtk(parent, text, plainTextMode) + + def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): + """Create a CheckBox Frame widget.""" + return YCheckBoxFrameGtk(parent, label, checked) + + def createProgressBar(self, parent, label, max_value=100): + return YProgressBarGtk(parent, label, max_value) + + def createRadioButton(self, parent, label:str = "", isChecked:bool = False): + """Create a Radio Button widget.""" + return YRadioButtonGtk(parent, label, isChecked) + def createReplacePoint(self, parent): """Create a ReplacePoint widget (GTK backend).""" return YReplacePointGtk(parent) @@ -275,23 +295,25 @@ def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, si - `size_px`: spacing size in pixels (device units, integer) """ return YSpacingGtk(parent, dim, stretchable, size_px) + + # Create a Spacing widget variant + def createHStretch(self, parent): + """Create a Horizontal Stretch widget.""" + return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=True) + + def createVStretch(self, parent): + """Create a Vertical Stretch widget.""" + return self.createSpacing(parent, YUIDimension.Vertical, stretchable=True) + + def createHSpacing(self, parent, size_px: int = 8): + """Create a Horizontal Spacing widget.""" + return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=False, size_px=size_px) + + def createVSpacing(self, parent, size_px: int = 16): + """Create a Vertical Spacing widget.""" + return self.createSpacing(parent, YUIDimension.Vertical, stretchable=False, size_px=size_px) def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) - def createRichText(self, parent, text: str = "", plainTextMode: bool = False): - """Create a RichText widget (GTK backend).""" - from .backends.gtk.richtextgtk import YRichTextGtk - return YRichTextGtk(parent, text, plainTextMode) - - def createCheckBoxFrame(self, parent, label: str = "", checked: bool = False): - """Create a CheckBox Frame widget.""" - return YCheckBoxFrameGtk(parent, label, checked) - - def createProgressBar(self, parent, label, max_value=100): - return YProgressBarGtk(parent, label, max_value) - - def createRadioButton(self, parent, label:str = "", isChecked:bool = False): - """Create a Radio Button widget.""" - return YRadioButtonGtk(parent, label, isChecked) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 2852a2a..f3f1bdb 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -180,7 +180,11 @@ def createComboBox(self, parent, label, editable=False): def createSelectionBox(self, parent, label): return YSelectionBoxQt(parent, label) - + + #Multi-selection box variant + def createMultiSelectionBox(self, parent, label): + return YSelectionBoxQt(parent, label, multi_selection=True) + def createProgressBar(self, parent, label, max_value=100): return YProgressBarQt(parent, label, max_value) @@ -253,3 +257,20 @@ def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, si - `size_px`: spacing size in pixels (device units, integer) """ return YSpacingQt(parent, dim, stretchable, size_px) + + # Create a Spacing widget variant + def createHStretch(self, parent): + """Create a Horizontal Stretch widget.""" + return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=True) + + def createVStretch(self, parent): + """Create a Vertical Stretch widget.""" + return self.createSpacing(parent, YUIDimension.Vertical, stretchable=True) + + def createHSpacing(self, parent, size_px: int = 8): + """Create a Horizontal Spacing widget.""" + return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=False, size_px=size_px) + + def createVSpacing(self, parent, size_px: int = 16): + """Create a Vertical Spacing widget.""" + return self.createSpacing(parent, YUIDimension.Vertical, stretchable=False, size_px=size_px) \ No newline at end of file From 5ace12df47bc5088c9fc756ea2b62e9c04586c7c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 12:42:31 +0100 Subject: [PATCH 311/523] logging information to help debugging --- manatools/aui/backends/curses/dialogcurses.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index b4fcc02..e53b1fb 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -158,6 +158,10 @@ def _draw_dialog(self): try: height, width = self._backend_widget.getmaxyx() + try: + self._logger.info("Dialog window size: height=%d width=%d", height, width) + except Exception: + pass # Clear screen self._backend_widget.clear() @@ -182,6 +186,10 @@ def _draw_dialog(self): # Draw content area - fixed coordinates for child content_height = height - 4 content_width = width - 4 + try: + self._logger.info("Dialog content area: y=%d x=%d h=%d w=%d", content_y, content_x, content_height, content_width) + except Exception: + pass content_y = 2 content_x = 2 From 75f06d31f1f946272dee6ee04b416e47188c8961 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 12:42:59 +0100 Subject: [PATCH 312/523] updated --- sow/TODO.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 850e1f8..9cadd41 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -9,7 +9,8 @@ Next is the starting todo list. Missing Widgets comparing libyui original factory: [X] YComboBox - [X] YSelectionBox + [X] YSelectionBox + [X] YMultiSelectionBox (implemented as YSelectionBox + multiselection enabled) [X] YPushButton [X] YLabel [X] YInputField @@ -35,8 +36,7 @@ Missing Widgets comparing libyui original factory: [ ] YSquash / createSquash Skipped widgets: - - [-] YMultiSelectionBox (implemented as YSelectionBox + multiselection enabled) + [-] YPackageSelector (not ported) [-] YRadioButtonGroup (not ported) [-] YMenuButton (legacy menus) From 589103aa6f085150f0c1e3914400686cb1c6dc10 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 15:37:08 +0100 Subject: [PATCH 313/523] First attempt to produce YImage --- manatools/aui/backends/curses/__init__.py | 2 + manatools/aui/backends/curses/imagecurses.py | 131 ++++++++++++++ manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/imagegtk.py | 172 +++++++++++++++++++ manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/imageqt.py | 153 +++++++++++++++++ manatools/aui/yui_curses.py | 4 + manatools/aui/yui_gtk.py | 4 + manatools/aui/yui_qt.py | 4 + share/images/manatools.png | Bin 0 -> 11128 bytes test/test_image.py | 100 +++++++++++ 11 files changed, 574 insertions(+) create mode 100644 manatools/aui/backends/curses/imagecurses.py create mode 100644 manatools/aui/backends/gtk/imagegtk.py create mode 100644 manatools/aui/backends/qt/imageqt.py create mode 100644 share/images/manatools.png create mode 100644 test/test_image.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 2ae6a9a..14d7b68 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -20,6 +20,7 @@ from .intfieldcurses import YIntFieldCurses from .multilineeditcurses import YMultiLineEditCurses from .spacingcurses import YSpacingCurses +from .imagecurses import YImageCurses __all__ = [ "YDialogCurses", @@ -44,5 +45,6 @@ "YIntFieldCurses", "YMultiLineEditCurses", "YSpacingCurses", + "YImageCurses", # ... ] diff --git a/manatools/aui/backends/curses/imagecurses.py b/manatools/aui/backends/curses/imagecurses.py new file mode 100644 index 0000000..d4fd94c --- /dev/null +++ b/manatools/aui/backends/curses/imagecurses.py @@ -0,0 +1,131 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +Python manatools.aui.backends.curses contains curses backend for YImage + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +""" +import curses +import logging +import os +from ...yui_common import * + +# Module-level logger for image curses backend +_mod_logger = logging.getLogger("manatools.aui.curses.image.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + + +class YImageCurses(YWidget): + def __init__(self, parent=None, imageFileName=""): + super().__init__(parent) + self._imageFileName = imageFileName + self._auto_scale = False + self._zero_size = {YUIDimension.YD_HORIZ: False, YUIDimension.YD_VERT: False} + self._height = 3 + self._width = 10 + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._logger.debug("%s.__init__ file=%s", self.__class__.__name__, imageFileName) + + def widgetClass(self): + return "YImage" + + def imageFileName(self): + return self._imageFileName + + def setImage(self, imageFileName): + try: + self._imageFileName = imageFileName + except Exception: + self._logger.exception("setImage failed") + + def autoScale(self): + return bool(self._auto_scale) + + def setAutoScale(self, on=True): + try: + self._auto_scale = bool(on) + except Exception: + self._logger.exception("setAutoScale failed") + + def hasZeroSize(self, dim): + return bool(self._zero_size.get(dim, False)) + + def setZeroSize(self, dim, zeroSize=True): + self._zero_size[dim] = bool(zeroSize) + + def _create_backend_widget(self): + try: + # nothing to create for curses; drawing uses _draw + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + self._logger.exception("_create_backend_widget failed") + + def _draw(self, window, y, x, width, height): + try: + if width <= 0 or height <= 0: + return + # draw a box using box-drawing unicode characters + try: + horiz = '═' + vert = '║' + tl = '╔' + tr = '╗' + bl = '╚' + br = '╝' + except Exception: + horiz = '-' + vert = '|' + tl = '+' + tr = '+' + bl = '+' + br = '+' + + # top border + try: + window.addstr(y, x, tl + horiz * max(0, width - 2) + tr) + except curses.error: + pass + # middle + for row in range(1, max(0, height - 1)): + try: + window.addstr(y + row, x, vert) + # fill space + try: + window.addstr(y + row, x + 1, ' ' * max(0, width - 2)) + except curses.error: + pass + window.addstr(y + row, x + max(0, width - 1), vert) + except curses.error: + pass + # bottom border + if height >= 2: + try: + window.addstr(y + max(0, height - 1), x, bl + horiz * max(0, width - 2) + br) + except curses.error: + pass + + # show filename centered (single line) if space + if self._imageFileName: + fname = os.path.basename(self._imageFileName) + line = f" {fname} " + if len(line) > max(0, width - 2): + line = line[:max(0, width - 5)] + '...' + try: + cx = x + max(1, (width - len(line)) // 2) + cy = y + max(1, height // 2) + window.addstr(cy, cx, line) + except curses.error: + pass + except Exception: + self._logger.exception("_draw failed") diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 3b9f2ac..ca88aa1 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -20,6 +20,7 @@ from .intfieldgtk import YIntFieldGtk from .multilineeditgtk import YMultiLineEditGtk from .spacinggtk import YSpacingGtk +from .imagegtk import YImageGtk __all__ = [ "YDialogGtk", @@ -44,5 +45,6 @@ "YIntFieldGtk", "YMultiLineEditGtk", "YSpacingGtk", + "YImageGtk", # ... ] diff --git a/manatools/aui/backends/gtk/imagegtk.py b/manatools/aui/backends/gtk/imagegtk.py new file mode 100644 index 0000000..c8f2126 --- /dev/null +++ b/manatools/aui/backends/gtk/imagegtk.py @@ -0,0 +1,172 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +Python manatools.aui.backends.gtk contains GTK backend for YImage + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +""" +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('GdkPixbuf', '2.0') +from gi.repository import Gtk, GdkPixbuf, GObject +import logging +import os +from ...yui_common import * +from .commongtk import _resolve_icon, _resolve_gicon + + +class YImageGtk(YWidget): + def __init__(self, parent=None, imageFileName=""): + super().__init__(parent) + self._imageFileName = imageFileName + self._auto_scale = False + self._zero_size = {YUIDimension.YD_HORIZ: False, YUIDimension.YD_VERT: False} + self._pixbuf = None + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._logger.debug("%s.__init__ file=%s", self.__class__.__name__, imageFileName) + + def widgetClass(self): + return "YImage" + + def imageFileName(self): + return self._imageFileName + + def setImage(self, imageFileName): + try: + self._imageFileName = imageFileName + if getattr(self, '_backend_widget', None) is not None: + # Try resolving via common GTK helper (may be theme icon or file) + try: + resolved = _resolve_icon(imageFileName, size=48) + if resolved is not None: + # If helper returned a Gtk.Image, attempt to set it on backend + try: + paintable = resolved.get_paintable() if hasattr(resolved, 'get_paintable') else None + if paintable: + self._backend_widget.set_from_paintable(paintable) + return + except Exception: + pass + try: + pix = resolved.get_pixbuf() if hasattr(resolved, 'get_pixbuf') else None + if pix: + self._pixbuf = pix + self._apply_pixbuf() + return + except Exception: + pass + except Exception: + pass + + # Fallback: try to load from file directly + if os.path.exists(imageFileName): + try: + self._pixbuf = GdkPixbuf.Pixbuf.new_from_file(imageFileName) + self._apply_pixbuf() + except Exception: + self._logger.exception("failed to load pixbuf") + else: + self._logger.error("setImage: file not found: %s", imageFileName) + except Exception: + self._logger.exception("setImage failed") + + def autoScale(self): + return bool(self._auto_scale) + + def setAutoScale(self, on=True): + try: + self._auto_scale = bool(on) + self._apply_size_policy() + self._apply_pixbuf() + except Exception: + self._logger.exception("setAutoScale failed") + + def hasZeroSize(self, dim): + return bool(self._zero_size.get(dim, False)) + + def setZeroSize(self, dim, zeroSize=True): + self._zero_size[dim] = bool(zeroSize) + try: + self._apply_size_policy() + except Exception: + pass + + def _on_size_allocate(self, widget, allocation): + try: + if self._auto_scale and self._pixbuf is not None: + self._apply_pixbuf() + except Exception: + self._logger.exception("_on_size_allocate failed") + + def _create_backend_widget(self): + try: + self._backend_widget = Gtk.Image() + if self._imageFileName and os.path.exists(self._imageFileName): + try: + self._pixbuf = GdkPixbuf.Pixbuf.new_from_file(self._imageFileName) + except Exception: + self._logger.exception("failed to load pixbuf") + if getattr(self, '_pixbuf', None) is not None: + self._apply_pixbuf() + # hook allocation to rescale if autoscale is enabled + try: + self._backend_widget.connect('size-allocate', self._on_size_allocate) + except Exception: + pass + self._apply_size_policy() + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + self._logger.exception("_create_backend_widget failed") + + def _apply_size_policy(self): + try: + if getattr(self, '_backend_widget', None) is None: + return + try: + if hasattr(self._backend_widget, 'set_hexpand'): + self._backend_widget.set_hexpand(self._auto_scale or self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + pass + try: + if hasattr(self._backend_widget, 'set_vexpand'): + self._backend_widget.set_vexpand(self._auto_scale or self.stretchable(YUIDimension.YD_VERT)) + except Exception: + pass + except Exception: + self._logger.exception("_apply_size_policy failed") + + def _apply_pixbuf(self): + try: + if getattr(self, '_backend_widget', None) is None: + return + if not self._pixbuf: + self._backend_widget.clear() + return + if self._auto_scale and hasattr(self._backend_widget, 'get_allocated_width'): + try: + w = self._backend_widget.get_allocated_width() + h = self._backend_widget.get_allocated_height() + if w > 1 and h > 1: + scaled = self._pixbuf.scale_simple(w, h, GdkPixbuf.InterpType.BILINEAR) + self._backend_widget.set_from_pixbuf(scaled) + return + except Exception: + pass + # fallback: set original pixbuf + self._backend_widget.set_from_pixbuf(self._pixbuf) + except Exception: + self._logger.exception("_apply_pixbuf failed") + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_size_policy() + except Exception: + pass diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index d0e5c4a..9bbe92e 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -20,6 +20,7 @@ from .intfieldqt import YIntFieldQt from .multilineeditqt import YMultiLineEditQt from .spacingqt import YSpacingQt +from .imageqt import YImageQt __all__ = [ @@ -45,5 +46,6 @@ "YIntFieldQt", "YMultiLineEditQt", "YSpacingQt", + "YImageQt", # ... ] diff --git a/manatools/aui/backends/qt/imageqt.py b/manatools/aui/backends/qt/imageqt.py new file mode 100644 index 0000000..34706f7 --- /dev/null +++ b/manatools/aui/backends/qt/imageqt.py @@ -0,0 +1,153 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +Python manatools.aui.backends.qt contains Qt backend for YImage + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +""" +from PySide6 import QtWidgets, QtGui, QtCore +import logging +import os +from ...yui_common import * +from .commonqt import _resolve_icon as _qt_resolve_icon + + +class YImageQt(YWidget): + def __init__(self, parent=None, imageFileName=""): + super().__init__(parent) + self._imageFileName = imageFileName + self._auto_scale = False + self._zero_size = {YUIDimension.YD_HORIZ: False, YUIDimension.YD_VERT: False} + self._pixmap = None + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._logger.debug("%s.__init__ file=%s", self.__class__.__name__, imageFileName) + + def widgetClass(self): + return "YImage" + + def imageFileName(self): + return self._imageFileName + + def setImage(self, imageFileName): + try: + self._imageFileName = imageFileName + if getattr(self, '_backend_widget', None) is not None: + # Try resolving via common Qt helper (theme icon or filesystem) + try: + ico = _qt_resolve_icon(imageFileName) + except Exception: + ico = None + if ico is not None: + try: + # store QIcon and let _apply_pixmap pick appropriate size + self._qicon = ico + self._apply_pixmap() + return + except Exception: + pass + + # Fallback: try loading as pixmap from filesystem + if os.path.exists(imageFileName): + try: + self._pixmap = QtGui.QPixmap(imageFileName) + self._apply_pixmap() + except Exception: + self._logger.exception("setImage: failed to load QPixmap %s", imageFileName) + else: + self._logger.error("setImage: file not found: %s", imageFileName) + except Exception: + self._logger.exception("setImage failed") + + def autoScale(self): + return bool(self._auto_scale) + + def setAutoScale(self, on=True): + try: + self._auto_scale = bool(on) + # If autoscale is enabled, let Qt expand the widget when stretchable + self._apply_size_policy() + self._apply_pixmap() + except Exception: + self._logger.exception("setAutoScale failed") + + def hasZeroSize(self, dim): + return bool(self._zero_size.get(dim, False)) + + def setZeroSize(self, dim, zeroSize=True): + self._zero_size[dim] = bool(zeroSize) + try: + self._apply_size_policy() + except Exception: + pass + + def _create_backend_widget(self): + try: + self._backend_widget = QtWidgets.QLabel() + self._backend_widget.setAlignment(QtCore.Qt.AlignCenter) + if self._imageFileName and os.path.exists(self._imageFileName): + self._pixmap = QtGui.QPixmap(self._imageFileName) + self._apply_pixmap() + self._apply_size_policy() + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + self._logger.exception("_create_backend_widget failed") + + def _apply_size_policy(self): + try: + if getattr(self, '_backend_widget', None) is None: + return + horiz = QtWidgets.QSizePolicy.Policy.Expanding if self._auto_scale or self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Fixed + vert = QtWidgets.QSizePolicy.Policy.Expanding if self._auto_scale or self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed + try: + sp = self._backend_widget.sizePolicy() + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + self._backend_widget.setSizePolicy(sp) + except Exception: + pass + except Exception: + self._logger.exception("_apply_size_policy failed") + + def _apply_pixmap(self): + try: + if getattr(self, '_backend_widget', None) is None: + return + if not self._pixmap: + self._backend_widget.clear() + return + # If we have a QIcon (resolved from theme/name) prefer it as it can provide + # appropriately scaled pixmaps. Otherwise use the stored QPixmap. + try: + if getattr(self, '_qicon', None) is not None: + if self._auto_scale and self._backend_widget.size().isValid(): + pm = self._qicon.pixmap(self._backend_widget.size()) + else: + pm = self._qicon.pixmap(64, 64) + if pm and not pm.isNull(): + self._backend_widget.setPixmap(pm) + return + except Exception: + pass + + if self._pixmap: + if self._auto_scale and self._backend_widget.width() > 1 and self._backend_widget.height() > 1: + scaled = self._pixmap.scaled(self._backend_widget.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + self._backend_widget.setPixmap(scaled) + else: + self._backend_widget.setPixmap(self._pixmap) + except Exception: + self._logger.exception("_apply_pixmap failed") + + def setStretchable(self, dim, new_stretch): + try: + super().setStretchable(dim, new_stretch) + except Exception: + pass + try: + self._apply_size_policy() + except Exception: + pass diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 4199df8..e35337d 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -247,6 +247,10 @@ def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, si using 8 px/char horizontally and ~16 px/row vertically. """ return YSpacingCurses(parent, dim, stretchable, size_px) + + def createImage(self, parent, imageFileName): + """Create an image widget as an empty frame for curses.""" + return YImageCurses(parent, imageFileName) # Create a Spacing widget variant def createHStretch(self, parent): diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 955ddb0..39bf21a 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -295,6 +295,10 @@ def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, si - `size_px`: spacing size in pixels (device units, integer) """ return YSpacingGtk(parent, dim, stretchable, size_px) + + def createImage(self, parent, imageFileName): + """Create an image widget.""" + return YImageGtk(parent, imageFileName) # Create a Spacing widget variant def createHStretch(self, parent): diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index f3f1bdb..c6ed530 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -258,6 +258,10 @@ def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, si """ return YSpacingQt(parent, dim, stretchable, size_px) + def createImage(self, parent, imageFileName): + """Create an image widget.""" + return YImageQt(parent, imageFileName) + # Create a Spacing widget variant def createHStretch(self, parent): """Create a Horizontal Stretch widget.""" diff --git a/share/images/manatools.png b/share/images/manatools.png new file mode 100644 index 0000000000000000000000000000000000000000..97492c9b02e5df3f4df18b25de4a63e97d0940c8 GIT binary patch literal 11128 zcmb7KRZtvFxLsgzcXv&2clY2B+(VGy?j9gm@BqOrxVw9B7S~0CEbgv%|Hu1qAMRB3 zOwH6x_w-El`OZ1t7p1N$kB&lu0ssKe6%}MOp~v9=8WI9@KOkm44gfHVE6PZI_Rcy9 zK=j7fe!4%OSWDW#omHepMwX?~1RkP9uX{*-mQi1H68F*vn=bWNw;xxJ8R+P~Qx?>E z{?tp5O<^o?}P{ z(lv6FFusCMNYmpp^1&u?Vz4)^H`sY;h4FwcvH;-0-T|!yyZrxsOIHLSEBDl^3_OiH z!;I7`Xl=BrD-*=-VdzWZ3l9?;y<@U+rK9d{%NuO>bGy& z=r_v4)V%4o(tTd0G^+#d;7V(FoBC7w+@k}BB;?XXGe`-DFW+91hB@kMD}fzc91~%X zpFRu}@wy*1SC0dT1XXu{`y3d5xaDm2{5=*K?@gc|`LwQbR{dNL2WiVs3fM&PN@G@R zYp{H7$O6ZKI`UNg`efK%r}Okm+*aP&`+c^}ycSsvdxZWdkn)+RvqOYHF$g1fS>oe& zn5{IY0^_H$xB~3g9@ejf->Ze0JBaYg9IJzy_&Yj`G;Ur(z*WH}Mu*-e^_Ek*58d~{ zT>w&~ys!_mAn7bh%D;O59KGfzYx$Cx>+&HBcH*6><3X9Qf6`$F0I3KdrG5b&r!RNV z(H1;H&!VwoWhp%5Ne!f1G5{+%@0xZ?f=7-35C@3)MMw)1Qc^(f$7L~tkL4&buBWNL zb6Y^gb{-&SW=Qz`6gCfWnHNw1II%QZt(R^-^Ub*KtOYg0F0 zx8_fb!!G78Kep%~fgZ3u));f*I5bC27pEB8()2d`Az|gq;^o*kEwUU0rmkoHk^(19 z{2yz7T1u@OXQlbL<}_w=x_k%JQ06PWgmW@9m9UL=`|w&cjTW3054zntlNW_n;0M~n z4crSctrU^={*?q&BP=roLtr0>V<~~Vw!yLz*iNake0K!JN-z1wJxi(LNeu&reYyuQ zv{cIbC=oA)C8Pr9FOVd=-Mb}Lto6in859I(-iNoc~HdD zj#|;j+3oDun#&y)tX^k8A3z)-g2kDj*xB+(D4R3D!0>2GggGm>d{`nNIwMYx!{tIvIqnV7{i!PUui@&o zq2LsGk%Av493t`7pCi6J0tp-exqy;6%#QWTz|p%5ntu_36A5=N&pEb`83&;H?!sg3 zyC_9FJOe|V%2$?xiAy0t@o4`Wq=z?~i_G^Lo5=UgyTv}AvxVr_gJHAox-5Nnv{^-% z52#cm^ok+xouql~=Ex%P0CyzWD zNGE`-_4~NsHY|5c7IB91*aF=bz^*1KKc8ONQ(HJuTcJNY!itP&sK&77|4ZdDKym0%#ZSbD@eFQ1XL zK3b38ac@RF%|pn;r+gGe^sG=I*0@4fWUJrVmH_nuN*EVzTxJMZ=vzG5@bV-wd@o|f z!X57*KcD)+n@Y3jH4Tqa1MC%BV#SPg!z5H~j0Z+;Bc^sn%j z7~)rM0^<0U3WAKmT$EI3WcX$*y>yXqJ47*MC-xmieW$>L(;vB(ClE&J^DB5;;vf7} z30v}|+ZY0(DcQlUII(Hpk^8b5G_1mZnzBW=KL)=G3>7Td?Ya289~*JT=uLniRWPsP#q<%tQQE&U}x28nKURej}SEt=UkNSB7KvmQ1`; z`P#{WG{NS)k4%LQ_|0?G$ld2H3V?&KR7YDZ!-Nfl z%l_3}p@a!6C=l{$-aHN znNwR7^}8b?vWSe0-7S=le}nXfz_C*qILY!Chc=)^H*V4#3?Xn5n64Y&o^5pb?c8q$ ziO+oHD$9L+dExN6+8sm2Ww^0(if$A2KCWts)#z4e$rk0&=aeJR@A?<_o8E1408`v( zN!!B{xGp3g2Uh)Q6>*Tg4qZF#v9V}|IywXmEA|O=$OzwImW;i!zx^AG3OU256Z3h? zk@vL5?`-C7kz=ah9k6YZG(%HFu510sNn~!qN6#|CE+5?;c-Hx2|MefJT{zT{9R0*` zyTXHu;ZJe#8!sIlu)Dh(2?HbI#}CB7$34PLI7AHpy|bsv{Z;FV6O+)AAg2E3a9S1? zv>cP1X}^bp<^6cLh5Y^ue~Y81-0qFXL1T+OHPgp|^R@MeyRG_MdP4()BU}OkPmQK_ z+l7yh%XJp8kFZ&8dMpG*MMW22r=Il@qK1wNQM`emr=xSPv8Iv%nVtk==!^J~t^{&h*}^~;A7h=8EFpY^11nr)H5(J(3X})C?ze8O-_#P;SBFHon~m7!H50GAvuv||6w8k9xsUYBX{ z_V!kCeQw?j3HieH_iS}(<)GcBm2XhT4f=5!s#9Bw@an8NtaBjO z^yPIh%-pBppQM^Sffne(ZocmnEsfO+PlCyGhf81Q6cY$y_CnV}xkW@p9o*T;i{4&e zUq8?scyin9s2j9fwSeoamN-uYO|H$o8vpD7Ybja2WnFt>d`H+6VC0bKo5c(y(p0bI z>}6%Ohxz;W?|?Fc7KfnQe5L~*xV65%K60_!g6lVp;)OdF@{O-(VyKs;u<^Exi}okG zrCS6zE;Yk2dZknu=rUea0XT{aDK?Wo5L&NMDQTO!emG|XwHc_o9?<9}@)+?gnFP za!9+nI*m+dT|u3mK&gUcGZuq}vI_9*N0_hR8F>;$U7pZuSWFP}W>~^2m057_m7FU* z7|p(p6+GWMf?BQRuBqeDKbVeympV@$c(tna zel#XqRYg#_yixnH|6t0!y@FkjvrvI@JT|^7=MZ;M$Q0bPGmw$XA8>MCA)K3)n834e zI!eG39t;l;uY*Gq?m?#>eeNMdj){Q*y1Bf3Y+;JF1{84*QT)q#=JY&+%ytz?c6N2W z~PBVF9_N~GdfPAu+Ji2f<9aZ<0*Qc&pm@PQhmVK_`G*A>^@ zx;gDrzHV0uk-u`*c5Ub6amLvs zAv19P>T^`o_7`;XR!+P5<}l(@1`m&4hxAHb(@>FmeIx;2`E)eOOG5Wp%_ijcMfS-< zqK>J39nC9)etFi|PX$U%ABummmnd@+T#;|is$1{rkJRF1%Aef`leIb%IiC24K4OmTPh%JE zPv!QsG@J^>1B}G3C?H@8z!Z)E4h$N+I;I&r(Z{}J8}5g6;dC)MW4#Yb%o7qvI2vi4 zy=`VHsNQ}$4$7Z?^RzXfd%%O*9v&gNU^h26VnGi)boH7gdlBUHFjWXIu7-g@^wrhX zzGVL8&hUGy-~AaF4i*-HYe*AMR?$3GRxt_Bx&>T_FMGM2l{_s%r#L%aZ=XV^m}05V zs)hw@`|!)2n+WZfZ&$Z6R@j#=lb%g84zd)>M;j}v{kp!HcF0rq(-Axq*#?Z9f)4S% zuP`vAkBs;1wG6Y1dvn3b=&q!r+s{k;tjg!{(blgdR(YV&j`8cnFPxOnEPI5q2cnO5=;b#qMZzAQw-OPpF*3Wh)DYJ% zUyRr=95P6~6q-fH`Vba4J$KrolExC_^Yx_Q&6fvyx8Jq(zD7|oD7!SJPL3Cz)!8Ob zl^AjQ1A0$m#E?IH_^^4p+4H{Oal9z1Se{MMoqAr9HPF ze}zVf{2dXPmWZPxTi&N>iA`EpY!@Cb^_6)9p|jl7qU03U2Oeb8*SLv%?#R-sc3Vfg zX8)mB;wy1NW$sgu=#H@WwAmV8x^^p0;LtH~4mU;S%-JYUm2k*xiKRpjdFa`yVaC)- zZZr2ERkNWM*_CEK!pN5r8={+<)Ugq<`X-qqQR%L(3L0FTYEke8LNh)z6qF%ikS+gC zPTtYg(wbYFD`S+Ck)h(?P{&u6l!Q@Oo;Qrbi;9YhjEd@8GBtG<$S0nER@{3wSN;zj&%an~uVfS#KM5D6jARmmHbBzwH5iG6pEA$73Nk4*TLhV}V+U z?}IwYIrnc}u&hWM8u|Wx>gA8G+cGG{7@t7|K0e;VptfFIF^7WAWrt`;4VZTZFDcKf z`ND&aPPOZ}KKP8Wp4objJM6b}TZ3eN{rdHn03W}LE&87}ln70h2S7mC<2iZh3CUKG z^hoeT-hSe@F;3Ayxs_Il>aPe)$~V`5fY%sI(VGNtLEC&%reR_J1cAKIZUAL9SS7?W}tP*;i-gO&OQ|IBv9iFe8+}z2D)ADg-QNOvZ z%llWF>uch zL6R7bwL{$Af50_UEen$UCt&`we?mOh&{^4XaA@csNcg;xl*eZJC-lRtt^Jx(l+J{z z{p%dK%?Cmbu4~DodfOq?UVUIN@@*&^i-O_B7RuwJ-C0!?`tvfl;@@-8?uVB4TS5V| zy|G_)f`Wp=OEspHk}hJ%vGalM?mV+|b1qQs`)?$Xw%k@#QBk9Y5FfuxG((KtpfOT8 zDuZg4Ohi7v1%c~>d%`4R11>psG{3b^h~iH-Y+@N_o=Gv>F6Z|S*^x2U_B^s#WsBjs zzYZ4`XFsdGaMQ>6@&uUFj$CYcmlCz%P?C+d0FsU zF;Qu49Mq=Me>TVTzBar4h6aSxPf7B%ecr{UpT;Kl&fcg^18x^u*E_tNmYW?iRWkwQ z)J#kY9&drK$ELJwY#2~Hl{GNP;EN176`9HvOEFfXsiM3G{0yZx0dss)TEE!8n8 zp^I(HjJY;uM8wcSjS=EdD1d;Oo4O7tbwSd59b17TdV3p`f}c&rQG>bmO3TH?b+*~+ zvNKhoQ#G6DwvTpn8MZf`RuPJZiK*)+bnCC#Wo~X>zM9Kxms);y7{~>0AfN!sN&RYz zdX^P8qxfgpr;nj5O z&Wlqp5p81sew&Iyn>*Qkfyo5e1Dp}e%FIr--;pAg+?C(e8=2zX$Sp$HO8kfERSa!8 z=5_U0-uU-+E74q*F!j3qA1l#G`B)4atk4T<rNQPa)7PlbB-vxE~Buc#gv=Mv$irCRJUy&WpZ%zuj8fDClJ%FO7pF?>k zoDFUR(Q)pS`#@n|an_eksr5cMqEUGBEAjNVXujclgqa(jZ!p+7?`NMYGA}0iH;w87 zp6}EaEA`=2PPO7anpalo)az^JJJMM6LrY7Uc-8V&&H`Vb%I-xTcasU{=Q55MzhFs@ z3<41mcYY!MkWNsZycC=BDG#z#gtQ|CoA0)znv;G7RTyK?BR({L9`^-nR_e75n5TM;fZ|18CdQz9RgwLqDCfUgCuIZ61Y0?@6C+`7nF&)&76miJ1K0z{iS56TUCBR*cN8b3I> zb`AFg6G$pBTe4(zxZbS77WUoHK!g}14(0;|as!_ZYbz@&nS3P-PO3$hBO{S`yYFvq z?8TUHrhNrhmf=weO1lBr-_QO~R0ag@|8~2UX{zT{f24DEFp<0lq^J&l*UiXt%0M4= zr@#^kg;^=E8vlUe?PSsPL~g5OV^dn*wCb7=q-kW7)oQ=O6%P{O*0t$z>yu;A2{ZgC zWMKYf_ST8y=9bs0;(c&OUWdV>kxVg(;RPzk%ehO)OVo08;=dze@@2xFmX?)ia0h#B z_>_DU;Z?l894>c0BtcFEX{p`a-w&5+6wbd#5yer}CyaRw(Y6f1G#eb;z^3Q@Fhhe) z8@*RIYxq>kCs$ZnG$TpQ`9;Q!psuR4y!=Z@NJu_;fC+2NDyIIMVfoD=69Q? z%*nIYXqKnI`NkN{jy|FWThR@{A8K`e+S2`HZ;-Z05gxMPZ#DRg0{>Z1jk!J0Yo$QJbBI#rcx zyfdCk-epf9h8H-%C(c+kw2AsVwjh|XZ1n57msk~{5qZB>AOuKPt^DH`Vm}uj5Y~(+ zV0tW?RU9eOA;-j-U1_k^_NWd~fKtNp2%jR&QP5l)B*#ycoRjWnFay4t7|buX)25{y z-4edRlUTo!;$Rm=@>P7aw_vK!n{JxS{1LJhkGj(8hH|=0o^MGRv9m|E$1^e}E$1h` zqkJ)Z2gigiY!c0WUQFy%Nd3*r%X%A{-g2U8zFoA)BP7^0=;ARJ{sl=Vq zGE2?to7Z8=piUp}-(lzW7)a)QAw*}%h=nCc43TCLLw%tOXH&nTFZQNWmB|xdDw}!^;FN)-HO$< zVx*b&gf$67K#~jM=wff32mIAUW;m#==OAXFHu4brQ0+w~;tEegy`a()wOVNYaIt+= zZ#n+v#{=jau<`vVV3XV>oN^Eu9sLA%`?V^q9ab=SekU7Oril3J`nt~JScex^0fYWG zx+@a$SJ=`u{RXQt85jDO%YlDe)@;EWg=LVVu3miR);}aV``=;LiS%MQkGuz<6rTN~ zKhLfE6)uxzp69n6RAgl2XYhL%Tbxoki~vMRXJ0kB(%T#UDODv#CB414%TnZaUds}? zzEL?dCy$7_DwOjOG*2ZZCE3CiRS9n2-d>+8;G{=KM~`0V05}`t>^=TD(xpp>7oc^q z+Ki(ud;(bE>j-_yDDG4Mo^}iq3!B!c%;~prvm|CLa|JsrVymf~Y|qVrdk>}?NAxs* z4`)ZvYR^)p^t%ld?~DHYbnuFPc)j1GtaV&(w~&vgu>2}NK0clad1-^aWtco`rwQfj ztk}a;xV3vv#>``7w~iffOg5`C$b^t2=*;ZH=6y^2Q6bc5Bh^tn$?r^;zCVya+#z~QV=K57}^c!vC>-ZCQ^!ZH^K8chJh z)wg@W?<3N?e|qTrKl+WOe9->(?OUQ71mw{H7nvggkb0|YiYY&a%_(I8$Kr1W#aRrq zv^u&91ii#hkr3H|)ekbAy4%cNpKjmjaOmjhNgxVgDqCaw4YK zgIU@McI9>w?$%(`B(`bblhkE(K#n?|sB)7m6Kg{`vvmOt8dFVa_+Ofx@AUEHS5{VfPQWefGMRdkBq3h{T=x&CwY=p)zb(=i^wmohF&MBV;jlx1=O2Ue zLi~R~=Ub7ijXV&6zO!WHHaUkW{&G=aEJ}(b8UlcaCxzS4yAjv!xYx%bA_63r5%4w{ zc~p+18nWmBfe%|1Q@Nl{-!;mJ#y=B~+#nMqOw1trsT@)EwA9q-qQK|d;~&)05v7;B z*dd*ED43&Prv-xgqRrwU3GKA_Et7%kaE!Y7Chp!`Cc@CAeoT;KKkhgep=drlJfGS z!-z9AE@)ec6oan;BvOsMt}P1aMn-H*c-i9lV$Ma0>jK&)o!+n zu3}Ckkf*SZ(c%Fev}kkFq|^Hn5gQx(w&&$4#@4HBI2mEuAEtb+ZX@miDMO`1npta{ z{^oqsR7F(nH<#shmY|!-5e8`xr;#gciuPjt>gp5fXGeAQyj8>bTYn=7&%1V2_K(=I zGv9%d+e9nY4nyd-s!vK9q0I=vz7?roJhSLeP*ycLSfgYRk+T~Cze%3R4gN({yMT^; zLoa4wEJiM8N916s zg|oA=Dgzhm-DHI7mx|gCrwO8=&aRc;Su178j**zdlebb~%8hZw!m5QQX3s8AL8;^L z`@es`a!t8CmCkZa9)!5~Yd(AAWg&XU?Jh45HIZV#wc0Q(<9yXub<5#9Ypn_9V^8?Z z`*(-V*%b@CgJ^_Sq&Q)9jnsm_tauZdCND3yvF1GIP$Sp%_+;E>AwNvTNeuvzm$s95 zzfPVbQV7)EP?A^4V}+ z_q*?TT~AR7YS{X2f1w426kepn_PpL!4P%~|uI5)6A0HhJCC$lWWHj3=XEJUH1fXGr zI!%QpGzY)L&dzG?AX54}Z)0er0Ysw#v6BfFJHz7HNnAwtn;r!DeUOO&nO1fM2EjJ2 zx2v=k3eir;Ym6h>VJy^Oa(8|Bq@=VkkymITU)+mr0NPjpPTxt9u7KM;Dun(%Z%KOi zyqBsHo*>*X!D96n1~rWEn-%bW3r0*r1k7mU91vQ47QG#bl$#ReSL^R5e7^-7gWsAu ztM=FonzSKx5jU1&(KWI>jgf=Nnx|GIRe)cZktXu~{z^R8+Wj z*BNU=m(c+UT{A_fT`V%g7a>8OU&Pe)5T|ngSn9LtrTeuMI{hn>`ekw9OFeH5mI% znQiwkQGh^CcW=_Lk4>uttZ6k4B$6_D8!Cg-d=azL zi;{FS(JbRTtL|=u1;aQ1qh(tTAgzZwjZ`RzH48-7$m8=jt*Zf6Rt5OX(jo?T_LzB@JC_g-YDXu=;{eR|!3gcS81XW1|oxA{c^TDrjspW>ejw$elV;r4S+e;+}X z0XH-98&EH30|em}m%zP)dK3%xR9U|D%(&a90g^7KD8!jB5g1j`u8h!rNf4Embt27M zeLoAhx64^iTUAK~bcIHLLe`0aH!;b%xUliRXlg?DzHMS1A~z(c^8CoQu#n{9u!W!! zMa6|$4XjY1@&F=(wRfi%2Q{=Z+LSiSrU`EJU`w$ctP1;-$SdxzKT4|HDrSip!{~pD zovE-R@hjVz$V_Hru03eNtP%I{*RJ=Ba}!elqYq*i)${h*TH0LpT&vgC)SUa|9*S9O z=8>0KaLGe?#1~zbM@LLw9vGsq+%oAybePC<)2Q-~qU@>P{__z8J&L$Dh5FDg10~5UDT0I}KioW}~9<_gYW-%e+8ji|T{2id4C zCcFSN@H%e(k`PMAGXN~6l(y_!m~{H4Dk%G(oBY|4=Vk&ar)qkyYBo(`htxS&^z#gy z7CU`ZaBZY}s`;Fqb-prgAj4jixEkMis|MpOdkwV(0+ar}iY272PjCYFju6p+tu5)s)0WZC zaxFd0AZ*bb0LRGqi7QWMmp>=fOzhUy(1Osy07r zCj;aQ`TZL;j?Iw~Z7_3gpLV>L*b)US&#B|AeDER`&J}nL>&yMq6A8dk#-JWwM`_1# zJ)=-ZS9i0MJG}%VSkm5S--M;8_-ZIanD z4S#S6q+0fww$Dr%$wwWWZsd}YC1yMD!N>o_wsb`QFrODRO6EX8nUy4Tx&R=D z7fhj*kK^(4@d?;%mJ$vg4+Xfmczp%$!kC8tC?`wpc*T0yEZhMwRL3s(+L!?d<8NyzUX`PxiqOJBwy~}hlCXk>Y^$Upy?+>lE>9JdR*YcMN zHhkd6ZBk_S&3sKhzq=EM!^6V{3kwVFA8LLjm}ES`7R|L)KpXZ9Ps(KIWk!#74*sor zY5+Bh@7vW#I&q1+Z-itSek<4deQ0ZD*a1uF*e7IocyWfQO#bs^yXv<6>Wb?2Xeg_R z2+2mCo)v}_s(^teO*sQ!yu}>XS}P!vnO>LVg&i#D7 zpOKSGMk65<11-GUDU#?+6d_iV8Mk{-w)I0TOQ7!4H8PJ%9(M0SE6i&8o?R5+?d>gH z$fMkKPK6&mSJ><9?C$lTS~@-pd!7VG%TX^k`Fg?EZQ#5lnBVLQm{+-mxUlfk@NXjE z<+Y2Gst<8sPB0X%+>9fg%1}ib;IDeG49$)P>FDURj(&ZrZ*6@bfM%zgXl+9IGr_M{ zRaOGYg98K7*Ni+)#kaBz_6^71lMT1x5-(*1mPJiBz@MIid+dt_WeAr`t$Ml`~-%0;> zf(BkAQC*DQg zoRr)%FCFRW!$N0^_xYIbldUHz5|lze{=cQC|EoInEeKjf`~F^p$KOyeygv;+0Vv9< K%2Z34h5ip7`RtGY literal 0 HcmV?d00001 diff --git a/test/test_image.py b/test/test_image.py new file mode 100644 index 0000000..5356f8d --- /dev/null +++ b/test/test_image.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + +def test_image(backend_name=None): + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createPopupDialog() + vbox = factory.createVBox(dialog) + factory.createHeading(vbox, "Image widget demo") + + # Use an example image path if available (relative to project), otherwise empty + example = os.path.join(os.path.dirname(__file__), '..', 'share/images', 'manatools.png') + example = os.path.abspath(example) + if not os.path.exists(example): + example = "" + + img = factory.createImage(vbox, example) + # allow image to expand horizontally + img.setStretchable(yui.YUIDimension.YD_HORIZ, True) + img.setStretchable(yui.YUIDimension.YD_VERT, True) + img.setAutoScale(True) + + # OK button + hbox = factory.createHBox(vbox) + ok = factory.createPushButton(hbox, "OK") + close = factory.createPushButton(hbox, "Close") + + dialog.open() + while True: + event = dialog.waitForEvent() + if not event: + continue + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == close: + dialog.destroy() + break + elif wdg == ok: + dialog.destroy() + break + + except Exception as e: + print(f"Error testing Image with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_image(sys.argv[1]) + else: + test_image() From 76b850886a7c300acc424770a759b748fb8024ef Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 15:57:52 +0100 Subject: [PATCH 314/523] Honoring stretching --- manatools/aui/backends/qt/imageqt.py | 35 +++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/qt/imageqt.py b/manatools/aui/backends/qt/imageqt.py index 34706f7..086ac84 100644 --- a/manatools/aui/backends/qt/imageqt.py +++ b/manatools/aui/backends/qt/imageqt.py @@ -86,7 +86,40 @@ def setZeroSize(self, dim, zeroSize=True): def _create_backend_widget(self): try: - self._backend_widget = QtWidgets.QLabel() + # Use a QLabel subclass that notifies this owner on resize so + # we can re-apply scaled pixmaps when the widget changes size. + class _ImageLabel(QtWidgets.QLabel): + def __init__(self, owner, *args, **kwargs): + super().__init__(*args, **kwargs) + self._owner = owner + + def resizeEvent(self, ev): + super().resizeEvent(ev) + try: + self._owner._apply_pixmap() + except Exception: + pass + + def sizeHint(self): + try: + # When autoscale is enabled prefer to give a small size hint + # so layouts can shrink the widget; actual pixmap will be + # scaled on resizeEvent. + if getattr(self._owner, '_auto_scale', False): + return QtCore.QSize(0, 0) + except Exception: + pass + return super().sizeHint() + + def minimumSizeHint(self): + try: + if getattr(self._owner, '_auto_scale', False): + return QtCore.QSize(0, 0) + except Exception: + pass + return super().minimumSizeHint() + + self._backend_widget = _ImageLabel(self) self._backend_widget.setAlignment(QtCore.Qt.AlignCenter) if self._imageFileName and os.path.exists(self._imageFileName): self._pixmap = QtGui.QPixmap(self._imageFileName) From 421263c453ca116d83e142d7baad37aef60e6856 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 16:12:09 +0100 Subject: [PATCH 315/523] Get image also from theme --- manatools/aui/backends/qt/imageqt.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/qt/imageqt.py b/manatools/aui/backends/qt/imageqt.py index 086ac84..90db21a 100644 --- a/manatools/aui/backends/qt/imageqt.py +++ b/manatools/aui/backends/qt/imageqt.py @@ -43,8 +43,10 @@ def setImage(self, imageFileName): ico = None if ico is not None: try: - # store QIcon and let _apply_pixmap pick appropriate size + # store QIcon (clear any stored QPixmap) and let + # _apply_pixmap pick an appropriate size self._qicon = ico + self._pixmap = None self._apply_pixmap() return except Exception: @@ -121,9 +123,19 @@ def minimumSizeHint(self): self._backend_widget = _ImageLabel(self) self._backend_widget.setAlignment(QtCore.Qt.AlignCenter) - if self._imageFileName and os.path.exists(self._imageFileName): - self._pixmap = QtGui.QPixmap(self._imageFileName) - self._apply_pixmap() + if self._imageFileName: + # Use setImage which will attempt to resolve theme icons via + # commonqt._resolve_icon and fall back to filesystem loading. + try: + self.setImage(self._imageFileName) + except Exception: + # Fallback: try direct filesystem load + try: + if os.path.exists(self._imageFileName): + self._pixmap = QtGui.QPixmap(self._imageFileName) + self._apply_pixmap() + except Exception: + pass self._apply_size_policy() self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -149,7 +161,8 @@ def _apply_pixmap(self): try: if getattr(self, '_backend_widget', None) is None: return - if not self._pixmap: + # If neither a QPixmap nor a QIcon is available, clear the widget. + if getattr(self, '_qicon', None) is None and not getattr(self, '_pixmap', None): self._backend_widget.clear() return # If we have a QIcon (resolved from theme/name) prefer it as it can provide From eaba46f92fb9e1723f5787cecdb742cf87d89ef2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 16:14:35 +0100 Subject: [PATCH 316/523] tested also setImage from theme --- test/test_image.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_image.py b/test/test_image.py index 5356f8d..266e829 100644 --- a/test/test_image.py +++ b/test/test_image.py @@ -59,15 +59,17 @@ def test_image(backend_name=None): if not os.path.exists(example): example = "" + images = [example, "system-software-install"] + img = factory.createImage(vbox, example) # allow image to expand horizontally img.setStretchable(yui.YUIDimension.YD_HORIZ, True) - img.setStretchable(yui.YUIDimension.YD_VERT, True) - img.setAutoScale(True) + img.setStretchable(yui.YUIDimension.YD_VERT, False) + #img.setAutoScale(True) # OK button hbox = factory.createHBox(vbox) - ok = factory.createPushButton(hbox, "OK") + toggle = factory.createPushButton(hbox, "Toggle Image") close = factory.createPushButton(hbox, "Close") dialog.open() @@ -84,9 +86,8 @@ def test_image(backend_name=None): if wdg == close: dialog.destroy() break - elif wdg == ok: - dialog.destroy() - break + elif wdg == toggle: + img.setImage(images[1] if img.imageFileName() == images[0] else images[0]) except Exception as e: print(f"Error testing Image with backend {backend_name}: {e}") From d7c021af5f3c1adf8ead95c73403c6829acd04f1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 16:17:46 +0100 Subject: [PATCH 317/523] stretchable and autoscale --- test/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_image.py b/test/test_image.py index 266e829..14c70e9 100644 --- a/test/test_image.py +++ b/test/test_image.py @@ -64,8 +64,8 @@ def test_image(backend_name=None): img = factory.createImage(vbox, example) # allow image to expand horizontally img.setStretchable(yui.YUIDimension.YD_HORIZ, True) - img.setStretchable(yui.YUIDimension.YD_VERT, False) - #img.setAutoScale(True) + img.setStretchable(yui.YUIDimension.YD_VERT, True) + img.setAutoScale(True) # OK button hbox = factory.createHBox(vbox) From d9a60a5e013c23333480f4b765a4043f23843c4a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 16:27:11 +0100 Subject: [PATCH 318/523] fixed initial image from theme --- manatools/aui/backends/gtk/imagegtk.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/manatools/aui/backends/gtk/imagegtk.py b/manatools/aui/backends/gtk/imagegtk.py index c8f2126..2b5d152 100644 --- a/manatools/aui/backends/gtk/imagegtk.py +++ b/manatools/aui/backends/gtk/imagegtk.py @@ -105,11 +105,17 @@ def _on_size_allocate(self, widget, allocation): def _create_backend_widget(self): try: self._backend_widget = Gtk.Image() - if self._imageFileName and os.path.exists(self._imageFileName): + if self._imageFileName: + # Use setImage to allow theme icon resolution via commongtk._resolve_icon try: - self._pixbuf = GdkPixbuf.Pixbuf.new_from_file(self._imageFileName) + self.setImage(self._imageFileName) except Exception: - self._logger.exception("failed to load pixbuf") + # Fallback: try direct filesystem load + try: + if os.path.exists(self._imageFileName): + self._pixbuf = GdkPixbuf.Pixbuf.new_from_file(self._imageFileName) + except Exception: + self._logger.exception("failed to load pixbuf") if getattr(self, '_pixbuf', None) is not None: self._apply_pixbuf() # hook allocation to rescale if autoscale is enabled From 3f66a9029ab15f2d75279c4ecfdc06a206aa3f22 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 16:29:02 +0100 Subject: [PATCH 319/523] updated --- sow/TODO.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 9cadd41..e466d0c 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -27,18 +27,18 @@ Missing Widgets comparing libyui original factory: [X] YAlignment helpers (createLeft/createRight/createTop/createBottom/createHCenter/createVCenter/createHVCenter) [X] YReplacePoint [X] YRadioButton - [ ] YWizard + [X] YImage [ ] YBusyIndicator - [ ] YImage [ ] YLogView - [ ] YItemSelector - [ ] YEmpty - [ ] YSquash / createSquash Skipped widgets: - [-] YPackageSelector (not ported) + [-] YPackageSelector (not ported) [-] YRadioButtonGroup (not ported) + [-] YWizard (not ported) + [-] YItemSelector (not ported) + [-] YEmpty (not ported) + [-] YSquash / createSquash (not ported) [-] YMenuButton (legacy menus) Optional/special widgets (from `YOptionalWidgetFactory`): From d8f6357cff7baaa51f7eb8a2b4ab58f533bfb3dd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 3 Jan 2026 16:30:24 +0100 Subject: [PATCH 320/523] removed duplicate item --- sow/TODO.md | 1 - 1 file changed, 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index e466d0c..dfe90e6 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -43,7 +43,6 @@ Skipped widgets: Optional/special widgets (from `YOptionalWidgetFactory`): - [ ] YWizard [ ] YDumbTab [ ] YSlider [ ] YDateField From aa789ae0ec54012dc6b44e894d839bd1ec6ff347 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 16:37:12 +0100 Subject: [PATCH 321/523] next generation should be 1.0.0 --- manatools/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/version.py b/manatools/version.py index 3a03297..a35dcde 100644 --- a/manatools/version.py +++ b/manatools/version.py @@ -10,4 +10,4 @@ __project_name__ = "python-manatools" ''' project version ''' -__project_version__ = "0.0.4" +__project_version__ = "0.99.0" From 6c2b7549ee5ab90d52b3ddb573a4efdacb828383 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 17:45:12 +0100 Subject: [PATCH 322/523] Added Min Size --- .../aui/backends/curses/alignmentcurses.py | 36 ++++++- manatools/aui/backends/gtk/alignmentgtk.py | 27 +++++ manatools/aui/backends/qt/alignmentqt.py | 43 ++++++++ manatools/aui/yui_curses.py | 24 +++++ manatools/aui/yui_gtk.py | 28 ++++++ manatools/aui/yui_qt.py | 24 +++++ test/test_min_size.py | 99 +++++++++++++++++++ 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 test/test_min_size.py diff --git a/manatools/aui/backends/curses/alignmentcurses.py b/manatools/aui/backends/curses/alignmentcurses.py index 648f074..7a081d4 100644 --- a/manatools/aui/backends/curses/alignmentcurses.py +++ b/manatools/aui/backends/curses/alignmentcurses.py @@ -16,6 +16,7 @@ import time import logging from ...yui_common import * +from .commoncurses import pixels_to_chars # Module-level logger for curses alignment backend _mod_logger = logging.getLogger("manatools.aui.curses.alignment.module") @@ -36,6 +37,8 @@ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUn self._halign_spec = horAlign self._valign_spec = vertAlign self._backend_widget = None # not used by curses + self._min_width_px = 0 + self._min_height_px = 0 # per-instance logger self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") if not self._logger.handlers and not logging.getLogger().handlers: @@ -106,6 +109,13 @@ def _draw(self, window, y, x, width, height): try: # width to give to the child: minimal needed (so it can be pushed) ch_min_w = self._child_min_width(self.child(), width) + # honor explicit minimum width in pixels, converting to character cells + try: + if getattr(self, '_min_width_px', 0) and self._min_width_px > 0: + min_chars = pixels_to_chars(int(self._min_width_px), YUIDimension.YD_HORIZ) + ch_min_w = max(ch_min_w, min_chars) + except Exception: + pass # Horizontal position if self._halign_spec == YAlignmentType.YAlignEnd: cx = x + max(0, width - ch_min_w) @@ -120,6 +130,30 @@ def _draw(self, window, y, x, width, height): cy = y + max(0, height - 1) else: cy = y - self.child()._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, getattr(self.child(), "_height", 1))) + # honor explicit minimum height in pixels for child + ch_height = getattr(self.child(), "_height", 1) + try: + if getattr(self, '_min_height_px', 0) and self._min_height_px > 0: + min_h = pixels_to_chars(int(self._min_height_px), YUIDimension.YD_VERT) + ch_height = max(ch_height, min_h) + except Exception: + pass + self.child()._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, ch_height)) except Exception: pass + + def setMinWidth(self, width_px: int): + try: + self._min_width_px = int(width_px) if width_px is not None else 0 + except Exception: + self._min_width_px = 0 + + def setMinHeight(self, height_px: int): + try: + self._min_height_px = int(height_px) if height_px is not None else 0 + except Exception: + self._min_height_px = 0 + + def setMinSize(self, width_px: int, height_px: int): + self.setMinWidth(width_px) + self.setMinHeight(height_px) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index fef0635..418f8da 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -38,6 +38,8 @@ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUn self._halign_spec = horAlign self._valign_spec = vertAlign self._background_pixbuf = None + self._min_width_px = 0 + self._min_height_px = 0 self._signal_id = None self._backend_widget = None # get a reference to the single row container @@ -232,6 +234,31 @@ def _ensure_child_attached(self): cw.set_halign(hal) cw.set_valign(val) + # enforce minimum size on child if requested + try: + mw = getattr(self, '_min_width_px', 0) + mh = getattr(self, '_min_height_px', 0) + if (mw and mw > 0) or (mh and mh > 0): + # GTK uses size-request on widgets for minimum size + w_req = -1 + h_req = -1 + try: + if mw and mw > 0: + w_req = int(mw) + except Exception: + w_req = -1 + try: + if mh and mh > 0: + h_req = int(mh) + except Exception: + h_req = -1 + try: + cw.set_size_request(w_req if w_req > 0 else -1, h_req if h_req > 0 else -1) + except Exception: + pass + except Exception: + pass + if hal == Gtk.Align.START: target_cb.set_start_widget(cw) elif hal == Gtk.Align.END: diff --git a/manatools/aui/backends/qt/alignmentqt.py b/manatools/aui/backends/qt/alignmentqt.py index e817865..86e89c9 100644 --- a/manatools/aui/backends/qt/alignmentqt.py +++ b/manatools/aui/backends/qt/alignmentqt.py @@ -24,6 +24,8 @@ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUn self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") self._halign_spec = horAlign self._valign_spec = vertAlign + self._min_width_px = 0 + self._min_height_px = 0 self._backend_widget = None self._layout = None @@ -108,6 +110,33 @@ def addChild(self, child): super().addChild(child) self._attach_child_backend() + def setMinWidth(self, width_px: int): + try: + self._min_width_px = int(width_px) if width_px is not None else 0 + except Exception: + self._min_width_px = 0 + try: + # if child already attached, re-apply + if self.child() and getattr(self, '_layout', None) is not None: + self._attach_child_backend() + except Exception: + pass + + def setMinHeight(self, height_px: int): + try: + self._min_height_px = int(height_px) if height_px is not None else 0 + except Exception: + self._min_height_px = 0 + try: + if self.child() and getattr(self, '_layout', None) is not None: + self._attach_child_backend() + except Exception: + pass + + def setMinSize(self, width_px: int, height_px: int): + self.setMinWidth(width_px) + self.setMinHeight(height_px) + def _attach_child_backend(self): if not (self._backend_widget and self._layout and self.child()): @@ -150,6 +179,20 @@ def _attach_child_backend(self): except Exception: pass w.setSizePolicy(sp) + # Enforce minimum size on child if requested (pixels) + try: + if getattr(self, '_min_width_px', 0) and hasattr(w, 'setMinimumWidth'): + try: + w.setMinimumWidth(int(self._min_width_px)) + except Exception: + pass + if getattr(self, '_min_height_px', 0) and hasattr(w, 'setMinimumHeight'): + try: + w.setMinimumHeight(int(self._min_height_px)) + except Exception: + pass + except Exception: + pass except Exception: pass self._layout.addWidget(w, 0, 0, flags) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index e35337d..ead65d2 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -202,6 +202,30 @@ def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: Y """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" return YAlignmentCurses(parent, horAlign=horAlignment, vertAlign=vertAlignment) + def createMinWidth(self, parent, minWidth: int): + a = YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignUnchanged) + try: + a.setMinWidth(int(minWidth)) + except Exception: + pass + return a + + def createMinHeight(self, parent, minHeight: int): + a = YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignUnchanged) + try: + a.setMinHeight(int(minHeight)) + except Exception: + pass + return a + + def createMinSize(self, parent, minWidth: int, minHeight: int): + a = YAlignmentCurses(parent, horAlign=YAlignmentType.YAlignUnchanged, vertAlign=YAlignmentType.YAlignUnchanged) + try: + a.setMinSize(int(minWidth), int(minHeight)) + except Exception: + pass + return a + def createTree(self, parent, label, multiselection=False, recursiveselection = False): """Create a Tree widget.""" return YTreeCurses(parent, label, multiselection, recursiveselection) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 39bf21a..826f6c1 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -258,6 +258,34 @@ def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: Y """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" return YAlignmentGtk(parent, horAlign=horAlignment, vertAlign=vertAlignment) + def createMinWidth(self, parent, minWidth: int): + a = YAlignmentGtk(parent) + try: + a._min_width_px = int(minWidth) + except Exception: + pass + return a + + def createMinHeight(self, parent, minHeight: int): + a = YAlignmentGtk(parent) + try: + a._min_height_px = int(minHeight) + except Exception: + pass + return a + + def createMinSize(self, parent, minWidth: int, minHeight: int): + a = YAlignmentGtk(parent) + try: + a._min_width_px = int(minWidth) + except Exception: + pass + try: + a._min_height_px = int(minHeight) + except Exception: + pass + return a + def createTree(self, parent, label, multiselection=False, recursiveselection = False): """Create a Tree widget.""" return YTreeGtk(parent, label, multiselection, recursiveselection) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index c6ed530..91e20ef 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -216,6 +216,30 @@ def createHVCenter(self, parent): def createAlignment(self, parent, horAlignment: YAlignmentType, vertAlignment: YAlignmentType): """Create a generic YAlignment using YAlignmentType enums (or compatible specs).""" return YAlignmentQt(parent, horAlign=horAlignment, vertAlign=vertAlignment) + + def createMinWidth(self, parent, minWidth: int): + a = YAlignmentQt(parent) + try: + a.setMinWidth(int(minWidth)) + except Exception: + pass + return a + + def createMinHeight(self, parent, minHeight: int): + a = YAlignmentQt(parent) + try: + a.setMinHeight(int(minHeight)) + except Exception: + pass + return a + + def createMinSize(self, parent, minWidth: int, minHeight: int): + a = YAlignmentQt(parent) + try: + a.setMinSize(int(minWidth), int(minHeight)) + except Exception: + pass + return a def createTree(self, parent, label, multiselection=False, recursiveselection = False): """Create a Tree widget.""" diff --git a/test/test_min_size.py b/test/test_min_size.py new file mode 100644 index 0000000..b7faf69 --- /dev/null +++ b/test/test_min_size.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + +def test_min_size(backend_name=None): + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createPopupDialog() + + # Try to set dialog minimum size (600x400 px) using generic API if available + mindialogsize = factory.createMinSize(dialog, 600, 400) + + + vbox = factory.createVBox(mindialogsize) + factory.createHeading(vbox, "Min-size test") + + # create alignment that forces child to min 100x100 px + a = factory.createMinSize(vbox, 100, 100) + m = factory.createMultiLineEdit(a, "Multiline") + + # OK/Close + hbox = factory.createHBox(vbox) + ok = factory.createPushButton(hbox, "OK") + close = factory.createPushButton(hbox, "Close") + + dialog.open() + while True: + event = dialog.waitForEvent() + if not event: + continue + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == close: + dialog.destroy() + break + elif wdg == ok: + dialog.destroy() + break + + except Exception as e: + print(f"Error testing min size with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_min_size(sys.argv[1]) + else: + test_min_size() From 7969a68a6c2e4803e56df71c2485ff5731843e9c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 18:59:45 +0100 Subject: [PATCH 323/523] Added askForExistingDirectory, askForExistingFile, askForSaveFileName --- manatools/aui/yui_qt.py | 75 +++++++++++++++++++++++ test/test_file_dialogs.py | 126 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 test/test_file_dialogs.py diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 91e20ef..01ff3bd 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -5,6 +5,7 @@ import sys from PySide6 import QtWidgets, QtCore, QtGui import os +import logging from .yui_common import * from .backends.qt import * from .backends.qt.commonqt import _resolve_icon @@ -18,6 +19,11 @@ def __init__(self): if not self._qapp: self._qapp = QtWidgets.QApplication(sys.argv) self._application = YApplicationQt() + # logger for the backend manager + try: + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + except Exception: + self._logger = logging.getLogger("manatools.aui.qt.YUIQt") def widgetFactory(self): return self._widget_factory @@ -42,6 +48,10 @@ def __init__(self): self._icon = "manatools" # default icon name # cached QIcon resolved from _icon (None if not resolved) self._qt_icon = None + try: + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + except Exception: + self._logger = logging.getLogger("manatools.aui.qt.YApplicationQt") def iconBasePath(self): return self._icon_base_path @@ -127,6 +137,71 @@ def applicationTitle(self): """Get the application title.""" return self._application_title + def askForExistingDirectory(self, startDir: str, headline: str): + """ + Prompt user to select an existing directory. + + Parameters: + - startDir: initial folder to display (string, may be empty) + - headline: explanatory text for the dialog + + Returns: selected directory path as string, or empty string if cancelled. + """ + try: + start = startDir or "" + # Use QFileDialog static helper for convenience + res = QtWidgets.QFileDialog.getExistingDirectory(None, headline or "Select Directory", start) + return res or "" + except Exception: + try: + self._logger.exception("askForExistingDirectory failed") + except Exception: + pass + return "" + + def askForExistingFile(self, startWith: str, filter: str, headline: str): + """ + Prompt user to select an existing file. + + Parameters: + - startWith: initial directory or file + - filter: file filter string (e.g. "*.txt") + - headline: explanatory text for the dialog + + Returns: selected filename as string, or empty string if cancelled. + """ + try: + start = startWith or "" + flt = f"Text files ({filter});;All files (*)" if filter else "All files (*)" + fn, _ = QtWidgets.QFileDialog.getOpenFileName(None, headline or "Open File", start, flt) + return fn or "" + except Exception: + try: + self._logger.exception("askForExistingFile failed") + except Exception: + pass + return "" + + def askForSaveFileName(self, startWith: str, filter: str, headline: str): + """ + Prompt user to choose a filename to save data. + + Parameters are as in `askForExistingFile`. + + Returns: selected filename as string, or empty string if cancelled. + """ + try: + start = startWith or "" + flt = f"Text files ({filter});;All files (*)" if filter else "All files (*)" + fn, _ = QtWidgets.QFileDialog.getSaveFileName(None, headline or "Save File", start, flt) + return fn or "" + except Exception: + try: + self._logger.exception("askForSaveFileName failed") + except Exception: + pass + return "" + def setApplicationIcon(self, Icon): """Set the application title.""" self._icon = Icon diff --git a/test/test_file_dialogs.py b/test/test_file_dialogs.py new file mode 100644 index 0000000..9bf7d53 --- /dev/null +++ b/test/test_file_dialogs.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + +def test_file_dialogs(backend_name=None): + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + app = ui.app() + factory = ui.widgetFactory() + dialog = factory.createPopupDialog() + + vbox = factory.createVBox(dialog) + factory.createHeading(vbox, "File dialog test") + + # main area: HBox with multiline edit on left and buttons on right + h = factory.createHBox(vbox) + mled = factory.createMultiLineEdit(h, "File content") + mled.setStretchable(yui.YUIDimension.YD_VERT, True) + mled.setStretchable(yui.YUIDimension.YD_HORIZ, True) + + right = factory.createVBox(h) + open_btn = factory.createPushButton(right, "Open") + save_btn = factory.createPushButton(right, "Save") + testdir_btn = factory.createPushButton(right, "Test Dir") + + dir_label = factory.createLabel(vbox, "No dir selected") + dir_label.setStretchable(yui.YUIDimension.YD_VERT, False) + dir_label.setStretchable(yui.YUIDimension.YD_HORIZ, True) + + factory.createSpacing(vbox, yui.YUIDimension.YD_VERT, False, 10) + close_btn = factory.createPushButton(factory.createHVCenter(vbox), "Close") + + # wire events in the event loop by checking WidgetEvent and comparing widgets + dialog.open() + while True: + event = dialog.waitForEvent() + if not event: + continue + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == open_btn: + # ask for text file + fname = app.askForExistingFile("", "*.txt", "Open text file") + if fname: + try: + with open(fname, 'r', encoding='utf-8') as f: + data = f.read() + mled.setValue(data) + except Exception as e: + print(f"Failed to read file: {e}") + elif wdg == save_btn: + fname = app.askForSaveFileName("", "*.txt", "Save text file") + if fname: + try: + data = mled.value() + with open(fname, 'w', encoding='utf-8') as f: + f.write(data) + except Exception as e: + print(f"Failed to save file: {e}") + elif wdg == testdir_btn: + d = app.askForExistingDirectory("", "Select directory") + if d: + dir_label.setText(d) + elif wdg == close_btn: + dialog.destroy() + break + + except Exception as e: + print(f"Error testing file dialogs with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_file_dialogs(sys.argv[1]) + else: + test_file_dialogs() From 644e6593be13433c901ce83e6f7d5d28b5ab77a4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 20:18:20 +0100 Subject: [PATCH 324/523] updated --- sow/TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sow/TODO.md b/sow/TODO.md index dfe90e6..d38dc9d 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -63,6 +63,7 @@ To check/review: [X] YInputField password mode [ ] adding factory create alternative methods (e.g. createMultiSelectionBox) [ ] managing shortcuts + [ ] localization Nice to have: improvements outside YUI API [ ] window title From 7da1fe25377bb506d782466a845f9ad9c22ea864 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 20:19:10 +0100 Subject: [PATCH 325/523] removed as we reached most of the goal --- sow/yui.py | 7656 ---------------------------------------------------- 1 file changed, 7656 deletions(-) delete mode 100644 sow/yui.py diff --git a/sow/yui.py b/sow/yui.py deleted file mode 100644 index cdc14e2..0000000 --- a/sow/yui.py +++ /dev/null @@ -1,7656 +0,0 @@ -# This file was automatically generated by SWIG (http://www.swig.org). -# Version 4.0.2 -# -# Do not make changes to this file unless you know what you are doing--modify -# the SWIG interface file instead. - -from sys import version_info as _swig_python_version_info -if _swig_python_version_info < (2, 7, 0): - raise RuntimeError("Python 2.7 or later required") - -# Import the low-level C/C++ module -if __package__ or "." in __name__: - from . import _yui -else: - import _yui - -try: - import builtins as __builtin__ -except ImportError: - import __builtin__ - -def _swig_repr(self): - try: - strthis = "proxy of " + self.this.__repr__() - except __builtin__.Exception: - strthis = "" - return "<%s.%s; %s >" % (self.__class__.__module__, self.__class__.__name__, strthis,) - - -def _swig_setattr_nondynamic_instance_variable(set): - def set_instance_attr(self, name, value): - if name == "thisown": - self.this.own(value) - elif name == "this": - set(self, name, value) - elif hasattr(self, name) and isinstance(getattr(type(self), name), property): - set(self, name, value) - else: - raise AttributeError("You cannot add instance attributes to %s" % self) - return set_instance_attr - - -def _swig_setattr_nondynamic_class_variable(set): - def set_class_attr(cls, name, value): - if hasattr(cls, name) and not isinstance(getattr(cls, name), property): - set(cls, name, value) - else: - raise AttributeError("You cannot add class attributes to %s" % cls) - return set_class_attr - - -def _swig_add_metaclass(metaclass): - """Class decorator for adding a metaclass to a SWIG wrapped class - a slimmed down version of six.add_metaclass""" - def wrapper(cls): - return metaclass(cls.__name__, cls.__bases__, cls.__dict__.copy()) - return wrapper - - -class _SwigNonDynamicMeta(type): - """Meta class to enforce nondynamic attributes (no new attributes) for a class""" - __setattr__ = _swig_setattr_nondynamic_class_variable(type.__setattr__) - - -class SwigPyIterator(object): - r"""Proxy of C++ swig::SwigPyIterator class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_SwigPyIterator - - def value(self): - r"""value(SwigPyIterator self) -> PyObject *""" - return _yui.SwigPyIterator_value(self) - - def incr(self, n=1): - r"""incr(SwigPyIterator self, size_t n=1) -> SwigPyIterator""" - return _yui.SwigPyIterator_incr(self, n) - - def decr(self, n=1): - r"""decr(SwigPyIterator self, size_t n=1) -> SwigPyIterator""" - return _yui.SwigPyIterator_decr(self, n) - - def distance(self, x): - r"""distance(SwigPyIterator self, SwigPyIterator x) -> ptrdiff_t""" - return _yui.SwigPyIterator_distance(self, x) - - def equal(self, x): - r"""equal(SwigPyIterator self, SwigPyIterator x) -> bool""" - return _yui.SwigPyIterator_equal(self, x) - - def copy(self): - r"""copy(SwigPyIterator self) -> SwigPyIterator""" - return _yui.SwigPyIterator_copy(self) - - def next(self): - r"""next(SwigPyIterator self) -> PyObject *""" - return _yui.SwigPyIterator_next(self) - - def __next__(self): - r"""__next__(SwigPyIterator self) -> PyObject *""" - return _yui.SwigPyIterator___next__(self) - - def previous(self): - r"""previous(SwigPyIterator self) -> PyObject *""" - return _yui.SwigPyIterator_previous(self) - - def advance(self, n): - r"""advance(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" - return _yui.SwigPyIterator_advance(self, n) - - def __eq__(self, x): - r"""__eq__(SwigPyIterator self, SwigPyIterator x) -> bool""" - return _yui.SwigPyIterator___eq__(self, x) - - def __ne__(self, x): - r"""__ne__(SwigPyIterator self, SwigPyIterator x) -> bool""" - return _yui.SwigPyIterator___ne__(self, x) - - def __iadd__(self, n): - r"""__iadd__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" - return _yui.SwigPyIterator___iadd__(self, n) - - def __isub__(self, n): - r"""__isub__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" - return _yui.SwigPyIterator___isub__(self, n) - - def __add__(self, n): - r"""__add__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator""" - return _yui.SwigPyIterator___add__(self, n) - - def __sub__(self, *args): - r""" - __sub__(SwigPyIterator self, ptrdiff_t n) -> SwigPyIterator - __sub__(SwigPyIterator self, SwigPyIterator x) -> ptrdiff_t - """ - return _yui.SwigPyIterator___sub__(self, *args) - def __iter__(self): - return self - -# Register SwigPyIterator in _yui: -_yui.SwigPyIterator_swigregister(SwigPyIterator) - -class YUI(object): - r"""Proxy of C++ YUI class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YUI - - def shutdownThreads(self): - r"""shutdownThreads(YUI self)""" - return _yui.YUI_shutdownThreads(self) - - @staticmethod - def ui(): - r"""ui() -> YUI""" - return _yui.YUI_ui() - - @staticmethod - def widgetFactory(): - r"""widgetFactory() -> YWidgetFactory""" - return _yui.YUI_widgetFactory() - - @staticmethod - def optionalWidgetFactory(): - r"""optionalWidgetFactory() -> YOptionalWidgetFactory""" - return _yui.YUI_optionalWidgetFactory() - - @staticmethod - def app(): - r"""app() -> YApplication""" - return _yui.YUI_app() - - @staticmethod - def application(): - r"""application() -> YApplication""" - return _yui.YUI_application() - - @staticmethod - def yApp(): - r"""yApp() -> YApplication""" - return _yui.YUI_yApp() - - @staticmethod - def ensureUICreated(): - r"""ensureUICreated()""" - return _yui.YUI_ensureUICreated() - - def blockEvents(self, block=True): - r"""blockEvents(YUI self, bool block=True)""" - return _yui.YUI_blockEvents(self, block) - - def unblockEvents(self): - r"""unblockEvents(YUI self)""" - return _yui.YUI_unblockEvents(self) - - def eventsBlocked(self): - r"""eventsBlocked(YUI self) -> bool""" - return _yui.YUI_eventsBlocked(self) - - def deleteNotify(self, widget): - r"""deleteNotify(YUI self, YWidget widget)""" - return _yui.YUI_deleteNotify(self, widget) - - def topmostConstructorHasFinished(self): - r"""topmostConstructorHasFinished(YUI self)""" - return _yui.YUI_topmostConstructorHasFinished(self) - - def runningWithThreads(self): - r"""runningWithThreads(YUI self) -> bool""" - return _yui.YUI_runningWithThreads(self) - - def uiThreadMainLoop(self): - r"""uiThreadMainLoop(YUI self)""" - return _yui.YUI_uiThreadMainLoop(self) - - def builtinCaller(self): - r"""builtinCaller(YUI self) -> YBuiltinCaller""" - return _yui.YUI_builtinCaller(self) - - def setBuiltinCaller(self, caller): - r"""setBuiltinCaller(YUI self, YBuiltinCaller caller)""" - return _yui.YUI_setBuiltinCaller(self, caller) - - def runPkgSelection(self, packageSelector): - r"""runPkgSelection(YUI self, YWidget packageSelector) -> YEvent""" - return _yui.YUI_runPkgSelection(self, packageSelector) - - def sendWidgetID(self, id): - r"""sendWidgetID(YUI self, std::string const & id) -> YWidget""" - return _yui.YUI_sendWidgetID(self, id) - -# Register YUI in _yui: -_yui.YUI_swigregister(YUI) - -def YUI_ui(): - r"""YUI_ui() -> YUI""" - return _yui.YUI_ui() - -def YUI_widgetFactory(): - r"""YUI_widgetFactory() -> YWidgetFactory""" - return _yui.YUI_widgetFactory() - -def YUI_optionalWidgetFactory(): - r"""YUI_optionalWidgetFactory() -> YOptionalWidgetFactory""" - return _yui.YUI_optionalWidgetFactory() - -def YUI_app(): - r"""YUI_app() -> YApplication""" - return _yui.YUI_app() - -def YUI_application(): - r"""YUI_application() -> YApplication""" - return _yui.YUI_application() - -def YUI_yApp(): - r"""YUI_yApp() -> YApplication""" - return _yui.YUI_yApp() - -def YUI_ensureUICreated(): - r"""YUI_ensureUICreated()""" - return _yui.YUI_ensureUICreated() - -def start_ui_thread(ui_int): - r"""start_ui_thread(void * ui_int) -> void *""" - return _yui.start_ui_thread(ui_int) - -YUI_LOG_DEBUG = _yui.YUI_LOG_DEBUG - -YUI_LOG_MILESTONE = _yui.YUI_LOG_MILESTONE - -YUI_LOG_WARNING = _yui.YUI_LOG_WARNING - -YUI_LOG_ERROR = _yui.YUI_LOG_ERROR - -class YUILog(object): - r"""Proxy of C++ YUILog class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - - @staticmethod - def debug(logComponent, sourceFileName, lineNo, functionName): - r"""debug(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_debug(logComponent, sourceFileName, lineNo, functionName) - - @staticmethod - def milestone(logComponent, sourceFileName, lineNo, functionName): - r"""milestone(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_milestone(logComponent, sourceFileName, lineNo, functionName) - - @staticmethod - def warning(logComponent, sourceFileName, lineNo, functionName): - r"""warning(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_warning(logComponent, sourceFileName, lineNo, functionName) - - @staticmethod - def error(logComponent, sourceFileName, lineNo, functionName): - r"""error(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_error(logComponent, sourceFileName, lineNo, functionName) - - def log(self, logLevel, logComponent, sourceFileName, lineNo, functionName): - r"""log(YUILog self, YUILogLevel_t logLevel, char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_log(self, logLevel, logComponent, sourceFileName, lineNo, functionName) - - @staticmethod - def instance(): - r"""instance() -> YUILog""" - return _yui.YUILog_instance() - - @staticmethod - def enableDebugLogging(debugLogging=True): - r"""enableDebugLogging(bool debugLogging=True)""" - return _yui.YUILog_enableDebugLogging(debugLogging) - - @staticmethod - def debugLoggingEnabled(): - r"""debugLoggingEnabled() -> bool""" - return _yui.YUILog_debugLoggingEnabled() - - @staticmethod - def setLogFileName(logFileName): - r"""setLogFileName(std::string const & logFileName) -> bool""" - return _yui.YUILog_setLogFileName(logFileName) - - @staticmethod - def logFileName(): - r"""logFileName() -> std::string""" - return _yui.YUILog_logFileName() - - @staticmethod - def setLoggerFunction(loggerFunction): - r"""setLoggerFunction(YUILoggerFunction loggerFunction)""" - return _yui.YUILog_setLoggerFunction(loggerFunction) - - @staticmethod - def loggerFunction(returnStdLogger=False): - r"""loggerFunction(bool returnStdLogger=False) -> YUILoggerFunction""" - return _yui.YUILog_loggerFunction(returnStdLogger) - - @staticmethod - def setEnableDebugLoggingHooks(enableFunction, isEnabledFunction): - r"""setEnableDebugLoggingHooks(YUIEnableDebugLoggingFunction enableFunction, YUIDebugLoggingEnabledFunction isEnabledFunction)""" - return _yui.YUILog_setEnableDebugLoggingHooks(enableFunction, isEnabledFunction) - - @staticmethod - def enableDebugLoggingHook(): - r"""enableDebugLoggingHook() -> YUIEnableDebugLoggingFunction""" - return _yui.YUILog_enableDebugLoggingHook() - - @staticmethod - def debugLoggingEnabledHook(): - r"""debugLoggingEnabledHook() -> YUIDebugLoggingEnabledFunction""" - return _yui.YUILog_debugLoggingEnabledHook() - - @staticmethod - def basename(fileNameWithPath): - r"""basename(std::string const & fileNameWithPath) -> std::string""" - return _yui.YUILog_basename(fileNameWithPath) - -# Register YUILog in _yui: -_yui.YUILog_swigregister(YUILog) - -def YUILog_debug(logComponent, sourceFileName, lineNo, functionName): - r"""YUILog_debug(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_debug(logComponent, sourceFileName, lineNo, functionName) - -def YUILog_milestone(logComponent, sourceFileName, lineNo, functionName): - r"""YUILog_milestone(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_milestone(logComponent, sourceFileName, lineNo, functionName) - -def YUILog_warning(logComponent, sourceFileName, lineNo, functionName): - r"""YUILog_warning(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_warning(logComponent, sourceFileName, lineNo, functionName) - -def YUILog_error(logComponent, sourceFileName, lineNo, functionName): - r"""YUILog_error(char const * logComponent, char const * sourceFileName, int lineNo, char const * functionName) -> std::ostream &""" - return _yui.YUILog_error(logComponent, sourceFileName, lineNo, functionName) - -def YUILog_instance(): - r"""YUILog_instance() -> YUILog""" - return _yui.YUILog_instance() - -def YUILog_enableDebugLogging(debugLogging=True): - r"""YUILog_enableDebugLogging(bool debugLogging=True)""" - return _yui.YUILog_enableDebugLogging(debugLogging) - -def YUILog_debugLoggingEnabled(): - r"""YUILog_debugLoggingEnabled() -> bool""" - return _yui.YUILog_debugLoggingEnabled() - -def YUILog_setLogFileName(logFileName): - r"""YUILog_setLogFileName(std::string const & logFileName) -> bool""" - return _yui.YUILog_setLogFileName(logFileName) - -def YUILog_logFileName(): - r"""YUILog_logFileName() -> std::string""" - return _yui.YUILog_logFileName() - -def YUILog_setLoggerFunction(loggerFunction): - r"""YUILog_setLoggerFunction(YUILoggerFunction loggerFunction)""" - return _yui.YUILog_setLoggerFunction(loggerFunction) - -def YUILog_loggerFunction(returnStdLogger=False): - r"""YUILog_loggerFunction(bool returnStdLogger=False) -> YUILoggerFunction""" - return _yui.YUILog_loggerFunction(returnStdLogger) - -def YUILog_setEnableDebugLoggingHooks(enableFunction, isEnabledFunction): - r"""YUILog_setEnableDebugLoggingHooks(YUIEnableDebugLoggingFunction enableFunction, YUIDebugLoggingEnabledFunction isEnabledFunction)""" - return _yui.YUILog_setEnableDebugLoggingHooks(enableFunction, isEnabledFunction) - -def YUILog_enableDebugLoggingHook(): - r"""YUILog_enableDebugLoggingHook() -> YUIEnableDebugLoggingFunction""" - return _yui.YUILog_enableDebugLoggingHook() - -def YUILog_debugLoggingEnabledHook(): - r"""YUILog_debugLoggingEnabledHook() -> YUIDebugLoggingEnabledFunction""" - return _yui.YUILog_debugLoggingEnabledHook() - -def YUILog_basename(fileNameWithPath): - r"""YUILog_basename(std::string const & fileNameWithPath) -> std::string""" - return _yui.YUILog_basename(fileNameWithPath) - -class YUIPlugin(object): - r"""Proxy of C++ YUIPlugin class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, pluginLibBaseName): - r"""__init__(YUIPlugin self, char const * pluginLibBaseName) -> YUIPlugin""" - _yui.YUIPlugin_swiginit(self, _yui.new_YUIPlugin(pluginLibBaseName)) - __swig_destroy__ = _yui.delete_YUIPlugin - - def unload(self): - r"""unload(YUIPlugin self)""" - return _yui.YUIPlugin_unload(self) - - def locateSymbol(self, symbol): - r"""locateSymbol(YUIPlugin self, char const * symbol) -> void *""" - return _yui.YUIPlugin_locateSymbol(self, symbol) - - def error(self): - r"""error(YUIPlugin self) -> bool""" - return _yui.YUIPlugin_error(self) - - def success(self): - r"""success(YUIPlugin self) -> bool""" - return _yui.YUIPlugin_success(self) - - def errorMsg(self): - r"""errorMsg(YUIPlugin self) -> std::string""" - return _yui.YUIPlugin_errorMsg(self) - -# Register YUIPlugin in _yui: -_yui.YUIPlugin_swigregister(YUIPlugin) - -YUIAllDimensions = _yui.YUIAllDimensions - -YD_HORIZ = _yui.YD_HORIZ - -YD_VERT = _yui.YD_VERT - -YAlignUnchanged = _yui.YAlignUnchanged - -YAlignBegin = _yui.YAlignBegin - -YAlignEnd = _yui.YAlignEnd - -YAlignCenter = _yui.YAlignCenter - -YMainDialog = _yui.YMainDialog - -YPopupDialog = _yui.YPopupDialog - -YWizardDialog = _yui.YWizardDialog - -YDialogNormalColor = _yui.YDialogNormalColor - -YDialogInfoColor = _yui.YDialogInfoColor - -YDialogWarnColor = _yui.YDialogWarnColor - -YCustomButton = _yui.YCustomButton - -YOKButton = _yui.YOKButton - -YApplyButton = _yui.YApplyButton - -YCancelButton = _yui.YCancelButton - -YHelpButton = _yui.YHelpButton - -YRelNotesButton = _yui.YRelNotesButton - -YMaxButtonRole = _yui.YMaxButtonRole - -YKDEButtonOrder = _yui.YKDEButtonOrder - -YGnomeButtonOrder = _yui.YGnomeButtonOrder - -class YWidget(object): - r"""Proxy of C++ YWidget class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YWidget - - def widgetClass(self): - r"""widgetClass(YWidget self) -> char const *""" - return _yui.YWidget_widgetClass(self) - - def debugLabel(self): - r"""debugLabel(YWidget self) -> std::string""" - return _yui.YWidget_debugLabel(self) - - def helpText(self): - r"""helpText(YWidget self) -> std::string""" - return _yui.YWidget_helpText(self) - - def setHelpText(self, helpText): - r"""setHelpText(YWidget self, std::string const & helpText)""" - return _yui.YWidget_setHelpText(self, helpText) - - def propertySet(self): - r"""propertySet(YWidget self) -> YPropertySet""" - return _yui.YWidget_propertySet(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YWidget self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YWidget_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YWidget self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YWidget_getProperty(self, propertyName) - - def hasChildren(self): - r"""hasChildren(YWidget self) -> bool""" - return _yui.YWidget_hasChildren(self) - - def firstChild(self): - r"""firstChild(YWidget self) -> YWidget""" - return _yui.YWidget_firstChild(self) - - def lastChild(self): - r"""lastChild(YWidget self) -> YWidget""" - return _yui.YWidget_lastChild(self) - - def childrenBegin(self): - r"""childrenBegin(YWidget self) -> YWidgetListIterator""" - return _yui.YWidget_childrenBegin(self) - - def childrenEnd(self): - r"""childrenEnd(YWidget self) -> YWidgetListIterator""" - return _yui.YWidget_childrenEnd(self) - - def childrenConstBegin(self): - r"""childrenConstBegin(YWidget self) -> YWidgetListConstIterator""" - return _yui.YWidget_childrenConstBegin(self) - - def childrenConstEnd(self): - r"""childrenConstEnd(YWidget self) -> YWidgetListConstIterator""" - return _yui.YWidget_childrenConstEnd(self) - - def begin(self): - r"""begin(YWidget self) -> YWidgetListIterator""" - return _yui.YWidget_begin(self) - - def end(self): - r"""end(YWidget self) -> YWidgetListIterator""" - return _yui.YWidget_end(self) - - def childrenCount(self): - r"""childrenCount(YWidget self) -> int""" - return _yui.YWidget_childrenCount(self) - - def contains(self, child): - r"""contains(YWidget self, YWidget child) -> bool""" - return _yui.YWidget_contains(self, child) - - def addChild(self, child): - r"""addChild(YWidget self, YWidget child)""" - return _yui.YWidget_addChild(self, child) - - def removeChild(self, child): - r"""removeChild(YWidget self, YWidget child)""" - return _yui.YWidget_removeChild(self, child) - - def deleteChildren(self): - r"""deleteChildren(YWidget self)""" - return _yui.YWidget_deleteChildren(self) - - def parent(self): - r"""parent(YWidget self) -> YWidget""" - return _yui.YWidget_parent(self) - - def hasParent(self): - r"""hasParent(YWidget self) -> bool""" - return _yui.YWidget_hasParent(self) - - def setParent(self, newParent): - r"""setParent(YWidget self, YWidget newParent)""" - return _yui.YWidget_setParent(self, newParent) - - def findDialog(self): - r"""findDialog(YWidget self) -> YDialog""" - return _yui.YWidget_findDialog(self) - - def findWidget(self, id, doThrow=True): - r"""findWidget(YWidget self, YWidgetID id, bool doThrow=True) -> YWidget""" - return _yui.YWidget_findWidget(self, id, doThrow) - - def preferredWidth(self): - r"""preferredWidth(YWidget self) -> int""" - return _yui.YWidget_preferredWidth(self) - - def preferredHeight(self): - r"""preferredHeight(YWidget self) -> int""" - return _yui.YWidget_preferredHeight(self) - - def preferredSize(self, dim): - r"""preferredSize(YWidget self, YUIDimension dim) -> int""" - return _yui.YWidget_preferredSize(self, dim) - - def setSize(self, newWidth, newHeight): - r"""setSize(YWidget self, int newWidth, int newHeight)""" - return _yui.YWidget_setSize(self, newWidth, newHeight) - - def isValid(self): - r"""isValid(YWidget self) -> bool""" - return _yui.YWidget_isValid(self) - - def beingDestroyed(self): - r"""beingDestroyed(YWidget self) -> bool""" - return _yui.YWidget_beingDestroyed(self) - - def widgetRep(self): - r"""widgetRep(YWidget self) -> void *""" - return _yui.YWidget_widgetRep(self) - - def setWidgetRep(self, toolkitWidgetRep): - r"""setWidgetRep(YWidget self, void * toolkitWidgetRep)""" - return _yui.YWidget_setWidgetRep(self, toolkitWidgetRep) - - def hasId(self): - r"""hasId(YWidget self) -> bool""" - return _yui.YWidget_hasId(self) - - def id(self): - r"""id(YWidget self) -> YWidgetID""" - return _yui.YWidget_id(self) - - def setId(self, newId_disown): - r"""setId(YWidget self, YWidgetID newId_disown)""" - return _yui.YWidget_setId(self, newId_disown) - - def setEnabled(self, enabled=True): - r"""setEnabled(YWidget self, bool enabled=True)""" - return _yui.YWidget_setEnabled(self, enabled) - - def setDisabled(self): - r"""setDisabled(YWidget self)""" - return _yui.YWidget_setDisabled(self) - - def isEnabled(self): - r"""isEnabled(YWidget self) -> bool""" - return _yui.YWidget_isEnabled(self) - - def stretchable(self, dim): - r"""stretchable(YWidget self, YUIDimension dim) -> bool""" - return _yui.YWidget_stretchable(self, dim) - - def setStretchable(self, dim, newStretch): - r"""setStretchable(YWidget self, YUIDimension dim, bool newStretch)""" - return _yui.YWidget_setStretchable(self, dim, newStretch) - - def setDefaultStretchable(self, dim, newStretch): - r"""setDefaultStretchable(YWidget self, YUIDimension dim, bool newStretch)""" - return _yui.YWidget_setDefaultStretchable(self, dim, newStretch) - - def weight(self, dim): - r"""weight(YWidget self, YUIDimension dim) -> int""" - return _yui.YWidget_weight(self, dim) - - def hasWeight(self, dim): - r"""hasWeight(YWidget self, YUIDimension dim) -> bool""" - return _yui.YWidget_hasWeight(self, dim) - - def setWeight(self, dim, weight): - r"""setWeight(YWidget self, YUIDimension dim, int weight)""" - return _yui.YWidget_setWeight(self, dim, weight) - - def setNotify(self, notify=True): - r"""setNotify(YWidget self, bool notify=True)""" - return _yui.YWidget_setNotify(self, notify) - - def notify(self): - r"""notify(YWidget self) -> bool""" - return _yui.YWidget_notify(self) - - def setNotifyContextMenu(self, notifyContextMenu=True): - r"""setNotifyContextMenu(YWidget self, bool notifyContextMenu=True)""" - return _yui.YWidget_setNotifyContextMenu(self, notifyContextMenu) - - def notifyContextMenu(self): - r"""notifyContextMenu(YWidget self) -> bool""" - return _yui.YWidget_notifyContextMenu(self) - - def sendKeyEvents(self): - r"""sendKeyEvents(YWidget self) -> bool""" - return _yui.YWidget_sendKeyEvents(self) - - def setSendKeyEvents(self, doSend): - r"""setSendKeyEvents(YWidget self, bool doSend)""" - return _yui.YWidget_setSendKeyEvents(self, doSend) - - def autoShortcut(self): - r"""autoShortcut(YWidget self) -> bool""" - return _yui.YWidget_autoShortcut(self) - - def setAutoShortcut(self, _newAutoShortcut): - r"""setAutoShortcut(YWidget self, bool _newAutoShortcut)""" - return _yui.YWidget_setAutoShortcut(self, _newAutoShortcut) - - def functionKey(self): - r"""functionKey(YWidget self) -> int""" - return _yui.YWidget_functionKey(self) - - def hasFunctionKey(self): - r"""hasFunctionKey(YWidget self) -> bool""" - return _yui.YWidget_hasFunctionKey(self) - - def setFunctionKey(self, fkey_no): - r"""setFunctionKey(YWidget self, int fkey_no)""" - return _yui.YWidget_setFunctionKey(self, fkey_no) - - def setKeyboardFocus(self): - r"""setKeyboardFocus(YWidget self) -> bool""" - return _yui.YWidget_setKeyboardFocus(self) - - def shortcutString(self): - r"""shortcutString(YWidget self) -> std::string""" - return _yui.YWidget_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YWidget self, std::string const & str)""" - return _yui.YWidget_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YWidget self) -> char const *""" - return _yui.YWidget_userInputProperty(self) - - def dumpWidgetTree(self, indentationLevel=0): - r"""dumpWidgetTree(YWidget self, int indentationLevel=0)""" - return _yui.YWidget_dumpWidgetTree(self, indentationLevel) - - def dumpDialogWidgetTree(self): - r"""dumpDialogWidgetTree(YWidget self)""" - return _yui.YWidget_dumpDialogWidgetTree(self) - - def setChildrenEnabled(self, enabled): - r"""setChildrenEnabled(YWidget self, bool enabled)""" - return _yui.YWidget_setChildrenEnabled(self, enabled) - - def saveUserInput(self, macroRecorder): - r"""saveUserInput(YWidget self, YMacroRecorder macroRecorder)""" - return _yui.YWidget_saveUserInput(self, macroRecorder) - - def startMultipleChanges(self): - r"""startMultipleChanges(YWidget self)""" - return _yui.YWidget_startMultipleChanges(self) - - def doneMultipleChanges(self): - r"""doneMultipleChanges(YWidget self)""" - return _yui.YWidget_doneMultipleChanges(self) - - def __eq__(self, w): - r"""__eq__(YWidget self, YWidget w) -> int""" - return _yui.YWidget___eq__(self, w) - - def __ne__(self, w): - r"""__ne__(YWidget self, YWidget w) -> int""" - return _yui.YWidget___ne__(self, w) - - def equals(self, w): - r"""equals(YWidget self, YWidget w) -> int""" - return _yui.YWidget_equals(self, w) - -# Register YWidget in _yui: -_yui.YWidget_swigregister(YWidget) - -class YSingleChildContainerWidget(YWidget): - r"""Proxy of C++ YSingleChildContainerWidget class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YSingleChildContainerWidget - - def preferredWidth(self): - r"""preferredWidth(YSingleChildContainerWidget self) -> int""" - return _yui.YSingleChildContainerWidget_preferredWidth(self) - - def preferredHeight(self): - r"""preferredHeight(YSingleChildContainerWidget self) -> int""" - return _yui.YSingleChildContainerWidget_preferredHeight(self) - - def setSize(self, newWidth, newHeight): - r"""setSize(YSingleChildContainerWidget self, int newWidth, int newHeight)""" - return _yui.YSingleChildContainerWidget_setSize(self, newWidth, newHeight) - - def stretchable(self, dim): - r"""stretchable(YSingleChildContainerWidget self, YUIDimension dim) -> bool""" - return _yui.YSingleChildContainerWidget_stretchable(self, dim) - -# Register YSingleChildContainerWidget in _yui: -_yui.YSingleChildContainerWidget_swigregister(YSingleChildContainerWidget) - -class YSelectionWidget(YWidget): - r"""Proxy of C++ YSelectionWidget class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YSelectionWidget - - def widgetClass(self): - r"""widgetClass(YSelectionWidget self) -> char const *""" - return _yui.YSelectionWidget_widgetClass(self) - - def label(self): - r"""label(YSelectionWidget self) -> std::string""" - return _yui.YSelectionWidget_label(self) - - def setLabel(self, newLabel): - r"""setLabel(YSelectionWidget self, std::string const & newLabel)""" - return _yui.YSelectionWidget_setLabel(self, newLabel) - - def addItem(self, *args): - r""" - addItem(YSelectionWidget self, YItem item_disown) - addItem(YSelectionWidget self, std::string const & itemLabel, bool selected=False) - addItem(YSelectionWidget self, std::string const & itemLabel, std::string const & iconName, bool selected=False) - """ - return _yui.YSelectionWidget_addItem(self, *args) - - def addItems(self, itemCollection): - r"""addItems(YSelectionWidget self, YItemCollection itemCollection)""" - return _yui.YSelectionWidget_addItems(self, itemCollection) - - def deleteAllItems(self): - r"""deleteAllItems(YSelectionWidget self)""" - return _yui.YSelectionWidget_deleteAllItems(self) - - def setItems(self, itemCollection): - r"""setItems(YSelectionWidget self, YItemCollection itemCollection)""" - return _yui.YSelectionWidget_setItems(self, itemCollection) - - def itemsBegin(self, *args): - r""" - itemsBegin(YSelectionWidget self) -> YItemIterator - itemsBegin(YSelectionWidget self) -> YItemConstIterator - """ - return _yui.YSelectionWidget_itemsBegin(self, *args) - - def itemsEnd(self, *args): - r""" - itemsEnd(YSelectionWidget self) -> YItemIterator - itemsEnd(YSelectionWidget self) -> YItemConstIterator - """ - return _yui.YSelectionWidget_itemsEnd(self, *args) - - def hasItems(self): - r"""hasItems(YSelectionWidget self) -> bool""" - return _yui.YSelectionWidget_hasItems(self) - - def itemsCount(self): - r"""itemsCount(YSelectionWidget self) -> int""" - return _yui.YSelectionWidget_itemsCount(self) - - def itemAt(self, index): - r"""itemAt(YSelectionWidget self, int index) -> YItem""" - return _yui.YSelectionWidget_itemAt(self, index) - - def firstItem(self): - r"""firstItem(YSelectionWidget self) -> YItem""" - return _yui.YSelectionWidget_firstItem(self) - - def selectedItem(self): - r"""selectedItem(YSelectionWidget self) -> YItem""" - return _yui.YSelectionWidget_selectedItem(self) - - def selectedItems(self): - r"""selectedItems(YSelectionWidget self) -> YItemCollection""" - return _yui.YSelectionWidget_selectedItems(self) - - def hasSelectedItem(self): - r"""hasSelectedItem(YSelectionWidget self) -> bool""" - return _yui.YSelectionWidget_hasSelectedItem(self) - - def selectItem(self, item, selected=True): - r"""selectItem(YSelectionWidget self, YItem item, bool selected=True)""" - return _yui.YSelectionWidget_selectItem(self, item, selected) - - def setItemStatus(self, item, status): - r"""setItemStatus(YSelectionWidget self, YItem item, int status)""" - return _yui.YSelectionWidget_setItemStatus(self, item, status) - - def deselectAllItems(self): - r"""deselectAllItems(YSelectionWidget self)""" - return _yui.YSelectionWidget_deselectAllItems(self) - - def setIconBasePath(self, basePath): - r"""setIconBasePath(YSelectionWidget self, std::string const & basePath)""" - return _yui.YSelectionWidget_setIconBasePath(self, basePath) - - def iconBasePath(self): - r"""iconBasePath(YSelectionWidget self) -> std::string""" - return _yui.YSelectionWidget_iconBasePath(self) - - def iconFullPath(self, *args): - r""" - iconFullPath(YSelectionWidget self, std::string const & iconName) -> std::string - iconFullPath(YSelectionWidget self, YItem item) -> std::string - """ - return _yui.YSelectionWidget_iconFullPath(self, *args) - - def itemsContain(self, item): - r"""itemsContain(YSelectionWidget self, YItem item) -> bool""" - return _yui.YSelectionWidget_itemsContain(self, item) - - def findItem(self, itemLabel): - r"""findItem(YSelectionWidget self, std::string const & itemLabel) -> YItem""" - return _yui.YSelectionWidget_findItem(self, itemLabel) - - def dumpItems(self): - r"""dumpItems(YSelectionWidget self)""" - return _yui.YSelectionWidget_dumpItems(self) - - def enforceSingleSelection(self): - r"""enforceSingleSelection(YSelectionWidget self) -> bool""" - return _yui.YSelectionWidget_enforceSingleSelection(self) - - def shortcutChanged(self): - r"""shortcutChanged(YSelectionWidget self)""" - return _yui.YSelectionWidget_shortcutChanged(self) - - def shortcutString(self): - r"""shortcutString(YSelectionWidget self) -> std::string""" - return _yui.YSelectionWidget_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YSelectionWidget self, std::string const & str)""" - return _yui.YSelectionWidget_setShortcutString(self, str) - -# Register YSelectionWidget in _yui: -_yui.YSelectionWidget_swigregister(YSelectionWidget) - -class YSimpleInputField(YWidget): - r"""Proxy of C++ YSimpleInputField class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YSimpleInputField - - def value(self): - r"""value(YSimpleInputField self) -> std::string""" - return _yui.YSimpleInputField_value(self) - - def setValue(self, text): - r"""setValue(YSimpleInputField self, std::string const & text)""" - return _yui.YSimpleInputField_setValue(self, text) - - def label(self): - r"""label(YSimpleInputField self) -> std::string""" - return _yui.YSimpleInputField_label(self) - - def setLabel(self, label): - r"""setLabel(YSimpleInputField self, std::string const & label)""" - return _yui.YSimpleInputField_setLabel(self, label) - - def setProperty(self, propertyName, val): - r"""setProperty(YSimpleInputField self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YSimpleInputField_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YSimpleInputField self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YSimpleInputField_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YSimpleInputField self) -> YPropertySet""" - return _yui.YSimpleInputField_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YSimpleInputField self) -> std::string""" - return _yui.YSimpleInputField_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YSimpleInputField self, std::string const & str)""" - return _yui.YSimpleInputField_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YSimpleInputField self) -> char const *""" - return _yui.YSimpleInputField_userInputProperty(self) - -# Register YSimpleInputField in _yui: -_yui.YSimpleInputField_swigregister(YSimpleInputField) - -class YItem(object): - r"""Proxy of C++ YItem class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YItem self, std::string const & label, bool selected=False) -> YItem - __init__(YItem self, std::string const & label, std::string const & iconName, bool selected=False) -> YItem - """ - _yui.YItem_swiginit(self, _yui.new_YItem(*args)) - __swig_destroy__ = _yui.delete_YItem - - def itemClass(self): - r"""itemClass(YItem self) -> char const *""" - return _yui.YItem_itemClass(self) - - def label(self): - r"""label(YItem self) -> std::string""" - return _yui.YItem_label(self) - - def setLabel(self, newLabel): - r"""setLabel(YItem self, std::string const & newLabel)""" - return _yui.YItem_setLabel(self, newLabel) - - def iconName(self): - r"""iconName(YItem self) -> std::string""" - return _yui.YItem_iconName(self) - - def hasIconName(self): - r"""hasIconName(YItem self) -> bool""" - return _yui.YItem_hasIconName(self) - - def setIconName(self, newIconName): - r"""setIconName(YItem self, std::string const & newIconName)""" - return _yui.YItem_setIconName(self, newIconName) - - def selected(self): - r"""selected(YItem self) -> bool""" - return _yui.YItem_selected(self) - - def setSelected(self, sel=True): - r"""setSelected(YItem self, bool sel=True)""" - return _yui.YItem_setSelected(self, sel) - - def status(self): - r"""status(YItem self) -> int""" - return _yui.YItem_status(self) - - def setStatus(self, newStatus): - r"""setStatus(YItem self, int newStatus)""" - return _yui.YItem_setStatus(self, newStatus) - - def setIndex(self, index): - r"""setIndex(YItem self, int index)""" - return _yui.YItem_setIndex(self, index) - - def index(self): - r"""index(YItem self) -> int""" - return _yui.YItem_index(self) - - def setData(self, newData): - r"""setData(YItem self, void * newData)""" - return _yui.YItem_setData(self, newData) - - def data(self): - r"""data(YItem self) -> void *""" - return _yui.YItem_data(self) - - def hasChildren(self): - r"""hasChildren(YItem self) -> bool""" - return _yui.YItem_hasChildren(self) - - def childrenBegin(self, *args): - r""" - childrenBegin(YItem self) -> YItemIterator - childrenBegin(YItem self) -> YItemConstIterator - """ - return _yui.YItem_childrenBegin(self, *args) - - def childrenEnd(self, *args): - r""" - childrenEnd(YItem self) -> YItemIterator - childrenEnd(YItem self) -> YItemConstIterator - """ - return _yui.YItem_childrenEnd(self, *args) - - def parent(self): - r"""parent(YItem self) -> YItem""" - return _yui.YItem_parent(self) - - def debugLabel(self): - r"""debugLabel(YItem self) -> std::string""" - return _yui.YItem_debugLabel(self) - - def limitLength(self, text, limit): - r"""limitLength(YItem self, std::string const & text, int limit) -> std::string""" - return _yui.YItem_limitLength(self, text, limit) - - def __eq__(self, i): - r"""__eq__(YItem self, YItem i) -> int""" - return _yui.YItem___eq__(self, i) - - def __ne__(self, i): - r"""__ne__(YItem self, YItem i) -> int""" - return _yui.YItem___ne__(self, i) - - def equals(self, i): - r"""equals(YItem self, YItem i) -> int""" - return _yui.YItem_equals(self, i) - -# Register YItem in _yui: -_yui.YItem_swigregister(YItem) - -class YTreeItem(YItem): - r"""Proxy of C++ YTreeItem class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YTreeItem self, std::string const & label, bool isOpen=False) -> YTreeItem - __init__(YTreeItem self, std::string const & label, std::string const & iconName, bool isOpen=False) -> YTreeItem - __init__(YTreeItem self, YTreeItem parent, std::string const & label, bool isOpen=False) -> YTreeItem - __init__(YTreeItem self, YTreeItem parent, std::string const & label, std::string const & iconName, bool isOpen=False) -> YTreeItem - """ - _yui.YTreeItem_swiginit(self, _yui.new_YTreeItem(*args)) - __swig_destroy__ = _yui.delete_YTreeItem - - def itemClass(self): - r"""itemClass(YTreeItem self) -> char const *""" - return _yui.YTreeItem_itemClass(self) - - def hasChildren(self): - r"""hasChildren(YTreeItem self) -> bool""" - return _yui.YTreeItem_hasChildren(self) - - def childrenBegin(self, *args): - r""" - childrenBegin(YTreeItem self) -> YItemIterator - childrenBegin(YTreeItem self) -> YItemConstIterator - """ - return _yui.YTreeItem_childrenBegin(self, *args) - - def childrenEnd(self, *args): - r""" - childrenEnd(YTreeItem self) -> YItemIterator - childrenEnd(YTreeItem self) -> YItemConstIterator - """ - return _yui.YTreeItem_childrenEnd(self, *args) - - def addChild(self, item_disown): - r"""addChild(YTreeItem self, YItem item_disown)""" - return _yui.YTreeItem_addChild(self, item_disown) - - def deleteChildren(self): - r"""deleteChildren(YTreeItem self)""" - return _yui.YTreeItem_deleteChildren(self) - - def isOpen(self): - r"""isOpen(YTreeItem self) -> bool""" - return _yui.YTreeItem_isOpen(self) - - def setOpen(self, open=True): - r"""setOpen(YTreeItem self, bool open=True)""" - return _yui.YTreeItem_setOpen(self, open) - - def setClosed(self): - r"""setClosed(YTreeItem self)""" - return _yui.YTreeItem_setClosed(self) - - def parent(self): - r"""parent(YTreeItem self) -> YTreeItem""" - return _yui.YTreeItem_parent(self) - -# Register YTreeItem in _yui: -_yui.YTreeItem_swigregister(YTreeItem) - -class YStringTree(object): - r"""Proxy of C++ YStringTree class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, textdomain): - r"""__init__(YStringTree self, char const * textdomain) -> YStringTree""" - _yui.YStringTree_swiginit(self, _yui.new_YStringTree(textdomain)) - __swig_destroy__ = _yui.delete_YStringTree - - def addBranch(self, content, delimiter=0, parent=None): - r"""addBranch(YStringTree self, std::string const & content, char delimiter=0, YStringTreeItem * parent=None) -> YStringTreeItem""" - return _yui.YStringTree_addBranch(self, content, delimiter, parent) - - def origPath(self, item, delimiter, startWithDelimiter=True): - r"""origPath(YStringTree self, YStringTreeItem const * item, char delimiter, bool startWithDelimiter=True) -> std::string""" - return _yui.YStringTree_origPath(self, item, delimiter, startWithDelimiter) - - def translatedPath(self, item, delimiter, startWithDelimiter=True): - r"""translatedPath(YStringTree self, YStringTreeItem const * item, char delimiter, bool startWithDelimiter=True) -> std::string""" - return _yui.YStringTree_translatedPath(self, item, delimiter, startWithDelimiter) - - def path(self, item, delimiter, startWithDelimiter=True): - r"""path(YStringTree self, YStringTreeItem const * item, char delimiter, bool startWithDelimiter=True) -> YTransText""" - return _yui.YStringTree_path(self, item, delimiter, startWithDelimiter) - - def logTree(self): - r"""logTree(YStringTree self)""" - return _yui.YStringTree_logTree(self) - - def root(self): - r"""root(YStringTree self) -> YStringTreeItem *""" - return _yui.YStringTree_root(self) - - def textdomain(self): - r"""textdomain(YStringTree self) -> char const *""" - return _yui.YStringTree_textdomain(self) - - def setTextdomain(self, domain): - r"""setTextdomain(YStringTree self, char const * domain)""" - return _yui.YStringTree_setTextdomain(self, domain) - - def translate(self, orig): - r"""translate(YStringTree self, std::string const & orig) -> std::string""" - return _yui.YStringTree_translate(self, orig) - -# Register YStringTree in _yui: -_yui.YStringTree_swigregister(YStringTree) - -class YWidgetFactory(object): - r"""Proxy of C++ YWidgetFactory class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - - def createMainDialog(self, colorMode=YDialogNormalColor): - r"""createMainDialog(YWidgetFactory self, YDialogColorMode colorMode=YDialogNormalColor) -> YDialog""" - return _yui.YWidgetFactory_createMainDialog(self, colorMode) - - def createPopupDialog(self, colorMode=YDialogNormalColor): - r"""createPopupDialog(YWidgetFactory self, YDialogColorMode colorMode=YDialogNormalColor) -> YDialog""" - return _yui.YWidgetFactory_createPopupDialog(self, colorMode) - - def createDialog(self, dialogType, colorMode=YDialogNormalColor): - r"""createDialog(YWidgetFactory self, YDialogType dialogType, YDialogColorMode colorMode=YDialogNormalColor) -> YDialog""" - return _yui.YWidgetFactory_createDialog(self, dialogType, colorMode) - - def createVBox(self, parent): - r"""createVBox(YWidgetFactory self, YWidget parent) -> YLayoutBox""" - return _yui.YWidgetFactory_createVBox(self, parent) - - def createHBox(self, parent): - r"""createHBox(YWidgetFactory self, YWidget parent) -> YLayoutBox""" - return _yui.YWidgetFactory_createHBox(self, parent) - - def createLayoutBox(self, parent, dimension): - r"""createLayoutBox(YWidgetFactory self, YWidget parent, YUIDimension dimension) -> YLayoutBox""" - return _yui.YWidgetFactory_createLayoutBox(self, parent, dimension) - - def createButtonBox(self, parent): - r"""createButtonBox(YWidgetFactory self, YWidget parent) -> YButtonBox *""" - return _yui.YWidgetFactory_createButtonBox(self, parent) - - def createPushButton(self, parent, label): - r"""createPushButton(YWidgetFactory self, YWidget parent, std::string const & label) -> YPushButton""" - return _yui.YWidgetFactory_createPushButton(self, parent, label) - - def createLabel(self, parent, text, isHeading=False, isOutputField=False): - r"""createLabel(YWidgetFactory self, YWidget parent, std::string const & text, bool isHeading=False, bool isOutputField=False) -> YLabel""" - return _yui.YWidgetFactory_createLabel(self, parent, text, isHeading, isOutputField) - - def createHeading(self, parent, label): - r"""createHeading(YWidgetFactory self, YWidget parent, std::string const & label) -> YLabel""" - return _yui.YWidgetFactory_createHeading(self, parent, label) - - def createInputField(self, parent, label, passwordMode=False): - r"""createInputField(YWidgetFactory self, YWidget parent, std::string const & label, bool passwordMode=False) -> YInputField""" - return _yui.YWidgetFactory_createInputField(self, parent, label, passwordMode) - - def createCheckBox(self, parent, label, isChecked=False): - r"""createCheckBox(YWidgetFactory self, YWidget parent, std::string const & label, bool isChecked=False) -> YCheckBox""" - return _yui.YWidgetFactory_createCheckBox(self, parent, label, isChecked) - - def createRadioButton(self, parent, label, isChecked=False): - r"""createRadioButton(YWidgetFactory self, YWidget parent, std::string const & label, bool isChecked=False) -> YRadioButton""" - return _yui.YWidgetFactory_createRadioButton(self, parent, label, isChecked) - - def createComboBox(self, parent, label, editable=False): - r"""createComboBox(YWidgetFactory self, YWidget parent, std::string const & label, bool editable=False) -> YComboBox""" - return _yui.YWidgetFactory_createComboBox(self, parent, label, editable) - - def createSelectionBox(self, parent, label): - r"""createSelectionBox(YWidgetFactory self, YWidget parent, std::string const & label) -> YSelectionBox""" - return _yui.YWidgetFactory_createSelectionBox(self, parent, label) - - def createTree(self, parent, label, multiselection=False, recursiveselection=False): - r"""createTree(YWidgetFactory self, YWidget parent, std::string const & label, bool multiselection=False, bool recursiveselection=False) -> YTree""" - return _yui.YWidgetFactory_createTree(self, parent, label, multiselection, recursiveselection) - - def createTable(self, parent, header_disown, multiSelection=False): - r"""createTable(YWidgetFactory self, YWidget parent, YTableHeader header_disown, bool multiSelection=False) -> YTable""" - return _yui.YWidgetFactory_createTable(self, parent, header_disown, multiSelection) - - def createProgressBar(self, parent, label, maxValue=100): - r"""createProgressBar(YWidgetFactory self, YWidget parent, std::string const & label, int maxValue=100) -> YProgressBar""" - return _yui.YWidgetFactory_createProgressBar(self, parent, label, maxValue) - - def createRichText(self, *args): - r"""createRichText(YWidgetFactory self, YWidget parent, std::string const & text=std::string(), bool plainTextMode=False) -> YRichText""" - return _yui.YWidgetFactory_createRichText(self, *args) - - def createBusyIndicator(self, parent, label, timeout=1000): - r"""createBusyIndicator(YWidgetFactory self, YWidget parent, std::string const & label, int timeout=1000) -> YBusyIndicator""" - return _yui.YWidgetFactory_createBusyIndicator(self, parent, label, timeout) - - def createIconButton(self, parent, iconName, fallbackTextLabel): - r"""createIconButton(YWidgetFactory self, YWidget parent, std::string const & iconName, std::string const & fallbackTextLabel) -> YPushButton""" - return _yui.YWidgetFactory_createIconButton(self, parent, iconName, fallbackTextLabel) - - def createOutputField(self, parent, label): - r"""createOutputField(YWidgetFactory self, YWidget parent, std::string const & label) -> YLabel""" - return _yui.YWidgetFactory_createOutputField(self, parent, label) - - def createIntField(self, parent, label, minVal, maxVal, initialVal): - r"""createIntField(YWidgetFactory self, YWidget parent, std::string const & label, int minVal, int maxVal, int initialVal) -> YIntField""" - return _yui.YWidgetFactory_createIntField(self, parent, label, minVal, maxVal, initialVal) - - def createPasswordField(self, parent, label): - r"""createPasswordField(YWidgetFactory self, YWidget parent, std::string const & label) -> YInputField""" - return _yui.YWidgetFactory_createPasswordField(self, parent, label) - - def createMenuButton(self, parent, label): - r"""createMenuButton(YWidgetFactory self, YWidget parent, std::string const & label) -> YMenuButton""" - return _yui.YWidgetFactory_createMenuButton(self, parent, label) - - def createMultiLineEdit(self, parent, label): - r"""createMultiLineEdit(YWidgetFactory self, YWidget parent, std::string const & label) -> YMultiLineEdit""" - return _yui.YWidgetFactory_createMultiLineEdit(self, parent, label) - - def createImage(self, parent, imageFileName, animated=False): - r"""createImage(YWidgetFactory self, YWidget parent, std::string const & imageFileName, bool animated=False) -> YImage""" - return _yui.YWidgetFactory_createImage(self, parent, imageFileName, animated) - - def createLogView(self, parent, label, visibleLines, storedLines=0): - r"""createLogView(YWidgetFactory self, YWidget parent, std::string const & label, int visibleLines, int storedLines=0) -> YLogView""" - return _yui.YWidgetFactory_createLogView(self, parent, label, visibleLines, storedLines) - - def createMultiSelectionBox(self, parent, label): - r"""createMultiSelectionBox(YWidgetFactory self, YWidget parent, std::string const & label) -> YMultiSelectionBox""" - return _yui.YWidgetFactory_createMultiSelectionBox(self, parent, label) - - def createPackageSelector(self, parent, ModeFlags=0): - r"""createPackageSelector(YWidgetFactory self, YWidget parent, long ModeFlags=0) -> YPackageSelector""" - return _yui.YWidgetFactory_createPackageSelector(self, parent, ModeFlags) - - def createPkgSpecial(self, parent, subwidgetName): - r"""createPkgSpecial(YWidgetFactory self, YWidget parent, std::string const & subwidgetName) -> YWidget""" - return _yui.YWidgetFactory_createPkgSpecial(self, parent, subwidgetName) - - def createHStretch(self, parent): - r"""createHStretch(YWidgetFactory self, YWidget parent) -> YSpacing""" - return _yui.YWidgetFactory_createHStretch(self, parent) - - def createVStretch(self, parent): - r"""createVStretch(YWidgetFactory self, YWidget parent) -> YSpacing""" - return _yui.YWidgetFactory_createVStretch(self, parent) - - def createHSpacing(self, parent, size=1.0): - r"""createHSpacing(YWidgetFactory self, YWidget parent, YLayoutSize_t size=1.0) -> YSpacing""" - return _yui.YWidgetFactory_createHSpacing(self, parent, size) - - def createVSpacing(self, parent, size=1.0): - r"""createVSpacing(YWidgetFactory self, YWidget parent, YLayoutSize_t size=1.0) -> YSpacing""" - return _yui.YWidgetFactory_createVSpacing(self, parent, size) - - def createSpacing(self, parent, dim, stretchable=False, size=0.0): - r"""createSpacing(YWidgetFactory self, YWidget parent, YUIDimension dim, bool stretchable=False, YLayoutSize_t size=0.0) -> YSpacing""" - return _yui.YWidgetFactory_createSpacing(self, parent, dim, stretchable, size) - - def createEmpty(self, parent): - r"""createEmpty(YWidgetFactory self, YWidget parent) -> YEmpty""" - return _yui.YWidgetFactory_createEmpty(self, parent) - - def createLeft(self, parent): - r"""createLeft(YWidgetFactory self, YWidget parent) -> YAlignment""" - return _yui.YWidgetFactory_createLeft(self, parent) - - def createRight(self, parent): - r"""createRight(YWidgetFactory self, YWidget parent) -> YAlignment""" - return _yui.YWidgetFactory_createRight(self, parent) - - def createTop(self, parent): - r"""createTop(YWidgetFactory self, YWidget parent) -> YAlignment""" - return _yui.YWidgetFactory_createTop(self, parent) - - def createBottom(self, parent): - r"""createBottom(YWidgetFactory self, YWidget parent) -> YAlignment""" - return _yui.YWidgetFactory_createBottom(self, parent) - - def createHCenter(self, parent): - r"""createHCenter(YWidgetFactory self, YWidget parent) -> YAlignment""" - return _yui.YWidgetFactory_createHCenter(self, parent) - - def createVCenter(self, parent): - r"""createVCenter(YWidgetFactory self, YWidget parent) -> YAlignment""" - return _yui.YWidgetFactory_createVCenter(self, parent) - - def createHVCenter(self, parent): - r"""createHVCenter(YWidgetFactory self, YWidget parent) -> YAlignment""" - return _yui.YWidgetFactory_createHVCenter(self, parent) - - def createMarginBox(self, *args): - r""" - createMarginBox(YWidgetFactory self, YWidget parent, YLayoutSize_t horMargin, YLayoutSize_t vertMargin) -> YAlignment - createMarginBox(YWidgetFactory self, YWidget parent, YLayoutSize_t leftMargin, YLayoutSize_t rightMargin, YLayoutSize_t topMargin, YLayoutSize_t bottomMargin) -> YAlignment - """ - return _yui.YWidgetFactory_createMarginBox(self, *args) - - def createMinWidth(self, parent, minWidth): - r"""createMinWidth(YWidgetFactory self, YWidget parent, YLayoutSize_t minWidth) -> YAlignment""" - return _yui.YWidgetFactory_createMinWidth(self, parent, minWidth) - - def createMinHeight(self, parent, minHeight): - r"""createMinHeight(YWidgetFactory self, YWidget parent, YLayoutSize_t minHeight) -> YAlignment""" - return _yui.YWidgetFactory_createMinHeight(self, parent, minHeight) - - def createMinSize(self, parent, minWidth, minHeight): - r"""createMinSize(YWidgetFactory self, YWidget parent, YLayoutSize_t minWidth, YLayoutSize_t minHeight) -> YAlignment""" - return _yui.YWidgetFactory_createMinSize(self, parent, minWidth, minHeight) - - def createAlignment(self, parent, horAlignment, vertAlignment): - r"""createAlignment(YWidgetFactory self, YWidget parent, YAlignmentType horAlignment, YAlignmentType vertAlignment) -> YAlignment""" - return _yui.YWidgetFactory_createAlignment(self, parent, horAlignment, vertAlignment) - - def createHSquash(self, parent): - r"""createHSquash(YWidgetFactory self, YWidget parent) -> YSquash""" - return _yui.YWidgetFactory_createHSquash(self, parent) - - def createVSquash(self, parent): - r"""createVSquash(YWidgetFactory self, YWidget parent) -> YSquash""" - return _yui.YWidgetFactory_createVSquash(self, parent) - - def createHVSquash(self, parent): - r"""createHVSquash(YWidgetFactory self, YWidget parent) -> YSquash""" - return _yui.YWidgetFactory_createHVSquash(self, parent) - - def createSquash(self, parent, horSquash, vertSquash): - r"""createSquash(YWidgetFactory self, YWidget parent, bool horSquash, bool vertSquash) -> YSquash""" - return _yui.YWidgetFactory_createSquash(self, parent, horSquash, vertSquash) - - def createFrame(self, parent, label): - r"""createFrame(YWidgetFactory self, YWidget parent, std::string const & label) -> YFrame""" - return _yui.YWidgetFactory_createFrame(self, parent, label) - - def createCheckBoxFrame(self, parent, label, checked): - r"""createCheckBoxFrame(YWidgetFactory self, YWidget parent, std::string const & label, bool checked) -> YCheckBoxFrame""" - return _yui.YWidgetFactory_createCheckBoxFrame(self, parent, label, checked) - - def createRadioButtonGroup(self, parent): - r"""createRadioButtonGroup(YWidgetFactory self, YWidget parent) -> YRadioButtonGroup""" - return _yui.YWidgetFactory_createRadioButtonGroup(self, parent) - - def createReplacePoint(self, parent): - r"""createReplacePoint(YWidgetFactory self, YWidget parent) -> YReplacePoint""" - return _yui.YWidgetFactory_createReplacePoint(self, parent) - - def createItemSelector(self, parent, enforceSingleSelection=True): - r"""createItemSelector(YWidgetFactory self, YWidget parent, bool enforceSingleSelection=True) -> YItemSelector""" - return _yui.YWidgetFactory_createItemSelector(self, parent, enforceSingleSelection) - - def createSingleItemSelector(self, parent): - r"""createSingleItemSelector(YWidgetFactory self, YWidget parent) -> YItemSelector""" - return _yui.YWidgetFactory_createSingleItemSelector(self, parent) - - def createMultiItemSelector(self, parent): - r"""createMultiItemSelector(YWidgetFactory self, YWidget parent) -> YItemSelector""" - return _yui.YWidgetFactory_createMultiItemSelector(self, parent) - - def createCustomStatusItemSelector(self, parent, customStates): - r"""createCustomStatusItemSelector(YWidgetFactory self, YWidget parent, YItemCustomStatusVector const & customStates) -> YItemSelector""" - return _yui.YWidgetFactory_createCustomStatusItemSelector(self, parent, customStates) - - def createMenuBar(self, parent): - r"""createMenuBar(YWidgetFactory self, YWidget parent) -> YMenuBar""" - return _yui.YWidgetFactory_createMenuBar(self, parent) - -# Register YWidgetFactory in _yui: -_yui.YWidgetFactory_swigregister(YWidgetFactory) - -class YDialog(YSingleChildContainerWidget): - r"""Proxy of C++ YDialog class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - - def widgetClass(self): - r"""widgetClass(YDialog self) -> char const *""" - return _yui.YDialog_widgetClass(self) - - def open(self): - r"""open(YDialog self)""" - return _yui.YDialog_open(self) - - def isOpen(self): - r"""isOpen(YDialog self) -> bool""" - return _yui.YDialog_isOpen(self) - - def waitForEvent(self, timeout_millisec=0): - r"""waitForEvent(YDialog self, int timeout_millisec=0) -> YEvent""" - return _yui.YDialog_waitForEvent(self, timeout_millisec) - - def pollEvent(self): - r"""pollEvent(YDialog self) -> YEvent""" - return _yui.YDialog_pollEvent(self) - - def isTopmostDialog(self): - r"""isTopmostDialog(YDialog self) -> bool""" - return _yui.YDialog_isTopmostDialog(self) - - def requestMultiPassLayout(self): - r"""requestMultiPassLayout(YDialog self)""" - return _yui.YDialog_requestMultiPassLayout(self) - - def layoutPass(self): - r"""layoutPass(YDialog self) -> int""" - return _yui.YDialog_layoutPass(self) - - def destroy(self, doThrow=True): - r"""destroy(YDialog self, bool doThrow=True) -> bool""" - return _yui.YDialog_destroy(self, doThrow) - - @staticmethod - def deleteTopmostDialog(doThrow=True): - r"""deleteTopmostDialog(bool doThrow=True) -> bool""" - return _yui.YDialog_deleteTopmostDialog(doThrow) - - @staticmethod - def deleteAllDialogs(): - r"""deleteAllDialogs()""" - return _yui.YDialog_deleteAllDialogs() - - @staticmethod - def deleteTo(dialog): - r"""deleteTo(YDialog dialog)""" - return _yui.YDialog_deleteTo(dialog) - - @staticmethod - def openDialogsCount(): - r"""openDialogsCount() -> int""" - return _yui.YDialog_openDialogsCount() - - @staticmethod - def currentDialog(doThrow=True): - r"""currentDialog(bool doThrow=True) -> YDialog""" - return _yui.YDialog_currentDialog(doThrow) - - @staticmethod - def topmostDialog(doThrow=True): - r"""topmostDialog(bool doThrow=True) -> YDialog""" - return _yui.YDialog_topmostDialog(doThrow) - - def setInitialSize(self): - r"""setInitialSize(YDialog self)""" - return _yui.YDialog_setInitialSize(self) - - def recalcLayout(self): - r"""recalcLayout(YDialog self)""" - return _yui.YDialog_recalcLayout(self) - - def dialogType(self): - r"""dialogType(YDialog self) -> YDialogType""" - return _yui.YDialog_dialogType(self) - - def isMainDialog(self): - r"""isMainDialog(YDialog self) -> bool""" - return _yui.YDialog_isMainDialog(self) - - def colorMode(self): - r"""colorMode(YDialog self) -> YDialogColorMode""" - return _yui.YDialog_colorMode(self) - - def checkShortcuts(self, force=False): - r"""checkShortcuts(YDialog self, bool force=False)""" - return _yui.YDialog_checkShortcuts(self, force) - - def postponeShortcutCheck(self): - r"""postponeShortcutCheck(YDialog self)""" - return _yui.YDialog_postponeShortcutCheck(self) - - def shortcutCheckPostponed(self): - r"""shortcutCheckPostponed(YDialog self) -> bool""" - return _yui.YDialog_shortcutCheckPostponed(self) - - def defaultButton(self): - r"""defaultButton(YDialog self) -> YPushButton""" - return _yui.YDialog_defaultButton(self) - - def deleteEvent(self, event): - r"""deleteEvent(YDialog self, YEvent event)""" - return _yui.YDialog_deleteEvent(self, event) - - def addEventFilter(self, eventFilter): - r"""addEventFilter(YDialog self, YEventFilter * eventFilter)""" - return _yui.YDialog_addEventFilter(self, eventFilter) - - def removeEventFilter(self, eventFilter): - r"""removeEventFilter(YDialog self, YEventFilter * eventFilter)""" - return _yui.YDialog_removeEventFilter(self, eventFilter) - - def highlight(self, child): - r"""highlight(YDialog self, YWidget child)""" - return _yui.YDialog_highlight(self, child) - - def setDefaultButton(self, defaultButton): - r"""setDefaultButton(YDialog self, YPushButton defaultButton)""" - return _yui.YDialog_setDefaultButton(self, defaultButton) - - def activate(self): - r"""activate(YDialog self)""" - return _yui.YDialog_activate(self) - - @staticmethod - def showText(text, richText=False): - r"""showText(std::string const & text, bool richText=False)""" - return _yui.YDialog_showText(text, richText) - - @staticmethod - def showHelpText(widget): - r"""showHelpText(YWidget widget) -> bool""" - return _yui.YDialog_showHelpText(widget) - - @staticmethod - def showRelNotesText(): - r"""showRelNotesText() -> bool""" - return _yui.YDialog_showRelNotesText() - -# Register YDialog in _yui: -_yui.YDialog_swigregister(YDialog) - -def YDialog_deleteTopmostDialog(doThrow=True): - r"""YDialog_deleteTopmostDialog(bool doThrow=True) -> bool""" - return _yui.YDialog_deleteTopmostDialog(doThrow) - -def YDialog_deleteAllDialogs(): - r"""YDialog_deleteAllDialogs()""" - return _yui.YDialog_deleteAllDialogs() - -def YDialog_deleteTo(dialog): - r"""YDialog_deleteTo(YDialog dialog)""" - return _yui.YDialog_deleteTo(dialog) - -def YDialog_openDialogsCount(): - r"""YDialog_openDialogsCount() -> int""" - return _yui.YDialog_openDialogsCount() - -def YDialog_currentDialog(doThrow=True): - r"""YDialog_currentDialog(bool doThrow=True) -> YDialog""" - return _yui.YDialog_currentDialog(doThrow) - -def YDialog_topmostDialog(doThrow=True): - r"""YDialog_topmostDialog(bool doThrow=True) -> YDialog""" - return _yui.YDialog_topmostDialog(doThrow) - -def YDialog_showText(text, richText=False): - r"""YDialog_showText(std::string const & text, bool richText=False)""" - return _yui.YDialog_showText(text, richText) - -def YDialog_showHelpText(widget): - r"""YDialog_showHelpText(YWidget widget) -> bool""" - return _yui.YDialog_showHelpText(widget) - -def YDialog_showRelNotesText(): - r"""YDialog_showRelNotesText() -> bool""" - return _yui.YDialog_showRelNotesText() - -class YAlignment(YSingleChildContainerWidget): - r"""Proxy of C++ YAlignment class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YAlignment - - def widgetClass(self): - r"""widgetClass(YAlignment self) -> char const *""" - return _yui.YAlignment_widgetClass(self) - - def alignment(self, dim): - r"""alignment(YAlignment self, YUIDimension dim) -> YAlignmentType""" - return _yui.YAlignment_alignment(self, dim) - - def leftMargin(self): - r"""leftMargin(YAlignment self) -> int""" - return _yui.YAlignment_leftMargin(self) - - def rightMargin(self): - r"""rightMargin(YAlignment self) -> int""" - return _yui.YAlignment_rightMargin(self) - - def topMargin(self): - r"""topMargin(YAlignment self) -> int""" - return _yui.YAlignment_topMargin(self) - - def bottomMargin(self): - r"""bottomMargin(YAlignment self) -> int""" - return _yui.YAlignment_bottomMargin(self) - - def totalMargins(self, dim): - r"""totalMargins(YAlignment self, YUIDimension dim) -> int""" - return _yui.YAlignment_totalMargins(self, dim) - - def setLeftMargin(self, margin): - r"""setLeftMargin(YAlignment self, int margin)""" - return _yui.YAlignment_setLeftMargin(self, margin) - - def setRightMargin(self, margin): - r"""setRightMargin(YAlignment self, int margin)""" - return _yui.YAlignment_setRightMargin(self, margin) - - def setTopMargin(self, margin): - r"""setTopMargin(YAlignment self, int margin)""" - return _yui.YAlignment_setTopMargin(self, margin) - - def setBottomMargin(self, margin): - r"""setBottomMargin(YAlignment self, int margin)""" - return _yui.YAlignment_setBottomMargin(self, margin) - - def minWidth(self): - r"""minWidth(YAlignment self) -> int""" - return _yui.YAlignment_minWidth(self) - - def minHeight(self): - r"""minHeight(YAlignment self) -> int""" - return _yui.YAlignment_minHeight(self) - - def setMinWidth(self, width): - r"""setMinWidth(YAlignment self, int width)""" - return _yui.YAlignment_setMinWidth(self, width) - - def setMinHeight(self, height): - r"""setMinHeight(YAlignment self, int height)""" - return _yui.YAlignment_setMinHeight(self, height) - - def setBackgroundPixmap(self, pixmapFileName): - r"""setBackgroundPixmap(YAlignment self, std::string const & pixmapFileName)""" - return _yui.YAlignment_setBackgroundPixmap(self, pixmapFileName) - - def backgroundPixmap(self): - r"""backgroundPixmap(YAlignment self) -> std::string""" - return _yui.YAlignment_backgroundPixmap(self) - - def addChild(self, child): - r"""addChild(YAlignment self, YWidget child)""" - return _yui.YAlignment_addChild(self, child) - - def moveChild(self, child, newx, newy): - r"""moveChild(YAlignment self, YWidget child, int newx, int newy)""" - return _yui.YAlignment_moveChild(self, child, newx, newy) - - def stretchable(self, dim): - r"""stretchable(YAlignment self, YUIDimension dim) -> bool""" - return _yui.YAlignment_stretchable(self, dim) - - def preferredWidth(self): - r"""preferredWidth(YAlignment self) -> int""" - return _yui.YAlignment_preferredWidth(self) - - def preferredHeight(self): - r"""preferredHeight(YAlignment self) -> int""" - return _yui.YAlignment_preferredHeight(self) - - def setSize(self, newWidth, newHeight): - r"""setSize(YAlignment self, int newWidth, int newHeight)""" - return _yui.YAlignment_setSize(self, newWidth, newHeight) - -# Register YAlignment in _yui: -_yui.YAlignment_swigregister(YAlignment) - -class YApplication(object): - r"""Proxy of C++ YApplication class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - - def findWidget(self, id, doThrow=True): - r"""findWidget(YApplication self, YWidgetID id, bool doThrow=True) -> YWidget""" - return _yui.YApplication_findWidget(self, id, doThrow) - - def iconBasePath(self): - r"""iconBasePath(YApplication self) -> std::string""" - return _yui.YApplication_iconBasePath(self) - - def setIconBasePath(self, newIconBasePath): - r"""setIconBasePath(YApplication self, std::string const & newIconBasePath)""" - return _yui.YApplication_setIconBasePath(self, newIconBasePath) - - def iconLoader(self): - r"""iconLoader(YApplication self) -> YIconLoader *""" - return _yui.YApplication_iconLoader(self) - - def defaultFunctionKey(self, label): - r"""defaultFunctionKey(YApplication self, std::string const & label) -> int""" - return _yui.YApplication_defaultFunctionKey(self, label) - - def setDefaultFunctionKey(self, label, fkey): - r"""setDefaultFunctionKey(YApplication self, std::string const & label, int fkey)""" - return _yui.YApplication_setDefaultFunctionKey(self, label, fkey) - - def clearDefaultFunctionKeys(self): - r"""clearDefaultFunctionKeys(YApplication self)""" - return _yui.YApplication_clearDefaultFunctionKeys(self) - - def setLanguage(self, *args): - r"""setLanguage(YApplication self, std::string const & language, std::string const & encoding=std::string())""" - return _yui.YApplication_setLanguage(self, *args) - - def language(self, stripEncoding=False): - r"""language(YApplication self, bool stripEncoding=False) -> std::string""" - return _yui.YApplication_language(self, stripEncoding) - - def glyph(self, glyphSymbolName): - r"""glyph(YApplication self, std::string const & glyphSymbolName) -> std::string""" - return _yui.YApplication_glyph(self, glyphSymbolName) - - def askForExistingDirectory(self, startDir, headline): - r"""askForExistingDirectory(YApplication self, std::string const & startDir, std::string const & headline) -> std::string""" - return _yui.YApplication_askForExistingDirectory(self, startDir, headline) - - def askForExistingFile(self, startWith, filter, headline): - r"""askForExistingFile(YApplication self, std::string const & startWith, std::string const & filter, std::string const & headline) -> std::string""" - return _yui.YApplication_askForExistingFile(self, startWith, filter, headline) - - def askForSaveFileName(self, startWith, filter, headline): - r"""askForSaveFileName(YApplication self, std::string const & startWith, std::string const & filter, std::string const & headline) -> std::string""" - return _yui.YApplication_askForSaveFileName(self, startWith, filter, headline) - - def askForWidgetStyle(self): - r"""askForWidgetStyle(YApplication self)""" - return _yui.YApplication_askForWidgetStyle(self) - - def openContextMenu(self, itemCollection): - r"""openContextMenu(YApplication self, YItemCollection itemCollection) -> bool""" - return _yui.YApplication_openContextMenu(self, itemCollection) - - def setProductName(self, productName): - r"""setProductName(YApplication self, std::string const & productName)""" - return _yui.YApplication_setProductName(self, productName) - - def productName(self): - r"""productName(YApplication self) -> std::string""" - return _yui.YApplication_productName(self) - - def setReleaseNotes(self, relNotes): - r"""setReleaseNotes(YApplication self, std::map< std::string,std::string,std::less< std::string >,std::allocator< std::pair< std::string const,std::string > > > const & relNotes)""" - return _yui.YApplication_setReleaseNotes(self, relNotes) - - def releaseNotes(self): - r"""releaseNotes(YApplication self) -> std::map< std::string,std::string,std::less< std::string >,std::allocator< std::pair< std::string const,std::string > > >""" - return _yui.YApplication_releaseNotes(self) - - def setShowProductLogo(self, show): - r"""setShowProductLogo(YApplication self, bool show)""" - return _yui.YApplication_setShowProductLogo(self, show) - - def showProductLogo(self): - r"""showProductLogo(YApplication self) -> bool""" - return _yui.YApplication_showProductLogo(self) - - def deviceUnits(self, dim, layoutUnits): - r"""deviceUnits(YApplication self, YUIDimension dim, float layoutUnits) -> int""" - return _yui.YApplication_deviceUnits(self, dim, layoutUnits) - - def layoutUnits(self, dim, deviceUnits): - r"""layoutUnits(YApplication self, YUIDimension dim, int deviceUnits) -> float""" - return _yui.YApplication_layoutUnits(self, dim, deviceUnits) - - def setReverseLayout(self, reverse): - r"""setReverseLayout(YApplication self, bool reverse)""" - return _yui.YApplication_setReverseLayout(self, reverse) - - def reverseLayout(self): - r"""reverseLayout(YApplication self) -> bool""" - return _yui.YApplication_reverseLayout(self) - - def busyCursor(self): - r"""busyCursor(YApplication self)""" - return _yui.YApplication_busyCursor(self) - - def normalCursor(self): - r"""normalCursor(YApplication self)""" - return _yui.YApplication_normalCursor(self) - - def makeScreenShot(self, fileName): - r"""makeScreenShot(YApplication self, std::string const & fileName)""" - return _yui.YApplication_makeScreenShot(self, fileName) - - def beep(self): - r"""beep(YApplication self)""" - return _yui.YApplication_beep(self) - - def redrawScreen(self): - r"""redrawScreen(YApplication self)""" - return _yui.YApplication_redrawScreen(self) - - def initConsoleKeyboard(self): - r"""initConsoleKeyboard(YApplication self)""" - return _yui.YApplication_initConsoleKeyboard(self) - - def setConsoleFont(self, console_magic, font, screen_map, unicode_map, language): - r"""setConsoleFont(YApplication self, std::string const & console_magic, std::string const & font, std::string const & screen_map, std::string const & unicode_map, std::string const & language)""" - return _yui.YApplication_setConsoleFont(self, console_magic, font, screen_map, unicode_map, language) - - def runInTerminal(self, command): - r"""runInTerminal(YApplication self, std::string const & command) -> int""" - return _yui.YApplication_runInTerminal(self, command) - - def openUI(self): - r"""openUI(YApplication self)""" - return _yui.YApplication_openUI(self) - - def closeUI(self): - r"""closeUI(YApplication self)""" - return _yui.YApplication_closeUI(self) - - def displayWidth(self): - r"""displayWidth(YApplication self) -> int""" - return _yui.YApplication_displayWidth(self) - - def displayHeight(self): - r"""displayHeight(YApplication self) -> int""" - return _yui.YApplication_displayHeight(self) - - def displayDepth(self): - r"""displayDepth(YApplication self) -> int""" - return _yui.YApplication_displayDepth(self) - - def displayColors(self): - r"""displayColors(YApplication self) -> long""" - return _yui.YApplication_displayColors(self) - - def defaultWidth(self): - r"""defaultWidth(YApplication self) -> int""" - return _yui.YApplication_defaultWidth(self) - - def defaultHeight(self): - r"""defaultHeight(YApplication self) -> int""" - return _yui.YApplication_defaultHeight(self) - - def isTextMode(self): - r"""isTextMode(YApplication self) -> bool""" - return _yui.YApplication_isTextMode(self) - - def hasImageSupport(self): - r"""hasImageSupport(YApplication self) -> bool""" - return _yui.YApplication_hasImageSupport(self) - - def hasIconSupport(self): - r"""hasIconSupport(YApplication self) -> bool""" - return _yui.YApplication_hasIconSupport(self) - - def hasAnimationSupport(self): - r"""hasAnimationSupport(YApplication self) -> bool""" - return _yui.YApplication_hasAnimationSupport(self) - - def hasFullUtf8Support(self): - r"""hasFullUtf8Support(YApplication self) -> bool""" - return _yui.YApplication_hasFullUtf8Support(self) - - def richTextSupportsTable(self): - r"""richTextSupportsTable(YApplication self) -> bool""" - return _yui.YApplication_richTextSupportsTable(self) - - def leftHandedMouse(self): - r"""leftHandedMouse(YApplication self) -> bool""" - return _yui.YApplication_leftHandedMouse(self) - - def hasWidgetStyleSupport(self): - r"""hasWidgetStyleSupport(YApplication self) -> bool""" - return _yui.YApplication_hasWidgetStyleSupport(self) - - def hasWizardDialogSupport(self): - r"""hasWizardDialogSupport(YApplication self) -> bool""" - return _yui.YApplication_hasWizardDialogSupport(self) - - def setApplicationTitle(self, title): - r"""setApplicationTitle(YApplication self, std::string const & title)""" - return _yui.YApplication_setApplicationTitle(self, title) - - def applicationTitle(self): - r"""applicationTitle(YApplication self) -> std::string const &""" - return _yui.YApplication_applicationTitle(self) - - def setApplicationIcon(self, icon): - r"""setApplicationIcon(YApplication self, std::string const & icon)""" - return _yui.YApplication_setApplicationIcon(self, icon) - - def applicationIcon(self): - r"""applicationIcon(YApplication self) -> std::string const &""" - return _yui.YApplication_applicationIcon(self) - -# Register YApplication in _yui: -_yui.YApplication_swigregister(YApplication) - -class YBarGraph(YWidget): - r"""Proxy of C++ YBarGraph class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YBarGraph - - def widgetClass(self): - r"""widgetClass(YBarGraph self) -> char const *""" - return _yui.YBarGraph_widgetClass(self) - - def addSegment(self, segment): - r"""addSegment(YBarGraph self, YBarGraphSegment segment)""" - return _yui.YBarGraph_addSegment(self, segment) - - def deleteAllSegments(self): - r"""deleteAllSegments(YBarGraph self)""" - return _yui.YBarGraph_deleteAllSegments(self) - - def segments(self): - r"""segments(YBarGraph self) -> int""" - return _yui.YBarGraph_segments(self) - - def segment(self, segmentIndex): - r"""segment(YBarGraph self, int segmentIndex) -> YBarGraphSegment""" - return _yui.YBarGraph_segment(self, segmentIndex) - - def setValue(self, segmentIndex, newValue): - r"""setValue(YBarGraph self, int segmentIndex, int newValue)""" - return _yui.YBarGraph_setValue(self, segmentIndex, newValue) - - def setLabel(self, segmentIndex, newLabel): - r"""setLabel(YBarGraph self, int segmentIndex, std::string const & newLabel)""" - return _yui.YBarGraph_setLabel(self, segmentIndex, newLabel) - - def setSegmentColor(self, segmentIndex, color): - r"""setSegmentColor(YBarGraph self, int segmentIndex, YColor color)""" - return _yui.YBarGraph_setSegmentColor(self, segmentIndex, color) - - def setTextColor(self, segmentIndex, color): - r"""setTextColor(YBarGraph self, int segmentIndex, YColor color)""" - return _yui.YBarGraph_setTextColor(self, segmentIndex, color) - - def setProperty(self, propertyName, val): - r"""setProperty(YBarGraph self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YBarGraph_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YBarGraph self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YBarGraph_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YBarGraph self) -> YPropertySet""" - return _yui.YBarGraph_propertySet(self) - -# Register YBarGraph in _yui: -_yui.YBarGraph_swigregister(YBarGraph) - -class YBarGraphSegment(object): - r"""Proxy of C++ YBarGraphSegment class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r"""__init__(YBarGraphSegment self, int value=0, std::string const & label=std::string(), YColor segmentColor=YColor(), YColor textColor=YColor()) -> YBarGraphSegment""" - _yui.YBarGraphSegment_swiginit(self, _yui.new_YBarGraphSegment(*args)) - - def value(self): - r"""value(YBarGraphSegment self) -> int""" - return _yui.YBarGraphSegment_value(self) - - def setValue(self, newValue): - r"""setValue(YBarGraphSegment self, int newValue)""" - return _yui.YBarGraphSegment_setValue(self, newValue) - - def label(self): - r"""label(YBarGraphSegment self) -> std::string""" - return _yui.YBarGraphSegment_label(self) - - def setLabel(self, newLabel): - r"""setLabel(YBarGraphSegment self, std::string const & newLabel)""" - return _yui.YBarGraphSegment_setLabel(self, newLabel) - - def segmentColor(self): - r"""segmentColor(YBarGraphSegment self) -> YColor""" - return _yui.YBarGraphSegment_segmentColor(self) - - def hasSegmentColor(self): - r"""hasSegmentColor(YBarGraphSegment self) -> bool""" - return _yui.YBarGraphSegment_hasSegmentColor(self) - - def setSegmentColor(self, color): - r"""setSegmentColor(YBarGraphSegment self, YColor color)""" - return _yui.YBarGraphSegment_setSegmentColor(self, color) - - def textColor(self): - r"""textColor(YBarGraphSegment self) -> YColor""" - return _yui.YBarGraphSegment_textColor(self) - - def hasTextColor(self): - r"""hasTextColor(YBarGraphSegment self) -> bool""" - return _yui.YBarGraphSegment_hasTextColor(self) - - def setTextColor(self, color): - r"""setTextColor(YBarGraphSegment self, YColor color)""" - return _yui.YBarGraphSegment_setTextColor(self, color) - __swig_destroy__ = _yui.delete_YBarGraphSegment - -# Register YBarGraphSegment in _yui: -_yui.YBarGraphSegment_swigregister(YBarGraphSegment) - -class YBarGraphMultiUpdate(object): - r"""Proxy of C++ YBarGraphMultiUpdate class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, barGraph): - r"""__init__(YBarGraphMultiUpdate self, YBarGraph barGraph) -> YBarGraphMultiUpdate""" - _yui.YBarGraphMultiUpdate_swiginit(self, _yui.new_YBarGraphMultiUpdate(barGraph)) - __swig_destroy__ = _yui.delete_YBarGraphMultiUpdate - -# Register YBarGraphMultiUpdate in _yui: -_yui.YBarGraphMultiUpdate_swigregister(YBarGraphMultiUpdate) - -class YBuiltinCaller(object): - r"""Proxy of C++ YBuiltinCaller class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YBuiltinCaller - - def call(self): - r"""call(YBuiltinCaller self)""" - return _yui.YBuiltinCaller_call(self) - -# Register YBuiltinCaller in _yui: -_yui.YBuiltinCaller_swigregister(YBuiltinCaller) - -class YBusyIndicator(YWidget): - r"""Proxy of C++ YBusyIndicator class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YBusyIndicator - - def widgetClass(self): - r"""widgetClass(YBusyIndicator self) -> char const *""" - return _yui.YBusyIndicator_widgetClass(self) - - def label(self): - r"""label(YBusyIndicator self) -> std::string""" - return _yui.YBusyIndicator_label(self) - - def setLabel(self, label): - r"""setLabel(YBusyIndicator self, std::string const & label)""" - return _yui.YBusyIndicator_setLabel(self, label) - - def timeout(self): - r"""timeout(YBusyIndicator self) -> int""" - return _yui.YBusyIndicator_timeout(self) - - def setTimeout(self, newTimeout): - r"""setTimeout(YBusyIndicator self, int newTimeout)""" - return _yui.YBusyIndicator_setTimeout(self, newTimeout) - - def alive(self): - r"""alive(YBusyIndicator self) -> bool""" - return _yui.YBusyIndicator_alive(self) - - def setAlive(self, newAlive): - r"""setAlive(YBusyIndicator self, bool newAlive)""" - return _yui.YBusyIndicator_setAlive(self, newAlive) - - def setProperty(self, propertyName, val): - r"""setProperty(YBusyIndicator self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YBusyIndicator_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YBusyIndicator self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YBusyIndicator_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YBusyIndicator self) -> YPropertySet""" - return _yui.YBusyIndicator_propertySet(self) - -# Register YBusyIndicator in _yui: -_yui.YBusyIndicator_swigregister(YBusyIndicator) - -class YCheckBoxFrame(YSingleChildContainerWidget): - r"""Proxy of C++ YCheckBoxFrame class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YCheckBoxFrame - - def widgetClass(self): - r"""widgetClass(YCheckBoxFrame self) -> char const *""" - return _yui.YCheckBoxFrame_widgetClass(self) - - def label(self): - r"""label(YCheckBoxFrame self) -> std::string""" - return _yui.YCheckBoxFrame_label(self) - - def setLabel(self, label): - r"""setLabel(YCheckBoxFrame self, std::string const & label)""" - return _yui.YCheckBoxFrame_setLabel(self, label) - - def setValue(self, isChecked): - r"""setValue(YCheckBoxFrame self, bool isChecked)""" - return _yui.YCheckBoxFrame_setValue(self, isChecked) - - def value(self): - r"""value(YCheckBoxFrame self) -> bool""" - return _yui.YCheckBoxFrame_value(self) - - def autoEnable(self): - r"""autoEnable(YCheckBoxFrame self) -> bool""" - return _yui.YCheckBoxFrame_autoEnable(self) - - def setAutoEnable(self, autoEnable): - r"""setAutoEnable(YCheckBoxFrame self, bool autoEnable)""" - return _yui.YCheckBoxFrame_setAutoEnable(self, autoEnable) - - def invertAutoEnable(self): - r"""invertAutoEnable(YCheckBoxFrame self) -> bool""" - return _yui.YCheckBoxFrame_invertAutoEnable(self) - - def setInvertAutoEnable(self, invertAutoEnable): - r"""setInvertAutoEnable(YCheckBoxFrame self, bool invertAutoEnable)""" - return _yui.YCheckBoxFrame_setInvertAutoEnable(self, invertAutoEnable) - - def handleChildrenEnablement(self, isChecked): - r"""handleChildrenEnablement(YCheckBoxFrame self, bool isChecked)""" - return _yui.YCheckBoxFrame_handleChildrenEnablement(self, isChecked) - - def shortcutString(self): - r"""shortcutString(YCheckBoxFrame self) -> std::string""" - return _yui.YCheckBoxFrame_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YCheckBoxFrame self, std::string const & str)""" - return _yui.YCheckBoxFrame_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YCheckBoxFrame self) -> char const *""" - return _yui.YCheckBoxFrame_userInputProperty(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YCheckBoxFrame self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YCheckBoxFrame_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YCheckBoxFrame self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YCheckBoxFrame_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YCheckBoxFrame self) -> YPropertySet""" - return _yui.YCheckBoxFrame_propertySet(self) - -# Register YCheckBoxFrame in _yui: -_yui.YCheckBoxFrame_swigregister(YCheckBoxFrame) - -YCheckBox_dont_care = _yui.YCheckBox_dont_care - -YCheckBox_off = _yui.YCheckBox_off - -YCheckBox_on = _yui.YCheckBox_on - -class YCheckBox(YWidget): - r"""Proxy of C++ YCheckBox class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YCheckBox - - def widgetClass(self): - r"""widgetClass(YCheckBox self) -> char const *""" - return _yui.YCheckBox_widgetClass(self) - - def value(self): - r"""value(YCheckBox self) -> YCheckBoxState""" - return _yui.YCheckBox_value(self) - - def setValue(self, state): - r"""setValue(YCheckBox self, YCheckBoxState state)""" - return _yui.YCheckBox_setValue(self, state) - - def isChecked(self): - r"""isChecked(YCheckBox self) -> bool""" - return _yui.YCheckBox_isChecked(self) - - def setChecked(self, checked=True): - r"""setChecked(YCheckBox self, bool checked=True)""" - return _yui.YCheckBox_setChecked(self, checked) - - def dontCare(self): - r"""dontCare(YCheckBox self) -> bool""" - return _yui.YCheckBox_dontCare(self) - - def setDontCare(self): - r"""setDontCare(YCheckBox self)""" - return _yui.YCheckBox_setDontCare(self) - - def label(self): - r"""label(YCheckBox self) -> std::string""" - return _yui.YCheckBox_label(self) - - def setLabel(self, label): - r"""setLabel(YCheckBox self, std::string const & label)""" - return _yui.YCheckBox_setLabel(self, label) - - def useBoldFont(self): - r"""useBoldFont(YCheckBox self) -> bool""" - return _yui.YCheckBox_useBoldFont(self) - - def setUseBoldFont(self, bold=True): - r"""setUseBoldFont(YCheckBox self, bool bold=True)""" - return _yui.YCheckBox_setUseBoldFont(self, bold) - - def setProperty(self, propertyName, val): - r"""setProperty(YCheckBox self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YCheckBox_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YCheckBox self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YCheckBox_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YCheckBox self) -> YPropertySet""" - return _yui.YCheckBox_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YCheckBox self) -> std::string""" - return _yui.YCheckBox_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YCheckBox self, std::string const & str)""" - return _yui.YCheckBox_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YCheckBox self) -> char const *""" - return _yui.YCheckBox_userInputProperty(self) - -# Register YCheckBox in _yui: -_yui.YCheckBox_swigregister(YCheckBox) - -class YColor(object): - r"""Proxy of C++ YColor class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YColor self, uchar red, uchar green, uchar blue) -> YColor - __init__(YColor self) -> YColor - """ - _yui.YColor_swiginit(self, _yui.new_YColor(*args)) - - def red(self): - r"""red(YColor self) -> uchar""" - return _yui.YColor_red(self) - - def green(self): - r"""green(YColor self) -> uchar""" - return _yui.YColor_green(self) - - def blue(self): - r"""blue(YColor self) -> uchar""" - return _yui.YColor_blue(self) - - def isUndefined(self): - r"""isUndefined(YColor self) -> bool""" - return _yui.YColor_isUndefined(self) - - def isDefined(self): - r"""isDefined(YColor self) -> bool""" - return _yui.YColor_isDefined(self) - __swig_destroy__ = _yui.delete_YColor - -# Register YColor in _yui: -_yui.YColor_swigregister(YColor) - -class YComboBox(YSelectionWidget): - r"""Proxy of C++ YComboBox class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YComboBox - - def widgetClass(self): - r"""widgetClass(YComboBox self) -> char const *""" - return _yui.YComboBox_widgetClass(self) - - def editable(self): - r"""editable(YComboBox self) -> bool""" - return _yui.YComboBox_editable(self) - - def value(self): - r"""value(YComboBox self) -> std::string""" - return _yui.YComboBox_value(self) - - def setValue(self, newText): - r"""setValue(YComboBox self, std::string const & newText)""" - return _yui.YComboBox_setValue(self, newText) - - def selectedItem(self): - r"""selectedItem(YComboBox self) -> YItem""" - return _yui.YComboBox_selectedItem(self) - - def selectedItems(self): - r"""selectedItems(YComboBox self) -> YItemCollection""" - return _yui.YComboBox_selectedItems(self) - - def selectItem(self, item, selected=True): - r"""selectItem(YComboBox self, YItem item, bool selected=True)""" - return _yui.YComboBox_selectItem(self, item, selected) - - def validChars(self): - r"""validChars(YComboBox self) -> std::string""" - return _yui.YComboBox_validChars(self) - - def setValidChars(self, validChars): - r"""setValidChars(YComboBox self, std::string const & validChars)""" - return _yui.YComboBox_setValidChars(self, validChars) - - def inputMaxLength(self): - r"""inputMaxLength(YComboBox self) -> int""" - return _yui.YComboBox_inputMaxLength(self) - - def setInputMaxLength(self, numberOfChars): - r"""setInputMaxLength(YComboBox self, int numberOfChars)""" - return _yui.YComboBox_setInputMaxLength(self, numberOfChars) - - def setProperty(self, propertyName, val): - r"""setProperty(YComboBox self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YComboBox_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YComboBox self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YComboBox_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YComboBox self) -> YPropertySet""" - return _yui.YComboBox_propertySet(self) - - def userInputProperty(self): - r"""userInputProperty(YComboBox self) -> char const *""" - return _yui.YComboBox_userInputProperty(self) - -# Register YComboBox in _yui: -_yui.YComboBox_swigregister(YComboBox) - -class YCommandLine(object): - r"""Proxy of C++ YCommandLine class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YCommandLine self) -> YCommandLine""" - _yui.YCommandLine_swiginit(self, _yui.new_YCommandLine()) - __swig_destroy__ = _yui.delete_YCommandLine - - def argc(self): - r"""argc(YCommandLine self) -> int""" - return _yui.YCommandLine_argc(self) - - def argv(self): - r"""argv(YCommandLine self) -> char **""" - return _yui.YCommandLine_argv(self) - - def size(self): - r"""size(YCommandLine self) -> int""" - return _yui.YCommandLine_size(self) - - def arg(self, index): - r"""arg(YCommandLine self, int index) -> std::string""" - return _yui.YCommandLine_arg(self, index) - - def add(self, arg): - r"""add(YCommandLine self, std::string const & arg)""" - return _yui.YCommandLine_add(self, arg) - - def remove(self, index): - r"""remove(YCommandLine self, int index)""" - return _yui.YCommandLine_remove(self, index) - - def replace(self, index, arg): - r"""replace(YCommandLine self, int index, std::string const & arg)""" - return _yui.YCommandLine_replace(self, index, arg) - - def find(self, argName): - r"""find(YCommandLine self, std::string const & argName) -> int""" - return _yui.YCommandLine_find(self, argName) - -# Register YCommandLine in _yui: -_yui.YCommandLine_swigregister(YCommandLine) - -class YDateField(YSimpleInputField): - r"""Proxy of C++ YDateField class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YDateField - - def widgetClass(self): - r"""widgetClass(YDateField self) -> char const *""" - return _yui.YDateField_widgetClass(self) - -# Register YDateField in _yui: -_yui.YDateField_swigregister(YDateField) - -class YDownloadProgress(YWidget): - r"""Proxy of C++ YDownloadProgress class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YDownloadProgress - - def widgetClass(self): - r"""widgetClass(YDownloadProgress self) -> char const *""" - return _yui.YDownloadProgress_widgetClass(self) - - def label(self): - r"""label(YDownloadProgress self) -> std::string""" - return _yui.YDownloadProgress_label(self) - - def setLabel(self, label): - r"""setLabel(YDownloadProgress self, std::string const & label)""" - return _yui.YDownloadProgress_setLabel(self, label) - - def filename(self): - r"""filename(YDownloadProgress self) -> std::string""" - return _yui.YDownloadProgress_filename(self) - - def setFilename(self, filename): - r"""setFilename(YDownloadProgress self, std::string const & filename)""" - return _yui.YDownloadProgress_setFilename(self, filename) - - def expectedSize(self): - r"""expectedSize(YDownloadProgress self) -> YFileSize_t""" - return _yui.YDownloadProgress_expectedSize(self) - - def setExpectedSize(self, newSize): - r"""setExpectedSize(YDownloadProgress self, YFileSize_t newSize)""" - return _yui.YDownloadProgress_setExpectedSize(self, newSize) - - def currentFileSize(self): - r"""currentFileSize(YDownloadProgress self) -> YFileSize_t""" - return _yui.YDownloadProgress_currentFileSize(self) - - def currentPercent(self): - r"""currentPercent(YDownloadProgress self) -> int""" - return _yui.YDownloadProgress_currentPercent(self) - - def value(self): - r"""value(YDownloadProgress self) -> int""" - return _yui.YDownloadProgress_value(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YDownloadProgress self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YDownloadProgress_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YDownloadProgress self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YDownloadProgress_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YDownloadProgress self) -> YPropertySet""" - return _yui.YDownloadProgress_propertySet(self) - -# Register YDownloadProgress in _yui: -_yui.YDownloadProgress_swigregister(YDownloadProgress) - -class YDumbTab(YSelectionWidget): - r"""Proxy of C++ YDumbTab class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YDumbTab - - def widgetClass(self): - r"""widgetClass(YDumbTab self) -> char const *""" - return _yui.YDumbTab_widgetClass(self) - - def addItem(self, item): - r"""addItem(YDumbTab self, YItem item)""" - return _yui.YDumbTab_addItem(self, item) - - def setProperty(self, propertyName, val): - r"""setProperty(YDumbTab self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YDumbTab_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YDumbTab self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YDumbTab_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YDumbTab self) -> YPropertySet""" - return _yui.YDumbTab_propertySet(self) - - def stretchable(self, dim): - r"""stretchable(YDumbTab self, YUIDimension dim) -> bool""" - return _yui.YDumbTab_stretchable(self, dim) - - def debugLabel(self): - r"""debugLabel(YDumbTab self) -> std::string""" - return _yui.YDumbTab_debugLabel(self) - - def activate(self): - r"""activate(YDumbTab self)""" - return _yui.YDumbTab_activate(self) - -# Register YDumbTab in _yui: -_yui.YDumbTab_swigregister(YDumbTab) - -class YEmpty(YWidget): - r"""Proxy of C++ YEmpty class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YEmpty - - def widgetClass(self): - r"""widgetClass(YEmpty self) -> char const *""" - return _yui.YEmpty_widgetClass(self) - - def preferredWidth(self): - r"""preferredWidth(YEmpty self) -> int""" - return _yui.YEmpty_preferredWidth(self) - - def preferredHeight(self): - r"""preferredHeight(YEmpty self) -> int""" - return _yui.YEmpty_preferredHeight(self) - -# Register YEmpty in _yui: -_yui.YEmpty_swigregister(YEmpty) - -class YEvent(object): - r"""Proxy of C++ YEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - NoEvent = _yui.YEvent_NoEvent - - UnknownEvent = _yui.YEvent_UnknownEvent - - WidgetEvent = _yui.YEvent_WidgetEvent - - MenuEvent = _yui.YEvent_MenuEvent - - KeyEvent = _yui.YEvent_KeyEvent - - CancelEvent = _yui.YEvent_CancelEvent - - TimeoutEvent = _yui.YEvent_TimeoutEvent - - DebugEvent = _yui.YEvent_DebugEvent - - SpecialKeyEvent = _yui.YEvent_SpecialKeyEvent - - InvalidEvent = _yui.YEvent_InvalidEvent - - UnknownReason = _yui.YEvent_UnknownReason - - Activated = _yui.YEvent_Activated - - SelectionChanged = _yui.YEvent_SelectionChanged - - ValueChanged = _yui.YEvent_ValueChanged - - ContextMenuActivated = _yui.YEvent_ContextMenuActivated - - - def __init__(self, *args): - r"""__init__(YEvent self, YEvent::EventType eventType=UnknownEvent) -> YEvent""" - _yui.YEvent_swiginit(self, _yui.new_YEvent(*args)) - - def eventType(self): - r"""eventType(YEvent self) -> YEvent::EventType""" - return _yui.YEvent_eventType(self) - - def serial(self): - r"""serial(YEvent self) -> unsigned long""" - return _yui.YEvent_serial(self) - - def widget(self): - r"""widget(YEvent self) -> YWidget""" - return _yui.YEvent_widget(self) - - def item(self): - r"""item(YEvent self) -> YItem""" - return _yui.YEvent_item(self) - - def dialog(self): - r"""dialog(YEvent self) -> YDialog""" - return _yui.YEvent_dialog(self) - - def isValid(self): - r"""isValid(YEvent self) -> bool""" - return _yui.YEvent_isValid(self) - - @staticmethod - def toString(*args): - r""" - toString(YEvent::EventType eventType) -> char const - toString(YEvent::EventReason reason) -> char const * - """ - return _yui.YEvent_toString(*args) - -# Register YEvent in _yui: -_yui.YEvent_swigregister(YEvent) - -def YEvent_toString(*args): - r""" - YEvent_toString(YEvent::EventType eventType) -> char const - YEvent_toString(YEvent::EventReason reason) -> char const * - """ - return _yui.YEvent_toString(*args) - -class YWidgetEvent(YEvent): - r"""Proxy of C++ YWidgetEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r"""__init__(YWidgetEvent self, YWidget widget=None, YEvent::EventReason reason=Activated, YEvent::EventType eventType=WidgetEvent) -> YWidgetEvent""" - _yui.YWidgetEvent_swiginit(self, _yui.new_YWidgetEvent(*args)) - - def widget(self): - r"""widget(YWidgetEvent self) -> YWidget""" - return _yui.YWidgetEvent_widget(self) - - def reason(self): - r"""reason(YWidgetEvent self) -> YEvent::EventReason""" - return _yui.YWidgetEvent_reason(self) - -# Register YWidgetEvent in _yui: -_yui.YWidgetEvent_swigregister(YWidgetEvent) - -class YKeyEvent(YEvent): - r"""Proxy of C++ YKeyEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, keySymbol, focusWidget=None): - r"""__init__(YKeyEvent self, std::string const & keySymbol, YWidget focusWidget=None) -> YKeyEvent""" - _yui.YKeyEvent_swiginit(self, _yui.new_YKeyEvent(keySymbol, focusWidget)) - - def keySymbol(self): - r"""keySymbol(YKeyEvent self) -> std::string""" - return _yui.YKeyEvent_keySymbol(self) - - def focusWidget(self): - r"""focusWidget(YKeyEvent self) -> YWidget""" - return _yui.YKeyEvent_focusWidget(self) - -# Register YKeyEvent in _yui: -_yui.YKeyEvent_swigregister(YKeyEvent) - -class YMenuEvent(YEvent): - r"""Proxy of C++ YMenuEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YMenuEvent self, YItem item) -> YMenuEvent - __init__(YMenuEvent self, char const * id) -> YMenuEvent - __init__(YMenuEvent self, std::string const & id) -> YMenuEvent - """ - _yui.YMenuEvent_swiginit(self, _yui.new_YMenuEvent(*args)) - - def item(self): - r"""item(YMenuEvent self) -> YItem""" - return _yui.YMenuEvent_item(self) - - def id(self): - r"""id(YMenuEvent self) -> std::string""" - return _yui.YMenuEvent_id(self) - -# Register YMenuEvent in _yui: -_yui.YMenuEvent_swigregister(YMenuEvent) - -class YCancelEvent(YEvent): - r"""Proxy of C++ YCancelEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YCancelEvent self) -> YCancelEvent""" - _yui.YCancelEvent_swiginit(self, _yui.new_YCancelEvent()) - -# Register YCancelEvent in _yui: -_yui.YCancelEvent_swigregister(YCancelEvent) - -class YDebugEvent(YEvent): - r"""Proxy of C++ YDebugEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YDebugEvent self) -> YDebugEvent""" - _yui.YDebugEvent_swiginit(self, _yui.new_YDebugEvent()) - -# Register YDebugEvent in _yui: -_yui.YDebugEvent_swigregister(YDebugEvent) - -class YSpecialKeyEvent(YEvent): - r"""Proxy of C++ YSpecialKeyEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YSpecialKeyEvent self, char const * id) -> YSpecialKeyEvent - __init__(YSpecialKeyEvent self, std::string const & id) -> YSpecialKeyEvent - """ - _yui.YSpecialKeyEvent_swiginit(self, _yui.new_YSpecialKeyEvent(*args)) - - def id(self): - r"""id(YSpecialKeyEvent self) -> std::string""" - return _yui.YSpecialKeyEvent_id(self) - -# Register YSpecialKeyEvent in _yui: -_yui.YSpecialKeyEvent_swigregister(YSpecialKeyEvent) - -class YTimeoutEvent(YEvent): - r"""Proxy of C++ YTimeoutEvent class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YTimeoutEvent self) -> YTimeoutEvent""" - _yui.YTimeoutEvent_swiginit(self, _yui.new_YTimeoutEvent()) - -# Register YTimeoutEvent in _yui: -_yui.YTimeoutEvent_swigregister(YTimeoutEvent) - -class YFrame(YSingleChildContainerWidget): - r"""Proxy of C++ YFrame class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YFrame - - def widgetClass(self): - r"""widgetClass(YFrame self) -> char const *""" - return _yui.YFrame_widgetClass(self) - - def setLabel(self, newLabel): - r"""setLabel(YFrame self, std::string const & newLabel)""" - return _yui.YFrame_setLabel(self, newLabel) - - def label(self): - r"""label(YFrame self) -> std::string""" - return _yui.YFrame_label(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YFrame self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YFrame_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YFrame self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YFrame_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YFrame self) -> YPropertySet""" - return _yui.YFrame_propertySet(self) - -# Register YFrame in _yui: -_yui.YFrame_swigregister(YFrame) - -class YImage(YWidget): - r"""Proxy of C++ YImage class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YImage - - def widgetClass(self): - r"""widgetClass(YImage self) -> char const *""" - return _yui.YImage_widgetClass(self) - - def imageFileName(self): - r"""imageFileName(YImage self) -> std::string""" - return _yui.YImage_imageFileName(self) - - def animated(self): - r"""animated(YImage self) -> bool""" - return _yui.YImage_animated(self) - - def setImage(self, imageFileName, animated=False): - r"""setImage(YImage self, std::string const & imageFileName, bool animated=False)""" - return _yui.YImage_setImage(self, imageFileName, animated) - - def setMovie(self, movieFileName): - r"""setMovie(YImage self, std::string const & movieFileName)""" - return _yui.YImage_setMovie(self, movieFileName) - - def hasZeroSize(self, dim): - r"""hasZeroSize(YImage self, YUIDimension dim) -> bool""" - return _yui.YImage_hasZeroSize(self, dim) - - def setZeroSize(self, dim, zeroSize=True): - r"""setZeroSize(YImage self, YUIDimension dim, bool zeroSize=True)""" - return _yui.YImage_setZeroSize(self, dim, zeroSize) - - def autoScale(self): - r"""autoScale(YImage self) -> bool""" - return _yui.YImage_autoScale(self) - - def setAutoScale(self, autoScale=True): - r"""setAutoScale(YImage self, bool autoScale=True)""" - return _yui.YImage_setAutoScale(self, autoScale) - -# Register YImage in _yui: -_yui.YImage_swigregister(YImage) - -class YInputField(YWidget): - r"""Proxy of C++ YInputField class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YInputField - - def widgetClass(self): - r"""widgetClass(YInputField self) -> char const *""" - return _yui.YInputField_widgetClass(self) - - def value(self): - r"""value(YInputField self) -> std::string""" - return _yui.YInputField_value(self) - - def setValue(self, text): - r"""setValue(YInputField self, std::string const & text)""" - return _yui.YInputField_setValue(self, text) - - def label(self): - r"""label(YInputField self) -> std::string""" - return _yui.YInputField_label(self) - - def setLabel(self, label): - r"""setLabel(YInputField self, std::string const & label)""" - return _yui.YInputField_setLabel(self, label) - - def passwordMode(self): - r"""passwordMode(YInputField self) -> bool""" - return _yui.YInputField_passwordMode(self) - - def validChars(self): - r"""validChars(YInputField self) -> std::string""" - return _yui.YInputField_validChars(self) - - def setValidChars(self, validChars): - r"""setValidChars(YInputField self, std::string const & validChars)""" - return _yui.YInputField_setValidChars(self, validChars) - - def inputMaxLength(self): - r"""inputMaxLength(YInputField self) -> int""" - return _yui.YInputField_inputMaxLength(self) - - def setInputMaxLength(self, numberOfChars): - r"""setInputMaxLength(YInputField self, int numberOfChars)""" - return _yui.YInputField_setInputMaxLength(self, numberOfChars) - - def shrinkable(self): - r"""shrinkable(YInputField self) -> bool""" - return _yui.YInputField_shrinkable(self) - - def setShrinkable(self, shrinkable=True): - r"""setShrinkable(YInputField self, bool shrinkable=True)""" - return _yui.YInputField_setShrinkable(self, shrinkable) - - def setProperty(self, propertyName, val): - r"""setProperty(YInputField self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YInputField_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YInputField self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YInputField_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YInputField self) -> YPropertySet""" - return _yui.YInputField_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YInputField self) -> std::string""" - return _yui.YInputField_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YInputField self, std::string const & str)""" - return _yui.YInputField_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YInputField self) -> char const *""" - return _yui.YInputField_userInputProperty(self) - - def saveUserInput(self, macroRecorder): - r"""saveUserInput(YInputField self, YMacroRecorder macroRecorder)""" - return _yui.YInputField_saveUserInput(self, macroRecorder) - -# Register YInputField in _yui: -_yui.YInputField_swigregister(YInputField) - -class YIntField(YWidget): - r"""Proxy of C++ YIntField class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YIntField - - def widgetClass(self): - r"""widgetClass(YIntField self) -> char const *""" - return _yui.YIntField_widgetClass(self) - - def value(self): - r"""value(YIntField self) -> int""" - return _yui.YIntField_value(self) - - def setValue(self, val): - r"""setValue(YIntField self, int val)""" - return _yui.YIntField_setValue(self, val) - - def minValue(self): - r"""minValue(YIntField self) -> int""" - return _yui.YIntField_minValue(self) - - def setMinValue(self, val): - r"""setMinValue(YIntField self, int val)""" - return _yui.YIntField_setMinValue(self, val) - - def maxValue(self): - r"""maxValue(YIntField self) -> int""" - return _yui.YIntField_maxValue(self) - - def setMaxValue(self, val): - r"""setMaxValue(YIntField self, int val)""" - return _yui.YIntField_setMaxValue(self, val) - - def label(self): - r"""label(YIntField self) -> std::string""" - return _yui.YIntField_label(self) - - def setLabel(self, label): - r"""setLabel(YIntField self, std::string const & label)""" - return _yui.YIntField_setLabel(self, label) - - def setProperty(self, propertyName, val): - r"""setProperty(YIntField self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YIntField_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YIntField self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YIntField_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YIntField self) -> YPropertySet""" - return _yui.YIntField_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YIntField self) -> std::string""" - return _yui.YIntField_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YIntField self, std::string const & str)""" - return _yui.YIntField_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YIntField self) -> char const *""" - return _yui.YIntField_userInputProperty(self) - -# Register YIntField in _yui: -_yui.YIntField_swigregister(YIntField) - -class YItemSelector(YSelectionWidget): - r"""Proxy of C++ YItemSelector class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YItemSelector - - def widgetClass(self): - r"""widgetClass(YItemSelector self) -> char const *""" - return _yui.YItemSelector_widgetClass(self) - - def visibleItems(self): - r"""visibleItems(YItemSelector self) -> int""" - return _yui.YItemSelector_visibleItems(self) - - def setVisibleItems(self, newVal): - r"""setVisibleItems(YItemSelector self, int newVal)""" - return _yui.YItemSelector_setVisibleItems(self, newVal) - - def setItemStatus(self, item, status): - r"""setItemStatus(YItemSelector self, YItem item, int status)""" - return _yui.YItemSelector_setItemStatus(self, item, status) - - def usingCustomStatus(self): - r"""usingCustomStatus(YItemSelector self) -> bool""" - return _yui.YItemSelector_usingCustomStatus(self) - - def customStatusCount(self): - r"""customStatusCount(YItemSelector self) -> int""" - return _yui.YItemSelector_customStatusCount(self) - - def customStatus(self, index): - r"""customStatus(YItemSelector self, int index) -> YItemCustomStatus const &""" - return _yui.YItemSelector_customStatus(self, index) - - def validCustomStatusIndex(self, index): - r"""validCustomStatusIndex(YItemSelector self, int index) -> bool""" - return _yui.YItemSelector_validCustomStatusIndex(self, index) - - def cycleCustomStatus(self, oldStatus): - r"""cycleCustomStatus(YItemSelector self, int oldStatus) -> int""" - return _yui.YItemSelector_cycleCustomStatus(self, oldStatus) - - def setProperty(self, propertyName, val): - r"""setProperty(YItemSelector self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YItemSelector_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YItemSelector self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YItemSelector_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YItemSelector self) -> YPropertySet""" - return _yui.YItemSelector_propertySet(self) - - def userInputProperty(self): - r"""userInputProperty(YItemSelector self) -> char const *""" - return _yui.YItemSelector_userInputProperty(self) - - def activateItem(self, item): - r"""activateItem(YItemSelector self, YItem item)""" - return _yui.YItemSelector_activateItem(self, item) - -# Register YItemSelector in _yui: -_yui.YItemSelector_swigregister(YItemSelector) - -class YLabel(YWidget): - r"""Proxy of C++ YLabel class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YLabel - - def widgetClass(self): - r"""widgetClass(YLabel self) -> char const *""" - return _yui.YLabel_widgetClass(self) - - def text(self): - r"""text(YLabel self) -> std::string""" - return _yui.YLabel_text(self) - - def value(self): - r"""value(YLabel self) -> std::string""" - return _yui.YLabel_value(self) - - def label(self): - r"""label(YLabel self) -> std::string""" - return _yui.YLabel_label(self) - - def setText(self, newText): - r"""setText(YLabel self, std::string const & newText)""" - return _yui.YLabel_setText(self, newText) - - def setValue(self, newValue): - r"""setValue(YLabel self, std::string const & newValue)""" - return _yui.YLabel_setValue(self, newValue) - - def setLabel(self, newLabel): - r"""setLabel(YLabel self, std::string const & newLabel)""" - return _yui.YLabel_setLabel(self, newLabel) - - def isHeading(self): - r"""isHeading(YLabel self) -> bool""" - return _yui.YLabel_isHeading(self) - - def isOutputField(self): - r"""isOutputField(YLabel self) -> bool""" - return _yui.YLabel_isOutputField(self) - - def useBoldFont(self): - r"""useBoldFont(YLabel self) -> bool""" - return _yui.YLabel_useBoldFont(self) - - def setUseBoldFont(self, bold=True): - r"""setUseBoldFont(YLabel self, bool bold=True)""" - return _yui.YLabel_setUseBoldFont(self, bold) - - def autoWrap(self): - r"""autoWrap(YLabel self) -> bool""" - return _yui.YLabel_autoWrap(self) - - def setAutoWrap(self, autoWrap=True): - r"""setAutoWrap(YLabel self, bool autoWrap=True)""" - return _yui.YLabel_setAutoWrap(self, autoWrap) - - def setProperty(self, propertyName, val): - r"""setProperty(YLabel self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YLabel_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YLabel self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YLabel_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YLabel self) -> YPropertySet""" - return _yui.YLabel_propertySet(self) - - def debugLabel(self): - r"""debugLabel(YLabel self) -> std::string""" - return _yui.YLabel_debugLabel(self) - -# Register YLabel in _yui: -_yui.YLabel_swigregister(YLabel) - -class YLayoutBox(YWidget): - r"""Proxy of C++ YLayoutBox class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YLayoutBox - - def widgetClass(self): - r"""widgetClass(YLayoutBox self) -> char const *""" - return _yui.YLayoutBox_widgetClass(self) - - def primary(self): - r"""primary(YLayoutBox self) -> YUIDimension""" - return _yui.YLayoutBox_primary(self) - - def secondary(self): - r"""secondary(YLayoutBox self) -> YUIDimension""" - return _yui.YLayoutBox_secondary(self) - - def debugLayout(self): - r"""debugLayout(YLayoutBox self) -> bool""" - return _yui.YLayoutBox_debugLayout(self) - - def setDebugLayout(self, deb=True): - r"""setDebugLayout(YLayoutBox self, bool deb=True)""" - return _yui.YLayoutBox_setDebugLayout(self, deb) - - def preferredSize(self, dim): - r"""preferredSize(YLayoutBox self, YUIDimension dim) -> int""" - return _yui.YLayoutBox_preferredSize(self, dim) - - def preferredWidth(self): - r"""preferredWidth(YLayoutBox self) -> int""" - return _yui.YLayoutBox_preferredWidth(self) - - def preferredHeight(self): - r"""preferredHeight(YLayoutBox self) -> int""" - return _yui.YLayoutBox_preferredHeight(self) - - def setSize(self, newWidth, newHeight): - r"""setSize(YLayoutBox self, int newWidth, int newHeight)""" - return _yui.YLayoutBox_setSize(self, newWidth, newHeight) - - def stretchable(self, dimension): - r"""stretchable(YLayoutBox self, YUIDimension dimension) -> bool""" - return _yui.YLayoutBox_stretchable(self, dimension) - - def moveChild(self, child, newX, newY): - r"""moveChild(YLayoutBox self, YWidget child, int newX, int newY)""" - return _yui.YLayoutBox_moveChild(self, child, newX, newY) - - @staticmethod - def isLayoutStretch(child, dimension): - r"""isLayoutStretch(YWidget child, YUIDimension dimension) -> bool""" - return _yui.YLayoutBox_isLayoutStretch(child, dimension) - -# Register YLayoutBox in _yui: -_yui.YLayoutBox_swigregister(YLayoutBox) - -def YLayoutBox_isLayoutStretch(child, dimension): - r"""YLayoutBox_isLayoutStretch(YWidget child, YUIDimension dimension) -> bool""" - return _yui.YLayoutBox_isLayoutStretch(child, dimension) - -class YLogView(YWidget): - r"""Proxy of C++ YLogView class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YLogView - - def widgetClass(self): - r"""widgetClass(YLogView self) -> char const *""" - return _yui.YLogView_widgetClass(self) - - def label(self): - r"""label(YLogView self) -> std::string""" - return _yui.YLogView_label(self) - - def setLabel(self, label): - r"""setLabel(YLogView self, std::string const & label)""" - return _yui.YLogView_setLabel(self, label) - - def visibleLines(self): - r"""visibleLines(YLogView self) -> int""" - return _yui.YLogView_visibleLines(self) - - def setVisibleLines(self, newVisibleLines): - r"""setVisibleLines(YLogView self, int newVisibleLines)""" - return _yui.YLogView_setVisibleLines(self, newVisibleLines) - - def maxLines(self): - r"""maxLines(YLogView self) -> int""" - return _yui.YLogView_maxLines(self) - - def setMaxLines(self, newMaxLines): - r"""setMaxLines(YLogView self, int newMaxLines)""" - return _yui.YLogView_setMaxLines(self, newMaxLines) - - def logText(self): - r"""logText(YLogView self) -> std::string""" - return _yui.YLogView_logText(self) - - def setLogText(self, text): - r"""setLogText(YLogView self, std::string const & text)""" - return _yui.YLogView_setLogText(self, text) - - def lastLine(self): - r"""lastLine(YLogView self) -> std::string""" - return _yui.YLogView_lastLine(self) - - def appendLines(self, text): - r"""appendLines(YLogView self, std::string const & text)""" - return _yui.YLogView_appendLines(self, text) - - def clearText(self): - r"""clearText(YLogView self)""" - return _yui.YLogView_clearText(self) - - def lines(self): - r"""lines(YLogView self) -> int""" - return _yui.YLogView_lines(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YLogView self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YLogView_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YLogView self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YLogView_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YLogView self) -> YPropertySet""" - return _yui.YLogView_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YLogView self) -> std::string""" - return _yui.YLogView_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YLogView self, std::string const & str)""" - return _yui.YLogView_setShortcutString(self, str) - -# Register YLogView in _yui: -_yui.YLogView_swigregister(YLogView) - -class YMacro(object): - r"""Proxy of C++ YMacro class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - - @staticmethod - def setRecorder(recorder): - r"""setRecorder(YMacroRecorder recorder)""" - return _yui.YMacro_setRecorder(recorder) - - @staticmethod - def setPlayer(player): - r"""setPlayer(YMacroPlayer player)""" - return _yui.YMacro_setPlayer(player) - - @staticmethod - def record(macroFile): - r"""record(std::string const & macroFile)""" - return _yui.YMacro_record(macroFile) - - @staticmethod - def endRecording(): - r"""endRecording()""" - return _yui.YMacro_endRecording() - - @staticmethod - def recording(): - r"""recording() -> bool""" - return _yui.YMacro_recording() - - @staticmethod - def play(macroFile): - r"""play(std::string const & macroFile)""" - return _yui.YMacro_play(macroFile) - - @staticmethod - def playNextBlock(): - r"""playNextBlock()""" - return _yui.YMacro_playNextBlock() - - @staticmethod - def playing(): - r"""playing() -> bool""" - return _yui.YMacro_playing() - - @staticmethod - def recorder(): - r"""recorder() -> YMacroRecorder""" - return _yui.YMacro_recorder() - - @staticmethod - def player(): - r"""player() -> YMacroPlayer""" - return _yui.YMacro_player() - - @staticmethod - def deleteRecorder(): - r"""deleteRecorder()""" - return _yui.YMacro_deleteRecorder() - - @staticmethod - def deletePlayer(): - r"""deletePlayer()""" - return _yui.YMacro_deletePlayer() - -# Register YMacro in _yui: -_yui.YMacro_swigregister(YMacro) - -def YMacro_setRecorder(recorder): - r"""YMacro_setRecorder(YMacroRecorder recorder)""" - return _yui.YMacro_setRecorder(recorder) - -def YMacro_setPlayer(player): - r"""YMacro_setPlayer(YMacroPlayer player)""" - return _yui.YMacro_setPlayer(player) - -def YMacro_record(macroFile): - r"""YMacro_record(std::string const & macroFile)""" - return _yui.YMacro_record(macroFile) - -def YMacro_endRecording(): - r"""YMacro_endRecording()""" - return _yui.YMacro_endRecording() - -def YMacro_recording(): - r"""YMacro_recording() -> bool""" - return _yui.YMacro_recording() - -def YMacro_play(macroFile): - r"""YMacro_play(std::string const & macroFile)""" - return _yui.YMacro_play(macroFile) - -def YMacro_playNextBlock(): - r"""YMacro_playNextBlock()""" - return _yui.YMacro_playNextBlock() - -def YMacro_playing(): - r"""YMacro_playing() -> bool""" - return _yui.YMacro_playing() - -def YMacro_recorder(): - r"""YMacro_recorder() -> YMacroRecorder""" - return _yui.YMacro_recorder() - -def YMacro_player(): - r"""YMacro_player() -> YMacroPlayer""" - return _yui.YMacro_player() - -def YMacro_deleteRecorder(): - r"""YMacro_deleteRecorder()""" - return _yui.YMacro_deleteRecorder() - -def YMacro_deletePlayer(): - r"""YMacro_deletePlayer()""" - return _yui.YMacro_deletePlayer() - -class YMacroPlayer(object): - r"""Proxy of C++ YMacroPlayer class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMacroPlayer - - def play(self, macroFile): - r"""play(YMacroPlayer self, std::string const & macroFile)""" - return _yui.YMacroPlayer_play(self, macroFile) - - def playNextBlock(self): - r"""playNextBlock(YMacroPlayer self)""" - return _yui.YMacroPlayer_playNextBlock(self) - - def playing(self): - r"""playing(YMacroPlayer self) -> bool""" - return _yui.YMacroPlayer_playing(self) - -# Register YMacroPlayer in _yui: -_yui.YMacroPlayer_swigregister(YMacroPlayer) - -class YMacroRecorder(object): - r"""Proxy of C++ YMacroRecorder class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMacroRecorder - - def record(self, macroFileName): - r"""record(YMacroRecorder self, std::string const & macroFileName)""" - return _yui.YMacroRecorder_record(self, macroFileName) - - def endRecording(self): - r"""endRecording(YMacroRecorder self)""" - return _yui.YMacroRecorder_endRecording(self) - - def recording(self): - r"""recording(YMacroRecorder self) -> bool""" - return _yui.YMacroRecorder_recording(self) - - def recordWidgetProperty(self, widget, propertyName): - r"""recordWidgetProperty(YMacroRecorder self, YWidget widget, char const * propertyName)""" - return _yui.YMacroRecorder_recordWidgetProperty(self, widget, propertyName) - - def recordMakeScreenShot(self, *args): - r"""recordMakeScreenShot(YMacroRecorder self, bool enabled=False, std::string const & filename=std::string())""" - return _yui.YMacroRecorder_recordMakeScreenShot(self, *args) - -# Register YMacroRecorder in _yui: -_yui.YMacroRecorder_swigregister(YMacroRecorder) - -class YMenuWidget(YSelectionWidget): - r"""Proxy of C++ YMenuWidget class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMenuWidget - - def widgetClass(self): - r"""widgetClass(YMenuWidget self) -> char const *""" - return _yui.YMenuWidget_widgetClass(self) - - def rebuildMenuTree(self): - r"""rebuildMenuTree(YMenuWidget self)""" - return _yui.YMenuWidget_rebuildMenuTree(self) - - def addItems(self, itemCollection): - r"""addItems(YMenuWidget self, YItemCollection itemCollection)""" - return _yui.YMenuWidget_addItems(self, itemCollection) - - def addItem(self, item_disown): - r"""addItem(YMenuWidget self, YItem item_disown)""" - return _yui.YMenuWidget_addItem(self, item_disown) - - def deleteAllItems(self): - r"""deleteAllItems(YMenuWidget self)""" - return _yui.YMenuWidget_deleteAllItems(self) - - def resolveShortcutConflicts(self): - r"""resolveShortcutConflicts(YMenuWidget self)""" - return _yui.YMenuWidget_resolveShortcutConflicts(self) - - def setItemEnabled(self, item, enabled): - r"""setItemEnabled(YMenuWidget self, YMenuItem item, bool enabled)""" - return _yui.YMenuWidget_setItemEnabled(self, item, enabled) - - def setItemVisible(self, item, visible): - r"""setItemVisible(YMenuWidget self, YMenuItem item, bool visible)""" - return _yui.YMenuWidget_setItemVisible(self, item, visible) - - def findItem(self, path): - r"""findItem(YMenuWidget self, std::vector< std::string,std::allocator< std::string > > & path) -> YMenuItem""" - return _yui.YMenuWidget_findItem(self, path) - - def activateItem(self, item): - r"""activateItem(YMenuWidget self, YMenuItem item)""" - return _yui.YMenuWidget_activateItem(self, item) - - def findMenuItem(self, index): - r"""findMenuItem(YMenuWidget self, int index) -> YMenuItem""" - return _yui.YMenuWidget_findMenuItem(self, index) - -# Register YMenuWidget in _yui: -_yui.YMenuWidget_swigregister(YMenuWidget) - -class YMenuBar(YMenuWidget): - r"""Proxy of C++ YMenuBar class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMenuBar - - def addMenu(self, *args): - r"""addMenu(YMenuBar self, std::string const & label, std::string const & iconName="") -> YMenuItem""" - return _yui.YMenuBar_addMenu(self, *args) - - def widgetClass(self): - r"""widgetClass(YMenuBar self) -> char const *""" - return _yui.YMenuBar_widgetClass(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YMenuBar self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YMenuBar_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YMenuBar self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YMenuBar_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YMenuBar self) -> YPropertySet""" - return _yui.YMenuBar_propertySet(self) - -# Register YMenuBar in _yui: -_yui.YMenuBar_swigregister(YMenuBar) - -class YMenuButton(YMenuWidget): - r"""Proxy of C++ YMenuButton class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMenuButton - - def addItem(self, *args): - r"""addItem(YMenuButton self, std::string const & label, std::string const & iconName="") -> YMenuItem""" - return _yui.YMenuButton_addItem(self, *args) - - def addMenu(self, *args): - r"""addMenu(YMenuButton self, std::string const & label, std::string const & iconName="") -> YMenuItem""" - return _yui.YMenuButton_addMenu(self, *args) - - def addSeparator(self): - r"""addSeparator(YMenuButton self) -> YMenuItem""" - return _yui.YMenuButton_addSeparator(self) - - def widgetClass(self): - r"""widgetClass(YMenuButton self) -> char const *""" - return _yui.YMenuButton_widgetClass(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YMenuButton self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YMenuButton_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YMenuButton self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YMenuButton_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YMenuButton self) -> YPropertySet""" - return _yui.YMenuButton_propertySet(self) - -# Register YMenuButton in _yui: -_yui.YMenuButton_swigregister(YMenuButton) - -class YMenuItem(YTreeItem): - r"""Proxy of C++ YMenuItem class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YMenuItem self, std::string const & label, std::string const & iconName="") -> YMenuItem - __init__(YMenuItem self, YMenuItem parent, std::string const & label, std::string const & iconName="") -> YMenuItem - """ - _yui.YMenuItem_swiginit(self, _yui.new_YMenuItem(*args)) - __swig_destroy__ = _yui.delete_YMenuItem - - def addItem(self, *args): - r"""addItem(YMenuItem self, std::string const & label, std::string const & iconName="") -> YMenuItem""" - return _yui.YMenuItem_addItem(self, *args) - - def addMenu(self, *args): - r"""addMenu(YMenuItem self, std::string const & label, std::string const & iconName="") -> YMenuItem""" - return _yui.YMenuItem_addMenu(self, *args) - - def addSeparator(self): - r"""addSeparator(YMenuItem self) -> YMenuItem""" - return _yui.YMenuItem_addSeparator(self) - - def parent(self): - r"""parent(YMenuItem self) -> YMenuItem""" - return _yui.YMenuItem_parent(self) - - def isMenu(self): - r"""isMenu(YMenuItem self) -> bool""" - return _yui.YMenuItem_isMenu(self) - - def isSeparator(self): - r"""isSeparator(YMenuItem self) -> bool""" - return _yui.YMenuItem_isSeparator(self) - - def isEnabled(self): - r"""isEnabled(YMenuItem self) -> bool""" - return _yui.YMenuItem_isEnabled(self) - - def setEnabled(self, enabled=True): - r"""setEnabled(YMenuItem self, bool enabled=True)""" - return _yui.YMenuItem_setEnabled(self, enabled) - - def isVisible(self): - r"""isVisible(YMenuItem self) -> bool""" - return _yui.YMenuItem_isVisible(self) - - def setVisible(self, visible=True): - r"""setVisible(YMenuItem self, bool visible=True)""" - return _yui.YMenuItem_setVisible(self, visible) - - def uiItem(self): - r"""uiItem(YMenuItem self) -> void *""" - return _yui.YMenuItem_uiItem(self) - - def setUiItem(self, uiItem): - r"""setUiItem(YMenuItem self, void * uiItem)""" - return _yui.YMenuItem_setUiItem(self, uiItem) - -# Register YMenuItem in _yui: -_yui.YMenuItem_swigregister(YMenuItem) - -class YMultiLineEdit(YWidget): - r"""Proxy of C++ YMultiLineEdit class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMultiLineEdit - - def widgetClass(self): - r"""widgetClass(YMultiLineEdit self) -> char const *""" - return _yui.YMultiLineEdit_widgetClass(self) - - def value(self): - r"""value(YMultiLineEdit self) -> std::string""" - return _yui.YMultiLineEdit_value(self) - - def setValue(self, text): - r"""setValue(YMultiLineEdit self, std::string const & text)""" - return _yui.YMultiLineEdit_setValue(self, text) - - def label(self): - r"""label(YMultiLineEdit self) -> std::string""" - return _yui.YMultiLineEdit_label(self) - - def setLabel(self, label): - r"""setLabel(YMultiLineEdit self, std::string const & label)""" - return _yui.YMultiLineEdit_setLabel(self, label) - - def inputMaxLength(self): - r"""inputMaxLength(YMultiLineEdit self) -> int""" - return _yui.YMultiLineEdit_inputMaxLength(self) - - def setInputMaxLength(self, numberOfChars): - r"""setInputMaxLength(YMultiLineEdit self, int numberOfChars)""" - return _yui.YMultiLineEdit_setInputMaxLength(self, numberOfChars) - - def defaultVisibleLines(self): - r"""defaultVisibleLines(YMultiLineEdit self) -> int""" - return _yui.YMultiLineEdit_defaultVisibleLines(self) - - def setDefaultVisibleLines(self, newVisibleLines): - r"""setDefaultVisibleLines(YMultiLineEdit self, int newVisibleLines)""" - return _yui.YMultiLineEdit_setDefaultVisibleLines(self, newVisibleLines) - - def setProperty(self, propertyName, val): - r"""setProperty(YMultiLineEdit self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YMultiLineEdit_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YMultiLineEdit self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YMultiLineEdit_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YMultiLineEdit self) -> YPropertySet""" - return _yui.YMultiLineEdit_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YMultiLineEdit self) -> std::string""" - return _yui.YMultiLineEdit_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YMultiLineEdit self, std::string const & str)""" - return _yui.YMultiLineEdit_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YMultiLineEdit self) -> char const *""" - return _yui.YMultiLineEdit_userInputProperty(self) - -# Register YMultiLineEdit in _yui: -_yui.YMultiLineEdit_swigregister(YMultiLineEdit) - -class YMultiProgressMeter(YWidget): - r"""Proxy of C++ YMultiProgressMeter class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMultiProgressMeter - - def widgetClass(self): - r"""widgetClass(YMultiProgressMeter self) -> char const *""" - return _yui.YMultiProgressMeter_widgetClass(self) - - def dimension(self): - r"""dimension(YMultiProgressMeter self) -> YUIDimension""" - return _yui.YMultiProgressMeter_dimension(self) - - def horizontal(self): - r"""horizontal(YMultiProgressMeter self) -> bool""" - return _yui.YMultiProgressMeter_horizontal(self) - - def vertical(self): - r"""vertical(YMultiProgressMeter self) -> bool""" - return _yui.YMultiProgressMeter_vertical(self) - - def segments(self): - r"""segments(YMultiProgressMeter self) -> int""" - return _yui.YMultiProgressMeter_segments(self) - - def maxValue(self, segment): - r"""maxValue(YMultiProgressMeter self, int segment) -> float""" - return _yui.YMultiProgressMeter_maxValue(self, segment) - - def currentValue(self, segment): - r"""currentValue(YMultiProgressMeter self, int segment) -> float""" - return _yui.YMultiProgressMeter_currentValue(self, segment) - - def setCurrentValue(self, segment, value): - r"""setCurrentValue(YMultiProgressMeter self, int segment, float value)""" - return _yui.YMultiProgressMeter_setCurrentValue(self, segment, value) - - def setCurrentValues(self, values): - r"""setCurrentValues(YMultiProgressMeter self, std::vector< float,std::allocator< float > > const & values)""" - return _yui.YMultiProgressMeter_setCurrentValues(self, values) - - def setProperty(self, propertyName, val): - r"""setProperty(YMultiProgressMeter self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YMultiProgressMeter_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YMultiProgressMeter self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YMultiProgressMeter_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YMultiProgressMeter self) -> YPropertySet""" - return _yui.YMultiProgressMeter_propertySet(self) - - def doUpdate(self): - r"""doUpdate(YMultiProgressMeter self)""" - return _yui.YMultiProgressMeter_doUpdate(self) - -# Register YMultiProgressMeter in _yui: -_yui.YMultiProgressMeter_swigregister(YMultiProgressMeter) - -class YMultiSelectionBox(YSelectionWidget): - r"""Proxy of C++ YMultiSelectionBox class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMultiSelectionBox - - def widgetClass(self): - r"""widgetClass(YMultiSelectionBox self) -> char const *""" - return _yui.YMultiSelectionBox_widgetClass(self) - - def shrinkable(self): - r"""shrinkable(YMultiSelectionBox self) -> bool""" - return _yui.YMultiSelectionBox_shrinkable(self) - - def setShrinkable(self, shrinkable=True): - r"""setShrinkable(YMultiSelectionBox self, bool shrinkable=True)""" - return _yui.YMultiSelectionBox_setShrinkable(self, shrinkable) - - def setProperty(self, propertyName, val): - r"""setProperty(YMultiSelectionBox self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YMultiSelectionBox_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YMultiSelectionBox self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YMultiSelectionBox_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YMultiSelectionBox self) -> YPropertySet""" - return _yui.YMultiSelectionBox_propertySet(self) - - def userInputProperty(self): - r"""userInputProperty(YMultiSelectionBox self) -> char const *""" - return _yui.YMultiSelectionBox_userInputProperty(self) - - def currentItem(self): - r"""currentItem(YMultiSelectionBox self) -> YItem""" - return _yui.YMultiSelectionBox_currentItem(self) - - def setCurrentItem(self, item): - r"""setCurrentItem(YMultiSelectionBox self, YItem item)""" - return _yui.YMultiSelectionBox_setCurrentItem(self, item) - - def saveUserInput(self, macroRecorder): - r"""saveUserInput(YMultiSelectionBox self, YMacroRecorder macroRecorder)""" - return _yui.YMultiSelectionBox_saveUserInput(self, macroRecorder) - -# Register YMultiSelectionBox in _yui: -_yui.YMultiSelectionBox_swigregister(YMultiSelectionBox) - -YWizardID = _yui.YWizardID - -YWizardContentsReplacePointID = _yui.YWizardContentsReplacePointID - -YWizardMode_Standard = _yui.YWizardMode_Standard - -YWizardMode_Steps = _yui.YWizardMode_Steps - -YWizardMode_Tree = _yui.YWizardMode_Tree - -YWizardMode_TitleOnLeft = _yui.YWizardMode_TitleOnLeft - -class YWizard(YWidget): - r"""Proxy of C++ YWizard class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YWizard - - def widgetClass(self): - r"""widgetClass(YWizard self) -> char const *""" - return _yui.YWizard_widgetClass(self) - - def wizardMode(self): - r"""wizardMode(YWizard self) -> YWizardMode""" - return _yui.YWizard_wizardMode(self) - - def backButton(self): - r"""backButton(YWizard self) -> YPushButton""" - return _yui.YWizard_backButton(self) - - def abortButton(self): - r"""abortButton(YWizard self) -> YPushButton""" - return _yui.YWizard_abortButton(self) - - def nextButton(self): - r"""nextButton(YWizard self) -> YPushButton""" - return _yui.YWizard_nextButton(self) - - def contentsReplacePoint(self): - r"""contentsReplacePoint(YWizard self) -> YReplacePoint""" - return _yui.YWizard_contentsReplacePoint(self) - - def protectNextButton(self, protect): - r"""protectNextButton(YWizard self, bool protect)""" - return _yui.YWizard_protectNextButton(self, protect) - - def nextButtonIsProtected(self): - r"""nextButtonIsProtected(YWizard self) -> bool""" - return _yui.YWizard_nextButtonIsProtected(self) - - def setButtonLabel(self, button, newLabel): - r"""setButtonLabel(YWizard self, YPushButton button, std::string const & newLabel)""" - return _yui.YWizard_setButtonLabel(self, button, newLabel) - - def setHelpText(self, helpText): - r"""setHelpText(YWizard self, std::string const & helpText)""" - return _yui.YWizard_setHelpText(self, helpText) - - def setDialogIcon(self, iconName): - r"""setDialogIcon(YWizard self, std::string const & iconName)""" - return _yui.YWizard_setDialogIcon(self, iconName) - - def setDialogTitle(self, titleText): - r"""setDialogTitle(YWizard self, std::string const & titleText)""" - return _yui.YWizard_setDialogTitle(self, titleText) - - def getDialogTitle(self): - r"""getDialogTitle(YWizard self) -> std::string""" - return _yui.YWizard_getDialogTitle(self) - - def setDialogHeading(self, headingText): - r"""setDialogHeading(YWizard self, std::string const & headingText)""" - return _yui.YWizard_setDialogHeading(self, headingText) - - def getDialogHeading(self): - r"""getDialogHeading(YWizard self) -> std::string""" - return _yui.YWizard_getDialogHeading(self) - - def addStep(self, text, id): - r"""addStep(YWizard self, std::string const & text, std::string const & id)""" - return _yui.YWizard_addStep(self, text, id) - - def addStepHeading(self, text): - r"""addStepHeading(YWizard self, std::string const & text)""" - return _yui.YWizard_addStepHeading(self, text) - - def deleteSteps(self): - r"""deleteSteps(YWizard self)""" - return _yui.YWizard_deleteSteps(self) - - def setCurrentStep(self, id): - r"""setCurrentStep(YWizard self, std::string const & id)""" - return _yui.YWizard_setCurrentStep(self, id) - - def updateSteps(self): - r"""updateSteps(YWizard self)""" - return _yui.YWizard_updateSteps(self) - - def addTreeItem(self, parentID, text, id): - r"""addTreeItem(YWizard self, std::string const & parentID, std::string const & text, std::string const & id)""" - return _yui.YWizard_addTreeItem(self, parentID, text, id) - - def selectTreeItem(self, id): - r"""selectTreeItem(YWizard self, std::string const & id)""" - return _yui.YWizard_selectTreeItem(self, id) - - def currentTreeSelection(self): - r"""currentTreeSelection(YWizard self) -> std::string""" - return _yui.YWizard_currentTreeSelection(self) - - def deleteTreeItems(self): - r"""deleteTreeItems(YWizard self)""" - return _yui.YWizard_deleteTreeItems(self) - - def addMenu(self, text, id): - r"""addMenu(YWizard self, std::string const & text, std::string const & id)""" - return _yui.YWizard_addMenu(self, text, id) - - def addSubMenu(self, parentMenuID, text, id): - r"""addSubMenu(YWizard self, std::string const & parentMenuID, std::string const & text, std::string const & id)""" - return _yui.YWizard_addSubMenu(self, parentMenuID, text, id) - - def addMenuEntry(self, parentMenuID, text, id): - r"""addMenuEntry(YWizard self, std::string const & parentMenuID, std::string const & text, std::string const & id)""" - return _yui.YWizard_addMenuEntry(self, parentMenuID, text, id) - - def addMenuSeparator(self, parentMenuID): - r"""addMenuSeparator(YWizard self, std::string const & parentMenuID)""" - return _yui.YWizard_addMenuSeparator(self, parentMenuID) - - def deleteMenus(self): - r"""deleteMenus(YWizard self)""" - return _yui.YWizard_deleteMenus(self) - - def showReleaseNotesButton(self, label, id): - r"""showReleaseNotesButton(YWizard self, std::string const & label, std::string const & id)""" - return _yui.YWizard_showReleaseNotesButton(self, label, id) - - def hideReleaseNotesButton(self): - r"""hideReleaseNotesButton(YWizard self)""" - return _yui.YWizard_hideReleaseNotesButton(self) - - def retranslateInternalButtons(self): - r"""retranslateInternalButtons(YWizard self)""" - return _yui.YWizard_retranslateInternalButtons(self) - - def ping(self): - r"""ping(YWizard self)""" - return _yui.YWizard_ping(self) - - def getProperty(self, propertyName): - r"""getProperty(YWizard self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YWizard_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YWizard self) -> YPropertySet""" - return _yui.YWizard_propertySet(self) - -# Register YWizard in _yui: -_yui.YWizard_swigregister(YWizard) - -class YOptionalWidgetFactory(object): - r"""Proxy of C++ YOptionalWidgetFactory class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - - def hasWizard(self): - r"""hasWizard(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasWizard(self) - - def createWizard(self, parent, backButtonLabel, abortButtonLabel, nextButtonLabel, wizardMode=YWizardMode_Standard): - r"""createWizard(YOptionalWidgetFactory self, YWidget parent, std::string const & backButtonLabel, std::string const & abortButtonLabel, std::string const & nextButtonLabel, YWizardMode wizardMode=YWizardMode_Standard) -> YWizard""" - return _yui.YOptionalWidgetFactory_createWizard(self, parent, backButtonLabel, abortButtonLabel, nextButtonLabel, wizardMode) - - def hasDumbTab(self): - r"""hasDumbTab(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasDumbTab(self) - - def createDumbTab(self, parent): - r"""createDumbTab(YOptionalWidgetFactory self, YWidget parent) -> YDumbTab""" - return _yui.YOptionalWidgetFactory_createDumbTab(self, parent) - - def hasSlider(self): - r"""hasSlider(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasSlider(self) - - def createSlider(self, parent, label, minVal, maxVal, initialVal): - r"""createSlider(YOptionalWidgetFactory self, YWidget parent, std::string const & label, int minVal, int maxVal, int initialVal) -> YSlider""" - return _yui.YOptionalWidgetFactory_createSlider(self, parent, label, minVal, maxVal, initialVal) - - def hasDateField(self): - r"""hasDateField(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasDateField(self) - - def createDateField(self, parent, label): - r"""createDateField(YOptionalWidgetFactory self, YWidget parent, std::string const & label) -> YDateField""" - return _yui.YOptionalWidgetFactory_createDateField(self, parent, label) - - def hasTimeField(self): - r"""hasTimeField(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasTimeField(self) - - def createTimeField(self, parent, label): - r"""createTimeField(YOptionalWidgetFactory self, YWidget parent, std::string const & label) -> YTimeField""" - return _yui.YOptionalWidgetFactory_createTimeField(self, parent, label) - - def hasBarGraph(self): - r"""hasBarGraph(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasBarGraph(self) - - def createBarGraph(self, parent): - r"""createBarGraph(YOptionalWidgetFactory self, YWidget parent) -> YBarGraph""" - return _yui.YOptionalWidgetFactory_createBarGraph(self, parent) - - def hasPatternSelector(self): - r"""hasPatternSelector(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasPatternSelector(self) - - def createPatternSelector(self, parent, modeFlags=0): - r"""createPatternSelector(YOptionalWidgetFactory self, YWidget parent, long modeFlags=0) -> YWidget""" - return _yui.YOptionalWidgetFactory_createPatternSelector(self, parent, modeFlags) - - def hasSimplePatchSelector(self): - r"""hasSimplePatchSelector(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasSimplePatchSelector(self) - - def createSimplePatchSelector(self, parent, modeFlags=0): - r"""createSimplePatchSelector(YOptionalWidgetFactory self, YWidget parent, long modeFlags=0) -> YWidget""" - return _yui.YOptionalWidgetFactory_createSimplePatchSelector(self, parent, modeFlags) - - def hasMultiProgressMeter(self): - r"""hasMultiProgressMeter(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasMultiProgressMeter(self) - - def createHMultiProgressMeter(self, parent, maxValues): - r"""createHMultiProgressMeter(YOptionalWidgetFactory self, YWidget parent, std::vector< float,std::allocator< float > > const & maxValues) -> YMultiProgressMeter""" - return _yui.YOptionalWidgetFactory_createHMultiProgressMeter(self, parent, maxValues) - - def createVMultiProgressMeter(self, parent, maxValues): - r"""createVMultiProgressMeter(YOptionalWidgetFactory self, YWidget parent, std::vector< float,std::allocator< float > > const & maxValues) -> YMultiProgressMeter""" - return _yui.YOptionalWidgetFactory_createVMultiProgressMeter(self, parent, maxValues) - - def createMultiProgressMeter(self, parent, dim, maxValues): - r"""createMultiProgressMeter(YOptionalWidgetFactory self, YWidget parent, YUIDimension dim, std::vector< float,std::allocator< float > > const & maxValues) -> YMultiProgressMeter""" - return _yui.YOptionalWidgetFactory_createMultiProgressMeter(self, parent, dim, maxValues) - - def hasPartitionSplitter(self): - r"""hasPartitionSplitter(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasPartitionSplitter(self) - - def createPartitionSplitter(self, parent, usedSize, totalFreeSize, newPartSize, minNewPartSize, minFreeSize, usedLabel, freeLabel, newPartLabel, freeFieldLabel, newPartFieldLabel): - r"""createPartitionSplitter(YOptionalWidgetFactory self, YWidget parent, int usedSize, int totalFreeSize, int newPartSize, int minNewPartSize, int minFreeSize, std::string const & usedLabel, std::string const & freeLabel, std::string const & newPartLabel, std::string const & freeFieldLabel, std::string const & newPartFieldLabel) -> YPartitionSplitter""" - return _yui.YOptionalWidgetFactory_createPartitionSplitter(self, parent, usedSize, totalFreeSize, newPartSize, minNewPartSize, minFreeSize, usedLabel, freeLabel, newPartLabel, freeFieldLabel, newPartFieldLabel) - - def hasDownloadProgress(self): - r"""hasDownloadProgress(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasDownloadProgress(self) - - def createDownloadProgress(self, parent, label, filename, expectedFileSize): - r"""createDownloadProgress(YOptionalWidgetFactory self, YWidget parent, std::string const & label, std::string const & filename, YFileSize_t expectedFileSize) -> YDownloadProgress""" - return _yui.YOptionalWidgetFactory_createDownloadProgress(self, parent, label, filename, expectedFileSize) - - def hasDummySpecialWidget(self): - r"""hasDummySpecialWidget(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasDummySpecialWidget(self) - - def createDummySpecialWidget(self, parent): - r"""createDummySpecialWidget(YOptionalWidgetFactory self, YWidget parent) -> YWidget""" - return _yui.YOptionalWidgetFactory_createDummySpecialWidget(self, parent) - - def hasTimezoneSelector(self): - r"""hasTimezoneSelector(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasTimezoneSelector(self) - - def createTimezoneSelector(self, parent, timezoneMap, timezones): - r"""createTimezoneSelector(YOptionalWidgetFactory self, YWidget parent, std::string const & timezoneMap, std::map< std::string,std::string,std::less< std::string >,std::allocator< std::pair< std::string const,std::string > > > const & timezones) -> YTimezoneSelector""" - return _yui.YOptionalWidgetFactory_createTimezoneSelector(self, parent, timezoneMap, timezones) - - def hasGraph(self): - r"""hasGraph(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasGraph(self) - - def createGraph(self, *args): - r""" - createGraph(YOptionalWidgetFactory self, YWidget parent, std::string const & filename, std::string const & layoutAlgorithm) -> YGraph - createGraph(YOptionalWidgetFactory self, YWidget parent, void * graph) -> YGraph * - """ - return _yui.YOptionalWidgetFactory_createGraph(self, *args) - - def hasContextMenu(self): - r"""hasContextMenu(YOptionalWidgetFactory self) -> bool""" - return _yui.YOptionalWidgetFactory_hasContextMenu(self) - -# Register YOptionalWidgetFactory in _yui: -_yui.YOptionalWidgetFactory_swigregister(YOptionalWidgetFactory) - -YPkg_TestMode = _yui.YPkg_TestMode - -YPkg_OnlineUpdateMode = _yui.YPkg_OnlineUpdateMode - -YPkg_UpdateMode = _yui.YPkg_UpdateMode - -YPkg_SearchMode = _yui.YPkg_SearchMode - -YPkg_SummaryMode = _yui.YPkg_SummaryMode - -YPkg_RepoMode = _yui.YPkg_RepoMode - -YPkg_RepoMgr = _yui.YPkg_RepoMgr - -YPkg_ConfirmUnsupported = _yui.YPkg_ConfirmUnsupported - -YPkg_OnlineSearch = _yui.YPkg_OnlineSearch - -class YPackageSelector(YWidget): - r"""Proxy of C++ YPackageSelector class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - - def widgetClass(self): - r"""widgetClass(YPackageSelector self) -> char const *""" - return _yui.YPackageSelector_widgetClass(self) - - def testMode(self): - r"""testMode(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_testMode(self) - - def onlineUpdateMode(self): - r"""onlineUpdateMode(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_onlineUpdateMode(self) - - def updateMode(self): - r"""updateMode(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_updateMode(self) - - def searchMode(self): - r"""searchMode(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_searchMode(self) - - def summaryMode(self): - r"""summaryMode(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_summaryMode(self) - - def repoMode(self): - r"""repoMode(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_repoMode(self) - - def repoMgrEnabled(self): - r"""repoMgrEnabled(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_repoMgrEnabled(self) - - def confirmUnsupported(self): - r"""confirmUnsupported(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_confirmUnsupported(self) - - def onlineSearchEnabled(self): - r"""onlineSearchEnabled(YPackageSelector self) -> bool""" - return _yui.YPackageSelector_onlineSearchEnabled(self) - __swig_destroy__ = _yui.delete_YPackageSelector - -# Register YPackageSelector in _yui: -_yui.YPackageSelector_swigregister(YPackageSelector) - -class YPackageSelectorPlugin(YUIPlugin): - r"""Proxy of C++ YPackageSelectorPlugin class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - - def createPackageSelector(self, parent, modeFlags=0): - r"""createPackageSelector(YPackageSelectorPlugin self, YWidget parent, long modeFlags=0) -> YPackageSelector""" - return _yui.YPackageSelectorPlugin_createPackageSelector(self, parent, modeFlags) - -# Register YPackageSelectorPlugin in _yui: -_yui.YPackageSelectorPlugin_swigregister(YPackageSelectorPlugin) - -class YPartitionSplitter(YWidget): - r"""Proxy of C++ YPartitionSplitter class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YPartitionSplitter - - def widgetClass(self): - r"""widgetClass(YPartitionSplitter self) -> char const *""" - return _yui.YPartitionSplitter_widgetClass(self) - - def value(self): - r"""value(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_value(self) - - def setValue(self, newValue): - r"""setValue(YPartitionSplitter self, int newValue)""" - return _yui.YPartitionSplitter_setValue(self, newValue) - - def usedSize(self): - r"""usedSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_usedSize(self) - - def totalFreeSize(self): - r"""totalFreeSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_totalFreeSize(self) - - def minFreeSize(self): - r"""minFreeSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_minFreeSize(self) - - def maxFreeSize(self): - r"""maxFreeSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_maxFreeSize(self) - - def freeSize(self): - r"""freeSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_freeSize(self) - - def newPartSize(self): - r"""newPartSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_newPartSize(self) - - def minNewPartSize(self): - r"""minNewPartSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_minNewPartSize(self) - - def maxNewPartSize(self): - r"""maxNewPartSize(YPartitionSplitter self) -> int""" - return _yui.YPartitionSplitter_maxNewPartSize(self) - - def usedLabel(self): - r"""usedLabel(YPartitionSplitter self) -> std::string""" - return _yui.YPartitionSplitter_usedLabel(self) - - def freeLabel(self): - r"""freeLabel(YPartitionSplitter self) -> std::string""" - return _yui.YPartitionSplitter_freeLabel(self) - - def newPartLabel(self): - r"""newPartLabel(YPartitionSplitter self) -> std::string""" - return _yui.YPartitionSplitter_newPartLabel(self) - - def freeFieldLabel(self): - r"""freeFieldLabel(YPartitionSplitter self) -> std::string""" - return _yui.YPartitionSplitter_freeFieldLabel(self) - - def newPartFieldLabel(self): - r"""newPartFieldLabel(YPartitionSplitter self) -> std::string""" - return _yui.YPartitionSplitter_newPartFieldLabel(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YPartitionSplitter self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YPartitionSplitter_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YPartitionSplitter self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YPartitionSplitter_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YPartitionSplitter self) -> YPropertySet""" - return _yui.YPartitionSplitter_propertySet(self) - - def userInputProperty(self): - r"""userInputProperty(YPartitionSplitter self) -> char const *""" - return _yui.YPartitionSplitter_userInputProperty(self) - -# Register YPartitionSplitter in _yui: -_yui.YPartitionSplitter_swigregister(YPartitionSplitter) - -class YProgressBar(YWidget): - r"""Proxy of C++ YProgressBar class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YProgressBar - - def widgetClass(self): - r"""widgetClass(YProgressBar self) -> char const *""" - return _yui.YProgressBar_widgetClass(self) - - def label(self): - r"""label(YProgressBar self) -> std::string""" - return _yui.YProgressBar_label(self) - - def setLabel(self, label): - r"""setLabel(YProgressBar self, std::string const & label)""" - return _yui.YProgressBar_setLabel(self, label) - - def maxValue(self): - r"""maxValue(YProgressBar self) -> int""" - return _yui.YProgressBar_maxValue(self) - - def value(self): - r"""value(YProgressBar self) -> int""" - return _yui.YProgressBar_value(self) - - def setValue(self, newValue): - r"""setValue(YProgressBar self, int newValue)""" - return _yui.YProgressBar_setValue(self, newValue) - - def setProperty(self, propertyName, val): - r"""setProperty(YProgressBar self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YProgressBar_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YProgressBar self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YProgressBar_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YProgressBar self) -> YPropertySet""" - return _yui.YProgressBar_propertySet(self) - -# Register YProgressBar in _yui: -_yui.YProgressBar_swigregister(YProgressBar) - -YUnknownPropertyType = _yui.YUnknownPropertyType - -YOtherProperty = _yui.YOtherProperty - -YStringProperty = _yui.YStringProperty - -YBoolProperty = _yui.YBoolProperty - -YIntegerProperty = _yui.YIntegerProperty - -class YProperty(object): - r"""Proxy of C++ YProperty class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, name, type, isReadOnly=False): - r"""__init__(YProperty self, std::string const & name, YPropertyType type, bool isReadOnly=False) -> YProperty""" - _yui.YProperty_swiginit(self, _yui.new_YProperty(name, type, isReadOnly)) - - def name(self): - r"""name(YProperty self) -> std::string""" - return _yui.YProperty_name(self) - - def type(self): - r"""type(YProperty self) -> YPropertyType""" - return _yui.YProperty_type(self) - - def isReadOnly(self): - r"""isReadOnly(YProperty self) -> bool""" - return _yui.YProperty_isReadOnly(self) - - @staticmethod - def typeAsStr(*args): - r""" - typeAsStr() -> std::string - typeAsStr(YPropertyType type) -> std::string - """ - return _yui.YProperty_typeAsStr(*args) - __swig_destroy__ = _yui.delete_YProperty - -# Register YProperty in _yui: -_yui.YProperty_swigregister(YProperty) - -def YProperty_typeAsStr(*args): - r""" - YProperty_typeAsStr() -> std::string - YProperty_typeAsStr(YPropertyType type) -> std::string - """ - return _yui.YProperty_typeAsStr(*args) - -class YPropertyValue(object): - r"""Proxy of C++ YPropertyValue class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YPropertyValue self, std::string const & str) -> YPropertyValue - __init__(YPropertyValue self, char const * str) -> YPropertyValue - __init__(YPropertyValue self, bool b) -> YPropertyValue - __init__(YPropertyValue self, YInteger num) -> YPropertyValue - __init__(YPropertyValue self, int num) -> YPropertyValue - __init__(YPropertyValue self, YPropertyType type) -> YPropertyValue - __init__(YPropertyValue self) -> YPropertyValue - """ - _yui.YPropertyValue_swiginit(self, _yui.new_YPropertyValue(*args)) - __swig_destroy__ = _yui.delete_YPropertyValue - - def __eq__(self, other): - r"""__eq__(YPropertyValue self, YPropertyValue other) -> bool""" - return _yui.YPropertyValue___eq__(self, other) - - def __ne__(self, other): - r"""__ne__(YPropertyValue self, YPropertyValue other) -> bool""" - return _yui.YPropertyValue___ne__(self, other) - - def type(self): - r"""type(YPropertyValue self) -> YPropertyType""" - return _yui.YPropertyValue_type(self) - - def typeAsStr(self): - r"""typeAsStr(YPropertyValue self) -> std::string""" - return _yui.YPropertyValue_typeAsStr(self) - - def stringVal(self): - r"""stringVal(YPropertyValue self) -> std::string""" - return _yui.YPropertyValue_stringVal(self) - - def boolVal(self): - r"""boolVal(YPropertyValue self) -> bool""" - return _yui.YPropertyValue_boolVal(self) - - def integerVal(self): - r"""integerVal(YPropertyValue self) -> YInteger""" - return _yui.YPropertyValue_integerVal(self) - -# Register YPropertyValue in _yui: -_yui.YPropertyValue_swigregister(YPropertyValue) - -class YPropertySet(object): - r"""Proxy of C++ YPropertySet class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YPropertySet self) -> YPropertySet""" - _yui.YPropertySet_swiginit(self, _yui.new_YPropertySet()) - - def check(self, *args): - r""" - check(YPropertySet self, std::string const & propertyName) - check(YPropertySet self, std::string const & propertyName, YPropertyType type) - check(YPropertySet self, YProperty prop) - """ - return _yui.YPropertySet_check(self, *args) - - def contains(self, *args): - r""" - contains(YPropertySet self, std::string const & propertyName) -> bool - contains(YPropertySet self, std::string const & propertyName, YPropertyType type) -> bool - contains(YPropertySet self, YProperty prop) -> bool - """ - return _yui.YPropertySet_contains(self, *args) - - def isEmpty(self): - r"""isEmpty(YPropertySet self) -> bool""" - return _yui.YPropertySet_isEmpty(self) - - def size(self): - r"""size(YPropertySet self) -> int""" - return _yui.YPropertySet_size(self) - - def add(self, *args): - r""" - add(YPropertySet self, YProperty prop) - add(YPropertySet self, YPropertySet otherSet) - """ - return _yui.YPropertySet_add(self, *args) - - def propertiesBegin(self): - r"""propertiesBegin(YPropertySet self) -> YPropertySet::const_iterator""" - return _yui.YPropertySet_propertiesBegin(self) - - def propertiesEnd(self): - r"""propertiesEnd(YPropertySet self) -> YPropertySet::const_iterator""" - return _yui.YPropertySet_propertiesEnd(self) - __swig_destroy__ = _yui.delete_YPropertySet - -# Register YPropertySet in _yui: -_yui.YPropertySet_swigregister(YPropertySet) - -class YPushButton(YWidget): - r"""Proxy of C++ YPushButton class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YPushButton - - def widgetClass(self): - r"""widgetClass(YPushButton self) -> char const *""" - return _yui.YPushButton_widgetClass(self) - - def label(self): - r"""label(YPushButton self) -> std::string""" - return _yui.YPushButton_label(self) - - def setLabel(self, label): - r"""setLabel(YPushButton self, std::string const & label)""" - return _yui.YPushButton_setLabel(self, label) - - def setIcon(self, iconName): - r"""setIcon(YPushButton self, std::string const & iconName)""" - return _yui.YPushButton_setIcon(self, iconName) - - def isDefaultButton(self): - r"""isDefaultButton(YPushButton self) -> bool""" - return _yui.YPushButton_isDefaultButton(self) - - def setDefaultButton(self, _def=True): - r"""setDefaultButton(YPushButton self, bool _def=True)""" - return _yui.YPushButton_setDefaultButton(self, _def) - - def setRole(self, role): - r"""setRole(YPushButton self, YButtonRole role)""" - return _yui.YPushButton_setRole(self, role) - - def role(self): - r"""role(YPushButton self) -> YButtonRole""" - return _yui.YPushButton_role(self) - - def setFunctionKey(self, fkey_no): - r"""setFunctionKey(YPushButton self, int fkey_no)""" - return _yui.YPushButton_setFunctionKey(self, fkey_no) - - def isHelpButton(self): - r"""isHelpButton(YPushButton self) -> bool""" - return _yui.YPushButton_isHelpButton(self) - - def setHelpButton(self, helpButton=True): - r"""setHelpButton(YPushButton self, bool helpButton=True)""" - return _yui.YPushButton_setHelpButton(self, helpButton) - - def isRelNotesButton(self): - r"""isRelNotesButton(YPushButton self) -> bool""" - return _yui.YPushButton_isRelNotesButton(self) - - def setRelNotesButton(self, relNotesButton=True): - r"""setRelNotesButton(YPushButton self, bool relNotesButton=True)""" - return _yui.YPushButton_setRelNotesButton(self, relNotesButton) - - def setProperty(self, propertyName, val): - r"""setProperty(YPushButton self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YPushButton_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YPushButton self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YPushButton_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YPushButton self) -> YPropertySet""" - return _yui.YPushButton_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YPushButton self) -> std::string""" - return _yui.YPushButton_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YPushButton self, std::string const & str)""" - return _yui.YPushButton_setShortcutString(self, str) - - def activate(self): - r"""activate(YPushButton self)""" - return _yui.YPushButton_activate(self) - -# Register YPushButton in _yui: -_yui.YPushButton_swigregister(YPushButton) - -class YRadioButtonGroup(YSingleChildContainerWidget): - r"""Proxy of C++ YRadioButtonGroup class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YRadioButtonGroup - - def widgetClass(self): - r"""widgetClass(YRadioButtonGroup self) -> char const *""" - return _yui.YRadioButtonGroup_widgetClass(self) - - def currentButton(self): - r"""currentButton(YRadioButtonGroup self) -> YRadioButton""" - return _yui.YRadioButtonGroup_currentButton(self) - - def value(self): - r"""value(YRadioButtonGroup self) -> YRadioButton""" - return _yui.YRadioButtonGroup_value(self) - - def addRadioButton(self, radioButton): - r"""addRadioButton(YRadioButtonGroup self, YRadioButton radioButton)""" - return _yui.YRadioButtonGroup_addRadioButton(self, radioButton) - - def removeRadioButton(self, radioButton): - r"""removeRadioButton(YRadioButtonGroup self, YRadioButton radioButton)""" - return _yui.YRadioButtonGroup_removeRadioButton(self, radioButton) - - def uncheckOtherButtons(self, radioButton): - r"""uncheckOtherButtons(YRadioButtonGroup self, YRadioButton radioButton)""" - return _yui.YRadioButtonGroup_uncheckOtherButtons(self, radioButton) - - def setProperty(self, propertyName, val): - r"""setProperty(YRadioButtonGroup self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YRadioButtonGroup_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YRadioButtonGroup self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YRadioButtonGroup_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YRadioButtonGroup self) -> YPropertySet""" - return _yui.YRadioButtonGroup_propertySet(self) - -# Register YRadioButtonGroup in _yui: -_yui.YRadioButtonGroup_swigregister(YRadioButtonGroup) - -class YRadioButton(YWidget): - r"""Proxy of C++ YRadioButton class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YRadioButton - - def widgetClass(self): - r"""widgetClass(YRadioButton self) -> char const *""" - return _yui.YRadioButton_widgetClass(self) - - def value(self): - r"""value(YRadioButton self) -> bool""" - return _yui.YRadioButton_value(self) - - def setValue(self, checked): - r"""setValue(YRadioButton self, bool checked)""" - return _yui.YRadioButton_setValue(self, checked) - - def label(self): - r"""label(YRadioButton self) -> std::string""" - return _yui.YRadioButton_label(self) - - def setLabel(self, label): - r"""setLabel(YRadioButton self, std::string const & label)""" - return _yui.YRadioButton_setLabel(self, label) - - def useBoldFont(self): - r"""useBoldFont(YRadioButton self) -> bool""" - return _yui.YRadioButton_useBoldFont(self) - - def setUseBoldFont(self, bold=True): - r"""setUseBoldFont(YRadioButton self, bool bold=True)""" - return _yui.YRadioButton_setUseBoldFont(self, bold) - - def buttonGroup(self): - r"""buttonGroup(YRadioButton self) -> YRadioButtonGroup""" - return _yui.YRadioButton_buttonGroup(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YRadioButton self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YRadioButton_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YRadioButton self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YRadioButton_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YRadioButton self) -> YPropertySet""" - return _yui.YRadioButton_propertySet(self) - - def shortcutString(self): - r"""shortcutString(YRadioButton self) -> std::string""" - return _yui.YRadioButton_shortcutString(self) - - def setShortcutString(self, str): - r"""setShortcutString(YRadioButton self, std::string const & str)""" - return _yui.YRadioButton_setShortcutString(self, str) - - def userInputProperty(self): - r"""userInputProperty(YRadioButton self) -> char const *""" - return _yui.YRadioButton_userInputProperty(self) - -# Register YRadioButton in _yui: -_yui.YRadioButton_swigregister(YRadioButton) - -class YReplacePoint(YSingleChildContainerWidget): - r"""Proxy of C++ YReplacePoint class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - - def showChild(self): - r"""showChild(YReplacePoint self)""" - return _yui.YReplacePoint_showChild(self) - - def widgetClass(self): - r"""widgetClass(YReplacePoint self) -> char const *""" - return _yui.YReplacePoint_widgetClass(self) - __swig_destroy__ = _yui.delete_YReplacePoint - -# Register YReplacePoint in _yui: -_yui.YReplacePoint_swigregister(YReplacePoint) - -class YRichText(YWidget): - r"""Proxy of C++ YRichText class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YRichText - - def widgetClass(self): - r"""widgetClass(YRichText self) -> char const *""" - return _yui.YRichText_widgetClass(self) - - def setValue(self, newValue): - r"""setValue(YRichText self, std::string const & newValue)""" - return _yui.YRichText_setValue(self, newValue) - - def value(self): - r"""value(YRichText self) -> std::string""" - return _yui.YRichText_value(self) - - def setText(self, newText): - r"""setText(YRichText self, std::string const & newText)""" - return _yui.YRichText_setText(self, newText) - - def text(self): - r"""text(YRichText self) -> std::string""" - return _yui.YRichText_text(self) - - def plainTextMode(self): - r"""plainTextMode(YRichText self) -> bool""" - return _yui.YRichText_plainTextMode(self) - - def setPlainTextMode(self, on=True): - r"""setPlainTextMode(YRichText self, bool on=True)""" - return _yui.YRichText_setPlainTextMode(self, on) - - def autoScrollDown(self): - r"""autoScrollDown(YRichText self) -> bool""" - return _yui.YRichText_autoScrollDown(self) - - def setAutoScrollDown(self, on=True): - r"""setAutoScrollDown(YRichText self, bool on=True)""" - return _yui.YRichText_setAutoScrollDown(self, on) - - def shrinkable(self): - r"""shrinkable(YRichText self) -> bool""" - return _yui.YRichText_shrinkable(self) - - def setShrinkable(self, shrinkable=True): - r"""setShrinkable(YRichText self, bool shrinkable=True)""" - return _yui.YRichText_setShrinkable(self, shrinkable) - - def setProperty(self, propertyName, val): - r"""setProperty(YRichText self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YRichText_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YRichText self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YRichText_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YRichText self) -> YPropertySet""" - return _yui.YRichText_propertySet(self) - - def vScrollValue(self): - r"""vScrollValue(YRichText self) -> std::string""" - return _yui.YRichText_vScrollValue(self) - - def setVScrollValue(self, newValue): - r"""setVScrollValue(YRichText self, std::string const & newValue)""" - return _yui.YRichText_setVScrollValue(self, newValue) - - def hScrollValue(self): - r"""hScrollValue(YRichText self) -> std::string""" - return _yui.YRichText_hScrollValue(self) - - def setHScrollValue(self, newValue): - r"""setHScrollValue(YRichText self, std::string const & newValue)""" - return _yui.YRichText_setHScrollValue(self, newValue) - - def activateLink(self, url): - r"""activateLink(YRichText self, std::string const & url)""" - return _yui.YRichText_activateLink(self, url) - -# Register YRichText in _yui: -_yui.YRichText_swigregister(YRichText) - -class YSelectionBox(YSelectionWidget): - r"""Proxy of C++ YSelectionBox class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YSelectionBox - - def widgetClass(self): - r"""widgetClass(YSelectionBox self) -> char const *""" - return _yui.YSelectionBox_widgetClass(self) - - def shrinkable(self): - r"""shrinkable(YSelectionBox self) -> bool""" - return _yui.YSelectionBox_shrinkable(self) - - def setShrinkable(self, shrinkable=True): - r"""setShrinkable(YSelectionBox self, bool shrinkable=True)""" - return _yui.YSelectionBox_setShrinkable(self, shrinkable) - - def immediateMode(self): - r"""immediateMode(YSelectionBox self) -> bool""" - return _yui.YSelectionBox_immediateMode(self) - - def setImmediateMode(self, on=True): - r"""setImmediateMode(YSelectionBox self, bool on=True)""" - return _yui.YSelectionBox_setImmediateMode(self, on) - - def setProperty(self, propertyName, val): - r"""setProperty(YSelectionBox self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YSelectionBox_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YSelectionBox self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YSelectionBox_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YSelectionBox self) -> YPropertySet""" - return _yui.YSelectionBox_propertySet(self) - - def userInputProperty(self): - r"""userInputProperty(YSelectionBox self) -> char const *""" - return _yui.YSelectionBox_userInputProperty(self) - -# Register YSelectionBox in _yui: -_yui.YSelectionBox_swigregister(YSelectionBox) - -class YSettings(object): - r"""Proxy of C++ YSettings class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - - @staticmethod - def setProgDir(directory): - r"""setProgDir(std::string directory)""" - return _yui.YSettings_setProgDir(directory) - - @staticmethod - def progDir(): - r"""progDir() -> std::string""" - return _yui.YSettings_progDir() - - @staticmethod - def setIconDir(directory): - r"""setIconDir(std::string directory)""" - return _yui.YSettings_setIconDir(directory) - - @staticmethod - def iconDir(): - r"""iconDir() -> std::string""" - return _yui.YSettings_iconDir() - - @staticmethod - def setThemeDir(directory): - r"""setThemeDir(std::string directory)""" - return _yui.YSettings_setThemeDir(directory) - - @staticmethod - def themeDir(): - r"""themeDir() -> std::string""" - return _yui.YSettings_themeDir() - - @staticmethod - def setLocaleDir(directory): - r"""setLocaleDir(std::string directory)""" - return _yui.YSettings_setLocaleDir(directory) - - @staticmethod - def localeDir(): - r"""localeDir() -> std::string""" - return _yui.YSettings_localeDir() - - @staticmethod - def loadedUI(*args): - r""" - loadedUI(std::string ui) - loadedUI() -> std::string - """ - return _yui.YSettings_loadedUI(*args) - -# Register YSettings in _yui: -_yui.YSettings_swigregister(YSettings) - -def YSettings_setProgDir(directory): - r"""YSettings_setProgDir(std::string directory)""" - return _yui.YSettings_setProgDir(directory) - -def YSettings_progDir(): - r"""YSettings_progDir() -> std::string""" - return _yui.YSettings_progDir() - -def YSettings_setIconDir(directory): - r"""YSettings_setIconDir(std::string directory)""" - return _yui.YSettings_setIconDir(directory) - -def YSettings_iconDir(): - r"""YSettings_iconDir() -> std::string""" - return _yui.YSettings_iconDir() - -def YSettings_setThemeDir(directory): - r"""YSettings_setThemeDir(std::string directory)""" - return _yui.YSettings_setThemeDir(directory) - -def YSettings_themeDir(): - r"""YSettings_themeDir() -> std::string""" - return _yui.YSettings_themeDir() - -def YSettings_setLocaleDir(directory): - r"""YSettings_setLocaleDir(std::string directory)""" - return _yui.YSettings_setLocaleDir(directory) - -def YSettings_localeDir(): - r"""YSettings_localeDir() -> std::string""" - return _yui.YSettings_localeDir() - -def YSettings_loadedUI(*args): - r""" - YSettings_loadedUI(std::string ui) - YSettings_loadedUI() -> std::string - """ - return _yui.YSettings_loadedUI(*args) - -class YShortcut(object): - r"""Proxy of C++ YShortcut class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, shortcut_widget): - r"""__init__(YShortcut self, YWidget shortcut_widget) -> YShortcut""" - _yui.YShortcut_swiginit(self, _yui.new_YShortcut(shortcut_widget)) - __swig_destroy__ = _yui.delete_YShortcut - - def widget(self): - r"""widget(YShortcut self) -> YWidget""" - return _yui.YShortcut_widget(self) - - def widgetClass(self): - r"""widgetClass(YShortcut self) -> char const *""" - return _yui.YShortcut_widgetClass(self) - - def isButton(self): - r"""isButton(YShortcut self) -> bool""" - return _yui.YShortcut_isButton(self) - - def isWizardButton(self): - r"""isWizardButton(YShortcut self) -> bool""" - return _yui.YShortcut_isWizardButton(self) - - def isMenuItem(self): - r"""isMenuItem(YShortcut self) -> bool""" - return _yui.YShortcut_isMenuItem(self) - - def shortcutString(self): - r"""shortcutString(YShortcut self) -> std::string""" - return _yui.YShortcut_shortcutString(self) - - @staticmethod - def cleanShortcutString(*args): - r""" - cleanShortcutString() -> std::string - cleanShortcutString(std::string shortcutString) -> std::string - """ - return _yui.YShortcut_cleanShortcutString(*args) - - def preferred(self): - r"""preferred(YShortcut self) -> char""" - return _yui.YShortcut_preferred(self) - - def shortcut(self): - r"""shortcut(YShortcut self) -> char""" - return _yui.YShortcut_shortcut(self) - - def setShortcut(self, newShortcut): - r"""setShortcut(YShortcut self, char newShortcut)""" - return _yui.YShortcut_setShortcut(self, newShortcut) - - def clearShortcut(self): - r"""clearShortcut(YShortcut self)""" - return _yui.YShortcut_clearShortcut(self) - - def conflict(self): - r"""conflict(YShortcut self) -> bool""" - return _yui.YShortcut_conflict(self) - - def setConflict(self, newConflictState=True): - r"""setConflict(YShortcut self, bool newConflictState=True)""" - return _yui.YShortcut_setConflict(self, newConflictState) - - def distinctShortcutChars(self): - r"""distinctShortcutChars(YShortcut self) -> int""" - return _yui.YShortcut_distinctShortcutChars(self) - - def hasValidShortcutChar(self): - r"""hasValidShortcutChar(YShortcut self) -> bool""" - return _yui.YShortcut_hasValidShortcutChar(self) - - def debugLabel(self): - r"""debugLabel(YShortcut self) -> std::string""" - return _yui.YShortcut_debugLabel(self) - - @staticmethod - def shortcutMarker(): - r"""shortcutMarker() -> char""" - return _yui.YShortcut_shortcutMarker() - - @staticmethod - def findShortcutPos(str, start_pos=0): - r"""findShortcutPos(std::string const & str, std::string::size_type start_pos=0) -> std::string::size_type""" - return _yui.YShortcut_findShortcutPos(str, start_pos) - - @staticmethod - def findShortcut(str, start_pos=0): - r"""findShortcut(std::string const & str, std::string::size_type start_pos=0) -> char""" - return _yui.YShortcut_findShortcut(str, start_pos) - - @staticmethod - def isValid(c): - r"""isValid(char c) -> bool""" - return _yui.YShortcut_isValid(c) - - @staticmethod - def normalized(c): - r"""normalized(char c) -> char""" - return _yui.YShortcut_normalized(c) - - @staticmethod - def getShortcutString(widget): - r"""getShortcutString(YWidget widget) -> std::string""" - return _yui.YShortcut_getShortcutString(widget) - -# Register YShortcut in _yui: -_yui.YShortcut_swigregister(YShortcut) - -def YShortcut_cleanShortcutString(*args): - r""" - YShortcut_cleanShortcutString() -> std::string - YShortcut_cleanShortcutString(std::string shortcutString) -> std::string - """ - return _yui.YShortcut_cleanShortcutString(*args) - -def YShortcut_shortcutMarker(): - r"""YShortcut_shortcutMarker() -> char""" - return _yui.YShortcut_shortcutMarker() - -def YShortcut_findShortcutPos(str, start_pos=0): - r"""YShortcut_findShortcutPos(std::string const & str, std::string::size_type start_pos=0) -> std::string::size_type""" - return _yui.YShortcut_findShortcutPos(str, start_pos) - -def YShortcut_findShortcut(str, start_pos=0): - r"""YShortcut_findShortcut(std::string const & str, std::string::size_type start_pos=0) -> char""" - return _yui.YShortcut_findShortcut(str, start_pos) - -def YShortcut_isValid(c): - r"""YShortcut_isValid(char c) -> bool""" - return _yui.YShortcut_isValid(c) - -def YShortcut_normalized(c): - r"""YShortcut_normalized(char c) -> char""" - return _yui.YShortcut_normalized(c) - -def YShortcut_getShortcutString(widget): - r"""YShortcut_getShortcutString(YWidget widget) -> std::string""" - return _yui.YShortcut_getShortcutString(widget) - -class YItemShortcut(YShortcut): - r"""Proxy of C++ YItemShortcut class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, widget, item): - r"""__init__(YItemShortcut self, YWidget widget, YItem item) -> YItemShortcut""" - _yui.YItemShortcut_swiginit(self, _yui.new_YItemShortcut(widget, item)) - __swig_destroy__ = _yui.delete_YItemShortcut - - def item(self): - r"""item(YItemShortcut self) -> YItem""" - return _yui.YItemShortcut_item(self) - - def setShortcut(self, newShortcut): - r"""setShortcut(YItemShortcut self, char newShortcut)""" - return _yui.YItemShortcut_setShortcut(self, newShortcut) - - def isMenuItem(self): - r"""isMenuItem(YItemShortcut self) -> bool""" - return _yui.YItemShortcut_isMenuItem(self) - - def debugLabel(self): - r"""debugLabel(YItemShortcut self) -> std::string""" - return _yui.YItemShortcut_debugLabel(self) - -# Register YItemShortcut in _yui: -_yui.YItemShortcut_swigregister(YItemShortcut) - -class YShortcutManager(object): - r"""Proxy of C++ YShortcutManager class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, dialog): - r"""__init__(YShortcutManager self, YDialog dialog) -> YShortcutManager""" - _yui.YShortcutManager_swiginit(self, _yui.new_YShortcutManager(dialog)) - __swig_destroy__ = _yui.delete_YShortcutManager - - def checkShortcuts(self, autoResolve=True): - r"""checkShortcuts(YShortcutManager self, bool autoResolve=True)""" - return _yui.YShortcutManager_checkShortcuts(self, autoResolve) - - def conflictCount(self): - r"""conflictCount(YShortcutManager self) -> int""" - return _yui.YShortcutManager_conflictCount(self) - - def resolveAllConflicts(self): - r"""resolveAllConflicts(YShortcutManager self)""" - return _yui.YShortcutManager_resolveAllConflicts(self) - - def dialog(self): - r"""dialog(YShortcutManager self) -> YDialog""" - return _yui.YShortcutManager_dialog(self) - -# Register YShortcutManager in _yui: -_yui.YShortcutManager_swigregister(YShortcutManager) - -class YSimpleEventHandler(object): - r"""Proxy of C++ YSimpleEventHandler class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YSimpleEventHandler self) -> YSimpleEventHandler""" - _yui.YSimpleEventHandler_swiginit(self, _yui.new_YSimpleEventHandler()) - __swig_destroy__ = _yui.delete_YSimpleEventHandler - - def sendEvent(self, event_disown): - r"""sendEvent(YSimpleEventHandler self, YEvent event_disown)""" - return _yui.YSimpleEventHandler_sendEvent(self, event_disown) - - def eventPendingFor(self, widget): - r"""eventPendingFor(YSimpleEventHandler self, YWidget widget) -> bool""" - return _yui.YSimpleEventHandler_eventPendingFor(self, widget) - - def pendingEvent(self): - r"""pendingEvent(YSimpleEventHandler self) -> YEvent""" - return _yui.YSimpleEventHandler_pendingEvent(self) - - def consumePendingEvent(self): - r"""consumePendingEvent(YSimpleEventHandler self) -> YEvent""" - return _yui.YSimpleEventHandler_consumePendingEvent(self) - - def deletePendingEventsFor(self, widget): - r"""deletePendingEventsFor(YSimpleEventHandler self, YWidget widget)""" - return _yui.YSimpleEventHandler_deletePendingEventsFor(self, widget) - - def clear(self): - r"""clear(YSimpleEventHandler self)""" - return _yui.YSimpleEventHandler_clear(self) - - def blockEvents(self, block=True): - r"""blockEvents(YSimpleEventHandler self, bool block=True)""" - return _yui.YSimpleEventHandler_blockEvents(self, block) - - def unblockEvents(self): - r"""unblockEvents(YSimpleEventHandler self)""" - return _yui.YSimpleEventHandler_unblockEvents(self) - - def eventsBlocked(self): - r"""eventsBlocked(YSimpleEventHandler self) -> bool""" - return _yui.YSimpleEventHandler_eventsBlocked(self) - - def deleteEvent(self, event): - r"""deleteEvent(YSimpleEventHandler self, YEvent event)""" - return _yui.YSimpleEventHandler_deleteEvent(self, event) - -# Register YSimpleEventHandler in _yui: -_yui.YSimpleEventHandler_swigregister(YSimpleEventHandler) - -class YSlider(YIntField): - r"""Proxy of C++ YSlider class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YSlider - - def widgetClass(self): - r"""widgetClass(YSlider self) -> char const *""" - return _yui.YSlider_widgetClass(self) - -# Register YSlider in _yui: -_yui.YSlider_swigregister(YSlider) - -class YSpacing(YWidget): - r"""Proxy of C++ YSpacing class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YSpacing - - def widgetClass(self): - r"""widgetClass(YSpacing self) -> char const *""" - return _yui.YSpacing_widgetClass(self) - - def dimension(self): - r"""dimension(YSpacing self) -> YUIDimension""" - return _yui.YSpacing_dimension(self) - - def size(self, *args): - r""" - size(YSpacing self) -> int - size(YSpacing self, YUIDimension dim) -> int - """ - return _yui.YSpacing_size(self, *args) - - def preferredWidth(self): - r"""preferredWidth(YSpacing self) -> int""" - return _yui.YSpacing_preferredWidth(self) - - def preferredHeight(self): - r"""preferredHeight(YSpacing self) -> int""" - return _yui.YSpacing_preferredHeight(self) - -# Register YSpacing in _yui: -_yui.YSpacing_swigregister(YSpacing) - -class YSquash(YSingleChildContainerWidget): - r"""Proxy of C++ YSquash class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YSquash - - def widgetClass(self): - r"""widgetClass(YSquash self) -> char const *""" - return _yui.YSquash_widgetClass(self) - - def horSquash(self): - r"""horSquash(YSquash self) -> bool""" - return _yui.YSquash_horSquash(self) - - def vertSquash(self): - r"""vertSquash(YSquash self) -> bool""" - return _yui.YSquash_vertSquash(self) - - def stretchable(self, dim): - r"""stretchable(YSquash self, YUIDimension dim) -> bool""" - return _yui.YSquash_stretchable(self, dim) - -# Register YSquash in _yui: -_yui.YSquash_swigregister(YSquash) - -class YTable(YSelectionWidget): - r"""Proxy of C++ YTable class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YTable - - def widgetClass(self): - r"""widgetClass(YTable self) -> char const *""" - return _yui.YTable_widgetClass(self) - - def columns(self): - r"""columns(YTable self) -> int""" - return _yui.YTable_columns(self) - - def hasColumn(self, column): - r"""hasColumn(YTable self, int column) -> bool""" - return _yui.YTable_hasColumn(self, column) - - def header(self, column): - r"""header(YTable self, int column) -> std::string""" - return _yui.YTable_header(self, column) - - def alignment(self, column): - r"""alignment(YTable self, int column) -> YAlignmentType""" - return _yui.YTable_alignment(self, column) - - def immediateMode(self): - r"""immediateMode(YTable self) -> bool""" - return _yui.YTable_immediateMode(self) - - def setImmediateMode(self, immediateMode=True): - r"""setImmediateMode(YTable self, bool immediateMode=True)""" - return _yui.YTable_setImmediateMode(self, immediateMode) - - def keepSorting(self): - r"""keepSorting(YTable self) -> bool""" - return _yui.YTable_keepSorting(self) - - def setKeepSorting(self, keepSorting): - r"""setKeepSorting(YTable self, bool keepSorting)""" - return _yui.YTable_setKeepSorting(self, keepSorting) - - def hasMultiSelection(self): - r"""hasMultiSelection(YTable self) -> bool""" - return _yui.YTable_hasMultiSelection(self) - - def findItem(self, *args): - r""" - findItem(YTable self, std::string const & wantedItemLabel, int column) -> YItem - findItem(YTable self, std::string const & wantedItemLabel, int column, YItemConstIterator begin, YItemConstIterator end) -> YItem - """ - return _yui.YTable_findItem(self, *args) - - def cellChanged(self, cell): - r"""cellChanged(YTable self, YTableCell cell)""" - return _yui.YTable_cellChanged(self, cell) - - def setProperty(self, propertyName, val): - r"""setProperty(YTable self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YTable_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YTable self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YTable_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YTable self) -> YPropertySet""" - return _yui.YTable_propertySet(self) - - def userInputProperty(self): - r"""userInputProperty(YTable self) -> char const *""" - return _yui.YTable_userInputProperty(self) - -# Register YTable in _yui: -_yui.YTable_swigregister(YTable) - -class YTableHeader(object): - r"""Proxy of C++ YTableHeader class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YTableHeader self) -> YTableHeader""" - _yui.YTableHeader_swiginit(self, _yui.new_YTableHeader()) - __swig_destroy__ = _yui.delete_YTableHeader - - def addColumn(self, header, alignment=YAlignBegin): - r"""addColumn(YTableHeader self, std::string const & header, YAlignmentType alignment=YAlignBegin)""" - return _yui.YTableHeader_addColumn(self, header, alignment) - - def columns(self): - r"""columns(YTableHeader self) -> int""" - return _yui.YTableHeader_columns(self) - - def hasColumn(self, column): - r"""hasColumn(YTableHeader self, int column) -> bool""" - return _yui.YTableHeader_hasColumn(self, column) - - def header(self, column): - r"""header(YTableHeader self, int column) -> std::string""" - return _yui.YTableHeader_header(self, column) - - def alignment(self, column): - r"""alignment(YTableHeader self, int column) -> YAlignmentType""" - return _yui.YTableHeader_alignment(self, column) - -# Register YTableHeader in _yui: -_yui.YTableHeader_swigregister(YTableHeader) - -class YTableItem(YTreeItem): - r"""Proxy of C++ YTableItem class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YTableItem self) -> YTableItem - __init__(YTableItem self, YTableItem parent, bool isOpen=False) -> YTableItem - __init__(YTableItem self, std::string const & label_0, std::string const & label_1=std::string(), std::string const & label_2=std::string(), std::string const & label_3=std::string(), std::string const & label_4=std::string(), std::string const & label_5=std::string(), std::string const & label_6=std::string(), std::string const & label_7=std::string(), std::string const & label_8=std::string(), std::string const & label_9=std::string()) -> YTableItem - __init__(YTableItem self, YTableItem parent, std::string const & label_0, std::string const & label_1=std::string(), std::string const & label_2=std::string(), std::string const & label_3=std::string(), std::string const & label_4=std::string(), std::string const & label_5=std::string(), std::string const & label_6=std::string(), std::string const & label_7=std::string(), std::string const & label_8=std::string(), std::string const & label_9=std::string()) -> YTableItem - """ - _yui.YTableItem_swiginit(self, _yui.new_YTableItem(*args)) - __swig_destroy__ = _yui.delete_YTableItem - - def itemClass(self): - r"""itemClass(YTableItem self) -> char const *""" - return _yui.YTableItem_itemClass(self) - - def addCell(self, *args): - r""" - addCell(YTableItem self, YTableCell cell_disown) - addCell(YTableItem self, std::string const & label, std::string const & iconName=std::string(), std::string const & sortKey=std::string()) - """ - return _yui.YTableItem_addCell(self, *args) - - def addCells(self, *args): - r"""addCells(YTableItem self, std::string const & label_0, std::string const & label_1, std::string const & label_2=std::string(), std::string const & label_3=std::string(), std::string const & label_4=std::string(), std::string const & label_5=std::string(), std::string const & label_6=std::string(), std::string const & label_7=std::string(), std::string const & label_8=std::string(), std::string const & label_9=std::string())""" - return _yui.YTableItem_addCells(self, *args) - - def deleteCells(self): - r"""deleteCells(YTableItem self)""" - return _yui.YTableItem_deleteCells(self) - - def cellsBegin(self, *args): - r""" - cellsBegin(YTableItem self) -> YTableCellIterator - cellsBegin(YTableItem self) -> YTableCellConstIterator - """ - return _yui.YTableItem_cellsBegin(self, *args) - - def cellsEnd(self, *args): - r""" - cellsEnd(YTableItem self) -> YTableCellIterator - cellsEnd(YTableItem self) -> YTableCellConstIterator - """ - return _yui.YTableItem_cellsEnd(self, *args) - - def cell(self, *args): - r""" - cell(YTableItem self, int index) -> YTableCell - cell(YTableItem self, int index) -> YTableCell - """ - return _yui.YTableItem_cell(self, *args) - - def cellCount(self): - r"""cellCount(YTableItem self) -> int""" - return _yui.YTableItem_cellCount(self) - - def hasCell(self, index): - r"""hasCell(YTableItem self, int index) -> bool""" - return _yui.YTableItem_hasCell(self, index) - - def iconName(self, index): - r"""iconName(YTableItem self, int index) -> std::string""" - return _yui.YTableItem_iconName(self, index) - - def hasIconName(self, index): - r"""hasIconName(YTableItem self, int index) -> bool""" - return _yui.YTableItem_hasIconName(self, index) - - def label(self, *args): - r""" - label(YTableItem self, int index) -> std::string - label(YTableItem self) -> std::string - """ - return _yui.YTableItem_label(self, *args) - - def debugLabel(self): - r"""debugLabel(YTableItem self) -> std::string""" - return _yui.YTableItem_debugLabel(self) - -# Register YTableItem in _yui: -_yui.YTableItem_swigregister(YTableItem) - -class YTableCell(object): - r"""Proxy of C++ YTableCell class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YTableCell self, std::string const & label, std::string const & iconName="", std::string const & sortKey="") -> YTableCell - __init__(YTableCell self, YTableItem parent, int column, std::string const & label, std::string const & iconName="", std::string const & sortKey="") -> YTableCell - """ - _yui.YTableCell_swiginit(self, _yui.new_YTableCell(*args)) - __swig_destroy__ = _yui.delete_YTableCell - - def label(self): - r"""label(YTableCell self) -> std::string""" - return _yui.YTableCell_label(self) - - def setLabel(self, newLabel): - r"""setLabel(YTableCell self, std::string const & newLabel)""" - return _yui.YTableCell_setLabel(self, newLabel) - - def iconName(self): - r"""iconName(YTableCell self) -> std::string""" - return _yui.YTableCell_iconName(self) - - def hasIconName(self): - r"""hasIconName(YTableCell self) -> bool""" - return _yui.YTableCell_hasIconName(self) - - def setIconName(self, newIconName): - r"""setIconName(YTableCell self, std::string const & newIconName)""" - return _yui.YTableCell_setIconName(self, newIconName) - - def sortKey(self): - r"""sortKey(YTableCell self) -> std::string""" - return _yui.YTableCell_sortKey(self) - - def hasSortKey(self): - r"""hasSortKey(YTableCell self) -> bool""" - return _yui.YTableCell_hasSortKey(self) - - def setSortKey(self, newSortKey): - r"""setSortKey(YTableCell self, std::string const & newSortKey)""" - return _yui.YTableCell_setSortKey(self, newSortKey) - - def parent(self): - r"""parent(YTableCell self) -> YTableItem""" - return _yui.YTableCell_parent(self) - - def column(self): - r"""column(YTableCell self) -> int""" - return _yui.YTableCell_column(self) - - def itemIndex(self): - r"""itemIndex(YTableCell self) -> int""" - return _yui.YTableCell_itemIndex(self) - - def reparent(self, parent, column): - r"""reparent(YTableCell self, YTableItem parent, int column)""" - return _yui.YTableCell_reparent(self, parent, column) - -# Register YTableCell in _yui: -_yui.YTableCell_swigregister(YTableCell) - -class YTimeField(YSimpleInputField): - r"""Proxy of C++ YTimeField class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YTimeField - - def widgetClass(self): - r"""widgetClass(YTimeField self) -> char const *""" - return _yui.YTimeField_widgetClass(self) - -# Register YTimeField in _yui: -_yui.YTimeField_swigregister(YTimeField) - -class YTimezoneSelector(YWidget): - r"""Proxy of C++ YTimezoneSelector class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YTimezoneSelector - - def widgetClass(self): - r"""widgetClass(YTimezoneSelector self) -> char const *""" - return _yui.YTimezoneSelector_widgetClass(self) - - def setProperty(self, propertyName, val): - r"""setProperty(YTimezoneSelector self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YTimezoneSelector_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YTimezoneSelector self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YTimezoneSelector_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YTimezoneSelector self) -> YPropertySet""" - return _yui.YTimezoneSelector_propertySet(self) - - def currentZone(self): - r"""currentZone(YTimezoneSelector self) -> std::string""" - return _yui.YTimezoneSelector_currentZone(self) - - def setCurrentZone(self, zone, zoom): - r"""setCurrentZone(YTimezoneSelector self, std::string const & zone, bool zoom)""" - return _yui.YTimezoneSelector_setCurrentZone(self, zone, zoom) - -# Register YTimezoneSelector in _yui: -_yui.YTimezoneSelector_swigregister(YTimezoneSelector) - -class YTransText(object): - r"""Proxy of C++ YTransText class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YTransText self, std::string const & orig, std::string const & translation) -> YTransText - __init__(YTransText self, std::string const & orig) -> YTransText - __init__(YTransText self, YTransText src) -> YTransText - """ - _yui.YTransText_swiginit(self, _yui.new_YTransText(*args)) - - def orig(self): - r"""orig(YTransText self) -> std::string const &""" - return _yui.YTransText_orig(self) - - def translation(self): - r"""translation(YTransText self) -> std::string const &""" - return _yui.YTransText_translation(self) - - def trans(self): - r"""trans(YTransText self) -> std::string const &""" - return _yui.YTransText_trans(self) - - def setOrig(self, newOrig): - r"""setOrig(YTransText self, std::string const & newOrig)""" - return _yui.YTransText_setOrig(self, newOrig) - - def setTranslation(self, newTrans): - r"""setTranslation(YTransText self, std::string const & newTrans)""" - return _yui.YTransText_setTranslation(self, newTrans) - - def __lt__(self, other): - r"""__lt__(YTransText self, YTransText other) -> bool""" - return _yui.YTransText___lt__(self, other) - - def __gt__(self, other): - r"""__gt__(YTransText self, YTransText other) -> bool""" - return _yui.YTransText___gt__(self, other) - - def __eq__(self, other): - r"""__eq__(YTransText self, YTransText other) -> bool""" - return _yui.YTransText___eq__(self, other) - __swig_destroy__ = _yui.delete_YTransText - -# Register YTransText in _yui: -_yui.YTransText_swigregister(YTransText) - -class YTree(YSelectionWidget): - r"""Proxy of C++ YTree class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YTree - - def widgetClass(self): - r"""widgetClass(YTree self) -> char const *""" - return _yui.YTree_widgetClass(self) - - def rebuildTree(self): - r"""rebuildTree(YTree self)""" - return _yui.YTree_rebuildTree(self) - - def addItems(self, itemCollection): - r"""addItems(YTree self, YItemCollection itemCollection)""" - return _yui.YTree_addItems(self, itemCollection) - - def immediateMode(self): - r"""immediateMode(YTree self) -> bool""" - return _yui.YTree_immediateMode(self) - - def setImmediateMode(self, on=True): - r"""setImmediateMode(YTree self, bool on=True)""" - return _yui.YTree_setImmediateMode(self, on) - - def setProperty(self, propertyName, val): - r"""setProperty(YTree self, std::string const & propertyName, YPropertyValue val) -> bool""" - return _yui.YTree_setProperty(self, propertyName, val) - - def getProperty(self, propertyName): - r"""getProperty(YTree self, std::string const & propertyName) -> YPropertyValue""" - return _yui.YTree_getProperty(self, propertyName) - - def propertySet(self): - r"""propertySet(YTree self) -> YPropertySet""" - return _yui.YTree_propertySet(self) - - def userInputProperty(self): - r"""userInputProperty(YTree self) -> char const *""" - return _yui.YTree_userInputProperty(self) - - def hasMultiSelection(self): - r"""hasMultiSelection(YTree self) -> bool""" - return _yui.YTree_hasMultiSelection(self) - - def currentItem(self): - r"""currentItem(YTree self) -> YTreeItem""" - return _yui.YTree_currentItem(self) - - def activate(self): - r"""activate(YTree self)""" - return _yui.YTree_activate(self) - - def findItem(self, path): - r"""findItem(YTree self, std::vector< std::string,std::allocator< std::string > > const & path) -> YTreeItem""" - return _yui.YTree_findItem(self, path) - -# Register YTree in _yui: -_yui.YTree_swigregister(YTree) - -class YCodeLocation(object): - r"""Proxy of C++ YCodeLocation class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YCodeLocation self, std::string const & file_r, std::string const & func_r, int line_r) -> YCodeLocation - __init__(YCodeLocation self) -> YCodeLocation - """ - _yui.YCodeLocation_swiginit(self, _yui.new_YCodeLocation(*args)) - - def file(self): - r"""file(YCodeLocation self) -> std::string""" - return _yui.YCodeLocation_file(self) - - def func(self): - r"""func(YCodeLocation self) -> std::string""" - return _yui.YCodeLocation_func(self) - - def line(self): - r"""line(YCodeLocation self) -> int""" - return _yui.YCodeLocation_line(self) - - def asString(self): - r"""asString(YCodeLocation self) -> std::string""" - return _yui.YCodeLocation_asString(self) - __swig_destroy__ = _yui.delete_YCodeLocation - -# Register YCodeLocation in _yui: -_yui.YCodeLocation_swigregister(YCodeLocation) - -class YUIException(object): - r"""Proxy of C++ YUIException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YUIException self) -> YUIException - __init__(YUIException self, std::string const & msg_r) -> YUIException - """ - _yui.YUIException_swiginit(self, _yui.new_YUIException(*args)) - __swig_destroy__ = _yui.delete_YUIException - - def where(self): - r"""where(YUIException self) -> YCodeLocation""" - return _yui.YUIException_where(self) - - def relocate(self, newLocation): - r"""relocate(YUIException self, YCodeLocation newLocation)""" - return _yui.YUIException_relocate(self, newLocation) - - def msg(self): - r"""msg(YUIException self) -> std::string const &""" - return _yui.YUIException_msg(self) - - def setMsg(self, msg): - r"""setMsg(YUIException self, std::string const & msg)""" - return _yui.YUIException_setMsg(self, msg) - - def asString(self): - r"""asString(YUIException self) -> std::string""" - return _yui.YUIException_asString(self) - - @staticmethod - def strErrno(*args): - r""" - strErrno(int errno_r) -> std::string - strErrno(int errno_r, std::string const & msg) -> std::string - """ - return _yui.YUIException_strErrno(*args) - - @staticmethod - def log(exception, location, prefix): - r"""log(YUIException exception, YCodeLocation location, char const *const prefix)""" - return _yui.YUIException_log(exception, location, prefix) - - def what(self): - r"""what(YUIException self) -> char const *""" - return _yui.YUIException_what(self) - -# Register YUIException in _yui: -_yui.YUIException_swigregister(YUIException) - -def YUIException_strErrno(*args): - r""" - YUIException_strErrno(int errno_r) -> std::string - YUIException_strErrno(int errno_r, std::string const & msg) -> std::string - """ - return _yui.YUIException_strErrno(*args) - -def YUIException_log(exception, location, prefix): - r"""YUIException_log(YUIException exception, YCodeLocation location, char const *const prefix)""" - return _yui.YUIException_log(exception, location, prefix) - -class YUINullPointerException(YUIException): - r"""Proxy of C++ YUINullPointerException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YUINullPointerException self) -> YUINullPointerException""" - _yui.YUINullPointerException_swiginit(self, _yui.new_YUINullPointerException()) - __swig_destroy__ = _yui.delete_YUINullPointerException - -# Register YUINullPointerException in _yui: -_yui.YUINullPointerException_swigregister(YUINullPointerException) - -class YUIOutOfMemoryException(YUIException): - r"""Proxy of C++ YUIOutOfMemoryException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YUIOutOfMemoryException self) -> YUIOutOfMemoryException""" - _yui.YUIOutOfMemoryException_swiginit(self, _yui.new_YUIOutOfMemoryException()) - __swig_destroy__ = _yui.delete_YUIOutOfMemoryException - -# Register YUIOutOfMemoryException in _yui: -_yui.YUIOutOfMemoryException_swigregister(YUIOutOfMemoryException) - -class YUIInvalidWidgetException(YUIException): - r"""Proxy of C++ YUIInvalidWidgetException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YUIInvalidWidgetException self) -> YUIInvalidWidgetException""" - _yui.YUIInvalidWidgetException_swiginit(self, _yui.new_YUIInvalidWidgetException()) - __swig_destroy__ = _yui.delete_YUIInvalidWidgetException - -# Register YUIInvalidWidgetException in _yui: -_yui.YUIInvalidWidgetException_swigregister(YUIInvalidWidgetException) - -class YUIWidgetNotFoundException(YUIException): - r"""Proxy of C++ YUIWidgetNotFoundException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, idString): - r"""__init__(YUIWidgetNotFoundException self, std::string const & idString) -> YUIWidgetNotFoundException""" - _yui.YUIWidgetNotFoundException_swiginit(self, _yui.new_YUIWidgetNotFoundException(idString)) - __swig_destroy__ = _yui.delete_YUIWidgetNotFoundException - -# Register YUIWidgetNotFoundException in _yui: -_yui.YUIWidgetNotFoundException_swigregister(YUIWidgetNotFoundException) - -class YUINoDialogException(YUIException): - r"""Proxy of C++ YUINoDialogException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YUINoDialogException self) -> YUINoDialogException""" - _yui.YUINoDialogException_swiginit(self, _yui.new_YUINoDialogException()) - __swig_destroy__ = _yui.delete_YUINoDialogException - -# Register YUINoDialogException in _yui: -_yui.YUINoDialogException_swigregister(YUINoDialogException) - -class YUIDialogStackingOrderException(YUIException): - r"""Proxy of C++ YUIDialogStackingOrderException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YUIDialogStackingOrderException self) -> YUIDialogStackingOrderException""" - _yui.YUIDialogStackingOrderException_swiginit(self, _yui.new_YUIDialogStackingOrderException()) - __swig_destroy__ = _yui.delete_YUIDialogStackingOrderException - -# Register YUIDialogStackingOrderException in _yui: -_yui.YUIDialogStackingOrderException_swigregister(YUIDialogStackingOrderException) - -class YUISyntaxErrorException(YUIException): - r"""Proxy of C++ YUISyntaxErrorException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, msg): - r"""__init__(YUISyntaxErrorException self, std::string const & msg) -> YUISyntaxErrorException""" - _yui.YUISyntaxErrorException_swiginit(self, _yui.new_YUISyntaxErrorException(msg)) - __swig_destroy__ = _yui.delete_YUISyntaxErrorException - -# Register YUISyntaxErrorException in _yui: -_yui.YUISyntaxErrorException_swigregister(YUISyntaxErrorException) - -class YUIPropertyException(YUIException): - r"""Proxy of C++ YUIPropertyException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - - def property(self): - r"""property(YUIPropertyException self) -> YProperty""" - return _yui.YUIPropertyException_property(self) - - def widget(self): - r"""widget(YUIPropertyException self) -> YWidget""" - return _yui.YUIPropertyException_widget(self) - - def setWidget(self, w): - r"""setWidget(YUIPropertyException self, YWidget w)""" - return _yui.YUIPropertyException_setWidget(self, w) - -# Register YUIPropertyException in _yui: -_yui.YUIPropertyException_swigregister(YUIPropertyException) - -class YUIUnknownPropertyException(YUIPropertyException): - r"""Proxy of C++ YUIUnknownPropertyException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, propertyName, widget=None): - r"""__init__(YUIUnknownPropertyException self, std::string const & propertyName, YWidget widget=None) -> YUIUnknownPropertyException""" - _yui.YUIUnknownPropertyException_swiginit(self, _yui.new_YUIUnknownPropertyException(propertyName, widget)) - __swig_destroy__ = _yui.delete_YUIUnknownPropertyException - -# Register YUIUnknownPropertyException in _yui: -_yui.YUIUnknownPropertyException_swigregister(YUIUnknownPropertyException) - -class YUIPropertyTypeMismatchException(YUIPropertyException): - r"""Proxy of C++ YUIPropertyTypeMismatchException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, property, type, widget=None): - r"""__init__(YUIPropertyTypeMismatchException self, YProperty property, YPropertyType type, YWidget widget=None) -> YUIPropertyTypeMismatchException""" - _yui.YUIPropertyTypeMismatchException_swiginit(self, _yui.new_YUIPropertyTypeMismatchException(property, type, widget)) - __swig_destroy__ = _yui.delete_YUIPropertyTypeMismatchException - - def type(self): - r"""type(YUIPropertyTypeMismatchException self) -> YPropertyType""" - return _yui.YUIPropertyTypeMismatchException_type(self) - -# Register YUIPropertyTypeMismatchException in _yui: -_yui.YUIPropertyTypeMismatchException_swigregister(YUIPropertyTypeMismatchException) - -class YUISetReadOnlyPropertyException(YUIPropertyException): - r"""Proxy of C++ YUISetReadOnlyPropertyException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, property, widget=None): - r"""__init__(YUISetReadOnlyPropertyException self, YProperty property, YWidget widget=None) -> YUISetReadOnlyPropertyException""" - _yui.YUISetReadOnlyPropertyException_swiginit(self, _yui.new_YUISetReadOnlyPropertyException(property, widget)) - __swig_destroy__ = _yui.delete_YUISetReadOnlyPropertyException - -# Register YUISetReadOnlyPropertyException in _yui: -_yui.YUISetReadOnlyPropertyException_swigregister(YUISetReadOnlyPropertyException) - -class YUIBadPropertyArgException(YUIPropertyException): - r"""Proxy of C++ YUIBadPropertyArgException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r"""__init__(YUIBadPropertyArgException self, YProperty property, YWidget widget, std::string const & message="") -> YUIBadPropertyArgException""" - _yui.YUIBadPropertyArgException_swiginit(self, _yui.new_YUIBadPropertyArgException(*args)) - __swig_destroy__ = _yui.delete_YUIBadPropertyArgException - -# Register YUIBadPropertyArgException in _yui: -_yui.YUIBadPropertyArgException_swigregister(YUIBadPropertyArgException) - -class YUIUnsupportedWidgetException(YUIException): - r"""Proxy of C++ YUIUnsupportedWidgetException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, widgetType): - r"""__init__(YUIUnsupportedWidgetException self, std::string const & widgetType) -> YUIUnsupportedWidgetException""" - _yui.YUIUnsupportedWidgetException_swiginit(self, _yui.new_YUIUnsupportedWidgetException(widgetType)) - __swig_destroy__ = _yui.delete_YUIUnsupportedWidgetException - -# Register YUIUnsupportedWidgetException in _yui: -_yui.YUIUnsupportedWidgetException_swigregister(YUIUnsupportedWidgetException) - -class YUIInvalidDimensionException(YUIException): - r"""Proxy of C++ YUIInvalidDimensionException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YUIInvalidDimensionException self) -> YUIInvalidDimensionException""" - _yui.YUIInvalidDimensionException_swiginit(self, _yui.new_YUIInvalidDimensionException()) - __swig_destroy__ = _yui.delete_YUIInvalidDimensionException - -# Register YUIInvalidDimensionException in _yui: -_yui.YUIInvalidDimensionException_swigregister(YUIInvalidDimensionException) - -class YUIIndexOutOfRangeException(YUIException): - r"""Proxy of C++ YUIIndexOutOfRangeException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r"""__init__(YUIIndexOutOfRangeException self, int invalidIndex, int validMin, int validMax, std::string const & msg="") -> YUIIndexOutOfRangeException""" - _yui.YUIIndexOutOfRangeException_swiginit(self, _yui.new_YUIIndexOutOfRangeException(*args)) - __swig_destroy__ = _yui.delete_YUIIndexOutOfRangeException - - def invalidIndex(self): - r"""invalidIndex(YUIIndexOutOfRangeException self) -> int""" - return _yui.YUIIndexOutOfRangeException_invalidIndex(self) - - def validMin(self): - r"""validMin(YUIIndexOutOfRangeException self) -> int""" - return _yui.YUIIndexOutOfRangeException_validMin(self) - - def validMax(self): - r"""validMax(YUIIndexOutOfRangeException self) -> int""" - return _yui.YUIIndexOutOfRangeException_validMax(self) - -# Register YUIIndexOutOfRangeException in _yui: -_yui.YUIIndexOutOfRangeException_swigregister(YUIIndexOutOfRangeException) - -class YUIPluginException(YUIException): - r"""Proxy of C++ YUIPluginException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, pluginName): - r"""__init__(YUIPluginException self, std::string const & pluginName) -> YUIPluginException""" - _yui.YUIPluginException_swiginit(self, _yui.new_YUIPluginException(pluginName)) - __swig_destroy__ = _yui.delete_YUIPluginException - -# Register YUIPluginException in _yui: -_yui.YUIPluginException_swigregister(YUIPluginException) - -class YUICantLoadAnyUIException(YUIException): - r"""Proxy of C++ YUICantLoadAnyUIException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YUICantLoadAnyUIException self) -> YUICantLoadAnyUIException""" - _yui.YUICantLoadAnyUIException_swiginit(self, _yui.new_YUICantLoadAnyUIException()) - __swig_destroy__ = _yui.delete_YUICantLoadAnyUIException - -# Register YUICantLoadAnyUIException in _yui: -_yui.YUICantLoadAnyUIException_swigregister(YUICantLoadAnyUIException) - -class YUIButtonRoleMismatchException(YUIException): - r"""Proxy of C++ YUIButtonRoleMismatchException class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, msg): - r"""__init__(YUIButtonRoleMismatchException self, std::string const & msg) -> YUIButtonRoleMismatchException""" - _yui.YUIButtonRoleMismatchException_swiginit(self, _yui.new_YUIButtonRoleMismatchException(msg)) - __swig_destroy__ = _yui.delete_YUIButtonRoleMismatchException - -# Register YUIButtonRoleMismatchException in _yui: -_yui.YUIButtonRoleMismatchException_swigregister(YUIButtonRoleMismatchException) - -YUIPlugin_Qt = _yui.YUIPlugin_Qt - -YUIPlugin_NCurses = _yui.YUIPlugin_NCurses - -YUIPlugin_Gtk = _yui.YUIPlugin_Gtk - -YUIPlugin_RestAPI = _yui.YUIPlugin_RestAPI - -YUIPlugin_Ncurses_RestAPI = _yui.YUIPlugin_Ncurses_RestAPI - -YUIPlugin_Qt_RestAPI = _yui.YUIPlugin_Qt_RestAPI - -class YUILoader(object): - r"""Proxy of C++ YUILoader class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - - @staticmethod - def loadUI(withThreads=False): - r"""loadUI(bool withThreads=False)""" - return _yui.YUILoader_loadUI(withThreads) - - @staticmethod - def deleteUI(): - r"""deleteUI()""" - return _yui.YUILoader_deleteUI() - - @staticmethod - def loadRestAPIPlugin(wantedGUI, withThreads=False): - r"""loadRestAPIPlugin(std::string const & wantedGUI, bool withThreads=False)""" - return _yui.YUILoader_loadRestAPIPlugin(wantedGUI, withThreads) - - @staticmethod - def loadPlugin(name, withThreads=False): - r"""loadPlugin(std::string const & name, bool withThreads=False)""" - return _yui.YUILoader_loadPlugin(name, withThreads) - - @staticmethod - def pluginExists(pluginBaseName): - r"""pluginExists(std::string const & pluginBaseName) -> bool""" - return _yui.YUILoader_pluginExists(pluginBaseName) - - @staticmethod - def loadExternalWidgets(*args): - r"""loadExternalWidgets(std::string const & name, std::string const & symbol="_Z21createExternalWidgetsPKc")""" - return _yui.YUILoader_loadExternalWidgets(*args) - -# Register YUILoader in _yui: -_yui.YUILoader_swigregister(YUILoader) - -def YUILoader_loadUI(withThreads=False): - r"""YUILoader_loadUI(bool withThreads=False)""" - return _yui.YUILoader_loadUI(withThreads) - -def YUILoader_deleteUI(): - r"""YUILoader_deleteUI()""" - return _yui.YUILoader_deleteUI() - -def YUILoader_loadRestAPIPlugin(wantedGUI, withThreads=False): - r"""YUILoader_loadRestAPIPlugin(std::string const & wantedGUI, bool withThreads=False)""" - return _yui.YUILoader_loadRestAPIPlugin(wantedGUI, withThreads) - -def YUILoader_loadPlugin(name, withThreads=False): - r"""YUILoader_loadPlugin(std::string const & name, bool withThreads=False)""" - return _yui.YUILoader_loadPlugin(name, withThreads) - -def YUILoader_pluginExists(pluginBaseName): - r"""YUILoader_pluginExists(std::string const & pluginBaseName) -> bool""" - return _yui.YUILoader_pluginExists(pluginBaseName) - -def YUILoader_loadExternalWidgets(*args): - r"""YUILoader_loadExternalWidgets(std::string const & name, std::string const & symbol="_Z21createExternalWidgetsPKc")""" - return _yui.YUILoader_loadExternalWidgets(*args) - -YUIBuiltin_AskForExistingDirectory = _yui.YUIBuiltin_AskForExistingDirectory - -YUIBuiltin_AskForExistingFile = _yui.YUIBuiltin_AskForExistingFile - -YUIBuiltin_AskForSaveFileName = _yui.YUIBuiltin_AskForSaveFileName - -YUIBuiltin_AskForWidgetStyle = _yui.YUIBuiltin_AskForWidgetStyle - -YUIBuiltin_Beep = _yui.YUIBuiltin_Beep - -YUIBuiltin_BusyCursor = _yui.YUIBuiltin_BusyCursor - -YUIBuiltin_OpenContextMenu = _yui.YUIBuiltin_OpenContextMenu - -YUIBuiltin_ChangeWidget = _yui.YUIBuiltin_ChangeWidget - -YUIBuiltin_CloseDialog = _yui.YUIBuiltin_CloseDialog - -YUIBuiltin_CloseUI = _yui.YUIBuiltin_CloseUI - -YUIBuiltin_DumpWidgetTree = _yui.YUIBuiltin_DumpWidgetTree - -YUIBuiltin_GetDisplayInfo = _yui.YUIBuiltin_GetDisplayInfo - -YUIBuiltin_GetLanguage = _yui.YUIBuiltin_GetLanguage - -YUIBuiltin_GetProductName = _yui.YUIBuiltin_GetProductName - -YUIBuiltin_Glyph = _yui.YUIBuiltin_Glyph - -YUIBuiltin_HasSpecialWidget = _yui.YUIBuiltin_HasSpecialWidget - -YUIBuiltin_MakeScreenShot = _yui.YUIBuiltin_MakeScreenShot - -YUIBuiltin_NormalCursor = _yui.YUIBuiltin_NormalCursor - -YUIBuiltin_OpenDialog = _yui.YUIBuiltin_OpenDialog - -YUIBuiltin_OpenUI = _yui.YUIBuiltin_OpenUI - -YUIBuiltin_PollInput = _yui.YUIBuiltin_PollInput - -YUIBuiltin_QueryWidget = _yui.YUIBuiltin_QueryWidget - -YUIBuiltin_RecalcLayout = _yui.YUIBuiltin_RecalcLayout - -YUIBuiltin_Recode = _yui.YUIBuiltin_Recode - -YUIBuiltin_RedrawScreen = _yui.YUIBuiltin_RedrawScreen - -YUIBuiltin_ReplaceWidget = _yui.YUIBuiltin_ReplaceWidget - -YUIBuiltin_RunPkgSelection = _yui.YUIBuiltin_RunPkgSelection - -YUIBuiltin_SetConsoleFont = _yui.YUIBuiltin_SetConsoleFont - -YUIBuiltin_SetFocus = _yui.YUIBuiltin_SetFocus - -YUIBuiltin_SetFunctionKeys = _yui.YUIBuiltin_SetFunctionKeys - -YUIBuiltin_SetKeyboard = _yui.YUIBuiltin_SetKeyboard - -YUIBuiltin_RunInTerminal = _yui.YUIBuiltin_RunInTerminal - -YUIBuiltin_SetLanguage = _yui.YUIBuiltin_SetLanguage - -YUIBuiltin_SetProductName = _yui.YUIBuiltin_SetProductName - -YUIBuiltin_TimeoutUserInput = _yui.YUIBuiltin_TimeoutUserInput - -YUIBuiltin_UserInput = _yui.YUIBuiltin_UserInput - -YUIBuiltin_WaitForEvent = _yui.YUIBuiltin_WaitForEvent - -YUIBuiltin_WidgetExists = _yui.YUIBuiltin_WidgetExists - -YUIBuiltin_WizardCommand = _yui.YUIBuiltin_WizardCommand - -YUIBuiltin_PostponeShortcutCheck = _yui.YUIBuiltin_PostponeShortcutCheck - -YUIBuiltin_CheckShortcuts = _yui.YUIBuiltin_CheckShortcuts - -YUIBuiltin_RecordMacro = _yui.YUIBuiltin_RecordMacro - -YUIBuiltin_StopRecordMacro = _yui.YUIBuiltin_StopRecordMacro - -YUIBuiltin_PlayMacro = _yui.YUIBuiltin_PlayMacro - -YUIBuiltin_FakeUserInput = _yui.YUIBuiltin_FakeUserInput - -YUIBuiltin_WFM = _yui.YUIBuiltin_WFM - -YUIBuiltin_SCR = _yui.YUIBuiltin_SCR - -YUIWidget_Bottom = _yui.YUIWidget_Bottom - -YUIWidget_BusyIndicator = _yui.YUIWidget_BusyIndicator - -YUIWidget_ButtonBox = _yui.YUIWidget_ButtonBox - -YUIWidget_CheckBox = _yui.YUIWidget_CheckBox - -YUIWidget_CheckBoxFrame = _yui.YUIWidget_CheckBoxFrame - -YUIWidget_ComboBox = _yui.YUIWidget_ComboBox - -YUIWidget_CustomStatusItemSelector = _yui.YUIWidget_CustomStatusItemSelector - -YUIWidget_Empty = _yui.YUIWidget_Empty - -YUIWidget_Frame = _yui.YUIWidget_Frame - -YUIWidget_HBox = _yui.YUIWidget_HBox - -YUIWidget_HCenter = _yui.YUIWidget_HCenter - -YUIWidget_HSpacing = _yui.YUIWidget_HSpacing - -YUIWidget_HSquash = _yui.YUIWidget_HSquash - -YUIWidget_HStretch = _yui.YUIWidget_HStretch - -YUIWidget_HVCenter = _yui.YUIWidget_HVCenter - -YUIWidget_HVSquash = _yui.YUIWidget_HVSquash - -YUIWidget_HWeight = _yui.YUIWidget_HWeight - -YUIWidget_Heading = _yui.YUIWidget_Heading - -YUIWidget_IconButton = _yui.YUIWidget_IconButton - -YUIWidget_Image = _yui.YUIWidget_Image - -YUIWidget_InputField = _yui.YUIWidget_InputField - -YUIWidget_IntField = _yui.YUIWidget_IntField - -YUIWidget_Label = _yui.YUIWidget_Label - -YUIWidget_Left = _yui.YUIWidget_Left - -YUIWidget_LogView = _yui.YUIWidget_LogView - -YUIWidget_MarginBox = _yui.YUIWidget_MarginBox - -YUIWidget_MenuBar = _yui.YUIWidget_MenuBar - -YUIWidget_MenuButton = _yui.YUIWidget_MenuButton - -YUIWidget_MinHeight = _yui.YUIWidget_MinHeight - -YUIWidget_MinSize = _yui.YUIWidget_MinSize - -YUIWidget_MinWidth = _yui.YUIWidget_MinWidth - -YUIWidget_MultiItemSelector = _yui.YUIWidget_MultiItemSelector - -YUIWidget_MultiLineEdit = _yui.YUIWidget_MultiLineEdit - -YUIWidget_MultiSelectionBox = _yui.YUIWidget_MultiSelectionBox - -YUIWidget_PackageSelector = _yui.YUIWidget_PackageSelector - -YUIWidget_Password = _yui.YUIWidget_Password - -YUIWidget_PkgSpecial = _yui.YUIWidget_PkgSpecial - -YUIWidget_ProgressBar = _yui.YUIWidget_ProgressBar - -YUIWidget_PushButton = _yui.YUIWidget_PushButton - -YUIWidget_RadioButton = _yui.YUIWidget_RadioButton - -YUIWidget_RadioButtonGroup = _yui.YUIWidget_RadioButtonGroup - -YUIWidget_ReplacePoint = _yui.YUIWidget_ReplacePoint - -YUIWidget_RichText = _yui.YUIWidget_RichText - -YUIWidget_Right = _yui.YUIWidget_Right - -YUIWidget_SelectionBox = _yui.YUIWidget_SelectionBox - -YUIWidget_SingleItemSelector = _yui.YUIWidget_SingleItemSelector - -YUIWidget_Table = _yui.YUIWidget_Table - -YUIWidget_TextEntry = _yui.YUIWidget_TextEntry - -YUIWidget_Top = _yui.YUIWidget_Top - -YUIWidget_Tree = _yui.YUIWidget_Tree - -YUIWidget_VBox = _yui.YUIWidget_VBox - -YUIWidget_VCenter = _yui.YUIWidget_VCenter - -YUIWidget_VSpacing = _yui.YUIWidget_VSpacing - -YUIWidget_VSquash = _yui.YUIWidget_VSquash - -YUIWidget_VStretch = _yui.YUIWidget_VStretch - -YUIWidget_VWeight = _yui.YUIWidget_VWeight - -YUISpecialWidget_BarGraph = _yui.YUISpecialWidget_BarGraph - -YUISpecialWidget_Date = _yui.YUISpecialWidget_Date - -YUISpecialWidget_DateField = _yui.YUISpecialWidget_DateField - -YUISpecialWidget_DownloadProgress = _yui.YUISpecialWidget_DownloadProgress - -YUISpecialWidget_DumbTab = _yui.YUISpecialWidget_DumbTab - -YUISpecialWidget_DummySpecialWidget = _yui.YUISpecialWidget_DummySpecialWidget - -YUISpecialWidget_HMultiProgressMeter = _yui.YUISpecialWidget_HMultiProgressMeter - -YUISpecialWidget_VMultiProgressMeter = _yui.YUISpecialWidget_VMultiProgressMeter - -YUISpecialWidget_PartitionSplitter = _yui.YUISpecialWidget_PartitionSplitter - -YUISpecialWidget_PatternSelector = _yui.YUISpecialWidget_PatternSelector - -YUISpecialWidget_SimplePatchSelector = _yui.YUISpecialWidget_SimplePatchSelector - -YUISpecialWidget_Slider = _yui.YUISpecialWidget_Slider - -YUISpecialWidget_Time = _yui.YUISpecialWidget_Time - -YUISpecialWidget_TimeField = _yui.YUISpecialWidget_TimeField - -YUISpecialWidget_Wizard = _yui.YUISpecialWidget_Wizard - -YUISpecialWidget_TimezoneSelector = _yui.YUISpecialWidget_TimezoneSelector - -YUISpecialWidget_Graph = _yui.YUISpecialWidget_Graph - -YUISpecialWidget_ContextMenu = _yui.YUISpecialWidget_ContextMenu - -YUIProperty_Alive = _yui.YUIProperty_Alive - -YUIProperty_Cell = _yui.YUIProperty_Cell - -YUIProperty_ContextMenu = _yui.YUIProperty_ContextMenu - -YUIProperty_CurrentBranch = _yui.YUIProperty_CurrentBranch - -YUIProperty_CurrentButton = _yui.YUIProperty_CurrentButton - -YUIProperty_CurrentItem = _yui.YUIProperty_CurrentItem - -YUIProperty_CurrentSize = _yui.YUIProperty_CurrentSize - -YUIProperty_DebugLabel = _yui.YUIProperty_DebugLabel - -YUIProperty_EasterEgg = _yui.YUIProperty_EasterEgg - -YUIProperty_Enabled = _yui.YUIProperty_Enabled - -YUIProperty_EnabledItems = _yui.YUIProperty_EnabledItems - -YUIProperty_ExpectedSize = _yui.YUIProperty_ExpectedSize - -YUIProperty_Filename = _yui.YUIProperty_Filename - -YUIProperty_Layout = _yui.YUIProperty_Layout - -YUIProperty_HelpText = _yui.YUIProperty_HelpText - -YUIProperty_IconPath = _yui.YUIProperty_IconPath - -YUIProperty_InputMaxLength = _yui.YUIProperty_InputMaxLength - -YUIProperty_HWeight = _yui.YUIProperty_HWeight - -YUIProperty_HStretch = _yui.YUIProperty_HStretch - -YUIProperty_ID = _yui.YUIProperty_ID - -YUIProperty_Item = _yui.YUIProperty_Item - -YUIProperty_Items = _yui.YUIProperty_Items - -YUIProperty_ItemStatus = _yui.YUIProperty_ItemStatus - -YUIProperty_Label = _yui.YUIProperty_Label - -YUIProperty_Labels = _yui.YUIProperty_Labels - -YUIProperty_LastLine = _yui.YUIProperty_LastLine - -YUIProperty_MaxLines = _yui.YUIProperty_MaxLines - -YUIProperty_MaxValue = _yui.YUIProperty_MaxValue - -YUIProperty_MinValue = _yui.YUIProperty_MinValue - -YUIProperty_MultiSelection = _yui.YUIProperty_MultiSelection - -YUIProperty_Notify = _yui.YUIProperty_Notify - -YUIProperty_OpenItems = _yui.YUIProperty_OpenItems - -YUIProperty_SelectedItems = _yui.YUIProperty_SelectedItems - -YUIProperty_Text = _yui.YUIProperty_Text - -YUIProperty_Timeout = _yui.YUIProperty_Timeout - -YUIProperty_ValidChars = _yui.YUIProperty_ValidChars - -YUIProperty_Value = _yui.YUIProperty_Value - -YUIProperty_Values = _yui.YUIProperty_Values - -YUIProperty_VisibleLines = _yui.YUIProperty_VisibleLines - -YUIProperty_VisibleItems = _yui.YUIProperty_VisibleItems - -YUIProperty_VWeight = _yui.YUIProperty_VWeight - -YUIProperty_VStretch = _yui.YUIProperty_VStretch - -YUIProperty_WidgetClass = _yui.YUIProperty_WidgetClass - -YUIProperty_VScrollValue = _yui.YUIProperty_VScrollValue - -YUIProperty_HScrollValue = _yui.YUIProperty_HScrollValue - -YUIOpt_animated = _yui.YUIOpt_animated - -YUIOpt_autoWrap = _yui.YUIOpt_autoWrap - -YUIOpt_applyButton = _yui.YUIOpt_applyButton - -YUIOpt_autoScrollDown = _yui.YUIOpt_autoScrollDown - -YUIOpt_autoShortcut = _yui.YUIOpt_autoShortcut - -YUIOpt_boldFont = _yui.YUIOpt_boldFont - -YUIOpt_cancelButton = _yui.YUIOpt_cancelButton - -YUIOpt_centered = _yui.YUIOpt_centered - -YUIOpt_confirmUnsupported = _yui.YUIOpt_confirmUnsupported - -YUIOpt_customButton = _yui.YUIOpt_customButton - -YUIOpt_debugLayout = _yui.YUIOpt_debugLayout - -YUIOpt_decorated = _yui.YUIOpt_decorated - -YUIOpt_default = _yui.YUIOpt_default - -YUIOpt_defaultsize = _yui.YUIOpt_defaultsize - -YUIOpt_disabled = _yui.YUIOpt_disabled - -YUIOpt_easterEgg = _yui.YUIOpt_easterEgg - -YUIOpt_editable = _yui.YUIOpt_editable - -YUIOpt_helpButton = _yui.YUIOpt_helpButton - -YUIOpt_relNotesButton = _yui.YUIOpt_relNotesButton - -YUIOpt_hstretch = _yui.YUIOpt_hstretch - -YUIOpt_hvstretch = _yui.YUIOpt_hvstretch - -YUIOpt_immediate = _yui.YUIOpt_immediate - -YUIOpt_infocolor = _yui.YUIOpt_infocolor - -YUIOpt_invertAutoEnable = _yui.YUIOpt_invertAutoEnable - -YUIOpt_keepSorting = _yui.YUIOpt_keepSorting - -YUIOpt_keyEvents = _yui.YUIOpt_keyEvents - -YUIOpt_mainDialog = _yui.YUIOpt_mainDialog - -YUIOpt_multiSelection = _yui.YUIOpt_multiSelection - -YUIOpt_noAutoEnable = _yui.YUIOpt_noAutoEnable - -YUIOpt_notify = _yui.YUIOpt_notify - -YUIOpt_notifyContextMenu = _yui.YUIOpt_notifyContextMenu - -YUIOpt_onlineSearch = _yui.YUIOpt_onlineSearch - -YUIOpt_okButton = _yui.YUIOpt_okButton - -YUIOpt_outputField = _yui.YUIOpt_outputField - -YUIOpt_plainText = _yui.YUIOpt_plainText - -YUIOpt_recursiveSelection = _yui.YUIOpt_recursiveSelection - -YUIOpt_relaxSanityCheck = _yui.YUIOpt_relaxSanityCheck - -YUIOpt_repoMgr = _yui.YUIOpt_repoMgr - -YUIOpt_repoMode = _yui.YUIOpt_repoMode - -YUIOpt_scaleToFit = _yui.YUIOpt_scaleToFit - -YUIOpt_searchMode = _yui.YUIOpt_searchMode - -YUIOpt_shrinkable = _yui.YUIOpt_shrinkable - -YUIOpt_stepsEnabled = _yui.YUIOpt_stepsEnabled - -YUIOpt_summaryMode = _yui.YUIOpt_summaryMode - -YUIOpt_testMode = _yui.YUIOpt_testMode - -YUIOpt_tiled = _yui.YUIOpt_tiled - -YUIOpt_titleOnLeft = _yui.YUIOpt_titleOnLeft - -YUIOpt_treeEnabled = _yui.YUIOpt_treeEnabled - -YUIOpt_updateMode = _yui.YUIOpt_updateMode - -YUIOpt_vstretch = _yui.YUIOpt_vstretch - -YUIOpt_warncolor = _yui.YUIOpt_warncolor - -YUIOpt_wizardDialog = _yui.YUIOpt_wizardDialog - -YUIOpt_youMode = _yui.YUIOpt_youMode - -YUIOpt_zeroHeight = _yui.YUIOpt_zeroHeight - -YUIOpt_zeroWidth = _yui.YUIOpt_zeroWidth - -YUIOpt_key_F1 = _yui.YUIOpt_key_F1 - -YUIOpt_key_F2 = _yui.YUIOpt_key_F2 - -YUIOpt_key_F3 = _yui.YUIOpt_key_F3 - -YUIOpt_key_F4 = _yui.YUIOpt_key_F4 - -YUIOpt_key_F5 = _yui.YUIOpt_key_F5 - -YUIOpt_key_F6 = _yui.YUIOpt_key_F6 - -YUIOpt_key_F7 = _yui.YUIOpt_key_F7 - -YUIOpt_key_F8 = _yui.YUIOpt_key_F8 - -YUIOpt_key_F9 = _yui.YUIOpt_key_F9 - -YUIOpt_key_F10 = _yui.YUIOpt_key_F10 - -YUIOpt_key_F11 = _yui.YUIOpt_key_F11 - -YUIOpt_key_F12 = _yui.YUIOpt_key_F12 - -YUIOpt_key_F13 = _yui.YUIOpt_key_F13 - -YUIOpt_key_F14 = _yui.YUIOpt_key_F14 - -YUIOpt_key_F15 = _yui.YUIOpt_key_F15 - -YUIOpt_key_F16 = _yui.YUIOpt_key_F16 - -YUIOpt_key_F17 = _yui.YUIOpt_key_F17 - -YUIOpt_key_F18 = _yui.YUIOpt_key_F18 - -YUIOpt_key_F19 = _yui.YUIOpt_key_F19 - -YUIOpt_key_F20 = _yui.YUIOpt_key_F20 - -YUIOpt_key_F21 = _yui.YUIOpt_key_F21 - -YUIOpt_key_F22 = _yui.YUIOpt_key_F22 - -YUIOpt_key_F23 = _yui.YUIOpt_key_F23 - -YUIOpt_key_F24 = _yui.YUIOpt_key_F24 - -YUIOpt_key_none = _yui.YUIOpt_key_none - -YUIGlyph_ArrowLeft = _yui.YUIGlyph_ArrowLeft - -YUIGlyph_ArrowRight = _yui.YUIGlyph_ArrowRight - -YUIGlyph_ArrowUp = _yui.YUIGlyph_ArrowUp - -YUIGlyph_ArrowDown = _yui.YUIGlyph_ArrowDown - -YUIGlyph_CheckMark = _yui.YUIGlyph_CheckMark - -YUIGlyph_BulletArrowRight = _yui.YUIGlyph_BulletArrowRight - -YUIGlyph_BulletCircle = _yui.YUIGlyph_BulletCircle - -YUIGlyph_BulletSquare = _yui.YUIGlyph_BulletSquare - -YUICap_Width = _yui.YUICap_Width - -YUICap_Height = _yui.YUICap_Height - -YUICap_Depth = _yui.YUICap_Depth - -YUICap_Colors = _yui.YUICap_Colors - -YUICap_DefaultWidth = _yui.YUICap_DefaultWidth - -YUICap_DefaultHeight = _yui.YUICap_DefaultHeight - -YUICap_TextMode = _yui.YUICap_TextMode - -YUICap_HasImageSupport = _yui.YUICap_HasImageSupport - -YUICap_HasAnimationSupport = _yui.YUICap_HasAnimationSupport - -YUICap_HasIconSupport = _yui.YUICap_HasIconSupport - -YUICap_HasFullUtf8Support = _yui.YUICap_HasFullUtf8Support - -YUICap_HasWidgetStyleSupport = _yui.YUICap_HasWidgetStyleSupport - -YUICap_HasWizardDialogSupport = _yui.YUICap_HasWizardDialogSupport - -YUICap_RichTextSupportsTable = _yui.YUICap_RichTextSupportsTable - -YUICap_LeftHandedMouse = _yui.YUICap_LeftHandedMouse - -YUICap_y2debug = _yui.YUICap_y2debug - -YUISymbol_id = _yui.YUISymbol_id - -YUISymbol_opt = _yui.YUISymbol_opt - -YUISymbol_icon = _yui.YUISymbol_icon - -YUISymbol_sortKey = _yui.YUISymbol_sortKey - -YUISymbol_item = _yui.YUISymbol_item - -YUISymbol_cell = _yui.YUISymbol_cell - -YUISymbol_menu = _yui.YUISymbol_menu - -YUISymbol_header = _yui.YUISymbol_header - -YUISymbol_rgb = _yui.YUISymbol_rgb - -YUISymbol_leftMargin = _yui.YUISymbol_leftMargin - -YUISymbol_rightMargin = _yui.YUISymbol_rightMargin - -YUISymbol_topMargin = _yui.YUISymbol_topMargin - -YUISymbol_bottomMargin = _yui.YUISymbol_bottomMargin - -YUISymbol_BackgroundPixmap = _yui.YUISymbol_BackgroundPixmap - -YUISymbol_open = _yui.YUISymbol_open - -YUISymbol_closed = _yui.YUISymbol_closed - -YUISymbol_Left = _yui.YUISymbol_Left - -YUISymbol_Right = _yui.YUISymbol_Right - -YUISymbol_Center = _yui.YUISymbol_Center - -class YWidgetID(object): - r"""Proxy of C++ YWidgetID class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YWidgetID - - def isEqual(self, otherID): - r"""isEqual(YWidgetID self, YWidgetID otherID) -> bool""" - return _yui.YWidgetID_isEqual(self, otherID) - - def toString(self): - r"""toString(YWidgetID self) -> std::string""" - return _yui.YWidgetID_toString(self) - -# Register YWidgetID in _yui: -_yui.YWidgetID_swigregister(YWidgetID) - -class YStringWidgetID(YWidgetID): - r"""Proxy of C++ YStringWidgetID class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, value): - r"""__init__(YStringWidgetID self, std::string const & value) -> YStringWidgetID""" - _yui.YStringWidgetID_swiginit(self, _yui.new_YStringWidgetID(value)) - __swig_destroy__ = _yui.delete_YStringWidgetID - - def isEqual(self, otherID): - r"""isEqual(YStringWidgetID self, YWidgetID otherID) -> bool""" - return _yui.YStringWidgetID_isEqual(self, otherID) - - def toString(self): - r"""toString(YStringWidgetID self) -> std::string""" - return _yui.YStringWidgetID_toString(self) - - def value(self): - r"""value(YStringWidgetID self) -> std::string""" - return _yui.YStringWidgetID_value(self) - - def valueConstRef(self): - r"""valueConstRef(YStringWidgetID self) -> std::string const &""" - return _yui.YStringWidgetID_valueConstRef(self) - -# Register YStringWidgetID in _yui: -_yui.YStringWidgetID_swigregister(YStringWidgetID) - -class YExternalWidgetFactory(object): - r"""Proxy of C++ YExternalWidgetFactory class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined") - __repr__ = _swig_repr - -# Register YExternalWidgetFactory in _yui: -_yui.YExternalWidgetFactory_swigregister(YExternalWidgetFactory) - -class YExternalWidgets(object): - r"""Proxy of C++ YExternalWidgets class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YExternalWidgets - - @staticmethod - def externalWidgets(name): - r"""externalWidgets(std::string const & name) -> YExternalWidgets""" - return _yui.YExternalWidgets_externalWidgets(name) - - @staticmethod - def externalWidgetFactory(*args): - r""" - externalWidgetFactory() -> YExternalWidgetFactory - externalWidgetFactory(std::string const & name) -> YExternalWidgetFactory - """ - return _yui.YExternalWidgets_externalWidgetFactory(*args) - -# Register YExternalWidgets in _yui: -_yui.YExternalWidgets_swigregister(YExternalWidgets) - -def YExternalWidgets_externalWidgets(name): - r"""YExternalWidgets_externalWidgets(std::string const & name) -> YExternalWidgets""" - return _yui.YExternalWidgets_externalWidgets(name) - -def YExternalWidgets_externalWidgetFactory(*args): - r""" - YExternalWidgets_externalWidgetFactory() -> YExternalWidgetFactory - YExternalWidgets_externalWidgetFactory(std::string const & name) -> YExternalWidgetFactory - """ - return _yui.YExternalWidgets_externalWidgetFactory(*args) - -class YCBTableHeader(YTableHeader): - r"""Proxy of C++ YCBTableHeader class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self): - r"""__init__(YCBTableHeader self) -> YCBTableHeader""" - _yui.YCBTableHeader_swiginit(self, _yui.new_YCBTableHeader()) - __swig_destroy__ = _yui.delete_YCBTableHeader - - def addColumn(self, header, checkBox, alignment=YAlignBegin): - r"""addColumn(YCBTableHeader self, std::string const & header, bool checkBox, YAlignmentType alignment=YAlignBegin)""" - return _yui.YCBTableHeader_addColumn(self, header, checkBox, alignment) - - def cbColumn(self, column): - r"""cbColumn(YCBTableHeader self, int column) -> bool""" - return _yui.YCBTableHeader_cbColumn(self, column) - -# Register YCBTableHeader in _yui: -_yui.YCBTableHeader_swigregister(YCBTableHeader) - -class YCBTableCell(YTableCell): - r"""Proxy of C++ YCBTableCell class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YCBTableCell self, std::string const & label, std::string const & iconName="", std::string const & sortKey="") -> YCBTableCell - __init__(YCBTableCell self, char const * label) -> YCBTableCell - __init__(YCBTableCell self, bool const & checked) -> YCBTableCell - __init__(YCBTableCell self, YTableItem parent, int column, std::string const & label="", bool const & checked=False, std::string const & iconName="", std::string const & sortKey="") -> YCBTableCell - """ - _yui.YCBTableCell_swiginit(self, _yui.new_YCBTableCell(*args)) - __swig_destroy__ = _yui.delete_YCBTableCell - - def setChecked(self, val=True): - r"""setChecked(YCBTableCell self, bool val=True)""" - return _yui.YCBTableCell_setChecked(self, val) - - def checked(self): - r"""checked(YCBTableCell self) -> bool""" - return _yui.YCBTableCell_checked(self) - -# Register YCBTableCell in _yui: -_yui.YCBTableCell_swigregister(YCBTableCell) - -class YCBTableItem(YTableItem): - r"""Proxy of C++ YCBTableItem class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def __init__(self, *args): - r""" - __init__(YCBTableItem self) -> YCBTableItem - __init__(YCBTableItem self, YCBTableCell cell0_disown, YCBTableCell cell1_disown=None, YCBTableCell cell2_disown=None, YCBTableCell cell3_disown=None, YCBTableCell cell4_disown=None, YCBTableCell cell5_disown=None, YCBTableCell cell6_disown=None, YCBTableCell cell7_disown=None, YCBTableCell cell8_disown=None, YCBTableCell cell9_disown=None) -> YCBTableItem - """ - _yui.YCBTableItem_swiginit(self, _yui.new_YCBTableItem(*args)) - __swig_destroy__ = _yui.delete_YCBTableItem - - def itemClass(self): - r"""itemClass(YCBTableItem self) -> char const *""" - return _yui.YCBTableItem_itemClass(self) - - def addCell(self, *args): - r""" - addCell(YCBTableItem self, YCBTableCell cell_disown) - addCell(YCBTableItem self, std::string const & label, std::string const & iconName=std::string(), std::string const & sortKey=std::string()) - addCell(YCBTableItem self, bool checked) - addCell(YCBTableItem self, char const * label) - """ - return _yui.YCBTableItem_addCell(self, *args) - - def checked(self, index): - r"""checked(YCBTableItem self, int index) -> bool""" - return _yui.YCBTableItem_checked(self, index) - - def cellChanged(self): - r"""cellChanged(YCBTableItem self) -> YCBTableCell""" - return _yui.YCBTableItem_cellChanged(self) - - def setChangedColumn(self, column): - r"""setChangedColumn(YCBTableItem self, int column)""" - return _yui.YCBTableItem_setChangedColumn(self, column) - -# Register YCBTableItem in _yui: -_yui.YCBTableItem_swigregister(YCBTableItem) - -class YMGA_CBTable(YTable): - r"""Proxy of C++ YMGA_CBTable class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - __swig_destroy__ = _yui.delete_YMGA_CBTable - - def widgetClass(self): - r"""widgetClass(YMGA_CBTable self) -> char const *""" - return _yui.YMGA_CBTable_widgetClass(self) - - def isCheckBoxColumn(self, column): - r"""isCheckBoxColumn(YMGA_CBTable self, int column) -> bool""" - return _yui.YMGA_CBTable_isCheckBoxColumn(self, column) - - def setItemChecked(self, item, column, checked=True): - r"""setItemChecked(YMGA_CBTable self, YItem item, int column, bool checked=True)""" - return _yui.YMGA_CBTable_setItemChecked(self, item, column, checked) - - def setChangedItem(self, pItem): - r"""setChangedItem(YMGA_CBTable self, YCBTableItem pItem)""" - return _yui.YMGA_CBTable_setChangedItem(self, pItem) - - def changedItem(self): - r"""changedItem(YMGA_CBTable self) -> YCBTableItem""" - return _yui.YMGA_CBTable_changedItem(self) - - def nextItem(self, currentIterator): - r"""nextItem(YMGA_CBTable self, YItemIterator currentIterator) -> YItemIterator""" - return _yui.YMGA_CBTable_nextItem(self, currentIterator) - - def deleteAllItems(self): - r"""deleteAllItems(YMGA_CBTable self)""" - return _yui.YMGA_CBTable_deleteAllItems(self) - - def YItemIteratorToYItem(self, iter): - r"""YItemIteratorToYItem(YMGA_CBTable self, YItemIterator iter) -> YItem""" - return _yui.YMGA_CBTable_YItemIteratorToYItem(self, iter) - - def toCBYTableItem(self, item): - r"""toCBYTableItem(YMGA_CBTable self, YItem item) -> YCBTableItem""" - return _yui.YMGA_CBTable_toCBYTableItem(self, item) - -# Register YMGA_CBTable in _yui: -_yui.YMGA_CBTable_swigregister(YMGA_CBTable) - -class YMGAAboutDialog(object): - r"""Proxy of C++ YMGAAboutDialog class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - CLASSIC = _yui.YMGAAboutDialog_CLASSIC - - TABBED = _yui.YMGAAboutDialog_TABBED - - - def __init__(self, *args): - r"""__init__(YMGAAboutDialog self, std::string const & name, std::string const & version, std::string const & license, std::string const & authors, std::string const & description, std::string const & logo, std::string const & icon=std::string(), std::string const & credits=std::string(), std::string const & information=std::string()) -> YMGAAboutDialog""" - _yui.YMGAAboutDialog_swiginit(self, _yui.new_YMGAAboutDialog(*args)) - __swig_destroy__ = _yui.delete_YMGAAboutDialog - - def setMinSize(self, columns, lines): - r"""setMinSize(YMGAAboutDialog self, YLayoutSize_t columns, YLayoutSize_t lines)""" - return _yui.YMGAAboutDialog_setMinSize(self, columns, lines) - - def show(self, *args): - r"""show(YMGAAboutDialog self, YMGAAboutDialog::DLG_MODE type=TABBED)""" - return _yui.YMGAAboutDialog_show(self, *args) - -# Register YMGAAboutDialog in _yui: -_yui.YMGAAboutDialog_swigregister(YMGAAboutDialog) - -class YMGAMessageBox(object): - r"""Proxy of C++ YMGAMessageBox class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - B_ONE = _yui.YMGAMessageBox_B_ONE - - B_TWO = _yui.YMGAMessageBox_B_TWO - - D_NORMAL = _yui.YMGAMessageBox_D_NORMAL - - D_INFO = _yui.YMGAMessageBox_D_INFO - - D_WARNING = _yui.YMGAMessageBox_D_WARNING - - - def __init__(self, *args): - r"""__init__(YMGAMessageBox self, YMGAMessageBox::DLG_BUTTON b_num=B_ONE, YMGAMessageBox::DLG_MODE dlg_mode=D_NORMAL) -> YMGAMessageBox""" - _yui.YMGAMessageBox_swiginit(self, _yui.new_YMGAMessageBox(*args)) - __swig_destroy__ = _yui.delete_YMGAMessageBox - - def setIcon(self, icon): - r"""setIcon(YMGAMessageBox self, std::string const & icon)""" - return _yui.YMGAMessageBox_setIcon(self, icon) - - def setTitle(self, title): - r"""setTitle(YMGAMessageBox self, std::string const & title)""" - return _yui.YMGAMessageBox_setTitle(self, title) - - def setText(self, text, useRichText=False): - r"""setText(YMGAMessageBox self, std::string const & text, bool useRichText=False)""" - return _yui.YMGAMessageBox_setText(self, text, useRichText) - - def setMinSize(self, minWidth, minHeight): - r"""setMinSize(YMGAMessageBox self, YLayoutSize_t minWidth, YLayoutSize_t minHeight)""" - return _yui.YMGAMessageBox_setMinSize(self, minWidth, minHeight) - - def setButtonLabel(self, *args): - r"""setButtonLabel(YMGAMessageBox self, std::string const & label, YMGAMessageBox::DLG_BUTTON button=B_ONE)""" - return _yui.YMGAMessageBox_setButtonLabel(self, *args) - - def setDefaultButton(self, *args): - r"""setDefaultButton(YMGAMessageBox self, YMGAMessageBox::DLG_BUTTON button=B_ONE)""" - return _yui.YMGAMessageBox_setDefaultButton(self, *args) - - def show(self): - r"""show(YMGAMessageBox self) -> YMGAMessageBox::DLG_BUTTON""" - return _yui.YMGAMessageBox_show(self) - -# Register YMGAMessageBox in _yui: -_yui.YMGAMessageBox_swigregister(YMGAMessageBox) - -class YMGAWidgetFactory(YExternalWidgetFactory): - r"""Proxy of C++ YMGAWidgetFactory class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - - def __init__(self, *args, **kwargs): - raise AttributeError("No constructor defined - class is abstract") - __repr__ = _swig_repr - - def createAboutDialog(self, *args): - r"""createAboutDialog(YMGAWidgetFactory self, std::string const & appname, std::string const & appversion, std::string const & applicense, std::string const & appauthors, std::string const & appdescription, std::string const & applogo, std::string const & appicon=std::string(), std::string const & appcredits=std::string(), std::string const & appinfo=std::string()) -> YMGAAboutDialog""" - return _yui.YMGAWidgetFactory_createAboutDialog(self, *args) - - def createCBTable(self, parent, header_disown): - r"""createCBTable(YMGAWidgetFactory self, YWidget parent, YTableHeader header_disown) -> YMGA_CBTable""" - return _yui.YMGAWidgetFactory_createCBTable(self, parent, header_disown) - - def createMenuBar(self, parent): - r"""createMenuBar(YMGAWidgetFactory self, YWidget parent) -> YMGAMenuBar *""" - return _yui.YMGAWidgetFactory_createMenuBar(self, parent) - - def createDialogBox(self, *args): - r"""createDialogBox(YMGAWidgetFactory self, YMGAMessageBox::DLG_BUTTON button_number=B_ONE, YMGAMessageBox::DLG_MODE dialog_mode=D_NORMAL) -> YMGAMessageBox""" - return _yui.YMGAWidgetFactory_createDialogBox(self, *args) - - def createMessageBox(self, title, text, useRichText, btn_label): - r"""createMessageBox(YMGAWidgetFactory self, std::string const & title, std::string const & text, bool useRichText, std::string const & btn_label) -> YMGAMessageBox""" - return _yui.YMGAWidgetFactory_createMessageBox(self, title, text, useRichText, btn_label) - - def createInfoBox(self, title, text, useRichText, btn_label): - r"""createInfoBox(YMGAWidgetFactory self, std::string const & title, std::string const & text, bool useRichText, std::string const & btn_label) -> YMGAMessageBox""" - return _yui.YMGAWidgetFactory_createInfoBox(self, title, text, useRichText, btn_label) - - def createWarningBox(self, title, text, useRichText, btn_label): - r"""createWarningBox(YMGAWidgetFactory self, std::string const & title, std::string const & text, bool useRichText, std::string const & btn_label) -> YMGAMessageBox""" - return _yui.YMGAWidgetFactory_createWarningBox(self, title, text, useRichText, btn_label) - - @staticmethod - def getYMGAWidgetFactory(instance): - r"""getYMGAWidgetFactory(YExternalWidgetFactory instance) -> YMGAWidgetFactory""" - return _yui.YMGAWidgetFactory_getYMGAWidgetFactory(instance) - - @staticmethod - def getYWidgetEvent(event): - r"""getYWidgetEvent(YEvent event) -> YWidgetEvent""" - return _yui.YMGAWidgetFactory_getYWidgetEvent(event) - - @staticmethod - def getYKeyEvent(event): - r"""getYKeyEvent(YEvent event) -> YKeyEvent""" - return _yui.YMGAWidgetFactory_getYKeyEvent(event) - - @staticmethod - def getYMenuEvent(event): - r"""getYMenuEvent(YEvent event) -> YMenuEvent""" - return _yui.YMGAWidgetFactory_getYMenuEvent(event) - - @staticmethod - def getYCancelEvent(event): - r"""getYCancelEvent(YEvent event) -> YCancelEvent""" - return _yui.YMGAWidgetFactory_getYCancelEvent(event) - - @staticmethod - def getYDebugEvent(event): - r"""getYDebugEvent(YEvent event) -> YDebugEvent""" - return _yui.YMGAWidgetFactory_getYDebugEvent(event) - - @staticmethod - def getYTimeoutEvent(event): - r"""getYTimeoutEvent(YEvent event) -> YTimeoutEvent""" - return _yui.YMGAWidgetFactory_getYTimeoutEvent(event) - - @staticmethod - def toYMGAMenuItem(item): - r"""toYMGAMenuItem(YItem item) -> YMGAMenuItem *""" - return _yui.YMGAWidgetFactory_toYMGAMenuItem(item) - - @staticmethod - def toYMenuSeparator(item): - r"""toYMenuSeparator(YItem item) -> YMenuSeparator *""" - return _yui.YMGAWidgetFactory_toYMenuSeparator(item) - -# Register YMGAWidgetFactory in _yui: -_yui.YMGAWidgetFactory_swigregister(YMGAWidgetFactory) - -def YMGAWidgetFactory_getYMGAWidgetFactory(instance): - r"""YMGAWidgetFactory_getYMGAWidgetFactory(YExternalWidgetFactory instance) -> YMGAWidgetFactory""" - return _yui.YMGAWidgetFactory_getYMGAWidgetFactory(instance) - -def YMGAWidgetFactory_getYWidgetEvent(event): - r"""YMGAWidgetFactory_getYWidgetEvent(YEvent event) -> YWidgetEvent""" - return _yui.YMGAWidgetFactory_getYWidgetEvent(event) - -def YMGAWidgetFactory_getYKeyEvent(event): - r"""YMGAWidgetFactory_getYKeyEvent(YEvent event) -> YKeyEvent""" - return _yui.YMGAWidgetFactory_getYKeyEvent(event) - -def YMGAWidgetFactory_getYMenuEvent(event): - r"""YMGAWidgetFactory_getYMenuEvent(YEvent event) -> YMenuEvent""" - return _yui.YMGAWidgetFactory_getYMenuEvent(event) - -def YMGAWidgetFactory_getYCancelEvent(event): - r"""YMGAWidgetFactory_getYCancelEvent(YEvent event) -> YCancelEvent""" - return _yui.YMGAWidgetFactory_getYCancelEvent(event) - -def YMGAWidgetFactory_getYDebugEvent(event): - r"""YMGAWidgetFactory_getYDebugEvent(YEvent event) -> YDebugEvent""" - return _yui.YMGAWidgetFactory_getYDebugEvent(event) - -def YMGAWidgetFactory_getYTimeoutEvent(event): - r"""YMGAWidgetFactory_getYTimeoutEvent(YEvent event) -> YTimeoutEvent""" - return _yui.YMGAWidgetFactory_getYTimeoutEvent(event) - -def YMGAWidgetFactory_toYMGAMenuItem(item): - r"""YMGAWidgetFactory_toYMGAMenuItem(YItem item) -> YMGAMenuItem *""" - return _yui.YMGAWidgetFactory_toYMGAMenuItem(item) - -def YMGAWidgetFactory_toYMenuSeparator(item): - r"""YMGAWidgetFactory_toYMenuSeparator(YItem item) -> YMenuSeparator *""" - return _yui.YMGAWidgetFactory_toYMenuSeparator(item) - -class YItemCollection(object): - r"""Proxy of C++ std::vector< YItem * > class.""" - - thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") - __repr__ = _swig_repr - - def iterator(self): - r"""iterator(YItemCollection self) -> SwigPyIterator""" - return _yui.YItemCollection_iterator(self) - def __iter__(self): - return self.iterator() - - def __nonzero__(self): - r"""__nonzero__(YItemCollection self) -> bool""" - return _yui.YItemCollection___nonzero__(self) - - def __bool__(self): - r"""__bool__(YItemCollection self) -> bool""" - return _yui.YItemCollection___bool__(self) - - def __len__(self): - r"""__len__(YItemCollection self) -> std::vector< YItem * >::size_type""" - return _yui.YItemCollection___len__(self) - - def __getslice__(self, i, j): - r"""__getslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j) -> YItemCollection""" - return _yui.YItemCollection___getslice__(self, i, j) - - def __setslice__(self, *args): - r""" - __setslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j) - __setslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j, YItemCollection v) - """ - return _yui.YItemCollection___setslice__(self, *args) - - def __delslice__(self, i, j): - r"""__delslice__(YItemCollection self, std::vector< YItem * >::difference_type i, std::vector< YItem * >::difference_type j)""" - return _yui.YItemCollection___delslice__(self, i, j) - - def __delitem__(self, *args): - r""" - __delitem__(YItemCollection self, std::vector< YItem * >::difference_type i) - __delitem__(YItemCollection self, PySliceObject * slice) - """ - return _yui.YItemCollection___delitem__(self, *args) - - def __getitem__(self, *args): - r""" - __getitem__(YItemCollection self, PySliceObject * slice) -> YItemCollection - __getitem__(YItemCollection self, std::vector< YItem * >::difference_type i) -> YItem - """ - return _yui.YItemCollection___getitem__(self, *args) - - def __setitem__(self, *args): - r""" - __setitem__(YItemCollection self, PySliceObject * slice, YItemCollection v) - __setitem__(YItemCollection self, PySliceObject * slice) - __setitem__(YItemCollection self, std::vector< YItem * >::difference_type i, YItem x) - """ - return _yui.YItemCollection___setitem__(self, *args) - - def pop(self): - r"""pop(YItemCollection self) -> YItem""" - return _yui.YItemCollection_pop(self) - - def append(self, x): - r"""append(YItemCollection self, YItem x)""" - return _yui.YItemCollection_append(self, x) - - def empty(self): - r"""empty(YItemCollection self) -> bool""" - return _yui.YItemCollection_empty(self) - - def size(self): - r"""size(YItemCollection self) -> std::vector< YItem * >::size_type""" - return _yui.YItemCollection_size(self) - - def swap(self, v): - r"""swap(YItemCollection self, YItemCollection v)""" - return _yui.YItemCollection_swap(self, v) - - def begin(self): - r"""begin(YItemCollection self) -> std::vector< YItem * >::iterator""" - return _yui.YItemCollection_begin(self) - - def end(self): - r"""end(YItemCollection self) -> std::vector< YItem * >::iterator""" - return _yui.YItemCollection_end(self) - - def rbegin(self): - r"""rbegin(YItemCollection self) -> std::vector< YItem * >::reverse_iterator""" - return _yui.YItemCollection_rbegin(self) - - def rend(self): - r"""rend(YItemCollection self) -> std::vector< YItem * >::reverse_iterator""" - return _yui.YItemCollection_rend(self) - - def clear(self): - r"""clear(YItemCollection self)""" - return _yui.YItemCollection_clear(self) - - def get_allocator(self): - r"""get_allocator(YItemCollection self) -> std::vector< YItem * >::allocator_type""" - return _yui.YItemCollection_get_allocator(self) - - def pop_back(self): - r"""pop_back(YItemCollection self)""" - return _yui.YItemCollection_pop_back(self) - - def erase(self, *args): - r""" - erase(YItemCollection self, std::vector< YItem * >::iterator pos) -> std::vector< YItem * >::iterator - erase(YItemCollection self, std::vector< YItem * >::iterator first, std::vector< YItem * >::iterator last) -> std::vector< YItem * >::iterator - """ - return _yui.YItemCollection_erase(self, *args) - - def __init__(self, *args): - r""" - __init__(YItemCollection self) -> YItemCollection - __init__(YItemCollection self, YItemCollection other) -> YItemCollection - __init__(YItemCollection self, std::vector< YItem * >::size_type size) -> YItemCollection - __init__(YItemCollection self, std::vector< YItem * >::size_type size, YItem value) -> YItemCollection - """ - _yui.YItemCollection_swiginit(self, _yui.new_YItemCollection(*args)) - - def push_back(self, x): - r"""push_back(YItemCollection self, YItem x)""" - return _yui.YItemCollection_push_back(self, x) - - def front(self): - r"""front(YItemCollection self) -> YItem""" - return _yui.YItemCollection_front(self) - - def back(self): - r"""back(YItemCollection self) -> YItem""" - return _yui.YItemCollection_back(self) - - def assign(self, n, x): - r"""assign(YItemCollection self, std::vector< YItem * >::size_type n, YItem x)""" - return _yui.YItemCollection_assign(self, n, x) - - def resize(self, *args): - r""" - resize(YItemCollection self, std::vector< YItem * >::size_type new_size) - resize(YItemCollection self, std::vector< YItem * >::size_type new_size, YItem x) - """ - return _yui.YItemCollection_resize(self, *args) - - def insert(self, *args): - r""" - insert(YItemCollection self, std::vector< YItem * >::iterator pos, YItem x) -> std::vector< YItem * >::iterator - insert(YItemCollection self, std::vector< YItem * >::iterator pos, std::vector< YItem * >::size_type n, YItem x) - """ - return _yui.YItemCollection_insert(self, *args) - - def reserve(self, n): - r"""reserve(YItemCollection self, std::vector< YItem * >::size_type n)""" - return _yui.YItemCollection_reserve(self, n) - - def capacity(self): - r"""capacity(YItemCollection self) -> std::vector< YItem * >::size_type""" - return _yui.YItemCollection_capacity(self) - __swig_destroy__ = _yui.delete_YItemCollection - -# Register YItemCollection in _yui: -_yui.YItemCollection_swigregister(YItemCollection) - - -def toYWidgetEvent(event): - r"""toYWidgetEvent(YEvent event) -> YWidgetEvent""" - return _yui.toYWidgetEvent(event) - -def toYKeyEvent(event): - r"""toYKeyEvent(YEvent event) -> YKeyEvent""" - return _yui.toYKeyEvent(event) - -def toYMenuEvent(event): - r"""toYMenuEvent(YEvent event) -> YMenuEvent""" - return _yui.toYMenuEvent(event) - -def toYCancelEvent(event): - r"""toYCancelEvent(YEvent event) -> YCancelEvent""" - return _yui.toYCancelEvent(event) - -def toYDebugEvent(event): - r"""toYDebugEvent(YEvent event) -> YDebugEvent""" - return _yui.toYDebugEvent(event) - -def toYTimeoutEvent(event): - r"""toYTimeoutEvent(YEvent event) -> YTimeoutEvent""" - return _yui.toYTimeoutEvent(event) - -def toYTreeItem(item): - r"""toYTreeItem(YItem item) -> YTreeItem""" - return _yui.toYTreeItem(item) - -def toYTableItem(item): - r"""toYTableItem(YItem item) -> YTableItem""" - return _yui.toYTableItem(item) - -def toYItem(iter): - r"""toYItem(YItemIterator iter) -> YItem""" - return _yui.toYItem(iter) - -def toYTableCell(iter): - r"""toYTableCell(YTableCellIterator iter) -> YTableCell""" - return _yui.toYTableCell(iter) - -def incrYItemIterator(currentIterator): - r"""incrYItemIterator(YItemIterator currentIterator) -> YItemIterator""" - return _yui.incrYItemIterator(currentIterator) - -def beginYItemCollection(coll): - r"""beginYItemCollection(YItemCollection coll) -> YItemIterator""" - return _yui.beginYItemCollection(coll) - -def endYItemCollection(coll): - r"""endYItemCollection(YItemCollection coll) -> YItemIterator""" - return _yui.endYItemCollection(coll) - -def incrYTableCellIterator(currentIterator): - r"""incrYTableCellIterator(YTableCellIterator currentIterator) -> YTableCellIterator""" - return _yui.incrYTableCellIterator(currentIterator) - - From 8614827559761a5c6cd7ae1ea3166355460b96b8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 20:20:20 +0100 Subject: [PATCH 326/523] Don't change passed filter --- manatools/aui/yui_qt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 01ff3bd..93eca7c 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -165,14 +165,14 @@ def askForExistingFile(self, startWith: str, filter: str, headline: str): Parameters: - startWith: initial directory or file - - filter: file filter string (e.g. "*.txt") + - filter: semicolon-separated string containing a list of filters (e.g. "*.txt;*.md") - headline: explanatory text for the dialog Returns: selected filename as string, or empty string if cancelled. """ try: start = startWith or "" - flt = f"Text files ({filter});;All files (*)" if filter else "All files (*)" + flt = filter if filter else "All files (*)" fn, _ = QtWidgets.QFileDialog.getOpenFileName(None, headline or "Open File", start, flt) return fn or "" except Exception: @@ -192,7 +192,7 @@ def askForSaveFileName(self, startWith: str, filter: str, headline: str): """ try: start = startWith or "" - flt = f"Text files ({filter});;All files (*)" if filter else "All files (*)" + flt = filter if filter else "All files (*)" fn, _ = QtWidgets.QFileDialog.getSaveFileName(None, headline or "Save File", start, flt) return fn or "" except Exception: From 493677ff5ab1d856dc772e7d28ae7a86531df7c3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 22:31:58 +0100 Subject: [PATCH 327/523] Start implementin askForXXX dialogs --- manatools/aui/yui_gtk.py | 376 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 371 insertions(+), 5 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 826f6c1..3371850 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -4,18 +4,24 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Gdk', '4.0') -from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib -import cairo -import threading +from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib, Gio +from typing import List import os +import logging from .yui_common import * from .backends.gtk import * + class YUIGtk: def __init__(self): + # Use a dedicated widget factory to match other backends. self._widget_factory = YWidgetFactoryGtk() self._optional_widget_factory = None self._application = YApplicationGtk() + try: + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + except Exception: + self._logger = logging.getLogger("manatools.aui.gtk.YUIGtk") def widgetFactory(self): return self._widget_factory @@ -40,6 +46,10 @@ def __init__(self): self._icon = "manatools" # default icon name # cached resolved GdkPixbuf.Pixbuf (or None) self._gtk_icon_pixbuf = None + try: + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + except Exception: + self._logger = logging.getLogger("manatools.aui.gtk.YApplicationGtk") def _resolve_pixbuf(self, icon_spec): """Resolve icon_spec into a GdkPixbuf.Pixbuf if possible. @@ -180,10 +190,367 @@ def setApplicationIcon(self, Icon): def applicationIcon(self): return self._icon + def _create_gtk4_filters(self, filter_str: str) -> List[Gtk.FileFilter]: + """ + Create GTK4 file filters from a semicolon-separated filter string. + """ + filters = [] + + if not filter_str or filter_str.strip() == "": + return filters + + # Split and clean patterns + patterns = [p.strip() for p in filter_str.split(';') if p.strip()] + + # Create main filter + main_filter = Gtk.FileFilter() + + # Set a meaningful name + if len(patterns) == 1: + ext = patterns[0].replace('*.', '').replace('*', '') + main_filter.set_name(f"{ext.upper()} files") + else: + main_filter.set_name("Supported files") + + # Add patterns to the filter + for pattern in patterns: + pattern = pattern.strip() + if not pattern: + continue + + # Handle different pattern formats + if pattern == "*" or pattern == "*.*": + # All files + main_filter.add_pattern("*") + elif pattern.startswith("*."): + # Pattern like "*.txt" + main_filter.add_pattern(pattern) + # Also add without star for some systems + main_filter.add_pattern(pattern[1:]) # ".txt" + elif pattern.startswith("."): + # Pattern like ".txt" + main_filter.add_pattern(f"*{pattern}") # "*.txt" + main_filter.add_pattern(pattern) # ".txt" + else: + # Try as mime type or literal pattern + if '/' in pattern: # Looks like a mime type + main_filter.add_mime_type(pattern) + else: + main_filter.add_pattern(pattern) + + filters.append(main_filter) + + # Add "All files" filter + all_filter = Gtk.FileFilter() + all_filter.set_name("All files") + all_filter.add_pattern("*") + filters.append(all_filter) + + return filters + + def askForExistingDirectory(self, startDir: str, headline: str): + """ + Prompt user to select an existing directory (GTK implementation). + """ + try: + # try to find an active YDialogGtk window to use as transient parent + parent_window = None + try: + for od in getattr(YDialogGtk, "_open_dialogs", []) or []: + try: + w = getattr(od, "_window", None) + if w: + parent_window = w + break + except Exception: + pass + except Exception: + parent_window = None + + # ensure application name is set so recent manager can record resources + try: + GLib.set_prgname(self._product_name or "manatools") + except Exception: + pass + + # Use Gtk.FileDialog (GTK 4.10+) exclusively for directory selection + if not hasattr(Gtk, 'FileDialog'): + try: + self._logger.error('Gtk.FileDialog is not available in this GTK runtime') + except Exception: + pass + return "" + + try: + fd = Gtk.FileDialog.new() + try: + fd.set_title(headline or "Select Directory") + except Exception: + pass + + if startDir and os.path.exists(startDir): + try: + fd.set_initial_folder(Gio.File.new_for_path(startDir)) + except Exception: + try: + fd.set_initial_folder_uri(GLib.filename_to_uri(startDir, None)) + except Exception: + pass + + loop = GLib.MainLoop() + result_holder = {'file': None} + + def _on_opened(dialog, result, _lh=loop, _holder=result_holder, _fd=fd): + try: + f = _fd.select_folder_finish(result) + _holder['file'] = f + except Exception: + _holder['file'] = None + try: + _fd.close() + except Exception: + pass + try: + _lh.quit() + except Exception: + pass + + fd.select_folder(parent_window, None, _on_opened) + loop.run() + gf = result_holder.get('file') + if gf is not None: + try: + return gf.get_path() or gf.get_uri() or "" + except Exception: + return "" + return "" + except Exception: + try: + self._logger.exception('FileDialog.select_folder failed') + except Exception: + pass + return "" + except Exception: + try: + self._logger.exception("askForExistingDirectory failed") + except Exception: + pass + return "" + + def askForExistingFile(self, startWith: str, filter: str, headline: str): + """ + Prompt user to select an existing file. + + Parameters: + - startWith: initial directory or file + - filter: semicolon-separated string containing a list of filters (e.g. "*.txt;*.md") + - headline: explanatory text for the dialog + + Returns: selected filename as string, or empty string if cancelled. + """ + try: + # try to use an active dialog window as transient parent + parent_window = None + try: + for od in getattr(YDialogGtk, "_open_dialogs", []) or []: + try: + w = getattr(od, "_window", None) + if w: + parent_window = w + break + except Exception: + pass + except Exception: + parent_window = None + + if parent_window is None: + self._logger.info("askForExistingFile: no parent window found") + + try: + GLib.set_prgname(self._product_name or "manatools") + except Exception: + pass + + # Use Gtk.FileDialog (GTK 4.10+) exclusively for file open + if not hasattr(Gtk, 'FileDialog'): + try: + self._logger.error('Gtk.FileDialog is not available in this GTK runtime') + except Exception: + pass + return "" + + try: + fd = Gtk.FileDialog.new() + try: + fd.set_title(headline or "Open File") + except Exception: + pass + + # filters + try: + filters = self._create_gtk4_filters(filter) + if filters: + filter_list = Gio.ListStore.new(Gtk.FileFilter) + for file_filter in filters: + filter_list.append(file_filter) + fd.set_filters(filter_list) + except Exception: + self._logger.exception("askForExistingFile: setting filters failed") + pass + + if startWith and os.path.exists(startWith): + try: + target = os.path.dirname(startWith) if os.path.isfile(startWith) else startWith + fd.set_initial_folder(Gio.File.new_for_path(target)) + except Exception: + self._logger.exception("askForExistingFile: setting initial folder failed") + try: + fd.set_initial_folder_uri(GLib.filename_to_uri(target, None)) + except Exception: + pass + + loop = GLib.MainLoop() + result_holder = {'file': None} + + def _on_opened(dialog, result, _lh=loop, _holder=result_holder, _fd=fd): + try: + f = _fd.open_finish(result) + _holder['file'] = f + except Exception: + _holder['file'] = None + try: + _fd.close() + except Exception: + pass + try: + _lh.quit() + except Exception: + pass + + fd.open(parent_window, None, _on_opened) + loop.run() + gf = result_holder.get('file') + if gf is not None: + pathname = gf.get_path() or gf.get_uri() or "" + self._logger.debug("askForExistingFile: selected file: %s", pathname) + return pathname + return "" + except Exception: + try: + self._logger.exception('FileDialog.open failed') + except Exception: + pass + return "" + except Exception: + return "" + + def askForSaveFileName(self, startWith: str, filter: str, headline: str): + """ + Prompt user to choose a filename to save data. + + Returns selected filename or empty string if cancelled. + """ + try: + parent_window = None + try: + for od in getattr(YDialogGtk, "_open_dialogs", []) or []: + try: + w = getattr(od, "_window", None) + if w: + parent_window = w + break + except Exception: + pass + except Exception: + parent_window = None + + try: + GLib.set_prgname(self._product_name or "manatools") + except Exception: + pass + + # Use Gtk.FileDialog (GTK 4.10+) exclusively for save + if not hasattr(Gtk, 'FileDialog'): + try: + self._logger.error('Gtk.FileDialog is not available in this GTK runtime') + except Exception: + pass + return "" + + try: + fd = Gtk.FileDialog.new() + try: + fd.set_title(headline or "Save File") + except Exception: + pass + + if filter: + try: + filt = Gtk.FileFilter() + filt.set_name("Text files") + for pat in filter.split(): + try: + filt.add_pattern(pat) + except Exception: + pass + fd.set_filters([filt]) + except Exception: + pass + + if startWith and os.path.exists(startWith): + try: + target = os.path.dirname(startWith) if os.path.isfile(startWith) else startWith + fd.set_initial_folder(Gio.File.new_for_path(target)) + except Exception: + try: + fd.set_initial_folder_uri(GLib.filename_to_uri(target, None)) + except Exception: + pass + + loop = GLib.MainLoop() + result_holder = {'file': None} + + def _on_saved(dialog, result, _lh=loop, _holder=result_holder, _fd=fd): + try: + f = _fd.save_finish(result) + _holder['file'] = f + except Exception: + _holder['file'] = None + try: + _fd.close() + except Exception: + pass + try: + _lh.quit() + except Exception: + pass + + fd.save(parent_window, None, _on_saved) + loop.run() + gf = result_holder.get('file') + if gf is not None: + try: + return gf.get_path() or gf.get_uri() or "" + except Exception: + return "" + return "" + except Exception: + try: + self._logger.exception('FileDialog.save failed') + except Exception: + pass + return "" + except Exception: + try: + self._logger.exception("askForSaveFileName failed") + except Exception: + pass + return "" + class YWidgetFactoryGtk: def __init__(self): pass - + def createMainDialog(self, color_mode=YDialogColorMode.YDialogNormalColor): return YDialogGtk(YDialogType.YMainDialog, color_mode) @@ -348,4 +715,3 @@ def createVSpacing(self, parent, size_px: int = 16): def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) - From 6d00865ae05dce03db074599fc76d112b814f6c0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 4 Jan 2026 23:11:40 +0100 Subject: [PATCH 328/523] Added first implementation of AskForXXX functions --- manatools/aui/yui_curses.py | 306 ++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index ead65d2..9543de6 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -7,6 +7,7 @@ import sys import os import time +import fnmatch from .yui_common import * from .backends.curses import * @@ -80,6 +81,11 @@ def __init__(self): self._product_name = "manatools AUI Curses" self._icon_base_path = "" self._icon = "" + # Default directories + try: + self._default_documents_dir = os.path.expanduser('~/Documenti') + except Exception: + self._default_documents_dir = os.path.expanduser('~') def iconBasePath(self): return self._icon_base_path @@ -97,6 +103,52 @@ def setApplicationIcon(self, Icon): """Set the application icon.""" self._icon = Icon + def askForExistingDirectory(self, startDir: str, headline: str): + """ + NCurses overlay dialog to select an existing directory. + Presents a navigable list of directories similar to a simple file manager. + """ + try: + start_dir = startDir if (startDir and os.path.isdir(startDir)) else os.path.expanduser('~') + return self._browse_paths(start_dir, select_file=False, headline=headline or "Select Directory") + except Exception: + return "" + + def askForExistingFile(self, startWith: str, filter: str, headline: str): + """ + NCurses overlay dialog to select an existing file. + Shows a navigable list of directories and files, honoring simple filters like "*.txt;*.md". + """ + try: + if startWith and os.path.isfile(startWith): + start_dir = os.path.dirname(startWith) + elif startWith and os.path.isdir(startWith): + start_dir = startWith + else: + # Default to Documents if available, else home + start_dir = self._default_documents_dir if os.path.isdir(self._default_documents_dir) else os.path.expanduser('~') + return self._browse_paths(start_dir, select_file=True, headline=headline or "Open File", filter_str=filter) + except Exception: + return "" + + def askForSaveFileName(self, startWith: str, filter: str, headline: str): + """ + NCurses overlay to choose a filename: navigate directories and type the name. + """ + try: + if startWith and os.path.isfile(startWith): + start_dir = os.path.dirname(startWith) + default_name = os.path.basename(startWith) + elif startWith and os.path.isdir(startWith): + start_dir = startWith + default_name = "" + else: + start_dir = self._default_documents_dir if os.path.isdir(self._default_documents_dir) else os.path.expanduser('~') + default_name = "" + return self._save_path_dialog(start_dir, default_name, headline or "Save File", filter_str=filter) + except Exception: + return "" + def applicationIcon(self): """Get the application icon.""" return self._icon @@ -131,6 +183,260 @@ def applicationIcon(self): """Get the application title.""" return self.__icon + # --- Internal helpers for ncurses file/directory chooser --- + def _parse_filter_patterns(self, filter_str: str): + try: + if not filter_str: + return [] + parts = [p.strip() for p in filter_str.split(';') if p.strip()] + return parts + except Exception: + return [] + + def _list_entries(self, current_dir: str, select_file: bool, patterns): + """Return list of (label, path, type) for entries under current_dir. + type is 'dir' or 'file'. If select_file is True, apply patterns to files. + """ + entries = [] + try: + # Add parent directory entry + parent = os.path.dirname(current_dir.rstrip(os.sep)) or current_dir + if parent and parent != current_dir: + entries.append(("..", parent, 'dir')) + # List directory contents + with os.scandir(current_dir) as it: + dirs = [] + files = [] + for e in it: + try: + if e.is_dir(follow_symlinks=False): + dirs.append((e.name + '/', e.path, 'dir')) + elif e.is_file(follow_symlinks=False): + if not select_file: + continue + if not patterns: + files.append((e.name, e.path, 'file')) + else: + for pat in patterns: + if fnmatch.fnmatch(e.name, pat): + files.append((e.name, e.path, 'file')) + break + except Exception: + pass + # Sort directories and files separately + dirs.sort(key=lambda x: x[0].lower()) + files.sort(key=lambda x: x[0].lower()) + entries.extend(dirs) + entries.extend(files) + except Exception: + # On failure, just return parent + pass + return entries + + def _browse_paths(self, start_dir: str, select_file: bool, headline: str, filter_str: str = ""): + """Create an overlay ncurses dialog to navigate directories and pick a directory or file.""" + current_dir = start_dir if os.path.isdir(start_dir) else os.path.expanduser('~') + patterns = self._parse_filter_patterns(filter_str) + + # Build dialog UI + dlg = YDialogCurses(YDialogType.YPopupDialog, YDialogColorMode.YDialogNormalColor) + root = YVBoxCurses(dlg) + title_lbl = YLabelCurses(root, headline, isHeading=True) + path_lbl = YLabelCurses(root, f"Current: {current_dir}") + list_box = YSelectionBoxCurses(root, label="Items", multi_selection=False) + # Let list box stretch vertically + try: + list_box.setWeight(YUIDimension.YD_VERT, 1) + except Exception: + pass + buttons = YHBoxCurses(root) + btn_select = YPushButtonCurses(buttons, "Select") + btn_cancel = YPushButtonCurses(buttons, "Cancel") + + def refresh_listing(dir_path): + try: + list_box.deleteAllItems() + for (label, path, typ) in self._list_entries(dir_path, select_file, patterns): + it = YItem(label) + try: + it.setData({'path': path, 'type': typ}) + except Exception: + pass + list_box.addItem(it) + except Exception: + pass + + refresh_listing(current_dir) + try: + dlg.open() + except Exception: + return "" + + # Event loop + result = "" + while True: + ev = dlg.waitForEvent(0) + if isinstance(ev, YCancelEvent): + result = "" + break + if isinstance(ev, YWidgetEvent): + w = ev.widget() + if w == btn_cancel and ev.reason() == YEventReason.Activated: + result = "" + break + # Selecting from list via button + if w == btn_select and ev.reason() == YEventReason.Activated: + try: + sel = list_box.selectedItems() + if not sel: + continue + item = sel[0] + data = item.data() if hasattr(item, 'data') else None + if not data or 'path' not in data: + continue + if data.get('type') == 'dir': + # In directory mode, selecting chooses current dir + if not select_file: + result = data['path'] + break + # In file mode, selecting a dir navigates + current_dir = data['path'] + path_lbl.setText(f"Current: {current_dir}") + refresh_listing(current_dir) + continue + else: + # File selected -> choose in file mode + if select_file: + result = data['path'] + break + except Exception: + continue + # Enter/Space on list toggles selection -> use SelectionChanged to navigate/select + if w == list_box and ev.reason() == YEventReason.SelectionChanged: + try: + sel = list_box.selectedItems() + if not sel: + continue + item = sel[0] + data = item.data() if hasattr(item, 'data') else None + if not data or 'path' not in data: + continue + if data.get('type') == 'dir': + # Navigate into directory + current_dir = data['path'] + path_lbl.setText(f"Current: {current_dir}") + refresh_listing(current_dir) + continue + else: + # File chosen via Enter in file mode + if select_file: + result = data['path'] + break + except Exception: + continue + try: + dlg.destroy() + except Exception: + pass + # For directory mode, if user navigated, return the current directory when no explicit file was selected. + if not select_file and result == "": + try: + # Return the current_dir as selection. + return current_dir + except Exception: + return "" + return result + + def _save_path_dialog(self, start_dir: str, default_name: str, headline: str, filter_str: str = ""): + """Overlay dialog to navigate to a directory and enter a filename to save.""" + current_dir = start_dir if os.path.isdir(start_dir) else os.path.expanduser('~') + patterns = self._parse_filter_patterns(filter_str) + + dlg = YDialogCurses(YDialogType.YPopupDialog, YDialogColorMode.YDialogNormalColor) + root = YVBoxCurses(dlg) + YLabelCurses(root, headline, isHeading=True) + path_lbl = YLabelCurses(root, f"Current: {current_dir}") + list_box = YSelectionBoxCurses(root, label="Items", multi_selection=False) + try: + list_box.setWeight(YUIDimension.YD_VERT, 1) + except Exception: + pass + filename_input = YInputFieldCurses(root, label="Filename:") + try: + if default_name: + filename_input.setValue(default_name) + except Exception: + pass + buttons = YHBoxCurses(root) + btn_save = YPushButtonCurses(buttons, "Save") + btn_cancel = YPushButtonCurses(buttons, "Cancel") + + def refresh_listing(dir_path): + try: + list_box.deleteAllItems() + for (label, path, typ) in self._list_entries(dir_path, select_file=True, patterns=patterns): + it = YItem(label) + try: + it.setData({'path': path, 'type': typ}) + except Exception: + pass + list_box.addItem(it) + except Exception: + pass + + refresh_listing(current_dir) + try: + dlg.open() + except Exception: + return "" + + result = "" + while True: + ev = dlg.waitForEvent(0) + if isinstance(ev, YCancelEvent): + result = "" + break + if isinstance(ev, YWidgetEvent): + w = ev.widget() + if w == btn_cancel and ev.reason() == YEventReason.Activated: + result = "" + break + if w == btn_save and ev.reason() == YEventReason.Activated: + try: + name = filename_input.value() + if name: + result = os.path.join(current_dir, name) + break + except Exception: + continue + if w == list_box and ev.reason() == YEventReason.SelectionChanged: + try: + sel = list_box.selectedItems() + if not sel: + continue + item = sel[0] + data = item.data() if hasattr(item, 'data') else None + if not data or 'path' not in data: + continue + if data.get('type') == 'dir': + current_dir = data['path'] + path_lbl.setText(f"Current: {current_dir}") + refresh_listing(current_dir) + continue + else: + # Pre-fill filename with selected file's name + try: + filename_input.setValue(os.path.basename(data['path'])) + except Exception: + pass + except Exception: + continue + try: + dlg.destroy() + except Exception: + pass + return result + class YWidgetFactoryCurses: def __init__(self): pass From 963c86b60508642e4541384eeeefef97f35b0a89 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 12:26:56 +0100 Subject: [PATCH 329/523] fixed askforsave, though starting directory does not work for ask for file e dir --- manatools/aui/yui_gtk.py | 75 +++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 3371850..35df9bb 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -272,6 +272,12 @@ def askForExistingDirectory(self, startDir: str, headline: str): GLib.set_prgname(self._product_name or "manatools") except Exception: pass + # Log portal-related environment to help diagnose behavior differences + try: + self._logger.debug("GTK_USE_PORTAL=%s", os.environ.get("GTK_USE_PORTAL")) + self._logger.debug("XDG_CURRENT_DESKTOP=%s", os.environ.get("XDG_CURRENT_DESKTOP")) + except Exception: + pass # Use Gtk.FileDialog (GTK 4.10+) exclusively for directory selection if not hasattr(Gtk, 'FileDialog'): @@ -288,6 +294,11 @@ def askForExistingDirectory(self, startDir: str, headline: str): except Exception: pass + try: + fd.set_modal(True) + except Exception: + pass + if startDir and os.path.exists(startDir): try: fd.set_initial_folder(Gio.File.new_for_path(startDir)) @@ -315,6 +326,19 @@ def _on_opened(dialog, result, _lh=loop, _holder=result_holder, _fd=fd): except Exception: pass + # Fallback transient parent for portals/desktops that require it + if parent_window is None: + self._logger.warning("askForExistingDirectory: no parent window found") + try: + parent_window = Gtk.Window() + try: + parent_window.set_title(self._application_title) + except Exception: + pass + except Exception: + self._logger.exception("askForExistingDirectory: failed to create fallback parent window") + parent_window = None + fd.select_folder(parent_window, None, _on_opened) loop.run() gf = result_holder.get('file') @@ -370,6 +394,12 @@ def askForExistingFile(self, startWith: str, filter: str, headline: str): GLib.set_prgname(self._product_name or "manatools") except Exception: pass + # Log portal-related environment to help diagnose behavior differences + try: + self._logger.debug("GTK_USE_PORTAL=%s", os.environ.get("GTK_USE_PORTAL")) + self._logger.debug("XDG_CURRENT_DESKTOP=%s", os.environ.get("XDG_CURRENT_DESKTOP")) + except Exception: + pass # Use Gtk.FileDialog (GTK 4.10+) exclusively for file open if not hasattr(Gtk, 'FileDialog'): @@ -385,6 +415,14 @@ def askForExistingFile(self, startWith: str, filter: str, headline: str): fd.set_title(headline or "Open File") except Exception: pass + try: + fd.set_modal(True) + except Exception: + pass + try: + fd.set_accept_label("Open") + except Exception: + pass # filters try: @@ -398,7 +436,10 @@ def askForExistingFile(self, startWith: str, filter: str, headline: str): self._logger.exception("askForExistingFile: setting filters failed") pass + # Determine initial directory: requested path if valid, else default Documents + initial_dir = None if startWith and os.path.exists(startWith): + # Set both folder and URI to improve portal compatibility try: target = os.path.dirname(startWith) if os.path.isfile(startWith) else startWith fd.set_initial_folder(Gio.File.new_for_path(target)) @@ -483,25 +524,28 @@ def askForSaveFileName(self, startWith: str, filter: str, headline: str): fd.set_title(headline or "Save File") except Exception: pass + try: + fd.set_modal(True) + except Exception: + pass if filter: try: - filt = Gtk.FileFilter() - filt.set_name("Text files") - for pat in filter.split(): - try: - filt.add_pattern(pat) - except Exception: - pass - fd.set_filters([filt]) + filters = self._create_gtk4_filters(filter) + if filters: + filter_list = Gio.ListStore.new(Gtk.FileFilter) + for file_filter in filters: + filter_list.append(file_filter) + fd.set_filters(filter_list) except Exception: - pass + self._logger.exception("askForSaveFileName: setting filters failed") if startWith and os.path.exists(startWith): try: target = os.path.dirname(startWith) if os.path.isfile(startWith) else startWith fd.set_initial_folder(Gio.File.new_for_path(target)) except Exception: + self._logger.exception("askForSaveFileName: setting initial folder failed") try: fd.set_initial_folder_uri(GLib.filename_to_uri(target, None)) except Exception: @@ -525,6 +569,19 @@ def _on_saved(dialog, result, _lh=loop, _holder=result_holder, _fd=fd): except Exception: pass + # Fallback transient parent creation if none present + if parent_window is None: + self._logger.warning("askForSaveFileName: no parent window found") + try: + parent_window = Gtk.Window() + try: + parent_window.set_title(self._application_title) + except Exception: + pass + except Exception: + self._logger.exception("askForSaveFileName: failed to create fallback parent window") + parent_window = None + fd.save(parent_window, None, _on_saved) loop.run() gf = result_holder.get('file') From 4f2dd3735a5e72ed4a720db8e8e77df92e6aec77 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 12:51:50 +0100 Subject: [PATCH 330/523] Improving drawing with more widgets --- .../aui/backends/curses/pushbuttoncurses.py | 31 +++++++++++++------ manatools/aui/backends/curses/vboxcurses.py | 29 +++++++++++------ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index bd1f2c5..ea3bcaa 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -84,18 +84,29 @@ def _draw(self, window, y, x, width, height): try: # Center the button label within available width button_text = f"[ {self._label} ]" - text_x = x + max(0, (width - len(button_text)) // 2) + # Determine drawing position and clip text if necessary + if width <= 0: + return + if len(button_text) <= width: + text_x = x + max(0, (width - len(button_text)) // 2) + draw_text = button_text + else: + # Not enough space: draw truncated centered/left-aligned text + draw_text = button_text[:max(1, width)] + text_x = x - # Only draw if we have enough space - if text_x + len(button_text) <= x + width: - if not self.isEnabled(): - attr = curses.A_DIM - else: - attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - if self._focused: - attr |= curses.A_BOLD + if not self.isEnabled(): + attr = curses.A_DIM + else: + attr = curses.A_REVERSE if self._focused else curses.A_NORMAL + if self._focused: + attr |= curses.A_BOLD - window.addstr(y, text_x, button_text, attr) + try: + window.addstr(y, text_x, draw_text, attr) + except curses.error: + # Best-effort: if even this fails, ignore + pass except curses.error as e: try: self._logger.error("_draw curses.error: %s", e, exc_info=True) diff --git a/manatools/aui/backends/curses/vboxcurses.py b/manatools/aui/backends/curses/vboxcurses.py index 3818364..98d70df 100644 --- a/manatools/aui/backends/curses/vboxcurses.py +++ b/manatools/aui/backends/curses/vboxcurses.py @@ -118,20 +118,29 @@ def _draw(self, window, y, x, width, height): else: fixed_height_total += child_min - available_for_stretch = max(0, height - fixed_height_total - spacing) + # Determine spacing budget and allocate stretch space + # Compute base minimal total (children minima + minimal gaps) + base_min_total = sum(child_min_heights) + spacing + # If base minimal total exceeds height, reduce spacing to fit + if base_min_total > height: + # No space for all gaps; allow zero or minimal gaps + spacing_allowed = max(0, height - sum(child_min_heights)) + else: + spacing_allowed = spacing + + # Recompute available rows for stretch after honoring allowed spacing + available_for_stretch = max(0, height - sum(child_min_heights) - spacing_allowed) allocated = list(child_min_heights) if stretchable_indices: total_weight = sum(stretchable_weights) or len(stretchable_indices) - # Proportional distribution of extra rows extras = [0] * len(stretchable_indices) base = 0 for k, idx in enumerate(stretchable_indices): extra = (available_for_stretch * stretchable_weights[k]) // total_weight extras[k] = extra base += extra - # Distribute leftover rows due to integer division leftover = available_for_stretch - base for k in range(len(stretchable_indices)): if leftover <= 0: @@ -141,20 +150,20 @@ def _draw(self, window, y, x, width, height): for k, idx in enumerate(stretchable_indices): allocated[idx] = child_min_heights[idx] + extras[k] - total_alloc = sum(allocated) + spacing + # If still room, give remainder to last stretchable/last child + total_alloc = sum(allocated) + spacing_allowed if total_alloc < height: - # Give remainder to the last stretchable (or last child) extra = height - total_alloc target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) allocated[target] += extra elif total_alloc > height: - # Reduce overflow from the last stretchable (or last child) diff = total_alloc - height target = stretchable_indices[-1] if stretchable_indices else (num_children - 1) allocated[target] = max(1, allocated[target] - diff) - # Draw children with allocated heights + # Draw children with allocated heights, inserting at most spacing_allowed gaps cy = y + gaps_allowed = spacing_allowed for i, child in enumerate(self._children): ch = allocated[i] if ch <= 0: @@ -172,5 +181,7 @@ def _draw(self, window, y, x, width, height): except Exception: _mod_logger.error("_draw child error", exc_info=True) cy += ch - if i < num_children - 1 and cy < (y + height): - cy += 1 # one-line spacing + # Insert at most one-line gap between children if budget allows + if i < num_children - 1 and gaps_allowed > 0 and cy < (y + height): + cy += 1 + gaps_allowed -= 1 From d829b36c44d150027ccfe68717fb8d8483410b9f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 14:37:58 +0100 Subject: [PATCH 331/523] Added some useful information from widgets --- manatools/aui/yui_common.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 289622c..9a72082 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -149,8 +149,16 @@ def __init__(self, parent=None): def widgetClass(self): return self.__class__.__name__ + def widgetPathName(self): + ''' Return the full path name of this widget in the hierarchy. ''' + return f"{self._parent.widgetPathName()}/{self.widgetClass()}({self._id})" if self._parent else f"/{self.widgetClass()}({self._id})" + + def id(self): + ''' Return the unique identifier of this widget. ''' + return self._id + def debugLabel(self): - return f"{self._parent.debugLabel()}/{self.widgetClass()}({self._id})" if self._parent else f"{self.widgetClass()}({self._id})" + return f"{self.widgetClass()}({self._id})" def helpText(self): return self._help_text From 36e6508412766fd8e7c395bc247ddf3cc3fea5b9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 15:13:51 +0100 Subject: [PATCH 332/523] wrong logging level and position --- manatools/aui/backends/curses/dialogcurses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index e53b1fb..f6b6cd7 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -159,7 +159,7 @@ def _draw_dialog(self): try: height, width = self._backend_widget.getmaxyx() try: - self._logger.info("Dialog window size: height=%d width=%d", height, width) + self._logger.debug("Dialog window size: height=%d width=%d", height, width) except Exception: pass @@ -186,12 +186,12 @@ def _draw_dialog(self): # Draw content area - fixed coordinates for child content_height = height - 4 content_width = width - 4 + content_y = 2 + content_x = 2 try: - self._logger.info("Dialog content area: y=%d x=%d h=%d w=%d", content_y, content_x, content_height, content_width) + self._logger.debug("Dialog content area: y=%d x=%d h=%d w=%d", content_y, content_x, content_height, content_width) except Exception: pass - content_y = 2 - content_x = 2 # Draw child content if self.hasChildren(): From f10901f07be622a245fb8c2177d85bc032615e27 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 15:14:25 +0100 Subject: [PATCH 333/523] forced weight boundaries --- manatools/aui/yui_common.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 9a72082..3cfe11e 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -260,16 +260,24 @@ def setStretchable(self, dim, new_stretch): self._stretchable_vert = new_stretch def weight(self, dim): + """ + The weight is used when all widgets can get their preferred size and yet space is available. + The remaining space will be devided between all stretchable widgets according to their weights. + A widget with greater weight will get more space. The default weight for all widgets is 0. + The weight range is 0-99. + """ if dim == YUIDimension.YD_HORIZ: return self._weight_horiz else: return self._weight_vert - def setWeight(self, dim, weight): + def setWeight(self, dim, weight: int): + w = weight % 100 if weight >= 0 else 0 + if dim == YUIDimension.YD_HORIZ: - self._weight_horiz = weight + self._weight_horiz = w else: - self._weight_vert = weight + self._weight_vert = w def setNotify(self, notify=True): self._notify = notify From 73c4ce1163c3f2ee31e0d6ad17fdb5d3124f7932 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 15:50:14 +0100 Subject: [PATCH 334/523] fixed drawing and alignment issues, considered also weigth on drawing --- .../aui/backends/curses/alignmentcurses.py | 28 ++- manatools/aui/backends/curses/hboxcurses.py | 227 ++++++++++++++---- manatools/aui/backends/curses/vboxcurses.py | 132 +++++++--- 3 files changed, 302 insertions(+), 85 deletions(-) diff --git a/manatools/aui/backends/curses/alignmentcurses.py b/manatools/aui/backends/curses/alignmentcurses.py index 7a081d4..826c3cc 100644 --- a/manatools/aui/backends/curses/alignmentcurses.py +++ b/manatools/aui/backends/curses/alignmentcurses.py @@ -117,10 +117,22 @@ def _draw(self, window, y, x, width, height): except Exception: pass # Horizontal position + # determine the width we'll give to the child: prefer an + # explicit child _width (or the child's minimal width), but + # never exceed the available width + try: + child_pref_w = getattr(self.child(), "_width", None) + if child_pref_w is not None: + child_w = min(width, max(ch_min_w, int(child_pref_w))) + else: + child_w = min(width, ch_min_w) + except Exception: + child_w = min(width, ch_min_w) + if self._halign_spec == YAlignmentType.YAlignEnd: - cx = x + max(0, width - ch_min_w) + cx = x + max(0, width - child_w) elif self._halign_spec == YAlignmentType.YAlignCenter: - cx = x + max(0, (width - ch_min_w) // 2) + cx = x + max(0, (width - child_w) // 2) else: cx = x # Vertical position (single line widgets mostly) @@ -138,7 +150,17 @@ def _draw(self, window, y, x, width, height): ch_height = max(ch_height, min_h) except Exception: pass - self.child()._draw(window, cy, cx, min(ch_min_w, max(1, width)), min(height, ch_height)) + # give the computed width to the child (at least 1 char) + final_w = max(1, child_w) + try: + self._logger.debug("Alignment draw: child=%s halign=%s valign=%s container=(%d,%d) size=(%d,%d) child_min=%d child_pref=%s child_w=%d cx=%d cy=%d", + self.child().debugLabel() if hasattr(self.child(), 'debugLabel') else '', + self._halign_spec, self._valign_spec, + x, y, width, height, + ch_min_w, getattr(self.child(), '_width', None), final_w, cx, cy) + except Exception: + pass + self.child()._draw(window, cy, cx, final_w, min(height, ch_height)) except Exception: pass diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py index 963b9d4..d1924e6 100644 --- a/manatools/aui/backends/curses/hboxcurses.py +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -108,6 +108,16 @@ def _child_min_width(self, child, max_width): return min(max_width, max(1, int(child.minWidth()))) except Exception: pass + # If the child is a container, compute its recursive minimal width + try: + cls = child.widgetClass() if hasattr(child, "widgetClass") else "" + if cls in ("YVBox", "YHBox", "YFrame", "YCheckBoxFrame", "YAlignment", "YReplacePoint"): + try: + return min(max_width, max(1, _curses_recursive_min_width(child))) + except Exception: + pass + except Exception: + pass # Heuristics based on common attributes try: cls = child.widgetClass() if hasattr(child, "widgetClass") else "" @@ -137,73 +147,194 @@ def _draw(self, window, y, x, width, height): widths = [0] * num_children stretchables = [] min_reserved = [0] * num_children + pref_reserved = [0] * num_children + child_weights = [0] * num_children for i, child in enumerate(self._children): # compute each child's minimal width (best-effort) m = self._child_min_width(child, available) min_reserved[i] = max(1, m) - if child.stretchable(YUIDimension.YD_HORIZ): + # preferred width: allow explicit _width or min_reserved as default + try: + pref_w = int(getattr(child, "_width", min_reserved[i])) + except Exception: + pref_w = min_reserved[i] + pref_reserved[i] = max(min_reserved[i], pref_w) + # gather declared weight (if any) + try: + w = int(child.weight(YUIDimension.YD_HORIZ)) + except Exception: + try: + w = int(getattr(child, "_weight", 0)) + except Exception: + w = 0 + if w < 0: + w = 0 + child_weights[i] = w + # consider as stretchable if explicitly stretchable or has non-zero weight + if child.stretchable(YUIDimension.YD_HORIZ) or child_weights[i] > 0: stretchables.append(i) # Sum fixed (non-stretchable) minimal widths and minimal total for stretchables - fixed_total = sum(min_reserved[i] for i, c in enumerate(self._children) if not c.stretchable(YUIDimension.YD_HORIZ)) - min_stretch_total = sum(min_reserved[i] for i, c in enumerate(self._children) if c.stretchable(YUIDimension.YD_HORIZ)) - - # Available space already accounts for gaps - remaining = available - fixed_total - min_stretch_total - - if stretchables and remaining > 0: - # Start from each stretchable's minimum, then distribute leftover - per = remaining // len(stretchables) - extra = remaining % len(stretchables) - for k, idx in enumerate(stretchables): - widths[idx] = min_reserved[idx] + per + (1 if k < extra else 0) - # Fixed children get their reserved minima + fixed_total = sum(min_reserved[i] for i, c in enumerate(self._children) if not (c.stretchable(YUIDimension.YD_HORIZ) or child_weights[i] > 0)) + min_stretch_total = sum(min_reserved[i] for i, c in enumerate(self._children) if (c.stretchable(YUIDimension.YD_HORIZ) or child_weights[i] > 0)) + + # Start allocation from preferred widths, then adjust to fit available + widths = list(pref_reserved) + + # collect stretchable weights (0 means equal-share fallback) + stretch_weights = [] + for idx in stretchables: + w = child_weights[idx] if idx < len(child_weights) else 0 + # normalize weight to 0..100 range if out of bounds + try: + if w > 100: + w = 100 + elif w < 0: + w = 0 + except Exception: + w = 0 + stretch_weights.append(w) + + try: + # Detailed per-child diagnostics + details = [] for i, child in enumerate(self._children): - if not child.stretchable(YUIDimension.YD_HORIZ): - widths[i] = min_reserved[i] - else: - # Either no stretchables, or not enough space to give extra. - # In this case, try to honor minimal reservations as much as possible. - # If total minima exceed available, shrink proportionally but keep at least 1. - total_min = fixed_total + min_stretch_total - if total_min <= available: - # we have some leftover but no stretchables to expand; distribute among all - leftover = available - total_min - per = leftover // num_children if num_children else 0 - extra = leftover % num_children if num_children else 0 - for i in range(num_children): - widths[i] = min_reserved[i] + per + (1 if i < extra else 0) + try: + lbl = child.debugLabel() if hasattr(child, 'debugLabel') else f'child_{i}' + except Exception: + lbl = f'child_{i}' + try: + sw = bool(child.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + sw = False + try: + wv = int(child.weight(YUIDimension.YD_HORIZ)) + except Exception: + wv = 0 + details.append((i, lbl, min_reserved[i], pref_reserved[i], sw, wv)) + self._logger.debug("HBox allocation inputs: available=%d spacing=%d details=%s", + available, spacing, details) + except Exception: + pass + + total_pref = sum(widths) + # If preferred total fits, distribute surplus among stretchables by weight + if total_pref <= available: + surplus = available - total_pref + if stretchables: + total_weight = sum(stretch_weights) + if total_weight <= 0: + per = surplus // len(stretchables) if stretchables else 0 + extra = surplus % len(stretchables) if stretchables else 0 + for k, idx in enumerate(stretchables): + widths[idx] += per + (1 if k < extra else 0) + else: + assigned = [0] * len(stretchables) + base = 0 + for k, w in enumerate(stretch_weights): + add = (surplus * w) // total_weight + assigned[k] = add + base += add + leftover = surplus - base + for k in range(len(stretchables)): + if leftover <= 0: + break + assigned[k] += 1 + leftover -= 1 + for k, idx in enumerate(stretchables): + widths[idx] += assigned[k] else: - # Need to shrink some minima to fit; compute shrink ratio - # Start from minima and reduce from largest items first to preserve small widgets - widths = list(min_reserved) - overflow = total_min - available - # Sort indices by current width descending - order = sorted(range(num_children), key=lambda ii: widths[ii], reverse=True) + # No stretchables: spread leftover evenly among all children + per = surplus // num_children if num_children else 0 + extra = surplus % num_children if num_children else 0 + for i in range(num_children): + widths[i] += per + (1 if i < extra else 0) + else: + # Need to shrink preferred sizes down to minima to fit available + total_with_spacing = total_pref + if total_with_spacing + spacing > available: + overflow = total_with_spacing + spacing - available + reducible = [max(0, widths[i] - min_reserved[i]) for i in range(num_children)] + order = sorted(range(num_children), key=lambda ii: reducible[ii], reverse=True) for idx in order: if overflow <= 0: break - can_reduce = widths[idx] - 1 - if can_reduce <= 0: + can = reducible[idx] + if can <= 0: continue - take = min(can_reduce, overflow) + take = min(can, overflow) widths[idx] -= take overflow -= take - # If still overflow (shouldn't happen), clamp all to 1 if overflow > 0: for i in range(num_children): - widths[i] = 1 + if overflow <= 0: + break + can = max(0, widths[i] - 1) + take = min(can, overflow) + widths[i] -= take + overflow -= take - # Debug: log final allocation before drawing children + # Final debug of allocated widths try: - total_assigned = sum(widths) - self._logger.info("HBox final widths=%s total=%d (available=%d)", widths, total_assigned, available) - self._logger.debug("HBox internal min_reserved=%s fixed_total=%d min_stretch_total=%d num_stretch=%d", - min_reserved, fixed_total, min_stretch_total if 'min_stretch_total' in locals() else 0, - len(stretchables)) + self._logger.debug("HBox allocated widths=%s total=%d (available=%d)", widths, sum(widths), available) except Exception: pass + # Ensure containers get at least the width required by their children + def _required_width_for(widget): + try: + cls = widget.widgetClass() if hasattr(widget, 'widgetClass') else '' + except Exception: + cls = '' + try: + if cls == 'YPushButton': + lbl = getattr(widget, '_label', '') + return max(1, len(f"[ {lbl} ]")) + if cls == 'YLabel': + txt = getattr(widget, '_text', '') or getattr(widget, '_label', '') + return max(1, len(str(txt))) + if cls in ('YVBox', 'YHBox', 'YFrame', 'YCheckBoxFrame', 'YAlignment', 'YReplacePoint'): + # for containers, required width is max of children's required widths + mx = 1 + for ch in getattr(widget, '_children', []) or []: + try: + r = _required_width_for(ch) + if r > mx: + mx = r + except Exception: + pass + return mx + except Exception: + pass + # fallback to minimal reserved + try: + return max(1, _curses_recursive_min_width(widget)) + except Exception: + return 1 + + # try to satisfy required widths by borrowing from others + for i, child in enumerate(self._children): + req = _required_width_for(child) + if widths[i] < req: + need = req - widths[i] + # try to reduce other children that are above their minima + for j in range(num_children): + if j == i: + continue + avail = widths[j] - min_reserved[j] + if avail <= 0: + continue + take = min(avail, need) + widths[j] -= take + widths[i] += take + need -= take + if need <= 0: + break + # if still need, clamp to available overall (best-effort) + if widths[i] < req: + # nothing else we can do; leave as-is + pass + # Draw children and pass full container height to stretchable children cx = x for i, child in enumerate(self._children): @@ -223,6 +354,12 @@ def _draw(self, window, y, x, width, height): else: ch = min(height, max(1, getattr(child, "_height", 1))) if hasattr(child, "_draw"): + try: + self._logger.debug("HBox drawing child %d: lbl=%s alloc_w=%d x=%d height=%d ch_h=%d", i, + (child.debugLabel() if hasattr(child, 'debugLabel') else f'child_{i}'), + w, cx, height, ch) + except Exception: + pass child._draw(window, y, cx, w, ch) cx += w if i < num_children - 1: diff --git a/manatools/aui/backends/curses/vboxcurses.py b/manatools/aui/backends/curses/vboxcurses.py index 98d70df..f6f16ef 100644 --- a/manatools/aui/backends/curses/vboxcurses.py +++ b/manatools/aui/backends/curses/vboxcurses.py @@ -86,69 +86,127 @@ def _draw(self, window, y, x, width, height): spacing = max(0, num_children - 1) + # Compute both minimal heights and preferred (requested) heights child_min_heights = [] + child_pref_heights = [] stretchable_indices = [] stretchable_weights = [] fixed_height_total = 0 for i, child in enumerate(self._children): - # Use recursive min height for containers and frames - child_min = max(1, _curses_recursive_min_height(child)) - # If child can compute desired height for the current width, honor it + # Minimal height (hard lower bound) + min_h = max(1, _curses_recursive_min_height(child)) + # Preferred/requested height (may be larger than min_h) + pref_h = min_h + try: + # explicit _height attribute may express a preferred size + if hasattr(child, "_height"): + h = int(getattr(child, "_height", pref_h)) + pref_h = max(pref_h, h) + except Exception: + pass try: if hasattr(child, "_desired_height_for_width"): dh = int(child._desired_height_for_width(width)) - if dh > child_min: - child_min = dh + pref_h = max(pref_h, dh) except Exception: pass - child_min_heights.append(child_min) + child_min_heights.append(min_h) + child_pref_heights.append(pref_h) is_stretch = bool(child.stretchable(YUIDimension.YD_VERT)) if is_stretch: stretchable_indices.append(i) try: w = child.weight(YUIDimension.YD_VERT) - w = int(w) if w is not None else 1 + # normalize weight to 0..100; default 0 + w = int(w) if w is not None else 0 except Exception: - w = 1 - if w <= 0: - w = 1 + w = 0 + if w < 0: + w = 0 stretchable_weights.append(w) else: - fixed_height_total += child_min + fixed_height_total += min_h # Determine spacing budget and allocate stretch space - # Compute base minimal total (children minima + minimal gaps) - base_min_total = sum(child_min_heights) + spacing - # If base minimal total exceeds height, reduce spacing to fit - if base_min_total > height: - # No space for all gaps; allow zero or minimal gaps - spacing_allowed = max(0, height - sum(child_min_heights)) + # Compute minimal totals and decide allowed gaps + min_total = sum(child_min_heights) + # If even minimal sizes plus spacing don't fit, reduce spacing first + if min_total + spacing > height: + spacing_allowed = max(0, height - min_total) else: spacing_allowed = spacing - # Recompute available rows for stretch after honoring allowed spacing - available_for_stretch = max(0, height - sum(child_min_heights) - spacing_allowed) - - allocated = list(child_min_heights) - - if stretchable_indices: - total_weight = sum(stretchable_weights) or len(stretchable_indices) - extras = [0] * len(stretchable_indices) - base = 0 - for k, idx in enumerate(stretchable_indices): - extra = (available_for_stretch * stretchable_weights[k]) // total_weight - extras[k] = extra - base += extra - leftover = available_for_stretch - base - for k in range(len(stretchable_indices)): - if leftover <= 0: + # Preferred total is sum of preferred heights + pref_total = sum(child_pref_heights) + + # Start allocation from preferred heights clamped to available space + allocated = list(child_pref_heights) + + # If preferred total plus spacing fits, we'll later distribute surplus + total_with_pref = pref_total + spacing_allowed + if total_with_pref <= height: + surplus = height - total_with_pref + else: + surplus = 0 + + # If there's surplus after preferred sizes and allowed spacing, distribute + # it among stretchable children according to their weights (0..100). + if stretchable_indices and surplus > 0: + weights = [stretchable_weights[i] for i in range(len(stretchable_indices))] + total_weight = sum(weights) + # If all weights are zero, distribute equally + if total_weight <= 0: + per = surplus // len(stretchable_indices) + extra_left = surplus % len(stretchable_indices) + for k, idx in enumerate(stretchable_indices): + allocated[idx] += per + (1 if k < extra_left else 0) + else: + # Distribute proportional to weights + assigned = [0] * len(stretchable_indices) + base = 0 + for k, idx in enumerate(stretchable_indices): + add = (surplus * weights[k]) // total_weight + assigned[k] = add + base += add + leftover = surplus - base + for k in range(len(stretchable_indices)): + if leftover <= 0: + break + assigned[k] += 1 + leftover -= 1 + for k, idx in enumerate(stretchable_indices): + allocated[idx] += assigned[k] + + # If preferred sizes + spacing do not fit, we must shrink from preferred + # down to minima. Reduce largest available slack first. + total_alloc = sum(allocated) + spacing_allowed + if total_alloc > height: + overflow = total_alloc - height + # compute how much each child can be reduced (allocated - min) + reducible = [max(0, allocated[i] - child_min_heights[i]) for i in range(num_children)] + # sort indices by reducible descending to preserve small widgets + order = sorted(range(num_children), key=lambda ii: reducible[ii], reverse=True) + for idx in order: + if overflow <= 0: break - extras[k] += 1 - leftover -= 1 - for k, idx in enumerate(stretchable_indices): - allocated[idx] = child_min_heights[idx] + extras[k] + can = reducible[idx] + if can <= 0: + continue + take = min(can, overflow) + allocated[idx] -= take + overflow -= take + # if still overflow, clamp from any child down to 1 + if overflow > 0: + for i in range(num_children): + if overflow <= 0: + break + can = max(0, allocated[i] - 1) + take = min(can, overflow) + allocated[i] -= take + overflow -= take + total_alloc = sum(allocated) + spacing_allowed # If still room, give remainder to last stretchable/last child total_alloc = sum(allocated) + spacing_allowed From 286a181c73286761a9b50ad2f0cf9cdc895a701d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 19:53:15 +0100 Subject: [PATCH 335/523] Managed weight for Qt --- manatools/aui/backends/qt/hboxqt.py | 39 ++++++++++++++++++++++------- manatools/aui/backends/qt/vboxqt.py | 38 +++++++++++++++++++++------- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/manatools/aui/backends/qt/hboxqt.py b/manatools/aui/backends/qt/hboxqt.py index 3ed8941..26e885d 100644 --- a/manatools/aui/backends/qt/hboxqt.py +++ b/manatools/aui/backends/qt/hboxqt.py @@ -41,13 +41,27 @@ def _create_backend_widget(self): layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(5) - for child in self._children: + # Map YWidget weights and stretchable flags to Qt layout stretch factors. + # Weight semantics: if any child has a positive weight (>0) use those + # weights as stretch factors; otherwise give equal stretch (1) to + # children that are marked stretchable. Non-stretchable children get 0. + try: + child_weights = [int(child.weight(YUIDimension.YD_HORIZ) or 0) for child in self._children] + except Exception: + child_weights = [0 for _ in self._children] + has_positive = any(w > 0 for w in child_weights) + + for idx, child in enumerate(self._children): widget = child.get_backend_widget() - expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 + weight = int(child_weights[idx]) if idx < len(child_weights) else 0 + if has_positive: + stretch = weight + else: + stretch = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 - # If the child requests horizontal stretch, set its QSizePolicy to Expanding + # If the child will receive extra space, set its QSizePolicy to Expanding try: - if expand == 1: + if stretch > 0: sp = widget.sizePolicy() try: sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) @@ -59,12 +73,13 @@ def _create_backend_widget(self): widget.setSizePolicy(sp) except Exception: pass + self._backend_widget.setEnabled(bool(self._enabled)) try: - self._logger.debug("YHBoxQt: adding child %s expand=%s", child.widgetClass(), expand) + self._logger.debug("YHBoxQt: adding child %s stretch=%s weight=%s", child.widgetClass(), stretch, weight) except Exception: pass - layout.addWidget(widget, stretch=expand) + layout.addWidget(widget, stretch=stretch) try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -99,9 +114,15 @@ def addChild(self, child): def _deferred_attach(): try: widget = child.get_backend_widget() - expand = 1 if child.stretchable(YUIDimension.YD_HORIZ) else 0 try: - if expand == 1: + weight = int(child.weight(YUIDimension.YD_HORIZ) or 0) + except Exception: + weight = 0 + # If dynamic addition, use explicit weight when >0, otherwise + # fall back to stretchable flag (equal-share represented by 1). + stretch = weight if weight > 0 else (1 if child.stretchable(YUIDimension.YD_HORIZ) else 0) + try: + if stretch > 0: sp = widget.sizePolicy() try: sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) @@ -118,7 +139,7 @@ def _deferred_attach(): if lay is None: lay = QtWidgets.QHBoxLayout(self._backend_widget) self._backend_widget.setLayout(lay) - lay.addWidget(widget, stretch=expand) + lay.addWidget(widget, stretch=stretch) try: widget.show() except Exception: diff --git a/manatools/aui/backends/qt/vboxqt.py b/manatools/aui/backends/qt/vboxqt.py index 52728c4..f389c86 100644 --- a/manatools/aui/backends/qt/vboxqt.py +++ b/manatools/aui/backends/qt/vboxqt.py @@ -41,13 +41,27 @@ def _create_backend_widget(self): layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(5) - for child in self._children: + # Map YWidget weights and stretchable flags to Qt layout stretch factors. + # Weight semantics: if any child has a positive weight (>0) use those + # weights as stretch factors; otherwise give equal stretch (1) to + # children that are marked stretchable. Non-stretchable children get 0. + try: + child_weights = [int(child.weight(YUIDimension.YD_VERT) or 0) for child in self._children] + except Exception: + child_weights = [0 for _ in self._children] + has_positive = any(w > 0 for w in child_weights) + + for idx, child in enumerate(self._children): widget = child.get_backend_widget() - expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 + weight = int(child_weights[idx]) if idx < len(child_weights) else 0 + if has_positive: + stretch = weight + else: + stretch = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 - # If the child requests horizontal stretch, set its QSizePolicy to Expanding + # If the child will receive extra space, set its QSizePolicy to Expanding try: - if expand == 1: + if stretch > 0: sp = widget.sizePolicy() try: sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) @@ -62,10 +76,10 @@ def _create_backend_widget(self): self._backend_widget.setEnabled(bool(self._enabled)) try: - self._logger.debug("YVBoxQt: adding child %s expand=%s", child.widgetClass(), expand) + self._logger.debug("YVBoxQt: adding child %s stretch=%s weight=%s", child.widgetClass(), stretch, weight) except Exception: pass - layout.addWidget(widget, stretch=expand) + layout.addWidget(widget, stretch=stretch) try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -100,9 +114,15 @@ def addChild(self, child): def _deferred_attach(): try: widget = child.get_backend_widget() - expand = 1 if child.stretchable(YUIDimension.YD_VERT) else 0 try: - if expand == 1: + weight = int(child.weight(YUIDimension.YD_VERT) or 0) + except Exception: + weight = 0 + # If dynamic addition, use explicit weight when >0, otherwise + # fall back to stretchable flag (equal-share represented by 1). + stretch = weight if weight > 0 else (1 if child.stretchable(YUIDimension.YD_VERT) else 0) + try: + if stretch > 0: sp = widget.sizePolicy() try: sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) @@ -119,7 +139,7 @@ def _deferred_attach(): if lay is None: lay = QtWidgets.QVBoxLayout(self._backend_widget) self._backend_widget.setLayout(lay) - lay.addWidget(widget, stretch=expand) + lay.addWidget(widget, stretch=stretch) try: widget.show() except Exception: From b709354437ebcb4d248666471a98efe7489ee82b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 20:02:43 +0100 Subject: [PATCH 336/523] Managed weigth for gtk too --- manatools/aui/backends/gtk/hboxgtk.py | 62 ++++++++++++++++++++++++++- manatools/aui/backends/gtk/vboxgtk.py | 59 ++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py index a11f692..97b4ee5 100644 --- a/manatools/aui/backends/gtk/hboxgtk.py +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -40,7 +40,10 @@ def stretchable(self, dim): def _create_backend_widget(self): self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - for child in self._children: + # Collect children first so we can apply weight-based heuristics + children = list(self._children) + + for child in children: try: self._logger.debug("HBox child: %s stretch(H)=%s weight(H)=%s stretch(V)=%s", child.widgetClass(), child.stretchable(YUIDimension.YD_HORIZ), child.weight(YUIDimension.YD_HORIZ), child.stretchable(YUIDimension.YD_VERT)) except Exception: @@ -71,6 +74,63 @@ def _create_backend_widget(self): except Exception: pass self._backend_widget.set_sensitive(self._enabled) + + # If there are exactly two children and both declare positive horizontal + # weights, attempt to enforce a proportional split according to weights. + # Gtk.Box does not have native per-child weight factors, so we set + # size requests on the children based on the container allocation. + try: + if len(children) == 2: + w0 = int(children[0].weight(YUIDimension.YD_HORIZ) or 0) + w1 = int(children[1].weight(YUIDimension.YD_HORIZ) or 0) + if w0 > 0 and w1 > 0: + left = children[0].get_backend_widget() + right = children[1].get_backend_widget() + total_weight = max(1, w0 + w1) + + def _apply_weights(*args): + try: + alloc = self._backend_widget.get_allocated_width() + if not alloc or alloc <= 0: + return True + # subtract spacing and margins conservatively + spacing = getattr(self._backend_widget, 'get_spacing', lambda: 5)() + avail = max(0, alloc - spacing) + left_px = int(avail * w0 / total_weight) + right_px = max(0, avail - left_px) + try: + left.set_size_request(left_px, -1) + except Exception: + pass + try: + right.set_size_request(right_px, -1) + except Exception: + pass + except Exception: + pass + # remove idle after first successful sizing; keep size-allocate + return False + + try: + GLib.idle_add(_apply_weights) + except Exception: + try: + # fallback: call once + _apply_weights() + except Exception: + pass + # keep children proportional on subsequent resizes if possible + try: + def _on_size_allocate(widget, allocation): + try: + _apply_weights() + except Exception: + pass + self._backend_widget.connect('size-allocate', _on_size_allocate) + except Exception: + pass + except Exception: + pass try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index 7d2bcce..d581a1e 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -40,7 +40,10 @@ def stretchable(self, dim): def _create_backend_widget(self): self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) - for child in self._children: + # Collect children first so we can apply weight-based heuristics + children = list(self._children) + + for child in children: widget = child.get_backend_widget() try: # Respect the child's stretchable/weight hints instead of forcing expansion @@ -73,6 +76,60 @@ def _create_backend_widget(self): except Exception: pass + # If there are exactly two children and both declare positive vertical + # weights, attempt to enforce a proportional vertical split according + # to weights. Similar to horizontal HBox, Gtk.Box does not support + # per-child weight factors, so we set size requests on the children + # based on the container allocation. + try: + if len(children) == 2: + w0 = int(children[0].weight(YUIDimension.YD_VERT) or 0) + w1 = int(children[1].weight(YUIDimension.YD_VERT) or 0) + if w0 > 0 and w1 > 0: + top = children[0].get_backend_widget() + bottom = children[1].get_backend_widget() + total_weight = max(1, w0 + w1) + + def _apply_vweights(*args): + try: + alloc = self._backend_widget.get_allocated_height() + if not alloc or alloc <= 0: + return True + spacing = getattr(self._backend_widget, 'get_spacing', lambda: 5)() + avail = max(0, alloc - spacing) + top_px = int(avail * w0 / total_weight) + bot_px = max(0, avail - top_px) + try: + top.set_size_request(-1, top_px) + except Exception: + pass + try: + bottom.set_size_request(-1, bot_px) + except Exception: + pass + except Exception: + pass + return False + + try: + GLib.idle_add(_apply_vweights) + except Exception: + try: + _apply_vweights() + except Exception: + pass + try: + def _on_size_allocate(widget, allocation): + try: + _apply_vweights() + except Exception: + pass + self._backend_widget.connect('size-allocate', _on_size_allocate) + except Exception: + pass + except Exception: + pass + def _set_backend_enabled(self, enabled): """Enable/disable the VBox and propagate to children.""" From 00ad293553184041c54b9a8409a56aa19e9dda4e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 22:50:24 +0100 Subject: [PATCH 337/523] Fixed file and directory selection, unified browse code also for saving. --- manatools/aui/yui_curses.py | 244 ++++++++++++++++-------------------- 1 file changed, 109 insertions(+), 135 deletions(-) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 9543de6..d5511e6 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -110,7 +110,7 @@ def askForExistingDirectory(self, startDir: str, headline: str): """ try: start_dir = startDir if (startDir and os.path.isdir(startDir)) else os.path.expanduser('~') - return self._browse_paths(start_dir, select_file=False, headline=headline or "Select Directory") + return self._browse_paths(start_dir, select_file=False, headline=headline or "Select Directory", reason='directory') except Exception: return "" @@ -127,7 +127,7 @@ def askForExistingFile(self, startWith: str, filter: str, headline: str): else: # Default to Documents if available, else home start_dir = self._default_documents_dir if os.path.isdir(self._default_documents_dir) else os.path.expanduser('~') - return self._browse_paths(start_dir, select_file=True, headline=headline or "Open File", filter_str=filter) + return self._browse_paths(start_dir, select_file=True, headline=headline or "Open File", filter_str=filter, reason='file') except Exception: return "" @@ -145,7 +145,7 @@ def askForSaveFileName(self, startWith: str, filter: str, headline: str): else: start_dir = self._default_documents_dir if os.path.isdir(self._default_documents_dir) else os.path.expanduser('~') default_name = "" - return self._save_path_dialog(start_dir, default_name, headline or "Save File", filter_str=filter) + return self._browse_paths(start_dir, select_file=True, headline=headline or "Save File", filter_str=filter, reason='save', default_name=default_name) except Exception: return "" @@ -233,36 +233,70 @@ def _list_entries(self, current_dir: str, select_file: bool, patterns): pass return entries - def _browse_paths(self, start_dir: str, select_file: bool, headline: str, filter_str: str = ""): - """Create an overlay ncurses dialog to navigate directories and pick a directory or file.""" + def _browse_paths(self, start_dir: str, select_file: bool, headline: str, filter_str: str = "", reason: str = "file", default_name: str = ""): + """ + Unified ncurses overlay to navigate directories and pick a file, directory or save path. + - `reason` in ('file', 'directory', 'save') controls final behavior. + - Uses a `YTableCurses` with a single "Name" column to list entries. + - Avoids race between refresh and button-based selection by keeping + the current selection in a label/input field that is updated on + SelectionChanged and read when the Select/Save button is pressed. + """ current_dir = start_dir if os.path.isdir(start_dir) else os.path.expanduser('~') patterns = self._parse_filter_patterns(filter_str) # Build dialog UI dlg = YDialogCurses(YDialogType.YPopupDialog, YDialogColorMode.YDialogNormalColor) root = YVBoxCurses(dlg) - title_lbl = YLabelCurses(root, headline, isHeading=True) + YLabelCurses(root, headline, isHeading=True) path_lbl = YLabelCurses(root, f"Current: {current_dir}") - list_box = YSelectionBoxCurses(root, label="Items", multi_selection=False) - # Let list box stretch vertically + + # Table with single "Name" column + header = YTableHeader() + header.addColumn("Name") + table = YTableCurses(root, header, multiSelection=False) try: - list_box.setWeight(YUIDimension.YD_VERT, 1) + table.setWeight(YUIDimension.YD_VERT, 1) except Exception: pass + + # Selection preview and optional filename input (useful for save) + selected_lbl = YLabelCurses(root, "Selected: ") + filename_input = None + if reason == 'save': + filename_input = YInputFieldCurses(root, label="Filename:") + try: + if default_name: + filename_input.setValue(default_name) + except Exception: + pass + + # Buttons buttons = YHBoxCurses(root) - btn_select = YPushButtonCurses(buttons, "Select") + btn_label = "Save" if reason == 'save' else "Select" + btn_select = YPushButtonCurses(buttons, btn_label) btn_cancel = YPushButtonCurses(buttons, "Cancel") + selected_item_data = None + def refresh_listing(dir_path): + nonlocal selected_item_data try: - list_box.deleteAllItems() + table.deleteAllItems() for (label, path, typ) in self._list_entries(dir_path, select_file, patterns): - it = YItem(label) + it = YTableItem(label) try: + it.addCell(label) it.setData({'path': path, 'type': typ}) except Exception: pass - list_box.addItem(it) + table.addItem(it) + # reset selection state when navigating + selected_item_data = None + try: + selected_lbl.setText("Selected: ") + except Exception: + pass except Exception: pass @@ -275,7 +309,7 @@ def refresh_listing(dir_path): # Event loop result = "" while True: - ev = dlg.waitForEvent(0) + ev = dlg.waitForEvent() if isinstance(ev, YCancelEvent): result = "" break @@ -284,159 +318,99 @@ def refresh_listing(dir_path): if w == btn_cancel and ev.reason() == YEventReason.Activated: result = "" break - # Selecting from list via button + + # Select/Save pressed: read from selection preview / input field if w == btn_select and ev.reason() == YEventReason.Activated: try: - sel = list_box.selectedItems() - if not sel: - continue - item = sel[0] - data = item.data() if hasattr(item, 'data') else None - if not data or 'path' not in data: - continue - if data.get('type') == 'dir': - # In directory mode, selecting chooses current dir - if not select_file: - result = data['path'] + # If save: prefer filename_input value; if a file was selected, + # use that name as default when input is empty. + if reason == 'save': + name = None + try: + if filename_input is not None: + name = filename_input.value() + except Exception: + name = None + if selected_item_data and selected_item_data.get('type') == 'file': + sel_base = os.path.basename(selected_item_data.get('path')) + if not name: + name = sel_base + if name: + result = os.path.join(current_dir, name) break - # In file mode, selecting a dir navigates - current_dir = data['path'] - path_lbl.setText(f"Current: {current_dir}") - refresh_listing(current_dir) + # if no name, ignore press continue - else: - # File selected -> choose in file mode - if select_file: - result = data['path'] + + # Directory selection: if a directory row is selected, return it, + # otherwise return current_dir + if reason == 'directory': + if selected_item_data and selected_item_data.get('type') == 'dir': + result = selected_item_data.get('path') + else: + result = current_dir + break + + # File selection: if a file row is selected, return it + if reason == 'file': + if selected_item_data and selected_item_data.get('type') == 'file': + result = selected_item_data.get('path') break - except Exception: - continue - # Enter/Space on list toggles selection -> use SelectionChanged to navigate/select - if w == list_box and ev.reason() == YEventReason.SelectionChanged: - try: - sel = list_box.selectedItems() - if not sel: - continue - item = sel[0] - data = item.data() if hasattr(item, 'data') else None - if not data or 'path' not in data: - continue - if data.get('type') == 'dir': - # Navigate into directory - current_dir = data['path'] - path_lbl.setText(f"Current: {current_dir}") - refresh_listing(current_dir) + # nothing selected: ignore continue - else: - # File chosen via Enter in file mode - if select_file: - result = data['path'] - break except Exception: continue - try: - dlg.destroy() - except Exception: - pass - # For directory mode, if user navigated, return the current directory when no explicit file was selected. - if not select_file and result == "": - try: - # Return the current_dir as selection. - return current_dir - except Exception: - return "" - return result - - def _save_path_dialog(self, start_dir: str, default_name: str, headline: str, filter_str: str = ""): - """Overlay dialog to navigate to a directory and enter a filename to save.""" - current_dir = start_dir if os.path.isdir(start_dir) else os.path.expanduser('~') - patterns = self._parse_filter_patterns(filter_str) - dlg = YDialogCurses(YDialogType.YPopupDialog, YDialogColorMode.YDialogNormalColor) - root = YVBoxCurses(dlg) - YLabelCurses(root, headline, isHeading=True) - path_lbl = YLabelCurses(root, f"Current: {current_dir}") - list_box = YSelectionBoxCurses(root, label="Items", multi_selection=False) - try: - list_box.setWeight(YUIDimension.YD_VERT, 1) - except Exception: - pass - filename_input = YInputFieldCurses(root, label="Filename:") - try: - if default_name: - filename_input.setValue(default_name) - except Exception: - pass - buttons = YHBoxCurses(root) - btn_save = YPushButtonCurses(buttons, "Save") - btn_cancel = YPushButtonCurses(buttons, "Cancel") - - def refresh_listing(dir_path): - try: - list_box.deleteAllItems() - for (label, path, typ) in self._list_entries(dir_path, select_file=True, patterns=patterns): - it = YItem(label) - try: - it.setData({'path': path, 'type': typ}) - except Exception: - pass - list_box.addItem(it) - except Exception: - pass - - refresh_listing(current_dir) - try: - dlg.open() - except Exception: - return "" - - result = "" - while True: - ev = dlg.waitForEvent(0) - if isinstance(ev, YCancelEvent): - result = "" - break - if isinstance(ev, YWidgetEvent): - w = ev.widget() - if w == btn_cancel and ev.reason() == YEventReason.Activated: - result = "" - break - if w == btn_save and ev.reason() == YEventReason.Activated: - try: - name = filename_input.value() - if name: - result = os.path.join(current_dir, name) - break - except Exception: - continue - if w == list_box and ev.reason() == YEventReason.SelectionChanged: + # Table selection changed: either navigate into directories or update preview + if w == table and ev.reason() == YEventReason.SelectionChanged: try: - sel = list_box.selectedItems() + sel = table.selectedItems() if not sel: + selected_item_data = None + try: + selected_lbl.setText("Selected: ") + except Exception: + pass continue item = sel[0] data = item.data() if hasattr(item, 'data') else None if not data or 'path' not in data: + selected_item_data = None + try: + selected_lbl.setText("Selected: ") + except Exception: + pass continue if data.get('type') == 'dir': + # navigate into directory current_dir = data['path'] - path_lbl.setText(f"Current: {current_dir}") + try: + path_lbl.setText(f"Current: {current_dir}") + except Exception: + pass refresh_listing(current_dir) continue else: - # Pre-fill filename with selected file's name + # file selected: update preview and prefill filename when saving + selected_item_data = data try: - filename_input.setValue(os.path.basename(data['path'])) + selected_lbl.setText(f"Selected: {os.path.basename(data.get('path'))}") except Exception: pass + if reason == 'save' and filename_input is not None: + try: + filename_input.setValue(os.path.basename(data.get('path'))) + except Exception: + pass except Exception: continue + try: dlg.destroy() except Exception: pass return result + class YWidgetFactoryCurses: def __init__(self): pass From cf3493cc23469c358fa723222099923d0d1e11b0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 22:51:24 +0100 Subject: [PATCH 338/523] improved test case --- test/test_file_dialogs.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/test/test_file_dialogs.py b/test/test_file_dialogs.py index 9bf7d53..7b10899 100644 --- a/test/test_file_dialogs.py +++ b/test/test_file_dialogs.py @@ -29,6 +29,10 @@ except Exception as _e: print(f"Failed to configure file logger: {_e}") +# Use current working directory (where the log file is written) as starting directory +start_dir = os.path.abspath(os.getcwd()) +print(f"Using starting directory for dialogs: {start_dir}") + def test_file_dialogs(backend_name=None): if backend_name: @@ -58,16 +62,21 @@ def test_file_dialogs(backend_name=None): # main area: HBox with multiline edit on left and buttons on right h = factory.createHBox(vbox) + mled = factory.createMultiLineEdit(h, "File content") mled.setStretchable(yui.YUIDimension.YD_VERT, True) mled.setStretchable(yui.YUIDimension.YD_HORIZ, True) + mled.setWeight(yui.YUIDimension.YD_HORIZ, 60) right = factory.createVBox(h) + right.setWeight(yui.YUIDimension.YD_HORIZ, 40) open_btn = factory.createPushButton(right, "Open") save_btn = factory.createPushButton(right, "Save") - testdir_btn = factory.createPushButton(right, "Test Dir") + select_dir_btn = factory.createPushButton(right, "Select Directory") - dir_label = factory.createLabel(vbox, "No dir selected") + hbox = factory.createHBox(vbox) + factory.createLabel(hbox, "Selected directory:") + dir_label = factory.createLabel(hbox, "No dir selected") dir_label.setStretchable(yui.YUIDimension.YD_VERT, False) dir_label.setStretchable(yui.YUIDimension.YD_HORIZ, True) @@ -87,8 +96,8 @@ def test_file_dialogs(backend_name=None): elif typ == yui.YEventType.WidgetEvent: wdg = event.widget() if wdg == open_btn: - # ask for text file - fname = app.askForExistingFile("", "*.txt", "Open text file") + # ask for text file (start in current working directory) + fname = app.askForExistingFile(start_dir, "*.txt", "Open text file") if fname: try: with open(fname, 'r', encoding='utf-8') as f: @@ -97,7 +106,7 @@ def test_file_dialogs(backend_name=None): except Exception as e: print(f"Failed to read file: {e}") elif wdg == save_btn: - fname = app.askForSaveFileName("", "*.txt", "Save text file") + fname = app.askForSaveFileName(start_dir, "*.txt", "Save text file") if fname: try: data = mled.value() @@ -105,8 +114,8 @@ def test_file_dialogs(backend_name=None): f.write(data) except Exception as e: print(f"Failed to save file: {e}") - elif wdg == testdir_btn: - d = app.askForExistingDirectory("", "Select directory") + elif wdg == select_dir_btn: + d = app.askForExistingDirectory(start_dir, "Select directory") if d: dir_label.setText(d) elif wdg == close_btn: From cf3d00c14464e9482d7e8bfbf434156da0d8d5ef Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 22:57:10 +0100 Subject: [PATCH 339/523] updated --- sow/TODO.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index d38dc9d..7e93ffc 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -17,7 +17,7 @@ Missing Widgets comparing libyui original factory: [X] YCheckBox [X] YTree [X] YFrame - [X] YTable + [X] YTable (merging YMGACBTable) [X] YProgressBar [X] YRichText [X] YMultiLineEdit @@ -31,16 +31,6 @@ Missing Widgets comparing libyui original factory: [ ] YBusyIndicator [ ] YLogView -Skipped widgets: - - [-] YPackageSelector (not ported) - [-] YRadioButtonGroup (not ported) - [-] YWizard (not ported) - [-] YItemSelector (not ported) - [-] YEmpty (not ported) - [-] YSquash / createSquash (not ported) - [-] YMenuButton (legacy menus) - Optional/special widgets (from `YOptionalWidgetFactory`): [ ] YDumbTab @@ -58,9 +48,23 @@ Optional/special widgets (from `YOptionalWidgetFactory`): [ ] YGraph [ ] Context menu support / hasContextMenu +Skipped widgets: + + [-] YPackageSelector (not ported) + [-] YRadioButtonGroup (not ported) + [-] YWizard (not ported) + [-] YItemSelector (not ported) + [-] YEmpty (not ported) + [-] YSquash / createSquash (not ported) + [-] YMenuButton (legacy menus) + To check/review: how to manage YEvents [X] and YItems [X] (verify selection attirbute). [X] YInputField password mode + [X] askForExistingDirectory + [X] askForExistingFile + [X] askForSaveFileName + [ ] YAboutDialog (aka YMGAAboutDialog) [ ] adding factory create alternative methods (e.g. createMultiSelectionBox) [ ] managing shortcuts [ ] localization From df9f32d6602bef1a5e7ecdbab935b8bc50356dd1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 22:59:37 +0100 Subject: [PATCH 340/523] Skipped moved to bottom Removed duplicate entries for skipped widgets in TODO list. --- sow/TODO.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 7e93ffc..c1317a4 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -48,16 +48,6 @@ Optional/special widgets (from `YOptionalWidgetFactory`): [ ] YGraph [ ] Context menu support / hasContextMenu -Skipped widgets: - - [-] YPackageSelector (not ported) - [-] YRadioButtonGroup (not ported) - [-] YWizard (not ported) - [-] YItemSelector (not ported) - [-] YEmpty (not ported) - [-] YSquash / createSquash (not ported) - [-] YMenuButton (legacy menus) - To check/review: how to manage YEvents [X] and YItems [X] (verify selection attirbute). [X] YInputField password mode @@ -70,8 +60,19 @@ To check/review: [ ] localization Nice to have: improvements outside YUI API + [ ] window title [ ] window icons [ ] selected YItem(s) in event [ ] Improving YEvents management (adding info on widget event containing data such as item selection/s, checked item, rich text url, etc.) + +Skipped widgets: + + [-] YPackageSelector (not ported) + [-] YRadioButtonGroup (not ported) + [-] YWizard (not ported) + [-] YItemSelector (not ported) + [-] YEmpty (not ported) + [-] YSquash / createSquash (not ported) + [-] YMenuButton (legacy menus) From 310af2674797b59c2d3bc07417d015910cf12a0a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 5 Jan 2026 23:00:34 +0100 Subject: [PATCH 341/523] corrected alignment --- sow/TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sow/TODO.md b/sow/TODO.md index c1317a4..9dfd3b8 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -50,6 +50,7 @@ Optional/special widgets (from `YOptionalWidgetFactory`): To check/review: how to manage YEvents [X] and YItems [X] (verify selection attirbute). + [X] YInputField password mode [X] askForExistingDirectory [X] askForExistingFile From 3965bba64e903a09f8b691d6002b685b7de2190e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 17:56:32 +0100 Subject: [PATCH 342/523] First attempt to have YDumpTab on curses --- manatools/aui/backends/curses/__init__.py | 2 + manatools/aui/yui_curses.py | 4 + test/test_dumptabwidget.py | 120 ++++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 test/test_dumptabwidget.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 14d7b68..9c35196 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -21,6 +21,7 @@ from .multilineeditcurses import YMultiLineEditCurses from .spacingcurses import YSpacingCurses from .imagecurses import YImageCurses +from .dumbtabcurses import YDumbTabCurses __all__ = [ "YDialogCurses", @@ -46,5 +47,6 @@ "YMultiLineEditCurses", "YSpacingCurses", "YImageCurses", + "YDumbTabCurses", # ... ] diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index d5511e6..216d702 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -542,6 +542,10 @@ def createReplacePoint(self, parent): """Create a ReplacePoint widget (curses backend).""" return YReplacePointCurses(parent) + def createDumbTab(self, parent): + """Create a DumbTab widget (curses backend).""" + return YDumbTabCurses(parent) + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size_px: int = 0): """Create a Spacing/Stretch widget. diff --git a/test/test_dumptabwidget.py b/test/test_dumptabwidget.py new file mode 100644 index 0000000..06bfdfa --- /dev/null +++ b/test/test_dumptabwidget.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_dumbtab(backend_name=None): + """Interactive test showcasing YDumbTab with three tabs and a ReplacePoint.""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox(dialog) + factory.createHeading(vbox, "YDumbTab Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + + # Create DumbTab and items + dumbtab = factory.createDumbTab(vbox) + tabs = ["Options", "Notes", "Actions"] + # Add items (select the first by default) + it0 = yui.YItem(tabs[0], selected=True) + it1 = yui.YItem(tabs[1]) + it2 = yui.YItem(tabs[2]) + dumbtab.addItem(it0) + dumbtab.addItem(it1) + dumbtab.addItem(it2) + + # Content area: ReplacePoint as the single child + rp = factory.createReplacePoint(dumbtab) + + # Helper to render content of the active tab + def render_content(index: int): + # Clear previous content + try: + rp.deleteChildren() + except Exception: + pass + # Build new content depending on selected tab + if index == 0: + box = factory.createVBox(rp) + factory.createLabel(box, "Enable the option below:") + factory.createCheckBox(box, "Enable feature", is_checked=True) + factory.createLabel(box, "Use TAB/Shift+TAB to navigate") + rp.showChild() + elif index == 1: + box = factory.createVBox(rp) + factory.createLabel(box, "Notes:") + text = "This is a simple multi-tab demo.\nSwitch tabs with LEFT/RIGHT (or UI specific).\nThe content below changes per tab." + try: + factory.createRichText(box, text, plainTextMode=True) + except Exception: + factory.createLabel(box, text) + rp.showChild() + else: + box = factory.createVBox(rp) + factory.createLabel(box, "Choose an action:") + h = factory.createHBox(box) + factory.createPushButton(h, "OK") + factory.createPushButton(h, "Cancel") + rp.showChild() + + # Initial content for the first tab + render_content(0) + + # Close button + close_row = factory.createHBox(vbox) + close_btn = factory.createPushButton(close_row, "Close") + + print("\nOpening YDumbTab test dialog...") + + while True: + ev = dialog.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif et == yui.YEventType.WidgetEvent: + wdg = ev.widget() + reason = ev.reason() + if wdg == close_btn and reason == yui.YEventReason.Activated: + dialog.destroy() + break + if wdg == dumbtab and reason == yui.YEventReason.Activated: + sel = dumbtab.selectedItem() + if sel is not None: + try: + idx = tabs.index(sel.label()) + except Exception: + idx = 0 + render_content(idx) + + except Exception as e: + print(f"Error testing YDumbTab with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_dumbtab(sys.argv[1]) + else: + test_dumbtab() From 7e9604e9657d879ff5af232820a7c0a01042f4f3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 18:21:07 +0100 Subject: [PATCH 343/523] Added logging --- test/test_dumptabwidget.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/test_dumptabwidget.py b/test/test_dumptabwidget.py index 06bfdfa..3f7a103 100644 --- a/test/test_dumptabwidget.py +++ b/test/test_dumptabwidget.py @@ -2,10 +2,32 @@ import os import sys +import logging # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + def test_dumbtab(backend_name=None): """Interactive test showcasing YDumbTab with three tabs and a ReplacePoint.""" if backend_name: @@ -25,6 +47,10 @@ def test_dumbtab(backend_name=None): backend = YUI.backend() print(f"Using backend: {backend.value}") + # Basic logging for diagnosis + import logging + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(name)s: %(message)s") + ui = YUI_ui() factory = ui.widgetFactory() dialog = factory.createMainDialog() @@ -46,6 +72,7 @@ def test_dumbtab(backend_name=None): # Content area: ReplacePoint as the single child rp = factory.createReplacePoint(dumbtab) + print("ReplacePoint created:", rp.widgetClass()) # Helper to render content of the active tab def render_content(index: int): @@ -61,6 +88,7 @@ def render_content(index: int): factory.createCheckBox(box, "Enable feature", is_checked=True) factory.createLabel(box, "Use TAB/Shift+TAB to navigate") rp.showChild() + print("Rendered tab 0: Options") elif index == 1: box = factory.createVBox(rp) factory.createLabel(box, "Notes:") @@ -70,6 +98,7 @@ def render_content(index: int): except Exception: factory.createLabel(box, text) rp.showChild() + print("Rendered tab 1: Notes") else: box = factory.createVBox(rp) factory.createLabel(box, "Choose an action:") @@ -77,9 +106,11 @@ def render_content(index: int): factory.createPushButton(h, "OK") factory.createPushButton(h, "Cancel") rp.showChild() + print("Rendered tab 2: Actions") # Initial content for the first tab render_content(0) + print("Initial content rendered for tab 0.") # Close button close_row = factory.createHBox(vbox) @@ -106,6 +137,7 @@ def render_content(index: int): idx = tabs.index(sel.label()) except Exception: idx = 0 + print("Tab Activated:", sel.label(), "index=", idx) render_content(idx) except Exception as e: From 3b1ab522b28839dfcb663181f599e22144e6c648 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 18:21:23 +0100 Subject: [PATCH 344/523] Dumptab on Qt --- manatools/aui/backends/qt/dumbtabqt.py | 213 +++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 manatools/aui/backends/qt/dumbtabqt.py diff --git a/manatools/aui/backends/qt/dumbtabqt.py b/manatools/aui/backends/qt/dumbtabqt.py new file mode 100644 index 0000000..16ac85b --- /dev/null +++ b/manatools/aui/backends/qt/dumbtabqt.py @@ -0,0 +1,213 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Qt backend DumbTab (tab bar + single content area) + +Implements a simple tab bar using QTabBar and exposes a single child +content area where applications typically attach a YReplacePoint. + +This is a YSelectionWidget: it manages items, single selection, and +emits WidgetEvent(Activated) when the active tab changes. +''' +from PySide6 import QtWidgets, QtCore +import logging +from ...yui_common import * + +class YDumbTabQt(YSelectionWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._container = None + self._tabbar = None + self._content = None # placeholder QWidget for the single child + # DumbTab is horizontally stretchable and minimally vertically + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YDumbTab" + + def _create_backend_widget(self): + try: + container = QtWidgets.QWidget() + v = QtWidgets.QVBoxLayout(container) + v.setContentsMargins(0, 0, 0, 0) + v.setSpacing(5) + + tabbar = QtWidgets.QTabBar() + tabbar.setExpanding(False) + tabbar.setMovable(False) + tabbar.setTabsClosable(False) + + content = QtWidgets.QWidget() + content.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + content_layout = QtWidgets.QVBoxLayout(content) + content_layout.setContentsMargins(0, 0, 0, 0) + + v.addWidget(tabbar) + v.addWidget(content, 1) + + tabbar.currentChanged.connect(self._on_tab_changed) + + # populate from existing items respecting selected flags + selected_idx = -1 + for idx, it in enumerate(self._items): + try: + tabbar.addTab(it.label()) + if it.selected(): + selected_idx = idx + except Exception: + tabbar.addTab(str(it)) + if selected_idx < 0 and len(self._items) > 0: + selected_idx = 0 + try: + self._items[0].setSelected(True) + except Exception: + pass + if selected_idx >= 0: + try: + tabbar.setCurrentIndex(selected_idx) + except Exception: + pass + self._selected_items = [ self._items[selected_idx] ] + + container.setEnabled(bool(self._enabled)) + + self._backend_widget = container + self._container = container + self._tabbar = tabbar + self._content = content + # If a child was added before backend creation (common in tests), attach it now + try: + if self.hasChildren(): + ch = self.firstChild() + if ch is not None: + w = ch.get_backend_widget() + lay = self._content.layout() or QtWidgets.QVBoxLayout(self._content) + self._content.setLayout(lay) + try: + w.setParent(self._content) + except Exception: + pass + lay.addWidget(w) + try: + w.show() + w.updateGeometry() + except Exception: + pass + try: + self._logger.debug("YDumbTabQt._create_backend_widget: attached pre-existing child %s", ch.widgetClass()) + except Exception: + pass + except Exception: + pass + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + except Exception: + try: + self._logger.exception("YDumbTabQt _create_backend_widget failed") + except Exception: + pass + + def addChild(self, child): + # Accept only a single child; attach its backend into content area + if self.hasChildren(): + raise YUIInvalidWidgetException("YDumbTab can only have one child") + super().addChild(child) + try: + if self._content is not None: + w = child.get_backend_widget() + lay = self._content.layout() or QtWidgets.QVBoxLayout(self._content) + self._content.setLayout(lay) + try: + # Ensure proper parenting before adding to layout to avoid invisibility + w.setParent(self._content) + except Exception: + pass + lay.addWidget(w) + try: + w.show() + w.updateGeometry() + except Exception: + pass + try: + self._logger.debug("YDumbTabQt.addChild: attached %s into content", child.widgetClass()) + except Exception: + pass + except Exception: + try: + self._logger.exception("YDumbTabQt.addChild failed") + except Exception: + pass + + def addItem(self, item): + super().addItem(item) + try: + if self._tabbar is not None: + idx = self._tabbar.addTab(item.label()) + if item.selected(): + try: + self._tabbar.setCurrentIndex(idx) + except Exception: + pass + # sync internal selection list + self._sync_selection_from_tabbar() + except Exception: + try: + self._logger.exception("YDumbTabQt.addItem failed") + except Exception: + pass + + def selectItem(self, item, selected=True): + # single selection: set current tab to item's index + try: + if not selected: + return + idx = None + for i, it in enumerate(self._items): + if it is item: + idx = i + break + if idx is None: + return + if self._tabbar is not None: + self._tabbar.setCurrentIndex(idx) + # sync model + for it in self._items: + it.setSelected(False) + item.setSelected(True) + self._selected_items = [item] + except Exception: + try: + self._logger.exception("YDumbTabQt.selectItem failed") + except Exception: + pass + + def _on_tab_changed(self, index): + try: + # Update model selection + for i, it in enumerate(self._items): + try: + it.setSelected(i == index) + except Exception: + pass + self._selected_items = [ self._items[index] ] if 0 <= index < len(self._items) else [] + # Post activation event + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + try: + self._logger.exception("YDumbTabQt._on_tab_changed failed") + except Exception: + pass + + def _sync_selection_from_tabbar(self): + try: + idx = self._tabbar.currentIndex() if self._tabbar is not None else -1 + self._selected_items = [ self._items[idx] ] if 0 <= idx < len(self._items) else [] + except Exception: + self._selected_items = [] From 8a34789350f7b17f9ce6a2cfc9e8393b85c47b3a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 18:27:18 +0100 Subject: [PATCH 345/523] exporting symbol --- manatools/aui/backends/qt/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 9bbe92e..0a8e836 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -21,6 +21,7 @@ from .multilineeditqt import YMultiLineEditQt from .spacingqt import YSpacingQt from .imageqt import YImageQt +from .dumbtabqt import YDumbTabQt __all__ = [ @@ -47,5 +48,6 @@ "YMultiLineEditQt", "YSpacingQt", "YImageQt", + "YDumbTabQt", # ... ] From cc48f2c8600fb389dd52058aa7edacf42d092f0d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 18:27:52 +0100 Subject: [PATCH 346/523] and create --- manatools/aui/yui_qt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 93eca7c..2d16e71 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -348,6 +348,10 @@ def createReplacePoint(self, parent): """Create a ReplacePoint widget (Qt backend).""" return YReplacePointQt(parent) + def createDumbTab(self, parent): + """Create a DumbTab (tab bar with single content area, Qt backend).""" + return YDumbTabQt(parent) + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size_px: int = 0): """Create a Spacing/Stretch widget. From 9ff9e90a984cda073e1220c1e425855f6d84063e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 18:28:20 +0100 Subject: [PATCH 347/523] Real DumbTab curses implementation --- .../aui/backends/curses/dumbtabcurses.py | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 manatools/aui/backends/curses/dumbtabcurses.py diff --git a/manatools/aui/backends/curses/dumbtabcurses.py b/manatools/aui/backends/curses/dumbtabcurses.py new file mode 100644 index 0000000..bcded3f --- /dev/null +++ b/manatools/aui/backends/curses/dumbtabcurses.py @@ -0,0 +1,180 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +NCurses backend DumbTab: simple single-selection tab bar with one content area. + +- Renders a one-line tab bar with labels; the selected tab is highlighted. +- Acts as a single-child container: applications typically attach a ReplacePoint. +- Emits WidgetEvent(Activated) when the active tab changes. +''' +import curses +import curses.ascii +import logging +from ...yui_common import * + +_mod_logger = logging.getLogger("manatools.aui.curses.dumbtab.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + +class YDumbTabCurses(YSelectionWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._backend_widget = self # curses uses self for drawing + self._active_index = -1 + self._focused = False + self._can_focus = True + self._height = 2 # tab bar + at least one row for content + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YDumbTab" + + def _create_backend_widget(self): + # Initialize selection state based on items + try: + idx = -1 + for i, it in enumerate(self._items): + if it.selected(): + idx = i + if idx < 0 and len(self._items) > 0: + idx = 0 + try: + self._items[0].setSelected(True) + except Exception: + pass + self._active_index = idx + self._selected_items = [ self._items[idx] ] if idx >= 0 else [] + self._logger.debug("_create_backend_widget: active=%d items=%d", self._active_index, len(self._items)) + except Exception: + pass + self._backend_widget = self + + def addChild(self, child): + # single child only + if self.hasChildren(): + raise YUIInvalidWidgetException("YDumbTab can only have one child") + super().addChild(child) + + def addItem(self, item): + super().addItem(item) + # If this is the first item and nothing selected, select it + try: + if self._active_index < 0 and len(self._items) > 0: + self._active_index = 0 + try: + self._items[0].setSelected(True) + except Exception: + pass + self._selected_items = [ self._items[0] ] + except Exception: + pass + + def selectItem(self, item, selected=True): + # single selection: set as active + if not selected: + return + try: + target = None + for i, it in enumerate(self._items): + if it is item or it.label() == item.label(): + target = i + break + if target is None: + return + self._active_index = target + for i, it in enumerate(self._items): + try: + it.setSelected(i == target) + except Exception: + pass + self._selected_items = [ self._items[target] ] if target >= 0 else [] + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + self._can_focus = bool(enabled) + if not enabled and self._focused: + self._focused = False + except Exception: + pass + + def _draw(self, window, y, x, width, height): + try: + # Draw tab bar (one line) + bar_y = y + col = x + for i, it in enumerate(self._items): + label = it.label() + text = f" {label} " + attr = curses.A_REVERSE if i == self._active_index and self.isEnabled() else curses.A_BOLD + if not self.isEnabled(): + attr |= curses.A_DIM + # clip if needed + if col >= x + width: + break + avail = x + width - col + to_draw = text[:max(0, avail)] + try: + window.addstr(bar_y, col, to_draw, attr) + except curses.error: + pass + col += len(text) + 1 + # Draw a separator line below tabs if space + if height > 1: + try: + sep = "-" * max(0, width) + window.addstr(y + 1, x, sep[:width]) + except curses.error: + pass + # Delegate content to single child (if any) + ch = self.firstChild() + if ch is not None and height > 2: + try: + ch._draw(window, y + 2, x, width, height - 2) + except Exception: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self._can_focus or not self.isEnabled(): + return False + handled = True + if key in (curses.KEY_LEFT, curses.ascii.TAB): + if self._active_index > 0: + self._active_index -= 1 + self._update_from_active(change_reason=YEventReason.Activated) + elif key in (curses.KEY_RIGHT,): + if self._active_index < len(self._items) - 1: + self._active_index += 1 + self._update_from_active(change_reason=YEventReason.Activated) + elif key in (ord('\n'), ord(' ')): + # activate current + self._update_from_active(change_reason=YEventReason.Activated) + else: + handled = False + return handled + + def _update_from_active(self, change_reason=YEventReason.SelectionChanged): + try: + for i, it in enumerate(self._items): + try: + it.setSelected(i == self._active_index) + except Exception: + pass + self._selected_items = [ self._items[self._active_index] ] if 0 <= self._active_index < len(self._items) else [] + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, change_reason)) + except Exception: + pass From 40a326c2389871218ad677b22ce52b87983460b9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 18:41:47 +0100 Subject: [PATCH 348/523] First attempt to port YDumbTab on gtk4 --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/dumbtabgtk.py | 233 +++++++++++++++++++++++ manatools/aui/yui_gtk.py | 4 + 3 files changed, 239 insertions(+) create mode 100644 manatools/aui/backends/gtk/dumbtabgtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index ca88aa1..f30f679 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -21,6 +21,7 @@ from .multilineeditgtk import YMultiLineEditGtk from .spacinggtk import YSpacingGtk from .imagegtk import YImageGtk +from .dumbtabgtk import YDumbTabGtk __all__ = [ "YDialogGtk", @@ -46,5 +47,6 @@ "YMultiLineEditGtk", "YSpacingGtk", "YImageGtk", + 'YDumbTabGtk', # ... ] diff --git a/manatools/aui/backends/gtk/dumbtabgtk.py b/manatools/aui/backends/gtk/dumbtabgtk.py new file mode 100644 index 0000000..28c27a4 --- /dev/null +++ b/manatools/aui/backends/gtk/dumbtabgtk.py @@ -0,0 +1,233 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +GTK4 backend DumbTab (tab bar + single content area) + +Implements a simple tab bar using a row of toggle buttons (single selection) +placed above a content area where applications typically attach a ReplacePoint. + +This is a YSelectionWidget: it manages items, single selection, and +emits WidgetEvent(Activated) when the active tab changes. +''' +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GLib +import logging +from ...yui_common import * + +class YDumbTabGtk(YSelectionWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._box = None + self._tabbar = None + self._content = None + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + + def widgetClass(self): + return "YDumbTab" + + def _create_backend_widget(self): + try: + self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + # Gtk4: Use Gtk.StackSwitcher + Gtk.Stack (Notebook-like) + self._stack = Gtk.Stack() + try: + self._stack.set_hexpand(True) + self._stack.set_vexpand(False) + # keep stack small; content is below + self._stack.set_size_request(1, 1) + except Exception: + pass + switcher = Gtk.StackSwitcher() + try: + switcher.set_stack(self._stack) + except Exception: + pass + self._tabbar = switcher + # separate content area below tabs + self._content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + try: + self._tabbar.set_hexpand(True) + self._content.set_hexpand(True) + self._content.set_vexpand(True) + except Exception: + pass + self._box.append(self._tabbar) + self._box.append(self._stack) + self._box.append(self._content) + + # populate tabs/pages from items + active_idx = -1 + for idx, it in enumerate(self._items): + # Create an empty page for each tab label + page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + name = f"tab-{idx}" + try: + self._stack.add_titled(page, name, it.label()) + except Exception: + # Fallback: use add_child then set properties + try: + self._stack.add_child(page) + except Exception: + pass + if it.selected(): + active_idx = idx + if active_idx < 0 and len(self._items) > 0: + active_idx = 0 + try: + self._items[0].setSelected(True) + except Exception: + pass + # set active after building to avoid intermediate signals + if active_idx >= 0: + try: + self._stack.set_visible_child_name(f"tab-{active_idx}") + self._selected_items = [ self._items[active_idx] ] + except Exception: + pass + + # attach pre-existing child content (e.g., ReplacePoint) + try: + if self.hasChildren(): + ch = self.firstChild() + if ch is not None: + w = ch.get_backend_widget() + try: + self._content.append(w) + except Exception: + pass + try: + self._logger.debug("YDumbTabGtk._create_backend_widget: attached pre-existing child %s", ch.widgetClass()) + except Exception: + pass + except Exception: + pass + + self._box.set_sensitive(self._enabled) + self._backend_widget = self._box + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + # Notify on tab changes + try: + self._stack.connect('notify::visible-child-name', self._on_stack_changed) + except Exception: + pass + except Exception: + try: + self._logger.exception("YDumbTabGtk _create_backend_widget failed") + except Exception: + pass + + def addChild(self, child): + # Accept only a single child; attach its backend into content area + if self.hasChildren(): + raise YUIInvalidWidgetException("YDumbTab can only have one child") + super().addChild(child) + try: + if self._content is not None: + w = child.get_backend_widget() + self._content.append(w) + try: + self._logger.debug("YDumbTabGtk.addChild: attached %s into content", child.widgetClass()) + except Exception: + pass + except Exception: + try: + self._logger.exception("YDumbTabGtk.addChild failed") + except Exception: + pass + + def addItem(self, item): + super().addItem(item) + try: + if getattr(self, '_stack', None) is not None: + page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + name = f"tab-{len(self._items) - 1}" + self._stack.add_titled(page, name, item.label()) + if item.selected(): + try: + self._stack.set_visible_child_name(name) + except Exception: + pass + self._sync_selection_from_stack() + except Exception: + try: + self._logger.exception("YDumbTabGtk.addItem failed") + except Exception: + pass + + def selectItem(self, item, selected=True): + try: + if not selected: + return + target_idx = None + for i, it in enumerate(self._items): + if it is item: + target_idx = i + break + if target_idx is None: + return + try: + if getattr(self, '_stack', None) is not None: + self._stack.set_visible_child_name(f"tab-{target_idx}") + except Exception: + pass + for it in self._items: + it.setSelected(False) + item.setSelected(True) + self._selected_items = [item] + except Exception: + try: + self._logger.exception("YDumbTabGtk.selectItem failed") + except Exception: + pass + + def _on_stack_changed(self, stack, pspec): + try: + name = None + try: + name = stack.get_visible_child_name() + except Exception: + pass + index = -1 + if name and name.startswith("tab-"): + try: + index = int(name.split("-", 1)[1]) + except Exception: + index = -1 + # update model + if 0 <= index < len(self._items): + for i, it in enumerate(self._items): + try: + it.setSelected(i == index) + except Exception: + pass + self._selected_items = [ self._items[index] ] + else: + self._selected_items = [] + # post event + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + try: + self._logger.exception("YDumbTabGtk._on_stack_changed failed") + except Exception: + pass + + def _sync_selection_from_stack(self): + try: + name = self._stack.get_visible_child_name() if getattr(self, '_stack', None) is not None else None + if name and name.startswith("tab-"): + i = int(name.split("-", 1)[1]) + if 0 <= i < len(self._items): + self._selected_items = [ self._items[i] ] + return + self._selected_items = [] + except Exception: + self._selected_items = [] diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 35df9bb..d9c9041 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -739,6 +739,10 @@ def createReplacePoint(self, parent): """Create a ReplacePoint widget (GTK backend).""" return YReplacePointGtk(parent) + def createDumbTab(self, parent): + from .backends.gtk import YDumbTabGtk + return YDumbTabGtk(parent) + def createSpacing(self, parent, dim: YUIDimension, stretchable: bool = False, size_px: int = 0): """Create a Spacing/Stretch widget. From eaf0aba587a680c14311bfd961bab0afcb3a798a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 18:53:24 +0100 Subject: [PATCH 349/523] fix stretching management --- .../aui/backends/curses/alignmentcurses.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/curses/alignmentcurses.py b/manatools/aui/backends/curses/alignmentcurses.py index 826c3cc..5e52448 100644 --- a/manatools/aui/backends/curses/alignmentcurses.py +++ b/manatools/aui/backends/curses/alignmentcurses.py @@ -107,7 +107,7 @@ def _draw(self, window, y, x, width, height): if not self.hasChildren() or not hasattr(self.child(), "_draw"): return try: - # width to give to the child: minimal needed (so it can be pushed) + # width to give to the child: minimal needed or full width if stretchable ch_min_w = self._child_min_width(self.child(), width) # honor explicit minimum width in pixels, converting to character cells try: @@ -122,10 +122,19 @@ def _draw(self, window, y, x, width, height): # never exceed the available width try: child_pref_w = getattr(self.child(), "_width", None) - if child_pref_w is not None: - child_w = min(width, max(ch_min_w, int(child_pref_w))) + # If child is stretchable horizontally or has weight, let it use full width + is_h_stretch = False + try: + is_h_stretch = bool(self.child().stretchable(YUIDimension.YD_HORIZ)) or bool(self.child().weight(YUIDimension.YD_HORIZ)) + except Exception: + is_h_stretch = False + if is_h_stretch: + child_w = width else: - child_w = min(width, ch_min_w) + if child_pref_w is not None: + child_w = min(width, max(ch_min_w, int(child_pref_w))) + else: + child_w = min(width, ch_min_w) except Exception: child_w = min(width, ch_min_w) @@ -142,8 +151,14 @@ def _draw(self, window, y, x, width, height): cy = y + max(0, height - 1) else: cy = y - # honor explicit minimum height in pixels for child + # height to give to the child: prefer full height if stretchable, else min/explicit ch_height = getattr(self.child(), "_height", 1) + try: + is_v_stretch = bool(self.child().stretchable(YUIDimension.YD_VERT)) or bool(self.child().weight(YUIDimension.YD_VERT)) + if is_v_stretch: + ch_height = height + except Exception: + pass try: if getattr(self, '_min_height_px', 0) and self._min_height_px > 0: min_h = pixels_to_chars(int(self._min_height_px), YUIDimension.YD_VERT) From 53ecb5521bce29b4afd1f76f9234ed31f5257d13 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 19:03:33 +0100 Subject: [PATCH 350/523] Stretchable by default --- manatools/aui/backends/gtk/richtextgtk.py | 11 +++++++++++ manatools/aui/backends/qt/richtextqt.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/manatools/aui/backends/gtk/richtextgtk.py b/manatools/aui/backends/gtk/richtextgtk.py index 58d0847..c96facb 100644 --- a/manatools/aui/backends/gtk/richtextgtk.py +++ b/manatools/aui/backends/gtk/richtextgtk.py @@ -27,6 +27,12 @@ def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): self._backend_widget = None self._content_widget = None # TextView or Label self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + # Default: richtext should be stretchable both horizontally and vertically + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + except Exception: + pass def widgetClass(self): return "YRichText" @@ -169,6 +175,11 @@ def _create_backend_widget(self): try: sw.set_hexpand(True) sw.set_vexpand(True) + try: + sw.set_halign(Gtk.Align.FILL) + sw.set_valign(Gtk.Align.FILL) + except Exception: + pass except Exception: pass diff --git a/manatools/aui/backends/qt/richtextqt.py b/manatools/aui/backends/qt/richtextqt.py index a38b03a..d2ba6de 100644 --- a/manatools/aui/backends/qt/richtextqt.py +++ b/manatools/aui/backends/qt/richtextqt.py @@ -27,6 +27,12 @@ def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): self._auto_scroll = False self._last_url = None self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + # Default: richtext is stretchable in both dimensions + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + except Exception: + pass def widgetClass(self): return "YRichText" @@ -140,6 +146,21 @@ def _on_anchor_clicked(url: QtCore.QUrl): tb.linkActivated.connect(lambda _u: _on_anchor_clicked(QtCore.QUrl(str(_u)))) except Exception: pass + # Encourage expansion when placed in layouts + try: + sp = tb.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + tb.setSizePolicy(sp) + except Exception: + pass self._backend_widget = tb # respect initial enabled state try: From 857421db2c834a101450e26064073628fe5d183b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 19:03:53 +0100 Subject: [PATCH 351/523] fixed vertical stretching --- manatools/aui/backends/gtk/alignmentgtk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index 418f8da..4dc5071 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -61,8 +61,8 @@ def _to_gtk_halign(self): return Gtk.Align.CENTER if self._halign_spec == YAlignmentType.YAlignEnd: return Gtk.Align.END - #default - return Gtk.Align.CENTER + # default for Unchanged: let child fill available space + return Gtk.Align.FILL def _to_gtk_valign(self): """Convert Vertical YAlignmentType to Gtk.Align or Gtk.Align.CENTER.""" @@ -73,8 +73,8 @@ def _to_gtk_valign(self): return Gtk.Align.CENTER if self._valign_spec == YAlignmentType.YAlignEnd: return Gtk.Align.END - #default - return Gtk.Align.CENTER + # default for Unchanged: let child fill available space + return Gtk.Align.FILL #def stretchable(self, dim): # """Report whether this alignment should expand in given dimension. From 8f4c3e2c593c0d907da1bac627ae0651f03d6c28 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 19:04:30 +0100 Subject: [PATCH 352/523] a better test case for dumbtab widget --- test/test_dumptabwidget.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_dumptabwidget.py b/test/test_dumptabwidget.py index 3f7a103..d04d4b3 100644 --- a/test/test_dumptabwidget.py +++ b/test/test_dumptabwidget.py @@ -86,17 +86,17 @@ def render_content(index: int): box = factory.createVBox(rp) factory.createLabel(box, "Enable the option below:") factory.createCheckBox(box, "Enable feature", is_checked=True) - factory.createLabel(box, "Use TAB/Shift+TAB to navigate") + factory.createLabel(box, "Use this feature to blah blah...") rp.showChild() print("Rendered tab 0: Options") elif index == 1: box = factory.createVBox(rp) factory.createLabel(box, "Notes:") - text = "This is a simple multi-tab demo.\nSwitch tabs with LEFT/RIGHT (or UI specific).\nThe content below changes per tab." - try: - factory.createRichText(box, text, plainTextMode=True) - except Exception: - factory.createLabel(box, text) + text = "This is a simple multi-tab demo.\nSwitch between tabs.\nThe content below changes per tab." + minsize = factory.createMinSize(box, 320, 200) + rt = factory.createRichText(minsize, text, plainTextMode=True) + rt.setStretchable(yui.YUIDimension.YD_VERT, True) + rt.setStretchable(yui.YUIDimension.YD_HORIZ, True) rp.showChild() print("Rendered tab 1: Notes") else: From 4d9f28956df0a2b692b782bd588052865c18c24c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 19:06:34 +0100 Subject: [PATCH 353/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 9dfd3b8..963b1a7 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -33,7 +33,7 @@ Missing Widgets comparing libyui original factory: Optional/special widgets (from `YOptionalWidgetFactory`): - [ ] YDumbTab + [X] YDumbTab [ ] YSlider [ ] YDateField [ ] YTimeField From 34980b6efa39700413f6114e4e32a47ab6360259 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 23:00:17 +0100 Subject: [PATCH 354/523] Added header --- manatools/aui/backends/curses/dumbtabcurses.py | 9 +++++++++ manatools/aui/backends/gtk/dumbtabgtk.py | 9 +++++++++ manatools/aui/backends/qt/dumbtabqt.py | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/manatools/aui/backends/curses/dumbtabcurses.py b/manatools/aui/backends/curses/dumbtabcurses.py index bcded3f..8bb773e 100644 --- a/manatools/aui/backends/curses/dumbtabcurses.py +++ b/manatools/aui/backends/curses/dumbtabcurses.py @@ -1,6 +1,15 @@ # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: ''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +''' NCurses backend DumbTab: simple single-selection tab bar with one content area. - Renders a one-line tab bar with labels; the selected tab is highlighted. diff --git a/manatools/aui/backends/gtk/dumbtabgtk.py b/manatools/aui/backends/gtk/dumbtabgtk.py index 28c27a4..7bbb4b7 100644 --- a/manatools/aui/backends/gtk/dumbtabgtk.py +++ b/manatools/aui/backends/gtk/dumbtabgtk.py @@ -1,6 +1,15 @@ # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: ''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +''' GTK4 backend DumbTab (tab bar + single content area) Implements a simple tab bar using a row of toggle buttons (single selection) diff --git a/manatools/aui/backends/qt/dumbtabqt.py b/manatools/aui/backends/qt/dumbtabqt.py index 16ac85b..b76d3a1 100644 --- a/manatools/aui/backends/qt/dumbtabqt.py +++ b/manatools/aui/backends/qt/dumbtabqt.py @@ -1,6 +1,15 @@ # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: ''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +''' Qt backend DumbTab (tab bar + single content area) Implements a simple tab bar using QTabBar and exposes a single child From c42a564d300873fb27341570cb600e5f622e0199 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 23:01:13 +0100 Subject: [PATCH 355/523] Slide porting to manatools --- manatools/aui/backends/curses/__init__.py | 2 + manatools/aui/backends/curses/slidercurses.py | 153 ++++++++++++++++++ manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/slidergtk.py | 125 ++++++++++++++ manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/sliderqt.py | 131 +++++++++++++++ manatools/aui/yui_curses.py | 4 + manatools/aui/yui_gtk.py | 5 + manatools/aui/yui_qt.py | 4 + 9 files changed, 428 insertions(+) create mode 100644 manatools/aui/backends/curses/slidercurses.py create mode 100644 manatools/aui/backends/gtk/slidergtk.py create mode 100644 manatools/aui/backends/qt/sliderqt.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 9c35196..619d677 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -22,6 +22,7 @@ from .spacingcurses import YSpacingCurses from .imagecurses import YImageCurses from .dumbtabcurses import YDumbTabCurses +from .slidercurses import YSliderCurses __all__ = [ "YDialogCurses", @@ -48,5 +49,6 @@ "YSpacingCurses", "YImageCurses", "YDumbTabCurses", + "YSliderCurses", # ... ] diff --git a/manatools/aui/backends/curses/slidercurses.py b/manatools/aui/backends/curses/slidercurses.py new file mode 100644 index 0000000..96bdc3c --- /dev/null +++ b/manatools/aui/backends/curses/slidercurses.py @@ -0,0 +1,153 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +""" +NCurses backend Slider widget. +- Draws a horizontal track with Unicode box-drawing characters. + Track: ╞═══…══╡ ; position tick: ╪ at current value. +- Left/Right arrows to change, Home/End jump to min/max, PgUp/PgDn step. +- Default stretchable horizontally. +""" +import curses +import curses.ascii +import logging +from ...yui_common import * + +_mod_logger = logging.getLogger("manatools.aui.curses.slider.module") +if not logging.getLogger().handlers: + _h = logging.StreamHandler() + _h.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")) + _mod_logger.addHandler(_h) + _mod_logger.setLevel(logging.INFO) + +class YSliderCurses(YWidget): + def __init__(self, parent=None, label: str = "", minVal: int = 0, maxVal: int = 100, initialVal: int = 0): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + if not self._logger.handlers and not logging.getLogger().handlers: + for h in _mod_logger.handlers: + self._logger.addHandler(h) + self._label_text = str(label) if label else "" + self._min = int(minVal) + self._max = int(maxVal) + if self._min > self._max: + self._min, self._max = self._max, self._min + self._value = max(self._min, min(self._max, int(initialVal))) + self._height = 2 + self._can_focus = True + self._focused = False + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, False) + + def widgetClass(self): + return "YSlider" + + def value(self) -> int: + return int(self._value) + + def setValue(self, v: int): + prev = self._value + self._value = max(self._min, min(self._max, int(v))) + if self._value != prev and self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + + def _create_backend_widget(self): + self._backend_widget = self + try: + self._logger.debug("_create_backend_widget: <%s> range=[%d,%d] value=%d", self.debugLabel(), self._min, self._max, self._value) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + pass + + def _draw(self, window, y, x, width, height): + try: + line = y + # label on top if present + if self._label_text: + try: + window.addstr(line, x, self._label_text[:width]) + except curses.error: + pass + line += 1 + # draw track on the next line + track_y = line + # Ensure minimal width for endpoints and at least one segment + if width < 4: + return + # endpoints + left = "╞" + right = "╡" + seg = "═" + # compute inner width excluding endpoints and maybe arrows + inner_w = max(1, width - 2) + # draw left endpoint + try: + window.addstr(track_y, x, left) + except curses.error: + pass + # draw track segments + try: + window.addstr(track_y, x + 1, seg * (inner_w - 1)) + except curses.error: + pass + # draw right endpoint + try: + window.addstr(track_y, x + inner_w, right) + except curses.error: + pass + # position tick + rng = max(1, self._max - self._min) + pos_frac = (self._value - self._min) / rng + tick_x = x + 1 + int(pos_frac * (inner_w - 1)) + try: + window.addstr(track_y, tick_x, "╪") + except curses.error: + pass + # arrows near ends for hint + try: + if width >= 6: + window.addstr(track_y, x + 0, "◄") + window.addstr(track_y, x + inner_w, "►") + except curses.error: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + handled = True + step = max(1, (self._max - self._min) // 20) # 5% default step + if key in (curses.KEY_LEFT, ord('h')): + self.setValue(self._value - step) + elif key in (curses.KEY_RIGHT, ord('l')): + self.setValue(self._value + step) + elif key == curses.KEY_PPAGE: + self.setValue(self._value - max(1, (self._max - self._min) // 5)) + elif key == curses.KEY_NPAGE: + self.setValue(self._value + max(1, (self._max - self._min) // 5)) + elif key == curses.KEY_HOME: + self.setValue(self._min) + elif key == curses.KEY_END: + self.setValue(self._max) + elif key in (ord('\n'), ord(' ')): + # Activated event + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + else: + handled = False + return handled diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index f30f679..13c9564 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -22,6 +22,7 @@ from .spacinggtk import YSpacingGtk from .imagegtk import YImageGtk from .dumbtabgtk import YDumbTabGtk +from .slidergtk import YSliderGtk __all__ = [ "YDialogGtk", @@ -48,5 +49,6 @@ "YSpacingGtk", "YImageGtk", 'YDumbTabGtk', + "YSliderGtk", # ... ] diff --git a/manatools/aui/backends/gtk/slidergtk.py b/manatools/aui/backends/gtk/slidergtk.py new file mode 100644 index 0000000..238469d --- /dev/null +++ b/manatools/aui/backends/gtk/slidergtk.py @@ -0,0 +1,125 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +""" +GTK4 backend Slider widget. +- Horizontal Gtk.Scale synchronized with Gtk.SpinButton via Gtk.Adjustment. +- Emits ValueChanged on changes and Activated on user release (value-changed). +- Default stretchable horizontally. +""" +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk, GLib +import logging +from ...yui_common import * + +class YSliderGtk(YWidget): + def __init__(self, parent=None, label: str = "", minVal: int = 0, maxVal: int = 100, initialVal: int = 0): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._label_text = str(label) if label else "" + self._min = int(minVal) + self._max = int(maxVal) + if self._min > self._max: + self._min, self._max = self._max, self._min + self._value = max(self._min, min(self._max, int(initialVal))) + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, False) + self._backend_widget = None + self._adjustment = None + + def widgetClass(self): + return "YSlider" + + def value(self) -> int: + return int(self._value) + + def setValue(self, v: int): + prev = self._value + self._value = max(self._min, min(self._max, int(v))) + try: + if self._adjustment is not None: + self._adjustment.set_value(self._value) + except Exception: + pass + if self._value != prev and self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + + def _create_backend_widget(self): + try: + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + try: + box.set_hexpand(True) + box.set_vexpand(False) + except Exception: + pass + if self._label_text: + lbl = Gtk.Label.new(self._label_text) + try: + lbl.set_xalign(0.0) + except Exception: + pass + box.append(lbl) + + adj = Gtk.Adjustment.new(self._value, self._min, self._max, 1, 10, 0) + scale = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adj) + try: + scale.set_hexpand(True) + scale.set_vexpand(False) + except Exception: + pass + spin = Gtk.SpinButton.new(adj, 1, 0) + try: + spin.set_numeric(True) + except Exception: + pass + + box.append(scale) + box.append(spin) + + def _on_value_changed(widget): + try: + val = int(adj.get_value()) + except Exception: + val = self._value + old = self._value + self._value = val + if self.notify() and old != self._value: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + try: + scale.connect('value-changed', _on_value_changed) + spin.connect('value-changed', _on_value_changed) + except Exception: + pass + + self._backend_widget = box + self._adjustment = adj + self._backend_widget.set_sensitive(self._enabled) + try: + self._logger.debug("_create_backend_widget: <%s> range=[%d,%d] value=%d", self.debugLabel(), self._min, self._max, self._value) + except Exception: + pass + except Exception: + try: + self._logger.exception("YSliderGtk _create_backend_widget failed") + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_sensitive(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 0a8e836..99953f1 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -22,6 +22,7 @@ from .spacingqt import YSpacingQt from .imageqt import YImageQt from .dumbtabqt import YDumbTabQt +from .sliderqt import YSliderQt __all__ = [ @@ -49,5 +50,6 @@ "YSpacingQt", "YImageQt", "YDumbTabQt", + "YSliderQt", # ... ] diff --git a/manatools/aui/backends/qt/sliderqt.py b/manatools/aui/backends/qt/sliderqt.py new file mode 100644 index 0000000..cab1bd0 --- /dev/null +++ b/manatools/aui/backends/qt/sliderqt.py @@ -0,0 +1,131 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +Qt backend Slider widget. +- Horizontal slider synchronized with a spin box. +- Emits ValueChanged on changes and Activated on user release. +- Default stretchable horizontally. +""" +from PySide6 import QtWidgets, QtCore +import logging +from ...yui_common import * + +class YSliderQt(YWidget): + def __init__(self, parent=None, label: str = "", minVal: int = 0, maxVal: int = 100, initialVal: int = 0): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._label_text = str(label) if label else "" + self._min = int(minVal) + self._max = int(maxVal) + if self._min > self._max: + self._min, self._max = self._max, self._min + self._value = max(self._min, min(self._max, int(initialVal))) + # stretchable horizontally by default + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, False) + + def widgetClass(self): + return "YSlider" + + def value(self) -> int: + return int(self._value) + + def setValue(self, v: int): + prev = self._value + self._value = max(self._min, min(self._max, int(v))) + try: + if getattr(self, "_slider", None) is not None: + self._slider.setValue(self._value) + if getattr(self, "_spin", None) is not None: + self._spin.setValue(self._value) + except Exception: + pass + if self._value != prev and self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + + def _create_backend_widget(self): + try: + root = QtWidgets.QWidget() + lay = QtWidgets.QHBoxLayout(root) + lay.setContentsMargins(10, 10, 10, 10) + lay.setSpacing(8) + + if self._label_text: + lbl = QtWidgets.QLabel(self._label_text) + lay.addWidget(lbl) + + slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + slider.setRange(self._min, self._max) + slider.setValue(self._value) + slider.setTracking(True) + + spin = QtWidgets.QSpinBox() + spin.setRange(self._min, self._max) + spin.setValue(self._value) + + # expand policy for slider so it takes available space + try: + sp = slider.sizePolicy() + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + slider.setSizePolicy(sp) + except Exception: + pass + + lay.addWidget(slider, stretch=1) + lay.addWidget(spin) + + # wire signals + def _on_slider_changed(val): + try: + spin.setValue(val) + except Exception: + pass + old = self._value + self._value = int(val) + if self.notify() and old != self._value: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + def _on_slider_released(): + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + def _on_spin_changed(val): + try: + slider.setValue(int(val)) + except Exception: + pass + old = self._value + self._value = int(val) + if self.notify() and old != self._value: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + + slider.valueChanged.connect(_on_slider_changed) + slider.sliderReleased.connect(_on_slider_released) + spin.valueChanged.connect(_on_spin_changed) + + self._slider = slider + self._spin = spin + self._backend_widget = root + self._backend_widget.setEnabled(bool(self._enabled)) + try: + self._logger.debug("_create_backend_widget: <%s> range=[%d,%d] value=%d", self.debugLabel(), self._min, self._max, self._value) + except Exception: + pass + except Exception: + try: + self._logger.exception("YSliderQt _create_backend_widget failed") + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 216d702..b96e0b0 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -577,3 +577,7 @@ def createVSpacing(self, parent, size_px: int = 16): """Create a Vertical Spacing widget.""" return self.createSpacing(parent, YUIDimension.Vertical, stretchable=False, size_px=size_px) + def createSlider(self, parent, label: str, minVal: int, maxVal: int, initialVal: int): + """Create a Slider widget (ncurses backend).""" + return YSliderCurses(parent, label, minVal, maxVal, initialVal) + diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index d9c9041..852df2e 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -768,6 +768,11 @@ def createVStretch(self, parent): def createHSpacing(self, parent, size_px: int = 8): """Create a Horizontal Spacing widget.""" return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=False, size_px=size_px) + + def createSlider(self, parent, label: str, minVal: int, maxVal: int, initialVal: int): + """Create a Slider widget (GTK backend).""" + from .backends.gtk import YSliderGtk + return YSliderGtk(parent, label, minVal, maxVal, initialVal) def createVSpacing(self, parent, size_px: int = 16): """Create a Vertical Spacing widget.""" diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 2d16e71..96c96cb 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -377,6 +377,10 @@ def createVStretch(self, parent): def createHSpacing(self, parent, size_px: int = 8): """Create a Horizontal Spacing widget.""" return self.createSpacing(parent, YUIDimension.Horizontal, stretchable=False, size_px=size_px) + + def createSlider(self, parent, label: str, minVal: int, maxVal: int, initialVal: int): + """Create a Slider widget (Qt backend).""" + return YSliderQt(parent, label, minVal, maxVal, initialVal) def createVSpacing(self, parent, size_px: int = 16): """Create a Vertical Spacing widget.""" From 436e380aa34b27994c57fab7e61ce2746a659184 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 23:29:32 +0100 Subject: [PATCH 356/523] Aligned to gtk and qt to have an int input field, +,- move forward and backward of 1 --- manatools/aui/backends/curses/slidercurses.py | 191 ++++++++++++++---- 1 file changed, 149 insertions(+), 42 deletions(-) diff --git a/manatools/aui/backends/curses/slidercurses.py b/manatools/aui/backends/curses/slidercurses.py index 96bdc3c..87ac6c1 100644 --- a/manatools/aui/backends/curses/slidercurses.py +++ b/manatools/aui/backends/curses/slidercurses.py @@ -14,8 +14,11 @@ - Draws a horizontal track with Unicode box-drawing characters. Track: ╞═══…══╡ ; position tick: ╪ at current value. - Left/Right arrows to change, Home/End jump to min/max, PgUp/PgDn step. +- Up/Down arrows or '+'/'-' for fine-grained +/-1 changes. +- Numeric box on the right allows direct integer entry (like spinbox). - Default stretchable horizontally. """ + import curses import curses.ascii import logging @@ -28,6 +31,7 @@ _mod_logger.addHandler(_h) _mod_logger.setLevel(logging.INFO) + class YSliderCurses(YWidget): def __init__(self, parent=None, label: str = "", minVal: int = 0, maxVal: int = 100, initialVal: int = 0): super().__init__(parent) @@ -35,15 +39,22 @@ def __init__(self, parent=None, label: str = "", minVal: int = 0, maxVal: int = if not self._logger.handlers and not logging.getLogger().handlers: for h in _mod_logger.handlers: self._logger.addHandler(h) + self._label_text = str(label) if label else "" self._min = int(minVal) self._max = int(maxVal) if self._min > self._max: self._min, self._max = self._max, self._min self._value = max(self._min, min(self._max, int(initialVal))) + self._height = 2 self._can_focus = True self._focused = False + + # Inline numeric edit state + self._editing = False + self._edit_buf = "" + self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, False) @@ -64,7 +75,10 @@ def setValue(self, v: int): def _create_backend_widget(self): self._backend_widget = self try: - self._logger.debug("_create_backend_widget: <%s> range=[%d,%d] value=%d", self.debugLabel(), self._min, self._max, self._value) + self._logger.debug( + "_create_backend_widget: <%s> range=[%d,%d] value=%d", + self.debugLabel(), self._min, self._max, self._value + ) except Exception: pass @@ -81,73 +95,166 @@ def _draw(self, window, y, x, width, height): except curses.error: pass line += 1 - # draw track on the next line + track_y = line - # Ensure minimal width for endpoints and at least one segment - if width < 4: + + # Determine width for value box on the right + digits = max(len(str(self._min)), len(str(self._max))) + box_w = digits + 2 # [123] + space_w = 1 + min_track_w = 4 + have_box = width >= (min_track_w + space_w + box_w) + track_w = width - (space_w + box_w) if have_box else width + + if track_w < min_track_w: return - # endpoints + left = "╞" right = "╡" seg = "═" - # compute inner width excluding endpoints and maybe arrows - inner_w = max(1, width - 2) - # draw left endpoint + + seg_count = max(1, track_w - 2) + + # draw endpoints and track try: window.addstr(track_y, x, left) except curses.error: pass - # draw track segments try: - window.addstr(track_y, x + 1, seg * (inner_w - 1)) + window.addstr(track_y, x + 1, seg * seg_count) except curses.error: pass - # draw right endpoint try: - window.addstr(track_y, x + inner_w, right) + window.addstr(track_y, x + 1 + seg_count, right) except curses.error: pass - # position tick - rng = max(1, self._max - self._min) - pos_frac = (self._value - self._min) / rng - tick_x = x + 1 + int(pos_frac * (inner_w - 1)) + + # draw arrows first so the tick can overlay them at extremes try: - window.addstr(track_y, tick_x, "╪") + if track_w >= 6: + window.addstr(track_y, x + 0, "◄") + window.addstr(track_y, x + 1 + seg_count, "►") except curses.error: pass - # arrows near ends for hint + + # tick position clamped to interior [x+1, x+seg_count] + rng = max(1, self._max - self._min) + pos_frac = (self._value - self._min) / rng + tick_x = x + 1 + int(pos_frac * max(0, seg_count - 1)) + tick_x = max(x + 1, min(x + seg_count, tick_x)) try: - if width >= 6: - window.addstr(track_y, x + 0, "◄") - window.addstr(track_y, x + inner_w, "►") + window.addstr(track_y, tick_x, "╪") except curses.error: pass + + # Draw value box on the right if space allows + if have_box: + value_x = x + track_w + space_w + if self._editing: + text = self._edit_buf if self._edit_buf != "" else str(self._value) + else: + text = str(self._value) + text = text[:digits].rjust(digits) + disp = f"[{text}]" + try: + if self._editing: + window.addstr(track_y, value_x, disp, curses.A_REVERSE) + else: + window.addstr(track_y, value_x, disp) + except curses.error: + pass except curses.error: pass def _handle_key(self, key): if not self._focused or not self.isEnabled(): return False + handled = True - step = max(1, (self._max - self._min) // 20) # 5% default step - if key in (curses.KEY_LEFT, ord('h')): - self.setValue(self._value - step) - elif key in (curses.KEY_RIGHT, ord('l')): - self.setValue(self._value + step) - elif key == curses.KEY_PPAGE: - self.setValue(self._value - max(1, (self._max - self._min) // 5)) - elif key == curses.KEY_NPAGE: - self.setValue(self._value + max(1, (self._max - self._min) // 5)) - elif key == curses.KEY_HOME: - self.setValue(self._min) - elif key == curses.KEY_END: - self.setValue(self._max) - elif key in (ord('\n'), ord(' ')): - # Activated event - if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) - else: - handled = False + step = max(1, (self._max - self._min) // 20) # ~5% + + if self._editing: + # Editing mode: accept digits, backspace, ESC, Enter/Space + if key in (curses.KEY_BACKSPACE, 127, 8): + if self._edit_buf: + self._edit_buf = self._edit_buf[:-1] + elif curses.ascii.isdigit(key): + self._edit_buf += chr(key) + elif key in (ord('-'),): + # allow negative sign only at start and only if range allows negatives + if self._min < 0 and (self._edit_buf == "" or self._edit_buf == "-"): + self._edit_buf = "" if self._edit_buf == "-" else "-" + elif key in (ord('\n'), ord(' ')): + self._commit_edit() + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + elif key in ( + curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_UP, curses.KEY_DOWN, + curses.KEY_HOME, curses.KEY_END, curses.KEY_PPAGE, curses.KEY_NPAGE, + ord('+'), ord('-') + ): + # commit and re-handle as navigation + self._commit_edit() + handled = False + elif key == 9: # Tab + self._commit_edit() + return False + elif key == 27: # ESC + self._cancel_edit() + else: + handled = False + + if handled is False: + handled = True + + if not self._editing and handled: + if key in (curses.KEY_LEFT, ord('h')): + self.setValue(self._value - step) + elif key in (curses.KEY_RIGHT, ord('l')): + self.setValue(self._value + step) + elif key in (curses.KEY_UP, ord('+')): + self.setValue(self._value + 1) + elif key in (curses.KEY_DOWN, ord('-')): + self.setValue(self._value - 1) + elif key == curses.KEY_PPAGE: + self.setValue(self._value - max(1, (self._max - self._min) // 5)) + elif key == curses.KEY_NPAGE: + self.setValue(self._value + max(1, (self._max - self._min) // 5)) + elif key == curses.KEY_HOME: + self.setValue(self._min) + elif key == curses.KEY_END: + self.setValue(self._max) + elif key in (ord('\n'), ord(' ')): + if self.notify(): + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + elif curses.ascii.isdigit(key): + # start inline editing with first digit + self._begin_edit(chr(key)) + else: + handled = False + return handled + + def _begin_edit(self, initial_char=None): + self._editing = True + self._edit_buf = "" if initial_char is None else str(initial_char) + + def _cancel_edit(self): + self._editing = False + self._edit_buf = "" + + def _commit_edit(self): + if self._edit_buf in ("", "-"): + self._cancel_edit() + return + try: + v = int(self._edit_buf) + except ValueError: + self._cancel_edit() + return + self.setValue(v) + self._cancel_edit() From 9f467ccb9eebe033a63907da4dc1ee2840162079 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Wed, 7 Jan 2026 23:30:57 +0100 Subject: [PATCH 357/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 963b1a7..c000ee0 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -34,7 +34,7 @@ Missing Widgets comparing libyui original factory: Optional/special widgets (from `YOptionalWidgetFactory`): [X] YDumbTab - [ ] YSlider + [X] YSlider [ ] YDateField [ ] YTimeField [ ] YBarGraph From b38390bd1974755d3264cbce68a563755f5e32f3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 13:11:54 +0100 Subject: [PATCH 358/523] Let's show a name for any focused widget also those that don't have label field --- manatools/aui/backends/curses/dialogcurses.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index f6b6cd7..fca4c29 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -203,9 +203,13 @@ def _draw_dialog(self): if footer_x + len(footer_text) < width: self._backend_widget.addstr(height - 1, footer_x, footer_text, curses.A_DIM) - # Draw focus indicator + # Draw focus indicator: prefer widget label, otherwise use debugLabel() or 'unknown' if self._focused_widget: - focus_text = f" Focus: {getattr(self._focused_widget, '_label', 'Unknown')} " + lbl = getattr(self._focused_widget, '_label', None) + if not lbl: + lbl = (self._focused_widget.debugLabel() + if hasattr(self._focused_widget, 'debugLabel') else 'Unknown') + focus_text = f" Focus: {lbl} " if len(focus_text) < width: self._backend_widget.addstr(height - 1, 2, focus_text, curses.A_REVERSE) #if the focused widget has an expnded list (menus, combos,...), draw it on top From 8d2ba3afa2e8a87559624fbf2098bc5b51a19c0e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 13:30:00 +0100 Subject: [PATCH 359/523] Added DateField --- manatools/aui/backends/curses/__init__.py | 2 + .../aui/backends/curses/datefieldcurses.py | 224 ++++++++++++++++++ manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/datefieldgtk.py | 184 ++++++++++++++ manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/datefieldqt.py | 120 ++++++++++ manatools/aui/yui_curses.py | 4 + manatools/aui/yui_gtk.py | 4 + manatools/aui/yui_qt.py | 6 +- test/test_datefield.py | 73 ++++++ test/test_slide.py | 78 ++++++ 11 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/datefieldcurses.py create mode 100644 manatools/aui/backends/gtk/datefieldgtk.py create mode 100644 manatools/aui/backends/qt/datefieldqt.py create mode 100644 test/test_datefield.py create mode 100644 test/test_slide.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 619d677..90f935b 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -18,6 +18,7 @@ from .menubarcurses import YMenuBarCurses from .replacepointcurses import YReplacePointCurses from .intfieldcurses import YIntFieldCurses +from .datefieldcurses import YDateFieldCurses from .multilineeditcurses import YMultiLineEditCurses from .spacingcurses import YSpacingCurses from .imagecurses import YImageCurses @@ -45,6 +46,7 @@ "YMenuBarCurses", "YReplacePointCurses", "YIntFieldCurses", + "YDateFieldCurses", "YMultiLineEditCurses", "YSpacingCurses", "YImageCurses", diff --git a/manatools/aui/backends/curses/datefieldcurses.py b/manatools/aui/backends/curses/datefieldcurses.py new file mode 100644 index 0000000..d9d4d65 --- /dev/null +++ b/manatools/aui/backends/curses/datefieldcurses.py @@ -0,0 +1,224 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import logging +import locale +from ...yui_common import * + + +def _locale_date_order(): + try: + locale.setlocale(locale.LC_TIME, '') + except Exception: + pass + try: + fmt = locale.nl_langinfo(locale.D_FMT) + except Exception: + fmt = '%Y-%m-%d' + fmt = fmt or '%Y-%m-%d' + order = [] + i = 0 + while i < len(fmt): + if fmt[i] == '%': + i += 1 + if i < len(fmt): + c = fmt[i] + if c in ('Y', 'y'): + order.append('Y') + elif c in ('m', 'b', 'B'): + order.append('M') + elif c in ('d', 'e'): + order.append('D') + i += 1 + for x in ['Y', 'M', 'D']: + if x not in order: + order.append(x) + return order[:3] + + +class YDateFieldCurses(YWidget): + """NCurses backend YDateField with three integer segments (Y, M, D) ordered per locale. + value()/setValue() use ISO format YYYY-MM-DD. No change events posted. + Navigation: Left/Right to change segment, Up/Down or +/- to change value, digits to type. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + self._label = label or "" + self._y, self._m, self._d = 2000, 1, 1 + self._order = _locale_date_order() + self._seg_index = 0 # 0..2 + self._editing = False + self._edit_buf = "" + self._can_focus = True + self._focused = False + try: + self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, self._label) + except Exception: + pass + + def widgetClass(self): + return "YDateField" + + def value(self) -> str: + return f"{self._y:04d}-{self._m:02d}-{self._d:02d}" + + def setValue(self, datestr: str): + try: + y, m, d = [int(p) for p in str(datestr).split('-')] + except Exception: + return + y = max(1, min(9999, y)) + m = max(1, min(12, m)) + dmax = self._days_in_month(y, m) + d = max(1, min(dmax, d)) + self._y, self._m, self._d = y, m, d + + def _days_in_month(self, y, m): + if m in (1,3,5,7,8,10,12): + return 31 + if m in (4,6,9,11): + return 30 + leap = (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)) + return 29 if leap else 28 + + def _create_backend_widget(self): + self._backend_widget = self + + def _set_backend_enabled(self, enabled): + pass + + def _draw(self, window, y, x, width, height): + try: + line = y + label_to_show = self._label if self._label else (self.debugLabel() if hasattr(self, 'debugLabel') else "unknown") + try: + window.addstr(line, x, label_to_show[:width]) + except curses.error: + pass + line += 1 + + # build ordered parts + parts = {'Y': f"{self._y:04d}", 'M': f"{self._m:02d}", 'D': f"{self._d:02d}"} + disp = [] + for idx, p in enumerate(self._order): + text = parts[p] + if self._focused and idx == self._seg_index: + text = f"[{text}]" + else: + text = f" {text} " + disp.append(text) + if idx < 2: + disp.append("-") + out = ''.join(disp) + try: + window.addstr(line, x, out[:max(0, width)]) + except curses.error: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + + # Editing + if self._editing: + if key in (curses.KEY_BACKSPACE, 127, 8): + if self._edit_buf: + self._edit_buf = self._edit_buf[:-1] + return True + if curses.ascii.isdigit(key): + self._edit_buf += chr(key) + return True + if key in (ord('\n'), ord(' ')): + self._commit_edit() + return True + if key == 27: # ESC + self._cancel_edit() + return True + # navigation commits and re-handles + if key in (curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_UP, curses.KEY_DOWN, ord('+'), ord('-')): + self._commit_edit() + # fall through + + # Non-editing navigation + if key in (curses.KEY_LEFT,): + self._seg_index = (self._seg_index - 1) % 3 + return True + if key in (curses.KEY_RIGHT,): + self._seg_index = (self._seg_index + 1) % 3 + return True + if key in (curses.KEY_UP, ord('+')): + self._bump(+1) + return True + if key in (curses.KEY_DOWN, ord('-')): + self._bump(-1) + return True + if curses.ascii.isdigit(key): + self._begin_edit(chr(key)) + return True + return False + + def _seg_ref(self, idx): + seg = self._order[idx] + if seg == 'Y': + return '_y', 1, 9999 + if seg == 'M': + return '_m', 1, 12 + # D + return '_d', 1, self._days_in_month(self._y, self._m) + + def _bump(self, delta): + name, lo, hi = self._seg_ref(self._seg_index) + val = getattr(self, name) + if name == '_d': + hi = self._days_in_month(self._y, self._m) + val += delta + if val < lo: val = lo + if val > hi: val = hi + setattr(self, name, val) + # adjust day when year/month change affects max days + if name in ('_y','_m'): + dmax = self._days_in_month(self._y, self._m) + if self._d > dmax: + self._d = dmax + + def _begin_edit(self, initial_char=None): + self._editing = True + self._edit_buf = '' if initial_char is None else str(initial_char) + + def _cancel_edit(self): + self._editing = False + self._edit_buf = '' + + def _commit_edit(self): + if self._edit_buf in ('', '-'): + self._cancel_edit() + return + try: + v = int(self._edit_buf) + except ValueError: + self._cancel_edit() + return + name, lo, hi = self._seg_ref(self._seg_index) + if name == '_d': + hi = self._days_in_month(self._y, self._m) + v = max(lo, min(hi, v)) + setattr(self, name, v) + # adjust day if needed + if name in ('_y','_m'): + dmax = self._days_in_month(self._y, self._m) + if self._d > dmax: + self._d = dmax + self._cancel_edit() diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 13c9564..5d4ed46 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -18,6 +18,7 @@ from .menubargtk import YMenuBarGtk from .replacepointgtk import YReplacePointGtk from .intfieldgtk import YIntFieldGtk +from .datefieldgtk import YDateFieldGtk from .multilineeditgtk import YMultiLineEditGtk from .spacinggtk import YSpacingGtk from .imagegtk import YImageGtk @@ -45,6 +46,7 @@ "YMenuBarGtk", "YReplacePointGtk", "YIntFieldGtk", + "YDateFieldGtk", "YMultiLineEditGtk", "YSpacingGtk", "YImageGtk", diff --git a/manatools/aui/backends/gtk/datefieldgtk.py b/manatools/aui/backends/gtk/datefieldgtk.py new file mode 100644 index 0000000..1d8843e --- /dev/null +++ b/manatools/aui/backends/gtk/datefieldgtk.py @@ -0,0 +1,184 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all gtk backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +import logging +import locale +from gi.repository import Gtk +from ...yui_common import * + + +def _locale_date_order(): + try: + locale.setlocale(locale.LC_TIME, '') + except Exception: + pass + try: + fmt = locale.nl_langinfo(locale.D_FMT) + except Exception: + fmt = '%Y-%m-%d' + fmt = fmt or '%Y-%m-%d' + order = [] + i = 0 + while i < len(fmt): + if fmt[i] == '%': + i += 1 + if i < len(fmt): + c = fmt[i] + if c in ('Y', 'y'): + order.append('Y') + elif c in ('m', 'b', 'B'): + order.append('M') + elif c in ('d', 'e'): + order.append('D') + i += 1 + # ensure Y, M, D present + for x in ['Y', 'M', 'D']: + if x not in order: + order.append(x) + return order[:3] + + +class YDateFieldGtk(YWidget): + """GTK backend YDateField implemented with three SpinButtons ordered per system locale date order. + value()/setValue() use ISO format YYYY-MM-DD. No change events posted. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._label = label or "" + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + try: + self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, self._label) + except Exception: + pass + self._y = 2000 + self._m = 1 + self._d = 1 + self._order = _locale_date_order() + + def widgetClass(self): + return "YDateField" + + def value(self) -> str: + try: + return f"{self._y:04d}-{self._m:02d}-{self._d:02d}" + except Exception: + return "" + + def setValue(self, datestr: str): + try: + y, m, d = [int(p) for p in str(datestr).split('-')] + except Exception: + return + y = max(1, min(9999, y)) + m = max(1, min(12, m)) + dmax = self._days_in_month(y, m) + d = max(1, min(dmax, d)) + self._y, self._m, self._d = y, m, d + self._sync_spins() + + def _days_in_month(self, y, m): + if m in (1,3,5,7,8,10,12): + return 31 + if m in (4,6,9,11): + return 30 + # February + leap = (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)) + return 29 if leap else 28 + + def _sync_spins(self): + if getattr(self, '_spin_y', None): + try: self._spin_y.set_value(self._y) + except Exception: pass + if getattr(self, '_spin_m', None): + try: + self._spin_m.set_value(self._m) + except Exception: pass + if getattr(self, '_spin_d', None): + try: + self._spin_d.get_adjustment().set_upper(self._days_in_month(self._y, self._m)) + self._spin_d.set_value(self._d) + except Exception: pass + + def _create_backend_widget(self): + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + if self._label: + lbl = Gtk.Label.new(self._label) + lbl.set_xalign(0.0) + box.append(lbl) + self._label_widget = lbl + + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + box.append(row) + + # Create spins + self._spin_y = Gtk.SpinButton.new_with_range(1, 9999, 1) + self._spin_y.set_width_chars(4) + self._spin_m = Gtk.SpinButton.new_with_range(1, 12, 1) + self._spin_m.set_width_chars(2) + self._spin_d = Gtk.SpinButton.new_with_range(1, 31, 1) + self._spin_d.set_width_chars(2) + + # Initialize default date + self._sync_spins() + + # Connect to maintain boundaries but do not post events + def on_y_changed(spin): + try: + self._y = int(spin.get_value()) + # re-clamp day for new year (leap) + dmax = self._days_in_month(self._y, self._m) + if self._d > dmax: + self._d = dmax + self._spin_d.set_value(self._d) + except Exception: + pass + def on_m_changed(spin): + try: + self._m = int(spin.get_value()) + dmax = self._days_in_month(self._y, self._m) + self._spin_d.get_adjustment().set_upper(dmax) + if self._d > dmax: + self._d = dmax + self._spin_d.set_value(self._d) + except Exception: + pass + def on_d_changed(spin): + try: + self._d = int(spin.get_value()) + except Exception: + pass + + self._spin_y.connect('value-changed', on_y_changed) + self._spin_m.connect('value-changed', on_m_changed) + self._spin_d.connect('value-changed', on_d_changed) + + # Order per locale + for part in self._order: + if part == 'Y': + row.append(self._spin_y) + elif part == 'M': + row.append(self._spin_m) + else: + row.append(self._spin_d) + + self._backend_widget = box + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + for w in (getattr(self, '_spin_y', None), getattr(self, '_spin_m', None), getattr(self, '_spin_d', None), getattr(self, '_label_widget', None)): + if w is not None: + w.set_sensitive(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 99953f1..0cecedf 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -18,6 +18,7 @@ from .menubarqt import YMenuBarQt from .replacepointqt import YReplacePointQt from .intfieldqt import YIntFieldQt +from .datefieldqt import YDateFieldQt from .multilineeditqt import YMultiLineEditQt from .spacingqt import YSpacingQt from .imageqt import YImageQt @@ -46,6 +47,7 @@ "YMenuBarQt", "YReplacePointQt", "YIntFieldQt", + "YDateFieldQt", "YMultiLineEditQt", "YSpacingQt", "YImageQt", diff --git a/manatools/aui/backends/qt/datefieldqt.py b/manatools/aui/backends/qt/datefieldqt.py new file mode 100644 index 0000000..b7fd0a9 --- /dev/null +++ b/manatools/aui/backends/qt/datefieldqt.py @@ -0,0 +1,120 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore +import logging +from ...yui_common import * + + +class YDateFieldQt(YWidget): + """Qt backend YDateField implementation using QDateEdit. + value()/setValue() use ISO format YYYY-MM-DD. No change events posted. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._label = label or "" + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + try: + self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, self._label) + except Exception: + pass + self._date = QtCore.QDate.currentDate() + + def widgetClass(self): + return "YDateField" + + def value(self) -> str: + try: + return self._date.toString("yyyy-MM-dd") + except Exception: + return "" + + def setValue(self, datestr: str): + try: + parts = str(datestr).split("-") + if len(parts) == 3: + y, m, d = [int(p) for p in parts] + qd = QtCore.QDate(y, m, d) + if qd.isValid(): + self._date = qd + if getattr(self, '_dateedit', None) is not None: + try: + self._dateedit.setDate(self._date) + except Exception: + pass + except Exception: + try: + self._logger.debug("Invalid date for setValue: %r", datestr) + except Exception: + pass + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + if self._label: + lbl = QtWidgets.QLabel(self._label) + layout.addWidget(lbl) + self._label_widget = lbl + + de = QtWidgets.QDateEdit() + try: + de.setCalendarPopup(True) + except Exception: + pass + try: + # Display in system locale for user friendliness + loc = QtCore.QLocale.system() + fmt = loc.dateFormat(QtCore.QLocale.FormatType.ShortFormat) + if fmt: + de.setDisplayFormat(fmt) + except Exception: + pass + try: + de.setDate(self._date) + except Exception: + pass + # Do not emit any events; value is read on demand + layout.addWidget(de) + self._backend_widget = container + self._dateedit = de + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + # Apply size policy from stretchable hints + try: + sp = container.sizePolicy() + try: + horiz = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Preferred + vert = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed + except Exception: + horiz = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Preferred + vert = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + container.setSizePolicy(sp) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, '_dateedit', None) is not None: + self._dateedit.setEnabled(bool(enabled)) + except Exception: + pass + try: + if getattr(self, '_label_widget', None) is not None: + self._label_widget.setEnabled(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index b96e0b0..71c819b 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -581,3 +581,7 @@ def createSlider(self, parent, label: str, minVal: int, maxVal: int, initialVal: """Create a Slider widget (ncurses backend).""" return YSliderCurses(parent, label, minVal, maxVal, initialVal) + def createDateField(self, parent, label): + """Create a DateField widget (curses backend).""" + return YDateFieldCurses(parent, label) + diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 852df2e..f8e74f3 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -778,6 +778,10 @@ def createVSpacing(self, parent, size_px: int = 16): """Create a Vertical Spacing widget.""" return self.createSpacing(parent, YUIDimension.Vertical, stretchable=False, size_px=size_px) + def createDateField(self, parent, label): + """Create a DateField widget (GTK backend).""" + return YDateFieldGtk(parent, label) + def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 96c96cb..74dbf36 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -384,4 +384,8 @@ def createSlider(self, parent, label: str, minVal: int, maxVal: int, initialVal: def createVSpacing(self, parent, size_px: int = 16): """Create a Vertical Spacing widget.""" - return self.createSpacing(parent, YUIDimension.Vertical, stretchable=False, size_px=size_px) \ No newline at end of file + return self.createSpacing(parent, YUIDimension.Vertical, stretchable=False, size_px=size_px) + + def createDateField(self, parent, label): + """Create a DateField widget (Qt backend).""" + return YDateFieldQt(parent, label) \ No newline at end of file diff --git a/test/test_datefield.py b/test/test_datefield.py new file mode 100644 index 0000000..19cb449 --- /dev/null +++ b/test/test_datefield.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + + +def test_datefield(backend_name=None): + """Interactive test for YDateField widget.""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox(dialog) + factory.createHeading(vbox, "DateField Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + + # Create datefield + df = factory.createDateField(vbox, "Select Date:") + + # Buttons + h = factory.createHBox(vbox) + ok_btn = factory.createPushButton(h, "OK") + close_btn = factory.createPushButton(h, "Close") + + print("\nOpening DateField test dialog...") + + while True: + ev = dialog.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif et == yui.YEventType.WidgetEvent: + wdg = ev.widget() + reason = ev.reason() + if wdg == close_btn and reason == yui.YEventReason.Activated: + dialog.destroy() + break + if wdg == ok_btn and reason == yui.YEventReason.Activated: + print("OK clicked. Final date:", df.value()) + dialog.destroy() + break + except Exception as e: + print(f"Error testing DateField with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_datefield(sys.argv[1]) + else: + test_datefield() diff --git a/test/test_slide.py b/test/test_slide.py new file mode 100644 index 0000000..dbef0e5 --- /dev/null +++ b/test/test_slide.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_slide(backend_name=None): + """Interactive test showcasing Slider widget.""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox(dialog) + factory.createHeading(vbox, "Slider Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + + # Create slider + slider = factory.createSlider(vbox, "Volume:", 0, 100, 25) + + # Show current value + val_label = factory.createLabel(vbox, "Value: 25") + + # Buttons + h = factory.createHBox(vbox) + ok_btn = factory.createPushButton(h, "OK") + close_btn = factory.createPushButton(h, "Close") + + print("\nOpening Slider test dialog...") + + while True: + ev = dialog.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif et == yui.YEventType.WidgetEvent: + wdg = ev.widget() + reason = ev.reason() + if wdg == close_btn and reason == yui.YEventReason.Activated: + dialog.destroy() + break + if wdg == ok_btn and reason == yui.YEventReason.Activated: + print("OK clicked. Final value:", slider.value()) + if wdg == slider: + if reason == yui.YEventReason.ValueChanged: + val_label.setText(f"Value: {slider.value()}") + print("ValueChanged:", slider.value()) + elif reason == yui.YEventReason.Activated: + print("Activated at:", slider.value()) + except Exception as e: + print(f"Error testing Slider with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_slide(sys.argv[1]) + else: + test_slide() From 20e9098bb52d652ab551d030a241efeb29106982 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 15:08:59 +0100 Subject: [PATCH 360/523] fixing end value --- manatools/aui/backends/qt/datefieldqt.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/qt/datefieldqt.py b/manatools/aui/backends/qt/datefieldqt.py index b7fd0a9..f2f6507 100644 --- a/manatools/aui/backends/qt/datefieldqt.py +++ b/manatools/aui/backends/qt/datefieldqt.py @@ -22,10 +22,6 @@ def __init__(self, parent=None, label: str = ""): super().__init__(parent) self._label = label or "" self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") - try: - self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, self._label) - except Exception: - pass self._date = QtCore.QDate.currentDate() def widgetClass(self): @@ -56,6 +52,17 @@ def setValue(self, datestr: str): except Exception: pass + def _on_date_changed(self, qdate): + """Update internal date when the QDateEdit value changes in the UI.""" + try: + # qdate is a QtCore.QDate + self._date = qdate + except Exception: + try: + self._logger.debug("_on_date_changed: couldn't set date from %r", qdate) + except Exception: + pass + def _create_backend_widget(self): container = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(container) @@ -83,6 +90,14 @@ def _create_backend_widget(self): de.setDate(self._date) except Exception: pass + try: + # keep internal date in sync when user selects a new date + de.dateChanged.connect(self._on_date_changed) + except Exception: + try: + self._logger.debug("could not connect dateChanged signal") + except Exception: + pass # Do not emit any events; value is read on demand layout.addWidget(de) self._backend_widget = container From 0fb5341d8cab3a6bd63f627856e3d266e0d5c9aa Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 19:16:32 +0100 Subject: [PATCH 361/523] Fixed gtk behavior to look similar to qt --- manatools/aui/backends/gtk/datefieldgtk.py | 354 +++++++++++++++------ 1 file changed, 249 insertions(+), 105 deletions(-) diff --git a/manatools/aui/backends/gtk/datefieldgtk.py b/manatools/aui/backends/gtk/datefieldgtk.py index 1d8843e..2959721 100644 --- a/manatools/aui/backends/gtk/datefieldgtk.py +++ b/manatools/aui/backends/gtk/datefieldgtk.py @@ -11,78 +11,72 @@ ''' import logging import locale -from gi.repository import Gtk +import datetime +from gi.repository import Gtk, GLib from ...yui_common import * -def _locale_date_order(): - try: - locale.setlocale(locale.LC_TIME, '') - except Exception: - pass - try: - fmt = locale.nl_langinfo(locale.D_FMT) - except Exception: - fmt = '%Y-%m-%d' - fmt = fmt or '%Y-%m-%d' - order = [] - i = 0 - while i < len(fmt): - if fmt[i] == '%': - i += 1 - if i < len(fmt): - c = fmt[i] - if c in ('Y', 'y'): - order.append('Y') - elif c in ('m', 'b', 'B'): - order.append('M') - elif c in ('d', 'e'): - order.append('D') - i += 1 - # ensure Y, M, D present - for x in ['Y', 'M', 'D']: - if x not in order: - order.append(x) - return order[:3] - - class YDateFieldGtk(YWidget): - """GTK backend YDateField implemented with three SpinButtons ordered per system locale date order. + """GTK backend YDateField implemented as a compact button with popup Gtk.Calendar. value()/setValue() use ISO format YYYY-MM-DD. No change events posted. """ def __init__(self, parent=None, label: str = ""): super().__init__(parent) self._label = label or "" self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") - try: - self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, self._label) - except Exception: - pass - self._y = 2000 - self._m = 1 - self._d = 1 - self._order = _locale_date_order() + # store current value as a date object (use only the date portion) + self._date = datetime.date(2000, 1, 1) + self._calendar = None + self._menu_btn = None + self._date_label = None + self._popover = None + self._locale_date_fmt = None def widgetClass(self): return "YDateField" def value(self) -> str: try: - return f"{self._y:04d}-{self._m:02d}-{self._d:02d}" + d = getattr(self, '_date', None) + if d is None: + return '' + return d.isoformat() except Exception: return "" def setValue(self, datestr: str): try: - y, m, d = [int(p) for p in str(datestr).split('-')] + # accept ISO-like YYYY-MM-DD + parts = str(datestr).split('-') + if len(parts) != 3: + return + y, m, d = [int(p) for p in parts] except Exception: return - y = max(1, min(9999, y)) - m = max(1, min(12, m)) - dmax = self._days_in_month(y, m) - d = max(1, min(dmax, d)) - self._y, self._m, self._d = y, m, d - self._sync_spins() + try: + y = max(1, min(9999, y)) + m = max(1, min(12, m)) + dmax = self._days_in_month(y, m) + d = max(1, min(dmax, d)) + newdate = datetime.date(y, m, d) + except Exception: + return + self._set_date(newdate) + + def _get_locale_date_fmt(self): + if getattr(self, '_locale_date_fmt', None): + return self._locale_date_fmt + try: + try: + locale.setlocale(locale.LC_TIME, '') + except Exception: + pass + fmt = locale.nl_langinfo(locale.D_FMT) + except Exception: + fmt = '%Y-%m-%d' + fmt = fmt or '%Y-%m-%d' + self._locale_date_fmt = fmt + return fmt def _days_in_month(self, y, m): if m in (1,3,5,7,8,10,12): @@ -93,20 +87,6 @@ def _days_in_month(self, y, m): leap = (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)) return 29 if leap else 28 - def _sync_spins(self): - if getattr(self, '_spin_y', None): - try: self._spin_y.set_value(self._y) - except Exception: pass - if getattr(self, '_spin_m', None): - try: - self._spin_m.set_value(self._m) - except Exception: pass - if getattr(self, '_spin_d', None): - try: - self._spin_d.get_adjustment().set_upper(self._days_in_month(self._y, self._m)) - self._spin_d.set_value(self._d) - except Exception: pass - def _create_backend_widget(self): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) if self._label: @@ -115,59 +95,191 @@ def _create_backend_widget(self): box.append(lbl) self._label_widget = lbl + # Horizontal DateEdit: entry + calendar button row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - box.append(row) + self._date_entry = Gtk.Entry() + self._date_entry.set_hexpand(True) + # placeholder in system locale format + try: + fmt = self._get_locale_date_fmt() + sample = datetime.date(2000, 1, 15).strftime(fmt) + except Exception: + sample = "YYYY-MM-DD" + self._date_entry.set_placeholder_text(sample) + row.append(self._date_entry) - # Create spins - self._spin_y = Gtk.SpinButton.new_with_range(1, 9999, 1) - self._spin_y.set_width_chars(4) - self._spin_m = Gtk.SpinButton.new_with_range(1, 12, 1) - self._spin_m.set_width_chars(2) - self._spin_d = Gtk.SpinButton.new_with_range(1, 31, 1) - self._spin_d.set_width_chars(2) + # MenuButton styled as dropdown + self._cal_button = Gtk.MenuButton() + try: + img = Gtk.Image.new_from_icon_name("open-menu-symbolic") + self._cal_button.set_child(img) + except Exception: + try: + self._cal_button.set_label("▾") + except Exception: + pass + row.append(self._cal_button) - # Initialize default date - self._sync_spins() + # Popover with Calendar + self._popover = Gtk.Popover() + cal = Gtk.Calendar() - # Connect to maintain boundaries but do not post events - def on_y_changed(spin): + # GLib.DateTime uses 1-based months (January=1) + gdate = GLib.DateTime.new_utc(self._date.year, self._date.month, self._date.day, 0, 0, 0) + # Use set_date() for GTK 4.20+ + try: + self._calendar.set_date(gdate) + except Exception as e: + # Fall back to select_day() if set_date() fails unexpectedly + self._logger.debug("Failed to set_date() on calendar: %s", e) try: - self._y = int(spin.get_value()) - # re-clamp day for new year (leap) - dmax = self._days_in_month(self._y, self._m) - if self._d > dmax: - self._d = dmax - self._spin_d.set_value(self._d) + self._calendar.select_day(gdate) except Exception: - pass - def on_m_changed(spin): + self._logger.exception("Failed to initialize calendar select_day()") + + # Entry handlers: parse and commit to value + def _parse_and_set(datestr): try: - self._m = int(spin.get_value()) - dmax = self._days_in_month(self._y, self._m) - self._spin_d.get_adjustment().set_upper(dmax) - if self._d > dmax: - self._d = dmax - self._spin_d.set_value(self._d) + parts = str(datestr).strip() + if not parts: + return False + y = m = d = None + if '-' in parts: + ymd = parts.split('-') + if len(ymd) == 3 and all(p.isdigit() for p in ymd): + y, m, d = [int(p) for p in ymd] + if y is None: + try: + fmt = self._get_locale_date_fmt() + dt = datetime.datetime.strptime(parts, fmt) + y, m, d = dt.year, dt.month, dt.day + except Exception: + return False except Exception: - pass - def on_d_changed(spin): + return False + y = max(1, min(9999, y)) + m = max(1, min(12, m)) + dmax = self._days_in_month(y, m) + d = max(1, min(dmax, d)) try: - self._d = int(spin.get_value()) + newdate = datetime.date(y, m, d) except Exception: - pass + return False + self._date = newdate + # GLib.DateTime uses 1-based months (January=1) + gdate = GLib.DateTime.new_utc(self._date.year, self._date.month, self._date.day, 0, 0, 0) + # Use set_date() for GTK 4.20+ + try: + self._calendar.set_date(gdate) + except Exception as e: + # Fall back to select_day() if set_date() fails unexpectedly + self._logger.debug("Failed to set_date() on calendar: %s", e) + try: + self._calendar.select_day(gdate) + except Exception: + self._logger.exception("Failed to initialize calendar select_day()") + self._logger.debug("entry commit: %04d-%02d-%02d", y, m, d) + return True + + def _on_entry_activate(entry): + txt = entry.get_text() + _parse_and_set(txt) + + try: + self._date_entry.connect('activate', _on_entry_activate) + self._date_entry.connect('changed', _on_entry_activate) + except Exception: + self._logger.exception("Failed to connect entry activate") - self._spin_y.connect('value-changed', on_y_changed) - self._spin_m.connect('value-changed', on_m_changed) - self._spin_d.connect('value-changed', on_d_changed) + # Calendar handlers: pending selection, commit on popover close + def _refresh_from_calendar(calendar): + try: + try: + y = calendar.get_year() + m = calendar.get_month() + d = calendar.get_day() + except Exception: + self._logger.debug("calendar refresh: get_year/month/day failed (since gtk 4.14)") + try: + gdatetime = calendar.get_date() + y = gdatetime.get_year() + m = gdatetime.get_month() + d = gdatetime.get_day_of_month() + except Exception: + self._logger.exception("calendar refresh: get_date() failed") + return + # calendar.get_date() returns month as 0-based + y, m, d = int(y), int(m) + 1, int(d) + try: + pending = datetime.date(y, m, d) + except Exception: + pending = None + self._logger.debug("calendar refresh: invalid date %04d-%02d-%02d", y, m, d) + try: + if pending is not None: + self._logger.debug("calendar select pending=%04d-%02d-%02d", pending.year, pending.month, pending.day) + self._set_date(pending) + except Exception: + self._logger.exception("Failed to set date from calendar") + except Exception: + self._logger.exception("Failed in _refresh_from_calendar") - # Order per locale - for part in self._order: - if part == 'Y': - row.append(self._spin_y) - elif part == 'M': - row.append(self._spin_m) - else: - row.append(self._spin_d) + try: + cal.connect("day-selected", _refresh_from_calendar) + except Exception: + self._logger.exception("Failed to connect day-selected") + try: + cal.connect("next-month", _refresh_from_calendar) + except Exception: + self._logger.exception("Failed to connect next-month") + try: + cal.connect("prev-month", _refresh_from_calendar) + except Exception: + self._logger.exception("Failed to connect prev-month") + try: + cal.connect("next-year", _refresh_from_calendar) + except Exception: + self._logger.exception("Failed to connect next-year") + try: + cal.connect("prev-year", _refresh_from_calendar) + except Exception: + self._logger.exception("Failed to connect prev-year") + + # Sync calendar to current value when popover opens + try: + def _on_button_clicked(b): + try: + self._logger.debug("popover open: sync %04d-%02d-%02d", self._date.year, self._date.month, self._date.day) + cal.set_year(self._date.year) + cal.set_month(self._date.month - 1) + cal.set_day(self._date.day) + except Exception: + self._logger.exception("Failed to sync calendar on popover open") + self._cal_button.connect('activate', _on_button_clicked) + except Exception: + self._logger.exception("Failed to connect button activate") + + # Put calendar inside a box with margins inside the popover + popbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + try: + popbox.set_margin_top(6) + popbox.set_margin_bottom(6) + popbox.set_margin_start(6) + popbox.set_margin_end(6) + except Exception: + pass + popbox.append(cal) + self._popover.set_child(popbox) + try: + self._cal_button.set_popover(self._popover) + except Exception: + pass + + # initial display update + self._calendar = cal + self._update_display() + + box.append(row) self._backend_widget = box try: @@ -177,8 +289,40 @@ def on_d_changed(spin): def _set_backend_enabled(self, enabled): try: - for w in (getattr(self, '_spin_y', None), getattr(self, '_spin_m', None), getattr(self, '_spin_d', None), getattr(self, '_label_widget', None)): + for w in (getattr(self, '_date_entry', None), getattr(self, '_cal_button', None), getattr(self, '_label_widget', None)): if w is not None: w.set_sensitive(bool(enabled)) except Exception: pass + + def _update_display(self): + try: + if getattr(self, '_date_entry', None) is not None: + try: + fmt = self._get_locale_date_fmt() + d = getattr(self, '_date', None) or datetime.date(1, 1, 1) + txt = d.strftime(fmt) + except Exception: + d = getattr(self, '_date', None) + if d is None: + txt = '' + else: + txt = d.isoformat() + self._date_entry.set_text(txt) + except Exception: + pass + + def _set_date(self, newdate: datetime.date): + ''' + copies newdate into internal value and updates gtk.entry display + + :param self: this instance + :param newdate: new date value + :type newdate: datetime.date + ''' + try: + self._date = newdate + ## update entry display + self._update_display() + except Exception: + pass From 3c884a3809e0a6d301af38927ae162195ad42197 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 19:18:57 +0100 Subject: [PATCH 362/523] Leaving selectionbox style menubutton --- manatools/aui/backends/gtk/datefieldgtk.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/manatools/aui/backends/gtk/datefieldgtk.py b/manatools/aui/backends/gtk/datefieldgtk.py index 2959721..d9cefe7 100644 --- a/manatools/aui/backends/gtk/datefieldgtk.py +++ b/manatools/aui/backends/gtk/datefieldgtk.py @@ -110,14 +110,6 @@ def _create_backend_widget(self): # MenuButton styled as dropdown self._cal_button = Gtk.MenuButton() - try: - img = Gtk.Image.new_from_icon_name("open-menu-symbolic") - self._cal_button.set_child(img) - except Exception: - try: - self._cal_button.set_label("▾") - except Exception: - pass row.append(self._cal_button) # Popover with Calendar From 00926137fd7842ec7582951b47e9746ab89ff61e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 19:24:56 +0100 Subject: [PATCH 363/523] removed unused code --- manatools/aui/backends/gtk/datefieldgtk.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/gtk/datefieldgtk.py b/manatools/aui/backends/gtk/datefieldgtk.py index d9cefe7..e48ac03 100644 --- a/manatools/aui/backends/gtk/datefieldgtk.py +++ b/manatools/aui/backends/gtk/datefieldgtk.py @@ -27,8 +27,6 @@ def __init__(self, parent=None, label: str = ""): # store current value as a date object (use only the date portion) self._date = datetime.date(2000, 1, 1) self._calendar = None - self._menu_btn = None - self._date_label = None self._popover = None self._locale_date_fmt = None @@ -120,12 +118,12 @@ def _create_backend_widget(self): gdate = GLib.DateTime.new_utc(self._date.year, self._date.month, self._date.day, 0, 0, 0) # Use set_date() for GTK 4.20+ try: - self._calendar.set_date(gdate) + cal.set_date(gdate) except Exception as e: # Fall back to select_day() if set_date() fails unexpectedly self._logger.debug("Failed to set_date() on calendar: %s", e) try: - self._calendar.select_day(gdate) + cal.select_day(gdate) except Exception: self._logger.exception("Failed to initialize calendar select_day()") From e72816b89fbe484609ca060c9495f4682e3b0a9c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 19:29:41 +0100 Subject: [PATCH 364/523] added the availability to insert date by hands --- .../aui/backends/curses/datefieldcurses.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/manatools/aui/backends/curses/datefieldcurses.py b/manatools/aui/backends/curses/datefieldcurses.py index d9d4d65..efd4517 100644 --- a/manatools/aui/backends/curses/datefieldcurses.py +++ b/manatools/aui/backends/curses/datefieldcurses.py @@ -62,10 +62,6 @@ def __init__(self, parent=None, label: str = ""): self._edit_buf = "" self._can_focus = True self._focused = False - try: - self._logger.debug("%s.__init__ label=%s", self.__class__.__name__, self._label) - except Exception: - pass def widgetClass(self): return "YDateField" @@ -94,6 +90,10 @@ def _days_in_month(self, y, m): def _create_backend_widget(self): self._backend_widget = self + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass def _set_backend_enabled(self, enabled): pass @@ -112,11 +112,19 @@ def _draw(self, window, y, x, width, height): parts = {'Y': f"{self._y:04d}", 'M': f"{self._m:02d}", 'D': f"{self._d:02d}"} disp = [] for idx, p in enumerate(self._order): - text = parts[p] + seg_text = parts[p] + # when focused on segment, show edit buffer if editing if self._focused and idx == self._seg_index: - text = f"[{text}]" + if self._editing: + buf = self._edit_buf or '' + seg_w = 4 if p == 'Y' else 2 + # right-align buffer into field width + buf_disp = buf.rjust(seg_w) + text = f"[{buf_disp}]" + else: + text = f"[{seg_text}]" else: - text = f" {text} " + text = f" {seg_text} " disp.append(text) if idx < 2: disp.append("-") From 9c34426cb7944a58d1ed00ece57930cb775cfa6f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 19:30:40 +0100 Subject: [PATCH 365/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index c000ee0..5818a21 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -35,7 +35,7 @@ Optional/special widgets (from `YOptionalWidgetFactory`): [X] YDumbTab [X] YSlider - [ ] YDateField + [X] YDateField [ ] YTimeField [ ] YBarGraph [ ] YPatternSelector (createPatternSelector) From b9e9be114a9ed50dd77c2d0bab985df5295844ff Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 19:39:58 +0100 Subject: [PATCH 366/523] added logging --- test/test_datefield.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/test/test_datefield.py b/test/test_datefield.py index 19cb449..355906e 100644 --- a/test/test_datefield.py +++ b/test/test_datefield.py @@ -2,10 +2,34 @@ import os import sys +import logging +import datetime + # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + def test_datefield(backend_name=None): """Interactive test for YDateField widget.""" @@ -36,6 +60,8 @@ def test_datefield(backend_name=None): # Create datefield df = factory.createDateField(vbox, "Select Date:") + now = datetime.datetime.now() + df.setValue(now.strftime("%Y-%m-%d")) # Buttons h = factory.createHBox(vbox) @@ -48,18 +74,17 @@ def test_datefield(backend_name=None): ev = dialog.waitForEvent() et = ev.eventType() if et == yui.YEventType.CancelEvent: - dialog.destroy() break elif et == yui.YEventType.WidgetEvent: wdg = ev.widget() reason = ev.reason() - if wdg == close_btn and reason == yui.YEventReason.Activated: - dialog.destroy() + if wdg == close_btn and reason == yui.YEventReason.Activated: break if wdg == ok_btn and reason == yui.YEventReason.Activated: - print("OK clicked. Final date:", df.value()) - dialog.destroy() + print("OK clicked. Final date:", df.value()) break + logging.info("Date: %s", df.value()) + dialog.destroy() except Exception as e: print(f"Error testing DateField with backend {backend_name}: {e}") import traceback From 570f1a51fda9c2714a672b65b1dc4fa35aa7616a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 21:17:12 +0100 Subject: [PATCH 367/523] First attempt to por YLogView --- manatools/aui/backends/curses/__init__.py | 2 + .../aui/backends/curses/logviewcurses.py | 125 +++++++++++++ manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/logviewgtk.py | 149 ++++++++++++++++ manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/logviewqt.py | 167 ++++++++++++++++++ manatools/aui/yui_curses.py | 9 + manatools/aui/yui_gtk.py | 9 + manatools/aui/yui_qt.py | 11 +- test/test_logview.py | 101 +++++++++++ 10 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/logviewcurses.py create mode 100644 manatools/aui/backends/gtk/logviewgtk.py create mode 100644 manatools/aui/backends/qt/logviewqt.py create mode 100644 test/test_logview.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 90f935b..e2e6ae1 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -24,6 +24,7 @@ from .imagecurses import YImageCurses from .dumbtabcurses import YDumbTabCurses from .slidercurses import YSliderCurses +from .logviewcurses import YLogViewCurses __all__ = [ "YDialogCurses", @@ -52,5 +53,6 @@ "YImageCurses", "YDumbTabCurses", "YSliderCurses", + "YLogViewCurses", # ... ] diff --git a/manatools/aui/backends/curses/logviewcurses.py b/manatools/aui/backends/curses/logviewcurses.py new file mode 100644 index 0000000..eb1a425 --- /dev/null +++ b/manatools/aui/backends/curses/logviewcurses.py @@ -0,0 +1,125 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import logging +from ...yui_common import * + + +class YLogViewCurses(YWidget): + """ncurses backend for YLogView: renders a scrollable output-only log area. + - Stores lines with optional retention limit (storedLines==0 means unlimited). + - Stretchable horizontally and vertically. + """ + def __init__(self, parent=None, label: str = "", visibleLines: int = 10, storedLines: int = 0): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + self._label = label or "" + self._visible = max(1, int(visibleLines or 10)) + self._max_lines = max(0, int(storedLines or 0)) + self._lines = [] + self._backend_widget = self + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + except Exception: + pass + + def widgetClass(self): + return "YLogView" + + def label(self) -> str: + return self._label + + def setLabel(self, label: str): + self._label = label or "" + + def visibleLines(self) -> int: + return int(self._visible) + + def setVisibleLines(self, newVisibleLines: int): + self._visible = max(1, int(newVisibleLines or 1)) + + def maxLines(self) -> int: + return int(self._max_lines) + + def setMaxLines(self, newMaxLines: int): + self._max_lines = max(0, int(newMaxLines or 0)) + self._trim_if_needed() + + def logText(self) -> str: + return "\n".join(self._lines) + + def setLogText(self, text: str): + try: + raw = [] if text is None else str(text).splitlines() + self._lines = raw + self._trim_if_needed() + except Exception: + self._logger.exception("setLogText failed") + + def lastLine(self) -> str: + return self._lines[-1] if self._lines else "" + + def appendLines(self, text: str): + try: + if text is None: + return + for ln in str(text).splitlines(): + self._lines.append(ln) + self._trim_if_needed() + except Exception: + self._logger.exception("appendLines failed") + + def clearText(self): + self._lines = [] + + def lines(self) -> int: + return len(self._lines) + + # internals + def _trim_if_needed(self): + try: + if self._max_lines > 0 and len(self._lines) > self._max_lines: + self._lines = self._lines[-self._max_lines:] + except Exception: + self._logger.exception("trim failed") + + # curses drawing + def _draw(self, window, y, x, width, height): + try: + line = y + # label + label_to_show = self._label if self._label else (self.debugLabel() if hasattr(self, 'debugLabel') else "") + if label_to_show: + try: + window.addstr(line, x, label_to_show[:max(0, width)]) + except curses.error: + pass + line += 1 + # remaining height for log + avail_h = max(0, height - (line - y)) + if avail_h <= 0: + return + # pick the last avail_h lines + to_show = self._lines[-avail_h:] + start_line = line + for i in range(avail_h): + s = to_show[i] if i < len(to_show) else "" + try: + window.addstr(start_line + i, x, s[:max(0, width)]) + except curses.error: + pass + except curses.error: + pass + + def _set_backend_enabled(self, enabled): + pass diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 5d4ed46..ea34381 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -24,6 +24,7 @@ from .imagegtk import YImageGtk from .dumbtabgtk import YDumbTabGtk from .slidergtk import YSliderGtk +from .logviewgtk import YLogViewGtk __all__ = [ "YDialogGtk", @@ -52,5 +53,6 @@ "YImageGtk", 'YDumbTabGtk', "YSliderGtk", + "YLogViewGtk", # ... ] diff --git a/manatools/aui/backends/gtk/logviewgtk.py b/manatools/aui/backends/gtk/logviewgtk.py new file mode 100644 index 0000000..088e710 --- /dev/null +++ b/manatools/aui/backends/gtk/logviewgtk.py @@ -0,0 +1,149 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all gtk backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +import logging +from gi.repository import Gtk +from ...yui_common import * + + +class YLogViewGtk(YWidget): + """GTK backend for YLogView using Gtk.TextView inside Gtk.ScrolledWindow with optional caption label.""" + def __init__(self, parent=None, label: str = "", visibleLines: int = 10, storedLines: int = 0): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._label = label or "" + self._visible = max(1, int(visibleLines or 10)) + self._max_lines = max(0, int(storedLines or 0)) + self._lines = [] + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + except Exception: + pass + + def widgetClass(self): + return "YLogView" + + def label(self): + return self._label + + def setLabel(self, label: str): + self._label = label or "" + try: + if getattr(self, "_label_widget", None) is not None: + self._label_widget.set_text(self._label) + except Exception: + self._logger.exception("setLabel failed") + + def visibleLines(self) -> int: + return int(self._visible) + + def setVisibleLines(self, v: int): + self._visible = max(1, int(v or 1)) + # Height tuning could be done via CSS or size requests; skip for now. + + def maxLines(self) -> int: + return int(self._max_lines) + + def setMaxLines(self, m: int): + self._max_lines = max(0, int(m or 0)) + self._trim_if_needed() + self._update_display() + + def logText(self) -> str: + return "\n".join(self._lines) + + def setLogText(self, text: str): + try: + raw = [] if text is None else str(text).splitlines() + self._lines = raw + self._trim_if_needed() + self._update_display() + except Exception: + self._logger.exception("setLogText failed") + + def lastLine(self) -> str: + return self._lines[-1] if self._lines else "" + + def appendLines(self, text: str): + try: + if text is None: + return + for ln in str(text).splitlines(): + self._lines.append(ln) + self._trim_if_needed() + self._update_display(scroll_end=True) + except Exception: + self._logger.exception("appendLines failed") + + def clearText(self): + self._lines = [] + self._update_display() + + def lines(self) -> int: + return len(self._lines) + + # internals + def _trim_if_needed(self): + try: + if self._max_lines > 0 and len(self._lines) > self._max_lines: + self._lines = self._lines[-self._max_lines:] + except Exception: + self._logger.exception("trim failed") + + def _update_display(self, scroll_end: bool = False): + try: + if getattr(self, "_buffer", None) is not None: + self._buffer.set_text("\n".join(self._lines)) + if scroll_end and getattr(self, "_view", None) is not None: + try: + iter_ = self._buffer.get_end_iter() + self._view.scroll_to_iter(iter_, 0.0, False, 0.0, 1.0) + except Exception: + pass + except Exception: + self._logger.exception("update_display failed") + + def _create_backend_widget(self): + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + if self._label: + lbl = Gtk.Label(label=self._label) + lbl.set_xalign(0.0) + self._label_widget = lbl + box.append(lbl) + sw = Gtk.ScrolledWindow() + tv = Gtk.TextView() + try: + tv.set_editable(False) + tv.set_wrap_mode(Gtk.WrapMode.NONE) + tv.set_monospace(True) + except Exception: + pass + buf = tv.get_buffer() + self._buffer = buf + self._view = tv + sw.set_child(tv) + box.append(sw) + self._backend_widget = box + self._update_display() + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_view", None) is not None: + self._view.set_sensitive(bool(enabled)) + if getattr(self, "_label_widget", None) is not None: + self._label_widget.set_sensitive(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index 0cecedf..de15dca 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -24,6 +24,7 @@ from .imageqt import YImageQt from .dumbtabqt import YDumbTabQt from .sliderqt import YSliderQt +from .logviewqt import YLogViewQt __all__ = [ @@ -53,5 +54,6 @@ "YImageQt", "YDumbTabQt", "YSliderQt", + "YLogViewQt", # ... ] diff --git a/manatools/aui/backends/qt/logviewqt.py b/manatools/aui/backends/qt/logviewqt.py new file mode 100644 index 0000000..bd2af44 --- /dev/null +++ b/manatools/aui/backends/qt/logviewqt.py @@ -0,0 +1,167 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore +import logging +from ...yui_common import * + + +class YLogViewQt(YWidget): + """Qt backend for YLogView using QPlainTextEdit in a container with an optional QLabel. + - Stores log lines with optional max retention (storedLines==0 means unlimited). + - Stretchable horizontally and vertically. + - Public API mirrors libyui's YLogView: label, visibleLines, maxLines, appendLines, clearText, etc. + """ + def __init__(self, parent=None, label: str = "", visibleLines: int = 10, storedLines: int = 0): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._label = label or "" + self._visible = max(1, int(visibleLines or 10)) + self._max_lines = max(0, int(storedLines or 0)) + self._lines = [] + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + except Exception: + pass + + def widgetClass(self): + return "YLogView" + + # API + def label(self) -> str: + return self._label + + def setLabel(self, label: str): + self._label = label or "" + try: + if getattr(self, "_label_widget", None) is not None: + self._label_widget.setText(self._label) + except Exception: + self._logger.exception("setLabel failed") + + def visibleLines(self) -> int: + return int(self._visible) + + def setVisibleLines(self, newVisibleLines: int): + self._visible = max(1, int(newVisibleLines or 1)) + try: + self._apply_preferred_height() + except Exception: + self._logger.exception("setVisibleLines apply height failed") + + def maxLines(self) -> int: + return int(self._max_lines) + + def setMaxLines(self, newMaxLines: int): + self._max_lines = max(0, int(newMaxLines or 0)) + self._trim_if_needed() + self._update_display() + + def logText(self) -> str: + return "\n".join(self._lines) + + def setLogText(self, text: str): + try: + raw = [] if text is None else str(text).splitlines() + self._lines = raw + self._trim_if_needed() + self._update_display() + except Exception: + self._logger.exception("setLogText failed") + + def lastLine(self) -> str: + return self._lines[-1] if self._lines else "" + + def appendLines(self, text: str): + try: + if text is None: + return + for ln in str(text).splitlines(): + self._lines.append(ln) + self._trim_if_needed() + self._update_display(scroll_end=True) + except Exception: + self._logger.exception("appendLines failed") + + def clearText(self): + self._lines = [] + self._update_display() + + def lines(self) -> int: + return len(self._lines) + + # Internals + def _trim_if_needed(self): + try: + if self._max_lines > 0 and len(self._lines) > self._max_lines: + self._lines = self._lines[-self._max_lines:] + except Exception: + self._logger.exception("trim failed") + + def _apply_preferred_height(self): + try: + if getattr(self, "_text", None) is not None: + fm = self._text.fontMetrics() + h = fm.lineSpacing() * (self._visible + 1) + self._text.setMinimumHeight(h) + except Exception: + pass + + def _update_display(self, scroll_end: bool = False): + try: + if getattr(self, "_text", None) is not None: + self._text.setPlainText("\n".join(self._lines)) + if scroll_end: + try: + self._text.moveCursor(self._text.textCursor().End) + except Exception: + pass + except Exception: + self._logger.exception("update_display failed") + + def _create_backend_widget(self): + container = QtWidgets.QWidget() + lay = QtWidgets.QVBoxLayout(container) + lay.setContentsMargins(0, 0, 0, 0) + if self._label: + lbl = QtWidgets.QLabel(self._label) + self._label_widget = lbl + lay.addWidget(lbl) + txt = QtWidgets.QPlainTextEdit() + txt.setReadOnly(True) + try: + txt.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap) + except Exception: + pass + sp = txt.sizePolicy() + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) + except Exception: + pass + txt.setSizePolicy(sp) + lay.addWidget(txt) + self._text = txt + self._apply_preferred_height() + self._update_display() + self._backend_widget = container + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setEnabled(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 71c819b..39db685 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -585,3 +585,12 @@ def createDateField(self, parent, label): """Create a DateField widget (curses backend).""" return YDateFieldCurses(parent, label) + def createLogView(self, parent, label, visibleLines, storedLines=0): + """Create a LogView widget (ncurses backend).""" + from .backends.curses import YLogViewCurses + try: + return YLogViewCurses(parent, label, visibleLines, storedLines) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YLogViewCurses: %s", e) + raise + diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index f8e74f3..d1985f6 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -782,6 +782,15 @@ def createDateField(self, parent, label): """Create a DateField widget (GTK backend).""" return YDateFieldGtk(parent, label) + def createLogView(self, parent, label, visibleLines, storedLines=0): + """Create a LogView widget (GTK backend).""" + from .backends.gtk import YLogViewGtk + try: + return YLogViewGtk(parent, label, visibleLines, storedLines) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YLogViewGtk: %s", e) + raise + def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 74dbf36..943b9e5 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -388,4 +388,13 @@ def createVSpacing(self, parent, size_px: int = 16): def createDateField(self, parent, label): """Create a DateField widget (Qt backend).""" - return YDateFieldQt(parent, label) \ No newline at end of file + return YDateFieldQt(parent, label) + + def createLogView(self, parent, label, visibleLines, storedLines=0): + """Create a LogView widget (Qt backend).""" + from .backends.qt import YLogViewQt + try: + return YLogViewQt(parent, label, visibleLines, storedLines) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YLogViewQt: %s", e) + raise \ No newline at end of file diff --git a/test/test_logview.py b/test/test_logview.py new file mode 100644 index 0000000..8393785 --- /dev/null +++ b/test/test_logview.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +import os +import sys +import logging + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + +def test_logview(backend_name=None): + """Interactive test for YLogView widget.""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox(dialog) + factory.createHeading(vbox, "LogView Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + + # Create LogView + lv = factory.createLogView(vbox, "Log:", 10, 50) + lv.appendLines("Initial line 1\nInitial line 2") + + # Buttons + h = factory.createHBox(vbox) + add_btn = factory.createPushButton(h, "Append 5 lines") + clear_btn = factory.createPushButton(h, "Clear") + close_btn = factory.createPushButton(h, "Close") + + print("\nOpening LogView test dialog...") + + counter = 0 + while True: + ev = dialog.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + break + elif et == yui.YEventType.WidgetEvent: + wdg = ev.widget() + reason = ev.reason() + if wdg == close_btn and reason == yui.YEventReason.Activated: + break + if wdg == clear_btn and reason == yui.YEventReason.Activated: + lv.clearText() + if wdg == add_btn and reason == yui.YEventReason.Activated: + lines = [] + for i in range(5): + counter += 1 + lines.append(f"line {counter}") + lv.appendLines("\n".join(lines)) + dialog.destroy() + except Exception as e: + print(f"Error testing LogView with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_logview(sys.argv[1]) + else: + test_logview() From ababef25d969c210c286dd0a1c63f208b88c0ef9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 21:26:51 +0100 Subject: [PATCH 368/523] Fixed strethable --- manatools/aui/backends/gtk/logviewgtk.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/manatools/aui/backends/gtk/logviewgtk.py b/manatools/aui/backends/gtk/logviewgtk.py index 088e710..5d0ab20 100644 --- a/manatools/aui/backends/gtk/logviewgtk.py +++ b/manatools/aui/backends/gtk/logviewgtk.py @@ -120,11 +120,24 @@ def _create_backend_widget(self): self._label_widget = lbl box.append(lbl) sw = Gtk.ScrolledWindow() + try: + sw.set_hexpand(True) + sw.set_vexpand(True) + except Exception: + pass tv = Gtk.TextView() try: tv.set_editable(False) tv.set_wrap_mode(Gtk.WrapMode.NONE) tv.set_monospace(True) + tv.set_hexpand(True) + tv.set_vexpand(True) + except Exception: + pass + # approximate min height from visible lines so it appears with some space + try: + line_px = 18 + sw.set_min_content_height(line_px * max(1, int(self._visible))) except Exception: pass buf = tv.get_buffer() From ad5f8433994a0636134c332e4028a87e532867f7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 21:33:30 +0100 Subject: [PATCH 369/523] Fixed stretching change --- manatools/aui/backends/gtk/logviewgtk.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/gtk/logviewgtk.py b/manatools/aui/backends/gtk/logviewgtk.py index 5d0ab20..4d34175 100644 --- a/manatools/aui/backends/gtk/logviewgtk.py +++ b/manatools/aui/backends/gtk/logviewgtk.py @@ -120,9 +120,12 @@ def _create_backend_widget(self): self._label_widget = lbl box.append(lbl) sw = Gtk.ScrolledWindow() + # Respect base stretchable properties try: - sw.set_hexpand(True) - sw.set_vexpand(True) + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + vert = bool(self.stretchable(YUIDimension.YD_VERT)) + sw.set_hexpand(horiz) + sw.set_vexpand(vert) except Exception: pass tv = Gtk.TextView() @@ -130,8 +133,14 @@ def _create_backend_widget(self): tv.set_editable(False) tv.set_wrap_mode(Gtk.WrapMode.NONE) tv.set_monospace(True) - tv.set_hexpand(True) - tv.set_vexpand(True) + # Respect base stretchable properties + try: + horiz = bool(self.stretchable(YUIDimension.YD_HORIZ)) + vert = bool(self.stretchable(YUIDimension.YD_VERT)) + tv.set_hexpand(horiz) + tv.set_vexpand(vert) + except Exception: + pass except Exception: pass # approximate min height from visible lines so it appears with some space From f0b0907e3a941bdd1b3fce54a34ed5521f967325 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 21:38:54 +0100 Subject: [PATCH 370/523] Added scrolling and focus --- .../aui/backends/curses/logviewcurses.py | 122 ++++++++++++++++-- 1 file changed, 113 insertions(+), 9 deletions(-) diff --git a/manatools/aui/backends/curses/logviewcurses.py b/manatools/aui/backends/curses/logviewcurses.py index eb1a425..39ad82b 100644 --- a/manatools/aui/backends/curses/logviewcurses.py +++ b/manatools/aui/backends/curses/logviewcurses.py @@ -27,6 +27,11 @@ def __init__(self, parent=None, label: str = "", visibleLines: int = 10, storedL self._max_lines = max(0, int(storedLines or 0)) self._lines = [] self._backend_widget = self + # focus + scrolling state + self._can_focus = True + self._focused = False + self._scroll_y = 0 # top line index + self._scroll_x = 0 # left column index try: self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) @@ -81,6 +86,8 @@ def appendLines(self, text: str): def clearText(self): self._lines = [] + self._scroll_y = 0 + self._scroll_x = 0 def lines(self) -> int: return len(self._lines) @@ -105,21 +112,118 @@ def _draw(self, window, y, x, width, height): except curses.error: pass line += 1 - # remaining height for log - avail_h = max(0, height - (line - y)) - if avail_h <= 0: + + # compute content area reserving space for scrollbars if needed + total_lines = len(self._lines) + max_len = 0 + try: + max_len = max((len(s) for s in self._lines)) if self._lines else 0 + except Exception: + max_len = 0 + + content_h = max(0, height - (line - y)) + need_hbar = max_len > width and content_h > 1 + need_vbar = total_lines > content_h + + # reserve one row/col for scrollbars if needed + if need_hbar: + content_h -= 1 + content_w = width - (1 if need_vbar else 0) + if content_h <= 0 or content_w <= 0: return - # pick the last avail_h lines - to_show = self._lines[-avail_h:] - start_line = line - for i in range(avail_h): - s = to_show[i] if i < len(to_show) else "" + + # clamp scroll offsets + max_scroll_y = max(0, total_lines - content_h) + if self._scroll_y > max_scroll_y: + self._scroll_y = max_scroll_y + if self._scroll_y < 0: + self._scroll_y = 0 + max_scroll_x = max(0, max_len - content_w) + if self._scroll_x > max_scroll_x: + self._scroll_x = max_scroll_x + if self._scroll_x < 0: + self._scroll_x = 0 + # remember viewport for key handling + self._last_height = content_h + self._last_width = content_w + + # draw visible lines with horizontal scroll + for i in range(content_h): + idx = self._scroll_y + i + s = self._lines[idx] if 0 <= idx < total_lines else "" try: - window.addstr(start_line + i, x, s[:max(0, width)]) + window.addstr(line + i, x, (s[self._scroll_x:self._scroll_x + content_w]).ljust(content_w)) except curses.error: pass + + # draw vertical scrollbar + if need_vbar: + bar_x = x + content_w + # track + for i in range(content_h): + try: + window.addstr(line + i, bar_x, "│") + except curses.error: + pass + # thumb size and position + thumb_h = max(1, int(content_h * content_h / max(1, total_lines))) + thumb_y = line + int(self._scroll_y * content_h / max(1, total_lines)) + for i in range(thumb_h): + if thumb_y + i < line + content_h: + try: + window.addstr(thumb_y + i, bar_x, "█") + except curses.error: + pass + + # draw horizontal scrollbar + if need_hbar: + bar_y = line + content_h + # track + try: + window.addstr(bar_y, x, "─" * content_w) + except curses.error: + pass + thumb_w = max(1, int(content_w * content_w / max(1, max_len))) + thumb_x = x + int(self._scroll_x * content_w / max(1, max_len)) + for i in range(thumb_w): + if thumb_x + i < x + content_w: + try: + window.addstr(bar_y, thumb_x + i, "█") + except curses.error: + pass except curses.error: pass + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + + handled = True + # Estimate viewport for scrolling steps; if no layout info, choose 5 + view_h = getattr(self, "_last_height", 5) or 5 + view_w = getattr(self, "_last_width", 10) or 10 + + if key in (curses.KEY_UP, ord('k')): + self._scroll_y = max(0, self._scroll_y - 1) + elif key in (curses.KEY_DOWN, ord('j')): + self._scroll_y = self._scroll_y + 1 + elif key == curses.KEY_PPAGE: + self._scroll_y = max(0, self._scroll_y - max(1, view_h - 1)) + elif key == curses.KEY_NPAGE: + self._scroll_y = self._scroll_y + max(1, view_h - 1) + elif key == curses.KEY_HOME: + self._scroll_y = 0 + elif key == curses.KEY_END: + # will be clamped in draw + self._scroll_y = 1 << 30 + elif key in (curses.KEY_LEFT, ord('h')): + self._scroll_x = max(0, self._scroll_x - 1) + elif key in (curses.KEY_RIGHT, ord('l')): + self._scroll_x = self._scroll_x + 1 + else: + handled = False + + return handled + def _set_backend_enabled(self, enabled): pass From 94b0746125314443e141b17a93ffdd578cebebe6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 21:39:22 +0100 Subject: [PATCH 371/523] Added a long line to see Horizontal scroll --- test/test_logview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_logview.py b/test/test_logview.py index 8393785..8c3a353 100644 --- a/test/test_logview.py +++ b/test/test_logview.py @@ -58,7 +58,7 @@ def test_logview(backend_name=None): # Create LogView lv = factory.createLogView(vbox, "Log:", 10, 50) - lv.appendLines("Initial line 1\nInitial line 2") + lv.appendLines("Initial line 1\nInitial line 2\nLong line 3 that should be wrapped if the width is small enough to trigger wrapping behavior in the LogView widget.") # Buttons h = factory.createHBox(vbox) From ea8080225786977894d873b5a3d8755b48119abb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 21:40:13 +0100 Subject: [PATCH 372/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 5818a21..8ba5cd8 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -29,7 +29,7 @@ Missing Widgets comparing libyui original factory: [X] YRadioButton [X] YImage [ ] YBusyIndicator - [ ] YLogView + [X] YLogView Optional/special widgets (from `YOptionalWidgetFactory`): From a14996089698fdb34a57487d0cc43a827b52c42d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 8 Jan 2026 21:43:26 +0100 Subject: [PATCH 373/523] better line explanation. --- test/test_logview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_logview.py b/test/test_logview.py index 8c3a353..8930f4e 100644 --- a/test/test_logview.py +++ b/test/test_logview.py @@ -58,7 +58,7 @@ def test_logview(backend_name=None): # Create LogView lv = factory.createLogView(vbox, "Log:", 10, 50) - lv.appendLines("Initial line 1\nInitial line 2\nLong line 3 that should be wrapped if the width is small enough to trigger wrapping behavior in the LogView widget.") + lv.appendLines("Initial line 1\nInitial line 2\nLong line 3 that should be horizontally scrolled if the width is small enough that cannot be fully displayed without scrolling.") # Buttons h = factory.createHBox(vbox) From 658293129782206715a64257291f62a9e6a0b1f8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 10:12:27 +0100 Subject: [PATCH 374/523] TimeField for Qt --- manatools/aui/backends/qt/__init__.py | 2 + manatools/aui/backends/qt/timefieldqt.py | 120 +++++++++++++++++++++++ manatools/aui/yui_qt.py | 9 ++ 3 files changed, 131 insertions(+) create mode 100644 manatools/aui/backends/qt/timefieldqt.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index de15dca..b00540c 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -25,6 +25,7 @@ from .dumbtabqt import YDumbTabQt from .sliderqt import YSliderQt from .logviewqt import YLogViewQt +from .timefieldqt import YTimeFieldQt __all__ = [ @@ -55,5 +56,6 @@ "YDumbTabQt", "YSliderQt", "YLogViewQt", + "YTimeFieldQt", # ... ] diff --git a/manatools/aui/backends/qt/timefieldqt.py b/manatools/aui/backends/qt/timefieldqt.py new file mode 100644 index 0000000..52c7923 --- /dev/null +++ b/manatools/aui/backends/qt/timefieldqt.py @@ -0,0 +1,120 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' +from PySide6 import QtWidgets, QtCore +import logging +import datetime +from ...yui_common import * + + +class YTimeFieldQt(YWidget): + """Qt backend YTimeField implementation using QTimeEdit. + value()/setValue() use HH:MM:SS. No change events posted. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._label = label or "" + self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._time = datetime.time(0, 0, 0) + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, False) + except Exception: + pass + + def widgetClass(self): + return "YTimeField" + + def value(self) -> str: + try: + t = getattr(self, '_time', None) + if t is None: + return '' + return f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}" + except Exception: + return "" + + def setValue(self, timestr: str): + try: + parts = str(timestr).split(':') + if len(parts) != 3: + return + h, m, s = [int(p) for p in parts] + h = max(0, min(23, h)) + m = max(0, min(59, m)) + s = max(0, min(59, s)) + self._time = datetime.time(h, m, s) + if getattr(self, '_edit', None) is not None: + try: + self._edit.setTime(QtCore.QTime(h, m, s)) + except Exception: + pass + except Exception as e: + self._logger.exception("setValue failed: %s", e) + + def _create_backend_widget(self): + cont = QtWidgets.QWidget() + lay = QtWidgets.QHBoxLayout(cont) + lay.setContentsMargins(0, 0, 0, 0) + if self._label: + lbl = QtWidgets.QLabel(self._label) + lay.addWidget(lbl) + self._label_widget = lbl + edit = QtWidgets.QTimeEdit() + try: + edit.setDisplayFormat("HH:mm:ss") + edit.setTime(QtCore.QTime(self._time.hour, self._time.minute, self._time.second)) + except Exception: + pass + def _on_time_changed(qt: QtCore.QTime): + try: + self._time = datetime.time(qt.hour(), qt.minute(), qt.second()) + except Exception: + pass + try: + edit.timeChanged.connect(_on_time_changed) + except Exception: + pass + # Respect stretchable flags via size policy + try: + sp = edit.sizePolicy() + try: + horiz = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Preferred + vert = QtWidgets.QSizePolicy.Policy.Fixed if not self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Expanding + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + except Exception: + try: + horiz = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Preferred + vert = QtWidgets.QSizePolicy.Fixed if not self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Expanding + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + except Exception: + pass + edit.setSizePolicy(sp) + except Exception: + pass + lay.addWidget(edit) + self._edit = edit + self._backend_widget = cont + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + try: + if getattr(self, '_edit', None) is not None: + self._edit.setEnabled(bool(enabled)) + if getattr(self, '_label_widget', None) is not None: + self._label_widget.setEnabled(bool(enabled)) + except Exception: + pass diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 943b9e5..8070273 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -397,4 +397,13 @@ def createLogView(self, parent, label, visibleLines, storedLines=0): return YLogViewQt(parent, label, visibleLines, storedLines) except Exception as e: logging.getLogger(__name__).exception("Failed to create YLogViewQt: %s", e) + raise + + def createTimeField(self, parent, label): + """Create a TimeField widget (Qt backend).""" + from .backends.qt import YTimeFieldQt + try: + return YTimeFieldQt(parent, label) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YTimeFieldQt: %s", e) raise \ No newline at end of file From b206b8c44dc6ea1e3da221b4b07539f6ee75e4be Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 10:25:35 +0100 Subject: [PATCH 375/523] added also timefield test and renamed --- test/test_datetime_fields.py | 110 +++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 test/test_datetime_fields.py diff --git a/test/test_datetime_fields.py b/test/test_datetime_fields.py new file mode 100644 index 0000000..2759b9d --- /dev/null +++ b/test/test_datetime_fields.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +import os +import sys +import logging +import datetime + + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + +def test_datefield(backend_name=None): + """Interactive test for YDateField and YTimeField widgets.""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + + vbox = factory.createVBox(dialog) + factory.createHeading(vbox, "Date/TimeField Test") + factory.createLabel(vbox, f"Backend: {backend.value}") + + # Create datefield + df = factory.createDateField(vbox, "Select Date:") + now = datetime.datetime.now() + df.setValue(now.strftime("%Y-%m-%d")) + + # Create timefield + try: + tf = factory.createTimeField(vbox, "Select Time:") + tf.setValue(now.strftime("%H:%M:%S")) + except Exception as e: + tf = None + logging.getLogger(__name__).exception("Failed to create TimeField: %s", e) + + # Buttons + h = factory.createHBox(vbox) + ok_btn = factory.createPushButton(h, "OK") + close_btn = factory.createPushButton(h, "Close") + + print("\nOpening Date/TimeField test dialog...") + + while True: + ev = dialog.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + break + elif et == yui.YEventType.WidgetEvent: + wdg = ev.widget() + reason = ev.reason() + if wdg == close_btn and reason == yui.YEventReason.Activated: + break + if wdg == ok_btn and reason == yui.YEventReason.Activated: + print("OK clicked. Final date:", df.value()) + if tf is not None: + print("Final time:", tf.value()) + break + logging.info("Date: %s", df.value()) + if tf is not None: + logging.info("Time: %s", tf.value()) + dialog.destroy() + except Exception as e: + print(f"Error testing DateField with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_datefield(sys.argv[1]) + else: + test_datefield() From 244dc74e9c9733b33c6dc4047a0bc4103699d6ad Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 10:33:07 +0100 Subject: [PATCH 376/523] Added header --- manatools/aui/backends/qt/sliderqt.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manatools/aui/backends/qt/sliderqt.py b/manatools/aui/backends/qt/sliderqt.py index cab1bd0..f7be6aa 100644 --- a/manatools/aui/backends/qt/sliderqt.py +++ b/manatools/aui/backends/qt/sliderqt.py @@ -1,5 +1,14 @@ # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' """ Qt backend Slider widget. - Horizontal slider synchronized with a spin box. From 26fd0400da2ed109b38c5370a9258397634f995f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 10:55:32 +0100 Subject: [PATCH 377/523] Do not stretch by default --- manatools/aui/backends/qt/datefieldqt.py | 35 ++++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/manatools/aui/backends/qt/datefieldqt.py b/manatools/aui/backends/qt/datefieldqt.py index f2f6507..dbeca41 100644 --- a/manatools/aui/backends/qt/datefieldqt.py +++ b/manatools/aui/backends/qt/datefieldqt.py @@ -23,6 +23,8 @@ def __init__(self, parent=None, label: str = ""): self._label = label or "" self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") self._date = QtCore.QDate.currentDate() + self.setStretchable(YUIDimension.YD_HORIZ, False) + self.setStretchable(YUIDimension.YD_VERT, False) def widgetClass(self): return "YDateField" @@ -107,18 +109,33 @@ def _create_backend_widget(self): except Exception: pass - # Apply size policy from stretchable hints + # Apply size policy based on stretchable hints to both the date edit and its container try: - sp = container.sizePolicy() + # derive policies from stretchable flags try: - horiz = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Preferred - vert = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed + horiz_policy = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Fixed + vert_policy = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed except Exception: - horiz = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Preferred - vert = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed - sp.setHorizontalPolicy(horiz) - sp.setVerticalPolicy(vert) - container.setSizePolicy(sp) + horiz_policy = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Fixed + vert_policy = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed + + # apply to date edit + try: + sp_de = de.sizePolicy() + sp_de.setHorizontalPolicy(horiz_policy) + sp_de.setVerticalPolicy(vert_policy) + de.setSizePolicy(sp_de) + except Exception: + pass + + # apply to container as well so layout won't force expansion contrary to stretchable() + try: + sp_cont = container.sizePolicy() + sp_cont.setHorizontalPolicy(horiz_policy) + sp_cont.setVerticalPolicy(vert_policy) + container.setSizePolicy(sp_cont) + except Exception: + pass except Exception: pass From aeb5ae45cbaf90706de024ef5c2fec953f6c754b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 11:03:59 +0100 Subject: [PATCH 378/523] fixed stretching --- manatools/aui/backends/qt/timefieldqt.py | 48 +++++++++++++----------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/manatools/aui/backends/qt/timefieldqt.py b/manatools/aui/backends/qt/timefieldqt.py index 52c7923..e0049d3 100644 --- a/manatools/aui/backends/qt/timefieldqt.py +++ b/manatools/aui/backends/qt/timefieldqt.py @@ -24,11 +24,8 @@ def __init__(self, parent=None, label: str = ""): self._label = label or "" self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") self._time = datetime.time(0, 0, 0) - try: - self.setStretchable(YUIDimension.YD_HORIZ, True) - self.setStretchable(YUIDimension.YD_VERT, False) - except Exception: - pass + self.setStretchable(YUIDimension.YD_HORIZ, False) + self.setStretchable(YUIDimension.YD_VERT, False) def widgetClass(self): return "YTimeField" @@ -62,8 +59,9 @@ def setValue(self, timestr: str): def _create_backend_widget(self): cont = QtWidgets.QWidget() - lay = QtWidgets.QHBoxLayout(cont) + lay = QtWidgets.QVBoxLayout(cont) lay.setContentsMargins(0, 0, 0, 0) + lay.setSpacing(2) if self._label: lbl = QtWidgets.QLabel(self._label) lay.addWidget(lbl) @@ -73,7 +71,8 @@ def _create_backend_widget(self): edit.setDisplayFormat("HH:mm:ss") edit.setTime(QtCore.QTime(self._time.hour, self._time.minute, self._time.second)) except Exception: - pass + self._logger.exception("_create_backend_widget: couldn't set time edit format or time") + def _on_time_changed(qt: QtCore.QTime): try: self._time = datetime.time(qt.hour(), qt.minute(), qt.second()) @@ -83,23 +82,30 @@ def _on_time_changed(qt: QtCore.QTime): edit.timeChanged.connect(_on_time_changed) except Exception: pass - # Respect stretchable flags via size policy + # Apply size policy based on stretchable hints to both the time edit and its container try: - sp = edit.sizePolicy() try: - horiz = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Preferred - vert = QtWidgets.QSizePolicy.Policy.Fixed if not self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Expanding - sp.setHorizontalPolicy(horiz) - sp.setVerticalPolicy(vert) + horiz_policy = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Fixed + vert_policy = QtWidgets.QSizePolicy.Policy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed except Exception: - try: - horiz = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Preferred - vert = QtWidgets.QSizePolicy.Fixed if not self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Expanding - sp.setHorizontalPolicy(horiz) - sp.setVerticalPolicy(vert) - except Exception: - pass - edit.setSizePolicy(sp) + horiz_policy = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Fixed + vert_policy = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed + + try: + sp_edit = edit.sizePolicy() + sp_edit.setHorizontalPolicy(horiz_policy) + sp_edit.setVerticalPolicy(vert_policy) + edit.setSizePolicy(sp_edit) + except Exception: + pass + + try: + sp_cont = cont.sizePolicy() + sp_cont.setHorizontalPolicy(horiz_policy) + sp_cont.setVerticalPolicy(vert_policy) + cont.setSizePolicy(sp_cont) + except Exception: + pass except Exception: pass lay.addWidget(edit) From b9e487599747446dfa39d67646feb73d6ebb5e18 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 11:05:09 +0100 Subject: [PATCH 379/523] Added TimeField for gtk --- manatools/aui/backends/gtk/__init__.py | 2 ++ manatools/aui/yui_gtk.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index ea34381..4f4164a 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -25,6 +25,7 @@ from .dumbtabgtk import YDumbTabGtk from .slidergtk import YSliderGtk from .logviewgtk import YLogViewGtk +from .timefieldgtk import YTimeFieldGtk __all__ = [ "YDialogGtk", @@ -54,5 +55,6 @@ 'YDumbTabGtk', "YSliderGtk", "YLogViewGtk", + "YTimeFieldGtk", # ... ] diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index d1985f6..068f48d 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -791,6 +791,15 @@ def createLogView(self, parent, label, visibleLines, storedLines=0): logging.getLogger(__name__).exception("Failed to create YLogViewGtk: %s", e) raise + def createTimeField(self, parent, label): + """Create a TimeField widget (GTK backend).""" + from .backends.gtk import YTimeFieldGtk + try: + return YTimeFieldGtk(parent, label) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YTimeFieldGtk: %s", e) + raise + def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) From a3f2b2073497c8d2ff9638f5ce729704e9fd945d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 13:32:18 +0100 Subject: [PATCH 380/523] rinominato --- test/test_datefield.py | 98 ------------------------------------------ 1 file changed, 98 deletions(-) delete mode 100644 test/test_datefield.py diff --git a/test/test_datefield.py b/test/test_datefield.py deleted file mode 100644 index 355906e..0000000 --- a/test/test_datefield.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys -import logging -import datetime - - -# Add parent directory to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -try: - log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' - fh = logging.FileHandler(log_name, mode='w') - fh.setLevel(logging.DEBUG) - fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - existing = False - for h in list(root_logger.handlers): - try: - if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): - existing = True - break - except Exception: - pass - if not existing: - root_logger.addHandler(fh) - print(f"Logging test output to: {os.path.abspath(log_name)}") -except Exception as _e: - print(f"Failed to configure file logger: {_e}") - - -def test_datefield(backend_name=None): - """Interactive test for YDateField widget.""" - if backend_name: - print(f"Setting backend to: {backend_name}") - os.environ['YUI_BACKEND'] = backend_name - else: - print("Using auto-detection") - - try: - from manatools.aui.yui import YUI, YUI_ui - import manatools.aui.yui_common as yui - - # Force re-detection - YUI._instance = None - YUI._backend = None - - backend = YUI.backend() - print(f"Using backend: {backend.value}") - - ui = YUI_ui() - factory = ui.widgetFactory() - dialog = factory.createMainDialog() - - vbox = factory.createVBox(dialog) - factory.createHeading(vbox, "DateField Test") - factory.createLabel(vbox, f"Backend: {backend.value}") - - # Create datefield - df = factory.createDateField(vbox, "Select Date:") - now = datetime.datetime.now() - df.setValue(now.strftime("%Y-%m-%d")) - - # Buttons - h = factory.createHBox(vbox) - ok_btn = factory.createPushButton(h, "OK") - close_btn = factory.createPushButton(h, "Close") - - print("\nOpening DateField test dialog...") - - while True: - ev = dialog.waitForEvent() - et = ev.eventType() - if et == yui.YEventType.CancelEvent: - break - elif et == yui.YEventType.WidgetEvent: - wdg = ev.widget() - reason = ev.reason() - if wdg == close_btn and reason == yui.YEventReason.Activated: - break - if wdg == ok_btn and reason == yui.YEventReason.Activated: - print("OK clicked. Final date:", df.value()) - break - logging.info("Date: %s", df.value()) - dialog.destroy() - except Exception as e: - print(f"Error testing DateField with backend {backend_name}: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - if len(sys.argv) > 1: - test_datefield(sys.argv[1]) - else: - test_datefield() From ccc26e68af5376fb356f5935f00b253b99f92d44 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 13:32:41 +0100 Subject: [PATCH 381/523] Fixed start value and setValue --- manatools/aui/backends/gtk/timefieldgtk.py | 217 +++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 manatools/aui/backends/gtk/timefieldgtk.py diff --git a/manatools/aui/backends/gtk/timefieldgtk.py b/manatools/aui/backends/gtk/timefieldgtk.py new file mode 100644 index 0000000..6c94546 --- /dev/null +++ b/manatools/aui/backends/gtk/timefieldgtk.py @@ -0,0 +1,217 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all gtk backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' +import logging +import datetime +from gi.repository import Gtk +from ...yui_common import * + + +class YTimeFieldGtk(YWidget): + """GTK backend YTimeField implemented as an Entry + MenuButton Popover with three SpinButtons (H/M/S). + value()/setValue() use HH:MM:SS. No change events posted. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._label = label or "" + self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + self._time = datetime.time(0, 0, 0) + self._popover = None + self._pending = None # pending datetime.time (not used when updating live) + self._spin_h = None + self._spin_m = None + self._spin_s = None + self.setStretchable(YUIDimension.YD_HORIZ, False) + self.setStretchable(YUIDimension.YD_VERT, False) + + def widgetClass(self): + return "YTimeField" + + def value(self) -> str: + try: + t = getattr(self, '_time', None) + if t is None: + return '' + return f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}" + except Exception: + return "" + + def setValue(self, timestr: str): + try: + parts = str(timestr).split(':') + if len(parts) != 3: + return + h, m, s = [int(p) for p in parts] + h = max(0, min(23, h)) + m = max(0, min(59, m)) + s = max(0, min(59, s)) + self._time = datetime.time(h, m, s) + self._update_display() + # Keep spin buttons in sync with the current time + try: + self._sync_spins_from_time() + except Exception: + self._logger.exception("setValue: failed to sync spins from time") + except Exception as e: + self._logger.exception("setValue failed: %s", e) + + def _update_display(self): + try: + if getattr(self, '_entry', None) is not None: + t = getattr(self, '_time', None) + txt = '' if t is None else f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}" + self._entry.set_text(txt) + except Exception: + self._logger.exception("_update_display failed") + pass + + def _sync_spins_from_time(self): + try: + if self._spin_h is not None: + self._spin_h.set_value(self._time.hour) + if self._spin_m is not None: + self._spin_m.set_value(self._time.minute) + if self._spin_s is not None: + self._spin_s.set_value(self._time.second) + except Exception: + self._logger.exception("_sync_spins_from_time failed") + + def _create_backend_widget(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + if self._label: + lbl = Gtk.Label(label=self._label) + lbl.set_xalign(0.0) + outer.append(lbl) + self._label_widget = lbl + + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + + entry = Gtk.Entry() + # Default: do not expand horizontally unless the widget was created stretchable + entry.set_hexpand(bool(self.stretchable(YUIDimension.YD_HORIZ))) + row.append(entry) + self._entry = entry + + btn = Gtk.MenuButton() + row.append(btn) + + outer.append(row) + + pop = Gtk.Popover() + grid = Gtk.Grid(column_spacing=6, row_spacing=6) + + adj_h = Gtk.Adjustment(lower=0, upper=23, step_increment=1, page_increment=5) + spin_h = Gtk.SpinButton(adjustment=adj_h) + adj_m = Gtk.Adjustment(lower=0, upper=59, step_increment=1, page_increment=5) + spin_m = Gtk.SpinButton(adjustment=adj_m) + adj_s = Gtk.Adjustment(lower=0, upper=59, step_increment=1, page_increment=5) + spin_s = Gtk.SpinButton(adjustment=adj_s) + self._spin_h, self._spin_m, self._spin_s = spin_h, spin_m, spin_s + # Initialize spin buttons to the current time so popover opens consistent with entry + try: + self._sync_spins_from_time() + except Exception: + self._logger.exception("init: failed to sync spins from time") + + grid.attach(Gtk.Label(label="H:"), 0, 0, 1, 1) + grid.attach(spin_h, 1, 0, 1, 1) + grid.attach(Gtk.Label(label="M:"), 0, 1, 1, 1) + grid.attach(spin_m, 1, 1, 1, 1) + grid.attach(Gtk.Label(label="S:"), 0, 2, 1, 1) + grid.attach(spin_s, 1, 2, 1, 1) + + pop.set_child(grid) + btn.set_popover(pop) + + def _sync_spins(): + try: + self._sync_spins_from_time() + except Exception: + self._logger.exception("_sync_spins failed") + + def _on_spin_changed(*_): + try: + h = int(spin_h.get_value()) + m = int(spin_m.get_value()) + s = int(spin_s.get_value()) + self._time = datetime.time(max(0, min(23, h)), max(0, min(59, m)), max(0, min(59, s))) + self._logger.debug("spin commit: %02d:%02d:%02d", self._time.hour, self._time.minute, self._time.second) + self._update_display() + except Exception: + self._logger.exception("spin changed handler failed") + + for sp in (spin_h, spin_m, spin_s): + try: + sp.connect('value-changed', _on_spin_changed) + except Exception: + self._logger.exception("couldn't connect spin value-changed") + + def _on_btn_activate(*_): + try: + self._pending = None + _sync_spins() + except Exception: + self._logger.exception("menu button activate handler failed") + try: + # Gtk.MenuButton uses 'activate' in GTK4 + btn.connect('activate', _on_btn_activate) + except Exception: + self._logger.exception("couldn't connect button activate") + + # No need to commit on close as we update live on spin changes + + def _on_entry_activate(e): + try: + parts = str(e.get_text()).strip().split(':') + if len(parts) == 3: + h, m, s = [int(p) for p in parts] + h = max(0, min(23, h)) + m = max(0, min(59, m)) + s = max(0, min(59, s)) + self._time = datetime.time(h, m, s) + self._logger.debug("entry commit: %02d:%02d:%02d", h, m, s) + self._update_display() + # Sync spins if popover is open or later when it opens + self._sync_spins_from_time() + except Exception as ex: + self._logger.exception("entry activate parse failed: %s", ex) + try: + entry.connect('activate', _on_entry_activate) + except Exception: + self._logger.exception("couldn't connect entry activate") + try: + # Commit on focus loss in GTK4 via notify::has-focus + def _on_entry_focus_notify(e, pspec): + try: + if not e.has_focus(): + _on_entry_activate(e) + except Exception: + self._logger.exception("entry focus notify failed") + entry.connect('notify::has-focus', _on_entry_focus_notify) + except Exception: + self._logger.exception("couldn't connect entry focus notify") + + # Ensure spins are in sync at startup + try: + self._sync_spins_from_time() + except Exception: + self._logger.exception("startup: failed to sync spins from time") + self._update_display() + self._backend_widget = outer + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + + def _set_backend_enabled(self, enabled): + try: + for w in (getattr(self, '_entry', None), getattr(self, '_label_widget', None)): + if w is not None: + w.set_sensitive(bool(enabled)) + except Exception: + pass From b622d0f6164f69e9d319231ae2e93af3f4d7c9fe Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 13:43:28 +0100 Subject: [PATCH 382/523] TimeField on curses --- manatools/aui/backends/curses/__init__.py | 2 + .../aui/backends/curses/timefieldcurses.py | 175 ++++++++++++++++++ manatools/aui/yui_curses.py | 9 + 3 files changed, 186 insertions(+) create mode 100644 manatools/aui/backends/curses/timefieldcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index e2e6ae1..57be6d0 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -25,6 +25,7 @@ from .dumbtabcurses import YDumbTabCurses from .slidercurses import YSliderCurses from .logviewcurses import YLogViewCurses +from .timefieldcurses import YTimeFieldCurses __all__ = [ "YDialogCurses", @@ -54,5 +55,6 @@ "YDumbTabCurses", "YSliderCurses", "YLogViewCurses", + "YTimeFieldCurses", # ... ] diff --git a/manatools/aui/backends/curses/timefieldcurses.py b/manatools/aui/backends/curses/timefieldcurses.py new file mode 100644 index 0000000..1cb2e41 --- /dev/null +++ b/manatools/aui/backends/curses/timefieldcurses.py @@ -0,0 +1,175 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains all curses backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses +''' +import curses +import curses.ascii +import logging +from ...yui_common import * + + +class YTimeFieldCurses(YWidget): + """NCurses backend YTimeField with three integer segments (H, M, S). + value()/setValue() use HH:MM:SS. No change events posted. + Navigation: Left/Right to change segment, Up/Down or +/- to change value, digits to type. + """ + def __init__(self, parent=None, label: str = ""): + super().__init__(parent) + self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + self._label = label or "" + self._h, self._m, self._s = 0, 0, 0 + self._seg_index = 0 # 0..2 + self._editing = False + self._edit_buf = "" + self._can_focus = True + self._focused = False + # Default: do not stretch horizontally or vertically; respects external overrides + try: + self.setStretchable(YUIDimension.YD_HORIZ, False) + self.setStretchable(YUIDimension.YD_VERT, False) + except Exception: + pass + + def widgetClass(self): + return "YTimeField" + + def value(self) -> str: + return f"{self._h:02d}:{self._m:02d}:{self._s:02d}" + + def setValue(self, timestr: str): + try: + h, m, s = [int(p) for p in str(timestr).split(':')] + except Exception: + return + self._h = max(0, min(23, h)) + self._m = max(0, min(59, m)) + self._s = max(0, min(59, s)) + + def _create_backend_widget(self): + self._backend_widget = self + try: + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + except Exception: + pass + + def _set_backend_enabled(self, enabled): + pass + + def _draw(self, window, y, x, width, height): + try: + line = y + label_to_show = self._label if self._label else (self.debugLabel() if hasattr(self, 'debugLabel') else "unknown") + try: + window.addstr(line, x, label_to_show[:width]) + except curses.error: + pass + line += 1 + + parts = {'H': f"{self._h:02d}", 'M': f"{self._m:02d}", 'S': f"{self._s:02d}"} + disp = [] + order = ['H', 'M', 'S'] + for idx, p in enumerate(order): + seg_text = parts[p] + if self._focused and idx == self._seg_index: + if self._editing: + buf = self._edit_buf or '' + seg_w = 2 + buf_disp = buf.rjust(seg_w) + text = f"[{buf_disp}]" + else: + text = f"[{seg_text}]" + else: + text = f" {seg_text} " + disp.append(text) + if idx < 2: + disp.append(":") + out = ''.join(disp) + try: + window.addstr(line, x, out[:max(0, width)]) + except curses.error: + pass + except curses.error: + pass + + def _handle_key(self, key): + if not self._focused or not self.isEnabled(): + return False + + if self._editing: + if key in (curses.KEY_BACKSPACE, 127, 8): + if self._edit_buf: + self._edit_buf = self._edit_buf[:-1] + return True + if curses.ascii.isdigit(key): + self._edit_buf += chr(key) + return True + if key in (ord('\n'), ord(' ')): + self._commit_edit() + return True + if key == 27: # ESC + self._cancel_edit() + return True + if key in (curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_UP, curses.KEY_DOWN, ord('+'), ord('-')): + self._commit_edit() + # Non-editing navigation + if key in (curses.KEY_LEFT,): + self._seg_index = (self._seg_index - 1) % 3 + return True + if key in (curses.KEY_RIGHT,): + self._seg_index = (self._seg_index + 1) % 3 + return True + if key in (curses.KEY_UP, ord('+')): + self._bump(+1) + return True + if key in (curses.KEY_DOWN, ord('-')): + self._bump(-1) + return True + if curses.ascii.isdigit(key): + self._begin_edit(chr(key)) + return True + return False + + def _seg_ref(self, idx): + seg = ['H', 'M', 'S'][idx] + if seg == 'H': + return '_h', 0, 23 + if seg == 'M': + return '_m', 0, 59 + return '_s', 0, 59 + + def _bump(self, delta): + name, lo, hi = self._seg_ref(self._seg_index) + val = getattr(self, name) + val += delta + if val < lo: val = lo + if val > hi: val = hi + setattr(self, name, val) + + def _begin_edit(self, initial_char=None): + self._editing = True + self._edit_buf = '' if initial_char is None else str(initial_char) + + def _cancel_edit(self): + self._editing = False + self._edit_buf = '' + + def _commit_edit(self): + if self._edit_buf in ('', ':'): + self._cancel_edit() + return + try: + v = int(self._edit_buf) + except ValueError: + self._cancel_edit() + return + name, lo, hi = self._seg_ref(self._seg_index) + v = max(lo, min(hi, v)) + setattr(self, name, v) + self._cancel_edit() diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 39db685..403e955 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -594,3 +594,12 @@ def createLogView(self, parent, label, visibleLines, storedLines=0): logging.getLogger(__name__).exception("Failed to create YLogViewCurses: %s", e) raise + def createTimeField(self, parent, label): + """Create a TimeField widget (ncurses backend).""" + from .backends.curses import YTimeFieldCurses + try: + return YTimeFieldCurses(parent, label) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YTimeFieldCurses: %s", e) + raise + From a7a6e1f9165f2cbe51cc8d166321e411eea34f33 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 13:43:46 +0100 Subject: [PATCH 383/523] Not stretchable by default --- manatools/aui/backends/curses/labelcurses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index da52a93..3ec723b 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -37,6 +37,8 @@ def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): self._height = 1 self._focused = False self._can_focus = False # Labels don't get focus + self.setStretchable(YUIDimension.YD_HORIZ, False) + self.setStretchable(YUIDimension.YD_VERT, False) # per-instance logger self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") if not self._logger.handlers and not logging.getLogger().handlers: From 7f359c6066c21f903679dbf9479d1a3a69a782c0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 13:44:20 +0100 Subject: [PATCH 384/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 8ba5cd8..19449c8 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -36,7 +36,7 @@ Optional/special widgets (from `YOptionalWidgetFactory`): [X] YDumbTab [X] YSlider [X] YDateField - [ ] YTimeField + [X] YTimeField [ ] YBarGraph [ ] YPatternSelector (createPatternSelector) [ ] YSimplePatchSelector (createSimplePatchSelector) From 9a87e47bb3f64883f2a331c31f5e034e0e505c6b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 15:07:44 +0100 Subject: [PATCH 385/523] Not stretchable --- manatools/aui/backends/curses/datefieldcurses.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manatools/aui/backends/curses/datefieldcurses.py b/manatools/aui/backends/curses/datefieldcurses.py index efd4517..bc83359 100644 --- a/manatools/aui/backends/curses/datefieldcurses.py +++ b/manatools/aui/backends/curses/datefieldcurses.py @@ -62,6 +62,12 @@ def __init__(self, parent=None, label: str = ""): self._edit_buf = "" self._can_focus = True self._focused = False + # Default: do not stretch horizontally or vertically; respects external overrides + try: + self.setStretchable(YUIDimension.YD_HORIZ, False) + self.setStretchable(YUIDimension.YD_VERT, False) + except Exception: + pass def widgetClass(self): return "YDateField" From 7dec535180937b25099fc03858483163dc124de3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 15:13:28 +0100 Subject: [PATCH 386/523] Added application information --- manatools/aui/yui_curses.py | 58 +++++++++++++++++++++++++++++++++++++ manatools/aui/yui_gtk.py | 58 +++++++++++++++++++++++++++++++++++++ manatools/aui/yui_qt.py | 58 +++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 403e955..147b9d4 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -81,6 +81,15 @@ def __init__(self): self._product_name = "manatools AUI Curses" self._icon_base_path = "" self._icon = "" + # About dialog metadata + self._app_name = "" + self._version = "" + self._authors = "" + self._description = "" + self._license = "" + self._credits = "" + self._information = "" + self._logo = "" # Default directories try: self._default_documents_dir = os.path.expanduser('~/Documenti') @@ -103,6 +112,55 @@ def setApplicationIcon(self, Icon): """Set the application icon.""" self._icon = Icon + # --- About metadata getters/setters --- + def setApplicationName(self, name: str): + self._app_name = name or "" + + def applicationName(self) -> str: + return self._app_name or self._product_name or "" + + def setVersion(self, version: str): + self._version = version or "" + + def version(self) -> str: + return self._version or "" + + def setAuthors(self, authors: str): + self._authors = authors or "" + + def authors(self) -> str: + return self._authors or "" + + def setDescription(self, description: str): + self._description = description or "" + + def description(self) -> str: + return self._description or "" + + def setLicense(self, license_text: str): + self._license = license_text or "" + + def license(self) -> str: + return self._license or "" + + def setCredits(self, credits: str): + self._credits = credits or "" + + def credits(self) -> str: + return self._credits or "" + + def setInformation(self, information: str): + self._information = information or "" + + def information(self) -> str: + return self._information or "" + + def setLogo(self, logo_path: str): + self._logo = logo_path or "" + + def logo(self) -> str: + return self._logo or "" + def askForExistingDirectory(self, startDir: str, headline: str): """ NCurses overlay dialog to select an existing directory. diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 068f48d..89c5a56 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -46,6 +46,15 @@ def __init__(self): self._icon = "manatools" # default icon name # cached resolved GdkPixbuf.Pixbuf (or None) self._gtk_icon_pixbuf = None + # About dialog metadata + self._app_name = "" + self._version = "" + self._authors = "" + self._description = "" + self._license = "" + self._credits = "" + self._information = "" + self._logo = "" try: self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") except Exception: @@ -190,6 +199,55 @@ def setApplicationIcon(self, Icon): def applicationIcon(self): return self._icon + # --- About metadata getters/setters --- + def setApplicationName(self, name: str): + self._app_name = name or "" + + def applicationName(self) -> str: + return self._app_name or self._product_name or "" + + def setVersion(self, version: str): + self._version = version or "" + + def version(self) -> str: + return self._version or "" + + def setAuthors(self, authors: str): + self._authors = authors or "" + + def authors(self) -> str: + return self._authors or "" + + def setDescription(self, description: str): + self._description = description or "" + + def description(self) -> str: + return self._description or "" + + def setLicense(self, license_text: str): + self._license = license_text or "" + + def license(self) -> str: + return self._license or "" + + def setCredits(self, credits: str): + self._credits = credits or "" + + def credits(self) -> str: + return self._credits or "" + + def setInformation(self, information: str): + self._information = information or "" + + def information(self) -> str: + return self._information or "" + + def setLogo(self, logo_path: str): + self._logo = logo_path or "" + + def logo(self) -> str: + return self._logo or "" + def _create_gtk4_filters(self, filter_str: str) -> List[Gtk.FileFilter]: """ Create GTK4 file filters from a semicolon-separated filter string. diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 8070273..3a9bf86 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -48,6 +48,15 @@ def __init__(self): self._icon = "manatools" # default icon name # cached QIcon resolved from _icon (None if not resolved) self._qt_icon = None + # About dialog metadata + self._app_name = "" + self._version = "" + self._authors = "" + self._description = "" + self._license = "" + self._credits = "" + self._information = "" + self._logo = "" try: self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") except Exception: @@ -137,6 +146,55 @@ def applicationTitle(self): """Get the application title.""" return self._application_title + # --- About metadata getters/setters --- + def setApplicationName(self, name: str): + self._app_name = name or "" + + def applicationName(self) -> str: + return self._app_name or self._product_name or "" + + def setVersion(self, version: str): + self._version = version or "" + + def version(self) -> str: + return self._version or "" + + def setAuthors(self, authors: str): + self._authors = authors or "" + + def authors(self) -> str: + return self._authors or "" + + def setDescription(self, description: str): + self._description = description or "" + + def description(self) -> str: + return self._description or "" + + def setLicense(self, license_text: str): + self._license = license_text or "" + + def license(self) -> str: + return self._license or "" + + def setCredits(self, credits: str): + self._credits = credits or "" + + def credits(self) -> str: + return self._credits or "" + + def setInformation(self, information: str): + self._information = information or "" + + def information(self) -> str: + return self._information or "" + + def setLogo(self, logo_path: str): + self._logo = logo_path or "" + + def logo(self) -> str: + return self._logo or "" + def askForExistingDirectory(self, startDir: str, headline: str): """ Prompt user to select an existing directory. From aa91da0150a42a210f925deee21c0cec50a3447d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 15:59:24 +0100 Subject: [PATCH 387/523] Correct EventType --- manatools/aui/yui_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 3cfe11e..39662bc 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -115,7 +115,7 @@ def id(self): class YTimeoutEvent(YEvent): """Event generated on timeout""" def __init__(self): - super().__init__() + super().__init__(YEventType.TimeoutEvent) class YCancelEvent(YEvent): def __init__(self): From a821865241939857912303a55930a707fb8dbc42 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 16:38:06 +0100 Subject: [PATCH 388/523] better layout --- test/test_menubar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_menubar.py b/test/test_menubar.py index e9158ff..1e49cf2 100644 --- a/test/test_menubar.py +++ b/test/test_menubar.py @@ -103,7 +103,7 @@ def test_menubar_example(backend_name=None): # OK button ctrl_h = factory.createHBox(vbox) - ok_btn = factory.createPushButton(ctrl_h, "OK") + ok_btn = factory.createPushButton(factory.createHCenter(ctrl_h), "OK") root_logger.info("Opening MenuBar example dialog...") From 462c7d75b8a813bff6392911c83b6570219c5851 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 17:00:01 +0100 Subject: [PATCH 389/523] Label is just in some widgets --- manatools/aui/backends/gtk/alignmentgtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index 4dc5071..290515a 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -273,7 +273,7 @@ def _ensure_child_attached(self): self._child_attached = True col_index = 0 if hal == Gtk.Align.START else 2 if hal == Gtk.Align.END else 1 # center default - self._logger.debug("Successfully attached child %s %s [%d,%d]", child.widgetClass(), child.label(), row_index, col_index) + self._logger.debug("Successfully attached child %s %s [%d,%d]", child.widgetClass(), child.debugLabel(), row_index, col_index) except Exception as e: self._logger.error("Error building CenterBox layout: %s", e, exc_info=True) From 0e1fb6b6cf0c23b15412dc2377c6cb7e25f52dc0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 17:35:49 +0100 Subject: [PATCH 390/523] fixed stretching value --- .../aui/backends/curses/menubarcurses.py | 6 +++ manatools/aui/backends/gtk/menubargtk.py | 20 ++++++-- manatools/aui/backends/qt/menubarqt.py | 47 ++++++++++++++++--- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index 66ad5a6..4bfe21f 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -35,6 +35,12 @@ def __init__(self, parent=None): self._bar_y = 0 self._bar_x = 0 self._bar_width = 0 + # Default stretch flags: horizontal True, vertical False + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, False) + except Exception: + pass def widgetClass(self): return "YMenuBar" diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 64dcd01..a9d8eb1 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -26,6 +26,12 @@ def __init__(self, parent=None): self._row_to_item = {} self._row_to_popover = {} self._pending_rebuild = False + # Default stretch: horizontal True, vertical False + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, False) + except Exception: + pass def widgetClass(self): return "YMenuBar" @@ -300,10 +306,18 @@ def _render_menu_children(self, menu: YMenuItem): def _create_backend_widget(self): hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - # Do not allow the menubar box to expand vertically + # Expansion based on current stretchable flags: default hexpand True, vexpand False try: - hb.set_vexpand(False) - hb.set_hexpand(True) + try: + v_stretch = bool(self.stretchable(YUIDimension.YD_VERT)) + except Exception: + v_stretch = False + try: + h_stretch = bool(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + h_stretch = True + hb.set_vexpand(v_stretch) + hb.set_hexpand(h_stretch) except Exception: pass self._backend_widget = hb diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index 828b30d..07b1585 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -16,6 +16,12 @@ def __init__(self, parent=None): self._menus = [] # list of YMenuItem (is_menu=True) self._menu_to_qmenu = {} self._item_to_qaction = {} + # Default stretch: horizontal True, vertical False + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, False) + except Exception: + pass def widgetClass(self): return "YMenuBar" @@ -176,15 +182,42 @@ def _create_backend_widget(self): mb = QtWidgets.QMenuBar() self._backend_widget = mb try: - # Prevent vertical stretching: fix height to size hint and set fixed vertical size policy - h = mb.sizeHint().height() - if h and h > 0: - mb.setMinimumHeight(h) - mb.setMaximumHeight(h) + # Size policies based on current stretchable flags sp = mb.sizePolicy() - sp.setVerticalPolicy(QtWidgets.QSizePolicy.Fixed) - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + # Vertical: default Fixed unless user made it stretchable + try: + v_stretch = bool(self.stretchable(YUIDimension.YD_VERT)) + except Exception: + v_stretch = False + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding if v_stretch else QtWidgets.QSizePolicy.Fixed) + except Exception: + try: + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if v_stretch else QtWidgets.QSizePolicy.Policy.Fixed) + except Exception: + pass + # Horizontal: default Expanding unless user disabled stretchable + try: + h_stretch = bool(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + h_stretch = True + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding if h_stretch else QtWidgets.QSizePolicy.Fixed) + except Exception: + try: + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding if h_stretch else QtWidgets.QSizePolicy.Policy.Fixed) + except Exception: + pass mb.setSizePolicy(sp) + # Prevent vertical stretching by bounding height when not stretchable + try: + if not v_stretch: + h = mb.sizeHint().height() + if h and h > 0: + mb.setMinimumHeight(h) + mb.setMaximumHeight(h) + except Exception: + pass except Exception: pass # render any menus added before creation From ba383673f389643892a8c1f54e3960a9fcee0393 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 17:37:56 +0100 Subject: [PATCH 391/523] Added some aliases --- manatools/aui/yui_common.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 39662bc..8c04b8e 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -7,9 +7,13 @@ from typing import Optional # Enums +# Backwards-compatible aliases: allow using `YUIDimension.Horizontal` / `Vertical` +# in code that expects more readable names. class YUIDimension(Enum): YD_HORIZ = 0 + Horizontal = 0 YD_VERT = 1 + Vertical = 1 class YAlignmentType(Enum): YAlignUnchanged = 0 From 337a0157e91d43c9df5b680bb3a5bf6250faa601 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 17:39:52 +0100 Subject: [PATCH 392/523] Moving to manatools.aui --- manatools/ui/basedialog.py | 51 ++---- manatools/ui/common.py | 361 ++++++++++++++++++++----------------- manatools/ui/helpdialog.py | 10 +- 3 files changed, 219 insertions(+), 203 deletions(-) diff --git a/manatools/ui/basedialog.py b/manatools/ui/basedialog.py index 635a70e..dcb339d 100644 --- a/manatools/ui/basedialog.py +++ b/manatools/ui/basedialog.py @@ -22,12 +22,12 @@ @package manatools.ui.basedialog ''' -import yui +from ..aui import yui as yui from enum import Enum -import manatools.event as event -import manatools.eventmanager as eventManager +from .. import event as event +from .. import eventmanager as eventManager class DialogType(Enum): MAIN = 1 @@ -61,8 +61,8 @@ def __init__(self, title, icon="", dialogType=DialogType.MAIN, minWidth=-1, minH @param title dialog title @param icon dialog icon @param dialogType (DialogType.MAIN or DialogType.POPUP) - @param minWidth > 0 mim width size, see libYui createMinSize - @param minHeight > 0 mim height size, see libYui createMinSize + @param minWidth > 0 min width size in pixels + @param minHeight > 0 min height size in pixels ''' self._dialogType = dialogType self._icon = icon @@ -149,34 +149,14 @@ def factory(self): return yui widget factory ''' return yui.YUI.widgetFactory() - - @property - def optFactory(self): - ''' - return yui optional widget factory - ''' - return yui.YUI.optionalWidgetFactory() - - @property - def mgaFactory(self): - ''' - return yui mageia extended widget factory - ''' - if (not self._mgaFactory) : - self.factory - MGAPlugin = "mga" - mgaFact = yui.YExternalWidgets.externalWidgetFactory(MGAPlugin) - self._mgaFactory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(mgaFact) - return self._mgaFactory - def _setupUI(self): self.dialog = self.factory.createPopupDialog() if self._dialogType == DialogType.POPUP else self.factory.createMainDialog() parent = self.dialog - if self._minSize: - parent = self.factory.createMinSize(parent, self._minSize['minWidth'], self._minSize['minHeight']) + if self._minSize is not None: + parent = self.factory.createMinSize(self.dialog, self._minSize['minWidth'], self._minSize['minHeight']) vbox = self.factory.createVBox(parent) self.UIlayout(vbox) @@ -194,29 +174,26 @@ def _handleEvents(self): while self._running == True: event = self.dialog.waitForEvent(self.timeout) - eventType = event.eventType() rebuild_package_list = False group = None #event type checking - if (eventType == yui.YEvent.WidgetEvent) : + if (eventType == yui.YEventType.WidgetEvent) : # widget selected widget = event.widget() - wEvent = yui.toYWidgetEvent(event) - self.eventManager.widgetEvent(widget, wEvent) - elif (eventType == yui.YEvent.MenuEvent) : + self.eventManager.widgetEvent(widget, event) + elif (eventType == yui.YEventType.MenuEvent) : ### MENU ### item = event.item() - mEvent = yui.toYMenuEvent(event) - self.eventManager.menuEvent(item, mEvent) - elif (eventType == yui.YEvent.CancelEvent) : + self.eventManager.menuEvent(item, event) + elif (eventType == yui.YEventType.CancelEvent) : self.eventManager.cancelEvent() break - elif (eventType == yui.YEvent.TimeoutEvent) : + elif (eventType == yui.YEventType.TimeoutEvent) : self.eventManager.timeoutEvent() else: - print("Unmanaged event type %d"%(eventType)) + print(f"Unmanaged event type {eventType}") self.doSomethingIntoLoop() diff --git a/manatools/ui/common.py b/manatools/ui/common.py index f7115ae..9ef466d 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -11,7 +11,7 @@ @package manatools.ui.common ''' -import yui +from ..aui import yui from enum import Enum import gettext # https://pymotw.com/3/gettext/#module-localization @@ -25,14 +25,11 @@ def destroyUI () : ''' - destroy all the dialogs and delete YUI plugins. Use this at the very end of your application - to close everything correctly, expecially if you experience a crash in Qt plugin as explained - into ussue https://github.com/libyui/libyui-qt/issues/41 + Best-effort teardown for AUI dialogs. AUI manages backend lifecycle internally, + so there is typically no need to manually destroy UI plugins as in libyui. + This function exists for API compatibility and currently performs no action. ''' - yui.YDialog.deleteAllDialogs() - # next line seems to be a workaround to prevent the qt-app from crashing - # see https://github.com/libyui/libyui-qt/issues/41 - yui.YUILoader.deleteUI() + return def warningMsgBox (info) : @@ -48,28 +45,27 @@ def warningMsgBox (info) : if (not info) : return 0 - retVal = 0 - yui.YUI.widgetFactory - factory = yui.YExternalWidgets.externalWidgetFactory("mga") - factory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(factory) - dlg = factory.createDialogBox(yui.YMGAMessageBox.B_ONE, yui.YMGAMessageBox.D_WARNING) - - if ('title' in info.keys()) : - dlg.setTitle(info['title']) - - rt = False - if ("richtext" in info.keys()) : - rt = info['richtext'] - - if ('text' in info.keys()) : - dlg.setText(info['text'], rt) - - dlg.setButtonLabel(_("&Ok"), yui.YMGAMessageBox.B_ONE ) -# dlg.setMinSize(50, 5) - - retVal = dlg.show() - dlg = None - + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) + if info.get('title'): + factory.createHeading(vbox, info.get('title')) + text = info.get('text', "") + rt = bool(info.get('richtext', False)) + if rt: + t = factory.createRichText(vbox, "", False) + t.setValue(text) + else: + factory.createLabel(vbox, text) + align = factory.createRight(vbox) + ok_btn = factory.createPushButton(align, _("&Ok")) + while True: + ev = dlg.waitForEvent() + if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + break + dlg.destroy() return 1 @@ -86,28 +82,27 @@ def infoMsgBox (info) : if (not info) : return 0 - retVal = 0 - yui.YUI.widgetFactory - factory = yui.YExternalWidgets.externalWidgetFactory("mga") - factory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(factory) - dlg = factory.createDialogBox(yui.YMGAMessageBox.B_ONE, yui.YMGAMessageBox.D_INFO) - - if ('title' in info.keys()) : - dlg.setTitle(info['title']) - - rt = False - if ("richtext" in info.keys()) : - rt = info['richtext'] - - if ('text' in info.keys()) : - dlg.setText(info['text'], rt) - - dlg.setButtonLabel(_("&Ok"), yui.YMGAMessageBox.B_ONE ) -# dlg.setMinSize(50, 5) - - retVal = dlg.show() - dlg = None - + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) + if info.get('title'): + factory.createHeading(vbox, info.get('title')) + text = info.get('text', "") + rt = bool(info.get('richtext', False)) + if rt: + t = factory.createRichText(vbox, "", False) + t.setValue(text) + else: + factory.createLabel(vbox, text) + align = factory.createRight(vbox) + ok_btn = factory.createPushButton(align, _("&Ok")) + while True: + ev = dlg.waitForEvent() + if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + break + dlg.destroy() return 1 def msgBox (info) : @@ -122,28 +117,27 @@ def msgBox (info) : if (not info) : return 0 - retVal = 0 - yui.YUI.widgetFactory - factory = yui.YExternalWidgets.externalWidgetFactory("mga") - factory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(factory) - dlg = factory.createDialogBox(yui.YMGAMessageBox.B_ONE) - - if ('title' in info.keys()) : - dlg.setTitle(info['title']) - - rt = False - if ("richtext" in info.keys()) : - rt = info['richtext'] - - if ('text' in info.keys()) : - dlg.setText(info['text'], rt) - - dlg.setButtonLabel(_("&Ok"), yui.YMGAMessageBox.B_ONE ) -# dlg.setMinSize(50, 5) - - retVal = dlg.show() - dlg = None - + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) + if info.get('title'): + factory.createHeading(vbox, info.get('title')) + text = info.get('text', "") + rt = bool(info.get('richtext', False)) + if rt: + t = factory.createRichText(vbox, "", False) + t.setValue(text) + else: + factory.createLabel(vbox, text) + align = factory.createRight(vbox) + ok_btn = factory.createPushButton(align, _("&Ok")) + while True: + ev = dlg.waitForEvent() + if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + break + dlg.destroy() return 1 @@ -165,36 +159,40 @@ def askOkCancel (info) : if (not info) : return False - retVal = False - yui.YUI.widgetFactory - factory = yui.YExternalWidgets.externalWidgetFactory("mga") - factory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(factory) - dlg = factory.createDialogBox(yui.YMGAMessageBox.B_TWO) - - if ('title' in info.keys()) : - dlg.setTitle(info['title']) - - rt = False - if ("richtext" in info.keys()) : - rt = info['richtext'] - - if ('text' in info.keys()) : - dlg.setText(info['text'], rt) - - dlg.setButtonLabel(_("&Ok"), yui.YMGAMessageBox.B_ONE ) - dlg.setButtonLabel(_("&Cancel"), yui.YMGAMessageBox.B_TWO ) - - if ("default_button" in info.keys() and info["default_button"] == 1) : - dlg.setDefaultButton(yui.YMGAMessageBox.B_ONE) - else : - dlg.setDefaultButton(yui.YMGAMessageBox.B_TWO) - - dlg.setMinSize(50, 5) - - retVal = dlg.show() == yui.YMGAMessageBox.B_ONE; - dlg = None - - return retVal + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) + if info.get('title'): + factory.createHeading(vbox, info.get('title')) + text = info.get('text', "") + rt = bool(info.get('richtext', False)) + if rt: + t = factory.createRichText(vbox, "", False) + t.setValue(text) + else: + factory.createLabel(vbox, text) + btns = factory.createHBox(vbox) + ok_btn = factory.createPushButton(btns, _("&Ok")) + cancel_btn = factory.createPushButton(btns, _("&Cancel")) + default_ok = bool(info.get('default_button', 0) == 1) + # simple default: ignore focusing specifics for now + result = False + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + result = False + break + if et == yui.YEventType.WidgetEvent: + w = ev.widget() + if w == ok_btn and ev.reason() == yui.YEventReason.Activated: + result = True + break + if w == cancel_btn and ev.reason() == yui.YEventReason.Activated: + result = False + break + dlg.destroy() + return result def askYesOrNo (info) : ''' @@ -215,35 +213,45 @@ def askYesOrNo (info) : if (not info) : return False - retVal = False - yui.YUI.widgetFactory - factory = yui.YExternalWidgets.externalWidgetFactory("mga") - factory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(factory) - dlg = factory.createDialogBox(yui.YMGAMessageBox.B_TWO) - - if ('title' in info.keys()) : - dlg.setTitle(info['title']) - - rt = False - if ("richtext" in info.keys()) : - rt = info['richtext'] - - if ('text' in info.keys()) : - dlg.setText(info['text'], rt) - - dlg.setButtonLabel(_("&Yes"), yui.YMGAMessageBox.B_ONE ) - dlg.setButtonLabel(_("&No"), yui.YMGAMessageBox.B_TWO ) - if ("default_button" in info.keys() and info["default_button"] == 1) : - dlg.setDefaultButton(yui.YMGAMessageBox.B_ONE) - else : - dlg.setDefaultButton(yui.YMGAMessageBox.B_TWO) - if ('size' in info.keys()) : - dlg.setMinSize(info['size'][0], info['size'][1]) - - retVal = dlg.show() == yui.YMGAMessageBox.B_ONE; - dlg = None - - return retVal + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) + if info.get('title'): + factory.createHeading(vbox, info.get('title')) + text = info.get('text', "") + rt = bool(info.get('richtext', False)) + if rt: + t = factory.createRichText(vbox, "", False) + t.setValue(text) + else: + factory.createLabel(vbox, text) + if 'size' in info.keys(): + try: + dims = info['size'] + parent = factory.createMinSize(vbox, int(dims[0]), int(dims[1])) + vbox = parent + except Exception: + pass + btns = factory.createHBox(vbox) + yes_btn = factory.createPushButton(btns, _("&Yes")) + no_btn = factory.createPushButton(btns, _("&No")) + result = False + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: + result = False + break + if et == yui.YEventType.WidgetEvent: + w = ev.widget() + if w == yes_btn and ev.reason() == yui.YEventReason.Activated: + result = True + break + if w == no_btn and ev.reason() == yui.YEventReason.Activated: + result = False + break + dlg.destroy() + return result class AboutDialogMode(Enum): ''' @@ -275,31 +283,62 @@ def AboutDialog (info) : if (not info) : raise ValueError("Missing AboutDialog parameters") - yui.YUI.widgetFactory - factory = yui.YExternalWidgets.externalWidgetFactory("mga") - factory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(factory) - - name = info['name'] if 'name' in info.keys() else "" - version = info['version'] if 'version' in info.keys() else "" - license = info['license'] if 'license' in info.keys() else "" - authors = info['authors'] if 'authors' in info.keys() else "" - description = info['description'] if 'description' in info.keys() else "" - logo = info['logo'] if 'logo' in info.keys() else "" - icon = info['icon'] if 'icon' in info.keys() else "" - credits = info['credits'] if 'credits' in info.keys() else "" - information = info['information'] if 'information' in info.keys() else "" - dialog_mode = yui.YMGAAboutDialog.TABBED - if 'dialog_mode' in info.keys() : - dialog_mode = yui.YMGAAboutDialog.CLASSIC if info['dialog_mode'] == AboutDialogMode.CLASSIC else yui.YMGAAboutDialog.TABBED - - dlg = factory.createAboutDialog(name, version, license, - authors, description, logo, - icon, credits, information - ) - if 'size' in info.keys(): - if not 'column' in info['size'] or not 'lines' in info['size'] : - raise ValueError("size must contains <> and <> keys") - dlg.setMinSize(info['size']['column'], info['size']['lines']) - - dlg.show(dialog_mode) - dlg = None + # Build a simple About dialog using AUI widgets + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) + + name = info.get('name', "") + version = info.get('version', "") + license_txt = info.get('license', "") + authors = info.get('authors', "") + description = info.get('description', "") + logo = info.get('logo', "") + credits = info.get('credits', "") + information = info.get('information', "") + + title = _("About") + (f" {name}" if name else "") + factory.createHeading(vbox, title) + + # Header block + header = factory.createHBox(vbox) + if logo: + try: + factory.createImage(header, logo) + factory.createHSpacing(header, 8) + except Exception: + pass + labels = factory.createVBox(header) + if name: + factory.createLabel(labels, name) + if version: + factory.createLabel(labels, version) + if license_txt: + factory.createLabel(labels, license_txt) + + # Content block + if description: + rt = factory.createRichText(vbox, "", False) + rt.setValue(description) + if authors: + factory.createHeading(vbox, _("Authors")) + ra = factory.createRichText(vbox, "", False) + ra.setValue(authors) + if credits: + factory.createHeading(vbox, _("Credits")) + rc = factory.createRichText(vbox, "", False) + rc.setValue(credits) + if information: + factory.createHeading(vbox, _("Information")) + ri = factory.createRichText(vbox, "", False) + ri.setValue(information) + + align = factory.createRight(vbox) + close_btn = factory.createPushButton(align, _("&Close")) + while True: + ev = dlg.waitForEvent() + if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == close_btn and ev.reason() == yui.YEventReason.Activated: + break + dlg.destroy() diff --git a/manatools/ui/helpdialog.py b/manatools/ui/helpdialog.py index d6f23e5..ff7ec44 100644 --- a/manatools/ui/helpdialog.py +++ b/manatools/ui/helpdialog.py @@ -12,9 +12,9 @@ ''' import webbrowser -import manatools.ui.basedialog as basedialog -import manatools.basehelpinfo as helpdata -import yui +from . import basedialog as basedialog +from .. import basehelpinfo as helpdata +from ..aui import yui as yui import gettext # https://pymotw.com/3/gettext/#module-localization t = gettext.translation( @@ -43,12 +43,12 @@ def UIlayout(self, layout): ''' layout implementation called in base class to setup UI ''' - # URL events are sent as YMenuEvent by libyui + # URL events may be sent as MenuEvent by backends that support it self.eventManager.addMenuEvent(None, self.onURLEvent, False) self.text = self.factory.createRichText(layout, "", False) self.text.setValue(self.info.home()) align = self.factory.createRight(layout) - self.quitButton = self.factory.createPushButton(align, _("&Quit")) + self.quitButton = self.factory.createPushButton(align, _("Quit")) self.eventManager.addWidgetEvent(self.quitButton, self.onQuitEvent) def onQuitEvent(self) : From 187fe56f22137c714e32aee2a65b1f8040a949dc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 17:40:48 +0100 Subject: [PATCH 393/523] moved to manatools.aui --- test/testDialog.py | 57 ++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/test/testDialog.py b/test/testDialog.py index 8f6c74f..f8ad535 100644 --- a/test/testDialog.py +++ b/test/testDialog.py @@ -11,8 +11,12 @@ @package manatools ''' +import os +import sys +# Prefer using the local workspace package when running this test directly +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) import manatools.ui.basedialog as basedialog -import yui +from manatools.aui import yui import time ###################################################################### @@ -41,46 +45,49 @@ def newFunc(*args, **kwargs): class TestDialog(basedialog.BaseDialog): def __init__(self): - basedialog.BaseDialog.__init__(self, "Test dialog", "", basedialog.DialogType.POPUP, 80, 10) + basedialog.BaseDialog.__init__(self, "Test dialog", "", basedialog.DialogType.POPUP, 320, 200) def UIlayout(self, layout): ''' layout implementation called in base class to setup UI ''' - # Let's test a Menu widget - menu = self.factory.createMenuButton(self.factory.createLeft(layout), "Test &menu") - tm1 = yui.YMenuItem("menu item 1") - tm2 = yui.YMenuItem("menu item 2") - qm = yui.YMenuItem("&Quit") - menu.addItem(tm1) - menu.addItem(tm2) - menu.addItem(qm) - menu.rebuildMenuTree() - sendObjOnEvent=True + # Menu bar (top-level menubar) + menubar = self.factory.createMenuBar(layout) + top_menu = menubar.addMenu("Test menu") + tm1 = menubar.addItem(top_menu, "menu item 1") + tm2 = menubar.addItem(top_menu, "menu item 2") + qm = menubar.addItem(top_menu, "Quit") + # Ensure event handlers receive the menu item object + sendObjOnEvent = True self.eventManager.addMenuEvent(tm1, self.onMenuItem, sendObjOnEvent) self.eventManager.addMenuEvent(tm2, self.onMenuItem, sendObjOnEvent) self.eventManager.addMenuEvent(qm, self.onQuitEvent, sendObjOnEvent) #let's test some buttons hbox = self.factory.createHBox(layout) - self.pressButton = self.factory.createPushButton(hbox, "&Press") + self.pressButton = self.factory.createPushButton(hbox, "Press") self.eventManager.addWidgetEvent(self.pressButton, self.onPressButton) #Let's enable a time out on events - self.timeoutButton = self.factory.createPushButton(hbox, "&Test timeout") + self.timeoutButton = self.factory.createPushButton(hbox, "Test timeout") self.eventManager.addWidgetEvent(self.timeoutButton, self.onTimeOutButtonEvent) self.eventManager.addTimeOutEvent(self.onTimeOutEvent) + self.factory.createVStretch(layout) + align = self.factory.createHVCenter(layout) # Let's test a quitbutton (same handle as Quit menu) - self.quitButton = self.factory.createPushButton(layout, "&Quit") + self.quitButton = self.factory.createPushButton(align, "Quit") self.eventManager.addWidgetEvent(self.quitButton, self.onQuitEvent, sendObjOnEvent) # Let's test a cancel event self.eventManager.addCancelEvent(self.onCancelEvent) def onMenuItem(self, item): - print ("Menu item <<", item.label(), ">>") + try: + print ("Menu item <<", item.label(), ">>") + except Exception: + print("Menu item activated") def onTimeOutButtonEvent(self): if self.timeout > 0 : @@ -103,15 +110,21 @@ def onCancelEvent(self) : print ("Got a cancel event") def onQuitEvent(self, obj) : - if isinstance(obj, yui.YItem): - print ("Quit menu pressed") - else: - print ("Quit button pressed") + # obj can be a menu item (YMenuItem) or a widget (button) + try: + if isinstance(obj, yui.YMenuItem): + print ("Quit menu pressed") + else: + print ("Quit button pressed") + except Exception: + print ("Quit invoked") # BaseDialog needs to force to exit the handle event loop self.ExitLoop() -if __name__ == '__main__': - +if __name__ == '__main__': + if len(sys.argv) > 1: + os.environ['YUI_BACKEND'] = sys.argv[1] + td = TestDialog() td.run() From 25234f6611a61c5c3db3ec8e3f868c30d156d392 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 19:42:29 +0100 Subject: [PATCH 394/523] missing dependency --- manatools/aui/backends/gtk/menubargtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index a9d8eb1..4d623c3 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -10,7 +10,7 @@ from gi.repository import Gtk, Gio, GLib import logging import os -from ...yui_common import YWidget, YMenuEvent, YMenuItem +from ...yui_common import YWidget, YMenuEvent, YMenuItem, YUIDimension from .commongtk import _resolve_gicon, _resolve_icon From faf66eff7ed5c6fc9a1a5070ac6f768b430dadf7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 20:31:21 +0100 Subject: [PATCH 395/523] Added shortcut on menu and button converting '&' in '_' --- manatools/aui/backends/gtk/commongtk.py | 51 ++++++- manatools/aui/backends/gtk/menubargtk.py | 142 +++++++++++++++++++- manatools/aui/backends/gtk/pushbuttongtk.py | 5 +- 3 files changed, 188 insertions(+), 10 deletions(-) diff --git a/manatools/aui/backends/gtk/commongtk.py b/manatools/aui/backends/gtk/commongtk.py index de083ac..a22f5dd 100644 --- a/manatools/aui/backends/gtk/commongtk.py +++ b/manatools/aui/backends/gtk/commongtk.py @@ -21,7 +21,7 @@ from ...yui_common import * -__all__ = ["_resolve_icon", "_resolve_gicon"] +__all__ = ["_resolve_icon", "_resolve_gicon", "_convert_mnemonic_to_gtk"] def _resolve_icon(icon_name, size=16): @@ -145,4 +145,51 @@ def _resolve_gicon(icon_spec): pass except Exception: pass - return None \ No newline at end of file + return None + + +def _convert_mnemonic_to_gtk(label: str) -> str: + """Convert a Qt-style mnemonic in a label to GTK format. + + - Qt: uses '&' before a character to mark the mnemonic (e.g., "&Quit"). + A literal ampersand is written as "&&". + - GTK: uses '_' before the mnemonic character (e.g., "_Quit"). + + If no mnemonic marker ('&' not present), the string is returned unchanged. + Literal "&&" sequences are converted to a single '&'. Only the first + single '&' is converted to a mnemonic; subsequent single '&' are dropped. + + This function does not attempt to escape underscores; GTK treats '_' as + mnemonic when present, but the input is expected to be Qt-style. + """ + if label is None: + return label + s = str(label) + if '&' not in s: + return s + + out = [] + i = 0 + mnemonic_done = False + n = len(s) + while i < n: + ch = s[i] + if ch == '&': + # Handle literal ampersand + if i + 1 < n and s[i + 1] == '&': + out.append('&') + i += 2 + continue + # Single '&' indicates mnemonic; convert the first occurrence + if not mnemonic_done: + # Insert '_' before the next character (if any) + if i + 1 < n: + out.append('_') + mnemonic_done = True + # Skip this '&' (do not add to output) + i += 1 + continue + else: + out.append(ch) + i += 1 + return ''.join(out) \ No newline at end of file diff --git a/manatools/aui/backends/gtk/menubargtk.py b/manatools/aui/backends/gtk/menubargtk.py index 4d623c3..999d5b8 100644 --- a/manatools/aui/backends/gtk/menubargtk.py +++ b/manatools/aui/backends/gtk/menubargtk.py @@ -11,7 +11,7 @@ import logging import os from ...yui_common import YWidget, YMenuEvent, YMenuItem, YUIDimension -from .commongtk import _resolve_gicon, _resolve_icon +from .commongtk import _resolve_icon, _convert_mnemonic_to_gtk class YMenuBarGtk(YWidget): @@ -25,6 +25,7 @@ def __init__(self, parent=None): self._item_to_row = {} self._row_to_item = {} self._row_to_popover = {} + self._item_to_button = {} self._pending_rebuild = False # Default stretch: horizontal True, vertical False try: @@ -46,7 +47,7 @@ def addMenu(self, label: str="", icon_name: str = "", menu: YMenuItem = None) -> else: if not label: raise ValueError("Menu label must be provided when no YMenuItem is given") - m = YMenuItem(label, icon_name, enabled=True, is_menu=True) + m = YMenuItem(_convert_mnemonic_to_gtk(label), icon_name, enabled=True, is_menu=True) self._menus.append(m) if self._backend_widget: self._ensure_menu_rendered(m) @@ -54,7 +55,7 @@ def addMenu(self, label: str="", icon_name: str = "", menu: YMenuItem = None) -> return m def addItem(self, menu: YMenuItem, label: str, icon_name: str = "", enabled: bool = True) -> YMenuItem: - item = menu.addItem(label, icon_name) + item = menu.addItem(_convert_mnemonic_to_gtk(label), icon_name) item.setEnabled(enabled) if self._backend_widget: # update model for this menu and rebuild root @@ -72,6 +73,12 @@ def setItemEnabled(self, item: YMenuItem, on: bool = True): row.set_sensitive(bool(on)) except Exception: pass + try: + btnw = self._item_to_button.get(item) + if btnw is not None: + btnw.set_sensitive(bool(on)) + except Exception: + pass return pair = self._menu_to_button.get(item) if pair is not None: @@ -153,11 +160,31 @@ def _ensure_menu_rendered_button(self, menu: YMenuItem): if hb is None: return btn = Gtk.MenuButton() + btn.set_use_underline(True) try: - btn.set_label(menu.label()) + btn.set_label(_convert_mnemonic_to_gtk(menu.label())) btn.set_has_frame(False) except Exception: pass + # Ensure mnemonic-activate opens the popover (Alt+Key) + try: + def _on_mnemonic(btn_widget, _cycle): + try: + pop = btn_widget.get_popover() + if pop is not None: + try: + pop.popup() + except Exception: + try: + pop.set_visible(True) + except Exception: + pass + except Exception: + pass + return True + btn.connect('mnemonic-activate', _on_mnemonic) + except Exception: + pass # optional icon on the button if menu.iconName(): @@ -171,6 +198,26 @@ def _ensure_menu_rendered_button(self, menu: YMenuItem): try: hb_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) hb_box.append(img) + # Add a mnemonic label so Alt+Key continues to work + try: + conv = menu.label() + lbl = Gtk.Label(label=conv) + try: + lbl.set_use_underline(True) + except Exception: + pass + try: + lbl.set_xalign(0.0) + except Exception: + pass + hb_box.append(lbl) + # route label mnemonic to the button + try: + lbl.set_mnemonic_widget(btn) + except Exception: + pass + except Exception: + pass btn.set_child(hb_box) except Exception: self._logger.exception("Failed to set icon on MenuButton for '%s'", menu.label()) @@ -227,11 +274,30 @@ def _render_menu_children(self, menu: YMenuItem): if child.isMenu(): # submenu: create a nested MenuButton inside the row sub_btn = Gtk.MenuButton() + sub_btn.set_use_underline(True) try: - sub_btn.set_label(child.label()) + sub_btn.set_label(_convert_mnemonic_to_gtk(child.label())) sub_btn.set_has_frame(False) except Exception: pass + try: + def _on_mnemonic_sub(bw, _cycle): + try: + sp = bw.get_popover() + if sp is not None: + try: + sp.popup() + except Exception: + try: + sp.set_visible(True) + except Exception: + pass + except Exception: + pass + return True + sub_btn.connect('mnemonic-activate', _on_mnemonic_sub) + except Exception: + pass sub_pop = Gtk.Popover() sub_lb = Gtk.ListBox() sub_lb.set_selection_mode(Gtk.SelectionMode.NONE) @@ -249,6 +315,25 @@ def _render_menu_children(self, menu: YMenuItem): try: box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) box.append(img) + # Add a mnemonic label for submenu button too + try: + conv = child.label() + slbl = Gtk.Label(label=conv) + try: + slbl.set_use_underline(True) + except Exception: + pass + try: + slbl.set_xalign(0.0) + except Exception: + pass + box.append(slbl) + try: + slbl.set_mnemonic_widget(sub_btn) + except Exception: + pass + except Exception: + pass sub_btn.set_child(box) except Exception: self._logger.exception("Failed to set icon on submenu button '%s'", child.label()) @@ -290,8 +375,17 @@ def _render_menu_children(self, menu: YMenuItem): row_box.append(img) except Exception: self._logger.exception("Failed to resolve icon for menu item '%s'", child.label()) + # Display item label (no button) for reliable visibility across GTK versions lbl = Gtk.Label(label=child.label()) - lbl.set_xalign(0.0) + try: + # interpret '_' as mnemonic underline visually (no activation here) + lbl.set_use_underline(True) + except Exception: + pass + try: + lbl.set_xalign(0.0) + except Exception: + pass row_box.append(lbl) row.set_child(row_box) row.set_sensitive(child.enabled()) @@ -425,6 +519,10 @@ def rebuildMenus(self): self._item_to_row.clear() except Exception: self._item_to_row = {} + try: + self._item_to_button.clear() + except Exception: + self._item_to_button = {} try: self._row_to_item.clear() except Exception: @@ -513,6 +611,10 @@ def deleteMenus(self): self._item_to_row.clear() except Exception: self._item_to_row = {} + try: + self._item_to_button.clear() + except Exception: + self._item_to_button = {} try: self._row_to_item.clear() except Exception: @@ -574,3 +676,31 @@ def _on_row_activated(self, listbox: Gtk.ListBox, row: Gtk.ListBoxRow): self._logger.exception("Error closing popovers after activation") except Exception: self._logger.exception("Error handling row activation") + + def _on_menu_button_clicked(self, item: YMenuItem): + try: + if item and item.enabled(): + self._emit_activation(item) + # close all popovers + try: + for btn, _lb in list(self._menu_to_button.values()): + try: + pop = btn.get_popover() + if pop is None: + continue + try: + pop.popdown() + except Exception: + try: + pop.set_visible(False) + except Exception: + try: + pop.hide() + except Exception: + pass + except Exception: + pass + except Exception: + pass + except Exception: + pass diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index 5048636..cf3b92a 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -19,13 +19,13 @@ import os import logging from ...yui_common import * -from .commongtk import _resolve_icon, _resolve_gicon +from .commongtk import _resolve_icon, _convert_mnemonic_to_gtk class YPushButtonGtk(YWidget): def __init__(self, parent=None, label=""): super().__init__(parent) - self._label = label + self._label = _convert_mnemonic_to_gtk(label) self._icon_name = None self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") @@ -45,6 +45,7 @@ def setLabel(self, label): def _create_backend_widget(self): self._backend_widget = Gtk.Button(label=self._label) + self._backend_widget.set_use_underline(True) # apply icon if previously set try: if getattr(self, "_icon_name", None): From 9acff2d58bd1619737cebbae5613482388932b06 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 20:55:59 +0100 Subject: [PATCH 396/523] Added shortcut for Menus and buttons --- manatools/aui/backends/curses/commoncurses.py | 127 ++++++++++++++++++ manatools/aui/backends/curses/dialogcurses.py | 41 ++++++ .../aui/backends/curses/menubarcurses.py | 89 +++++++++++- .../aui/backends/curses/pushbuttoncurses.py | 36 ++++- 4 files changed, 285 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/curses/commoncurses.py b/manatools/aui/backends/curses/commoncurses.py index 8078f28..a993b59 100644 --- a/manatools/aui/backends/curses/commoncurses.py +++ b/manatools/aui/backends/curses/commoncurses.py @@ -8,15 +8,142 @@ Author: Angelo Naselli @package manatools.aui.backends.curses + +Also provides mnemonic extraction from labels that may use Qt ('&X' / '&&') +or GTK ('_X' / '__') style markers. ''' + import curses import curses.ascii import sys import os import time import logging +from typing import Tuple, Optional from ...yui_common import * +def extract_mnemonic(label: Optional[str]) -> Tuple[Optional[str], str]: + """Extract mnemonic character and return (mnemonic_char, clean_label). + + - Supports Qt-style '&' mnemonic: '&F' => mnemonic 'f'. '&&' => literal '&'. + - Supports GTK-style '_' mnemonic: '_F' => mnemonic 'f'. '__' => literal '_'. + - If no mnemonic found, returns (None, original_label). + + The returned clean_label has mnemonic markers removed/unescaped. + """ + if label is None: + return None, label + s = str(label) + if not s: + return None, s + + # Prefer Qt-style '&' parsing first + mn: Optional[str] = None + out = [] + i = 0 + n = len(s) + while i < n: + ch = s[i] + if ch == '&': + # literal '&&' + if i + 1 < n and s[i + 1] == '&': + out.append('&') + i += 2 + continue + # mnemonic (single '&') + if i + 1 < n and mn is None: + mn = s[i + 1].lower() + # do not add '&' to output; just skip marker and continue + i += 1 + continue + # stray '&' at end -> drop + i += 1 + continue + out.append(ch) + i += 1 + + # If no '&' mnemonic found, try '_' GTK style + if mn is None and '_' in s: + out2 = [] + i = 0 + n = len(s) + while i < n: + ch = s[i] + if ch == '_': + if i + 1 < n and s[i + 1] == '_': + out2.append('_') + i += 2 + continue + if i + 1 < n and mn is None: + mn = s[i + 1].lower() + i += 1 + continue + i += 1 + continue + out2.append(ch) + i += 1 + return mn, ''.join(out2) + + return mn, ''.join(out) + + +def split_mnemonic(label: Optional[str]) -> Tuple[Optional[str], Optional[int], str]: + """Return (mnemonic_char, mnemonic_index_in_clean, clean_label). + + Works like extract_mnemonic but also returns the index in the cleaned + label where the mnemonic applies, or None if not present. + """ + if label is None: + return None, None, label + s = str(label) + if not s: + return None, None, s + + mn = None + idx = None + out = [] + i = 0 + n = len(s) + while i < n: + ch = s[i] + if ch == '&': + if i + 1 < n and s[i + 1] == '&': + out.append('&') + i += 2 + continue + if i + 1 < n and mn is None: + mn = s[i + 1].lower() + idx = len(out) + i += 1 + continue + i += 1 + continue + out.append(ch) + i += 1 + if mn is None and '_' in s: + out2 = [] + i = 0 + n = len(s) + while i < n: + ch = s[i] + if ch == '_': + if i + 1 < n and s[i + 1] == '_': + out2.append('_') + i += 2 + continue + if i + 1 < n and mn is None: + mn = s[i + 1].lower() + idx = len(out2) + i += 1 + continue + i += 1 + continue + out2.append(ch) + i += 1 + return mn, idx, ''.join(out2) + return mn, idx, ''.join(out) + + __all__ = ["pixels_to_chars", "_curses_recursive_min_height", "_curses_recursive_min_width"] # Module-level logger for common curses helpers diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index fca4c29..204b3de 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -377,11 +377,17 @@ def waitForEvent(self, timeout_millisec=0): continue # Dispatch key to focused widget + handled = False if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): handled = self._focused_widget._handle_key(key) if handled: self._last_draw_time = 0 + # Global mnemonic fallback for pushbuttons when not handled + if not handled and ((key >= ord('a') and key <= ord('z')) or (key >= ord('A') and key <= ord('Z'))): + if self._activate_pushbutton_mnemonic(chr(key)): + self._last_draw_time = 0 + except KeyboardInterrupt: self._post_event(YCancelEvent()) break @@ -401,3 +407,38 @@ def waitForEvent(self, timeout_millisec=0): pass return self._event_result if self._event_result is not None else YEvent() + + def _activate_pushbutton_mnemonic(self, ch): + """Find an enabled pushbutton with matching mnemonic and activate it. + Returns True if a button was found and an event posted. + """ + try: + target = None + ch_low = ch.lower() + + def visit(widget): + nonlocal target + if target is not None: + return + try: + if getattr(widget, 'widgetClass', lambda: '')() == 'YPushButton': + if widget.isEnabled() and getattr(widget, '_mnemonic', None): + if str(widget._mnemonic).lower() == ch_low: + target = widget + return + except Exception: + pass + for c in getattr(widget, '_children', []) or []: + visit(c) + + if self.hasChildren(): + visit(self.child()) + if target is not None: + try: + self._post_event(YWidgetEvent(target, YEventReason.Activated)) + except Exception: + pass + return True + except Exception: + pass + return False diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index 4bfe21f..1869ff1 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -9,7 +9,8 @@ ''' import curses import logging -from ...yui_common import YWidget, YMenuEvent, YMenuItem +from ...yui_common import YWidget, YMenuEvent, YMenuItem, YUIDimension +from .commoncurses import extract_mnemonic, split_mnemonic class YMenuBarCurses(YWidget): @@ -171,7 +172,8 @@ def _draw(self, window, y, x, width, height): # reset menu positions self._menu_positions = [] for idx, menu in enumerate(self._menus): - label = f" {menu.label()} " + mn, mpos, clean = split_mnemonic(menu.label()) + label = f" {clean} " attr = bar_attr if idx == self._current_menu_index: attr |= curses.A_BOLD @@ -188,7 +190,19 @@ def _draw(self, window, y, x, width, height): except Exception: pass self._menu_positions.append(cx) - window.addstr(y, cx, label[:max(0, width - (cx - x))], attr) + # draw label with underline for mnemonic + maxlen = max(0, width - (cx - x)) + vis = label[:maxlen] + window.addstr(y, cx, vis, attr) + if mpos is not None: + try: + # underline position inside label (account for leading space) + ul_x = cx + 1 + mpos + if ul_x < cx + len(vis): + ch = clean[mpos] if 0 <= mpos < len(clean) else ' ' + window.addstr(y, ul_x, ch, attr | curses.A_UNDERLINE) + except curses.error: + pass except curses.error: self._menu_positions.append(cx) pass @@ -278,14 +292,22 @@ def _draw_expanded_list(self, window): continue sel = (self._menu_indices[level] == real_i) if level < len(self._menu_indices) else (i == 0) prefix = "* " if sel else " " - label_text = item.label() + mn, mpos, clean = split_mnemonic(item.label()) marker = " ►" if item.isMenu() else "" - text = prefix + label_text + marker + text = prefix + clean + marker attr = curses.A_REVERSE if sel else curses.A_NORMAL if not item.enabled(): attr |= curses.A_DIM try: - window.addstr(popup_y + i, popup_x, text.ljust(popup_width)[:popup_width], attr) + line = text.ljust(popup_width)[:popup_width] + window.addstr(popup_y + i, popup_x, line, attr) + if mpos is not None: + try: + ul_x = popup_x + len(prefix) + mpos + if ul_x < popup_x + popup_width and (0 <= mpos < len(clean)): + window.addstr(popup_y + i, ul_x, clean[mpos], attr | curses.A_UNDERLINE) + except curses.error: + pass except curses.error: pass # scroll indicators @@ -571,5 +593,60 @@ def _handle_key(self, key): self._menu_indices = [] self._scroll_offsets = [] else: + # mnemonic handling + ch = None + try: + ch = chr(key) + except Exception: + ch = None + if ch is not None and ch.isprintable(): + letter = ch.lower() + if not self._expanded: + # jump to top-level menu by mnemonic and expand + for idx, menu in enumerate(self._menus): + try: + if not getattr(menu, 'visible', lambda: True)(): + continue + except Exception: + pass + m, _ = extract_mnemonic(menu.label()) + if m and m.lower() == letter: + self._current_menu_index = idx + # expand this menu + self._expanded = True + self._menu_path = [self._menus[self._current_menu_index]] + # select first selectable or the mnemonic-matching item if present + items = list(self._menu_path[0]._children) + first = self._first_selectable_index(items) + self._menu_indices = [first if first is not None else 0] + self._scroll_offsets = [0] + return True + else: + # when expanded: select/activate an item by mnemonic in current popup + cur_menu = self._menu_path[-1] + items = [it for it in list(cur_menu._children) if getattr(it, 'visible', lambda: True)()] + target_idx = None + for i, it in enumerate(items): + m, _ = extract_mnemonic(it.label()) + if m and m.lower() == letter and self._is_selectable(it): + target_idx = i + break + if target_idx is not None: + self._menu_indices[-1] = target_idx + it = items[target_idx] + if it.isMenu(): + # descend into submenu + self._menu_path.append(it) + self._menu_indices.append(0) + self._scroll_offsets.append(0) + else: + if it.enabled(): + self._emit_activation(it) + # collapse after activation + self._expanded = False + self._menu_path = [] + self._menu_indices = [] + self._scroll_offsets = [] + return True handled = False return handled diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index ea3bcaa..d508cb6 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -16,6 +16,7 @@ import time import logging from ...yui_common import * +from .commoncurses import extract_mnemonic, split_mnemonic # Module-level logger for pushbutton curses backend _mod_logger = logging.getLogger("manatools.aui.curses.pushbutton.module") @@ -35,6 +36,11 @@ def __init__(self, parent=None, label=""): self._icon_name = None self._height = 1 # Fixed height - buttons are always one line self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") + # derive mnemonic and cleaned label if present + try: + self._mnemonic, self._mnemonic_index, self._clean_label = split_mnemonic(self._label) + except Exception: + self._mnemonic, self._mnemonic_index, self._clean_label = None, None, self._label if not self._logger.handlers and not logging.getLogger().handlers: for h in _mod_logger.handlers: self._logger.addHandler(h) @@ -48,6 +54,10 @@ def label(self): def setLabel(self, label): self._label = label + try: + self._mnemonic, self._mnemonic_index, self._clean_label = split_mnemonic(self._label) + except Exception: + self._mnemonic, self._mnemonic_index, self._clean_label = None, None, self._label def _create_backend_widget(self): try: @@ -82,8 +92,9 @@ def _set_backend_enabled(self, enabled): def _draw(self, window, y, x, width, height): try: - # Center the button label within available width - button_text = f"[ {self._label} ]" + # Center the button label within available width, show underline for mnemonic + clean = getattr(self, "_clean_label", None) or self._label + button_text = f"[ {clean} ]" # Determine drawing position and clip text if necessary if width <= 0: return @@ -104,6 +115,14 @@ def _draw(self, window, y, x, width, height): try: window.addstr(y, text_x, draw_text, attr) + # underline mnemonic if visible + if self._mnemonic_index is not None: + underline_pos = 2 + self._mnemonic_index # within "[ ]" + if 0 <= underline_pos < len(draw_text): + try: + window.addstr(y, text_x + underline_pos, draw_text[underline_pos], attr | curses.A_UNDERLINE) + except curses.error: + pass except curses.error: # Best-effort: if even this fails, ignore pass @@ -129,6 +148,19 @@ def _handle_key(self, key): except Exception: _mod_logger.error("_handle_key post event error", exc_info=True) return True + # mnemonic letter activates as well when focused + try: + if self._mnemonic: + if key == ord(self._mnemonic) or key == ord(self._mnemonic.upper()): + dlg = self.findDialog() + if dlg is not None: + try: + dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) + except Exception: + pass + return True + except Exception: + pass return False def setIcon(self, icon_name: str): From 866f822a745d74134cceec54e3071b0ad8ced9c3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 20:56:35 +0100 Subject: [PATCH 397/523] Added shortcuts --- test/test_menubar.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_menubar.py b/test/test_menubar.py index 1e49cf2..130cfa8 100644 --- a/test/test_menubar.py +++ b/test/test_menubar.py @@ -55,21 +55,22 @@ def test_menubar_example(backend_name=None): ui.app().setApplicationTitle(f"Menu Bar {backend.value} Test") dlg = factory.createMainDialog() - vbox = factory.createVBox(dlg) + minsize = factory.createMinSize(dlg, 640, 400) + vbox = factory.createVBox(minsize) # Menu bar menubar = factory.createMenuBar(vbox) # File menu - file_menu = menubar.addMenu("File", icon_name="application-menu") + file_menu = menubar.addMenu("&File", icon_name="application-menu") item_open = menubar.addItem(file_menu, "Open", icon_name="document-open", enabled=True) item_close = menubar.addItem(file_menu, "Close", icon_name="window-close", enabled=False) file_menu.addSeparator() - item_exit = menubar.addItem(file_menu, "Exit", icon_name="application-exit", enabled=True) + item_exit = menubar.addItem(file_menu, "E&xit", icon_name="application-exit", enabled=True) # Edit menu - edit_menu = menubar.addMenu("Edit") - menubar.addItem(edit_menu, "Copy", icon_name="edit-copy") + edit_menu = menubar.addMenu("&Edit") + menubar.addItem(edit_menu, "&Copy", icon_name="edit-copy") menubar.addItem(edit_menu, "Paste", icon_name="edit-paste") menubar.addItem(edit_menu, "Cut", icon_name="edit-cut") @@ -103,7 +104,7 @@ def test_menubar_example(backend_name=None): # OK button ctrl_h = factory.createHBox(vbox) - ok_btn = factory.createPushButton(factory.createHCenter(ctrl_h), "OK") + ok_btn = factory.createPushButton(factory.createHCenter(ctrl_h), "&OK") root_logger.info("Opening MenuBar example dialog...") From acc3dc055c351ae0a11b70481b855478b2c4780e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 20:57:01 +0100 Subject: [PATCH 398/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 19449c8..a80b7fc 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -57,7 +57,7 @@ To check/review: [X] askForSaveFileName [ ] YAboutDialog (aka YMGAAboutDialog) [ ] adding factory create alternative methods (e.g. createMultiSelectionBox) - [ ] managing shortcuts + [X] managing shortcuts (only menu and pushbutton) [ ] localization Nice to have: improvements outside YUI API From a1abcd5001e704c5e77a0c65f4bedc0145c74fa2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Fri, 9 Jan 2026 20:57:48 +0100 Subject: [PATCH 399/523] Added a shortcut --- test/testDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testDialog.py b/test/testDialog.py index f8ad535..f2720d5 100644 --- a/test/testDialog.py +++ b/test/testDialog.py @@ -77,7 +77,7 @@ def UIlayout(self, layout): self.factory.createVStretch(layout) align = self.factory.createHVCenter(layout) # Let's test a quitbutton (same handle as Quit menu) - self.quitButton = self.factory.createPushButton(align, "Quit") + self.quitButton = self.factory.createPushButton(align, "&Quit") self.eventManager.addWidgetEvent(self.quitButton, self.onQuitEvent, sendObjOnEvent) # Let's test a cancel event From 8e66f64858f5f03e5dab686e083b11a6e5c8d116 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 10 Jan 2026 12:30:45 +0100 Subject: [PATCH 400/523] Added isTextMode into app --- manatools/aui/yui_curses.py | 4 ++++ manatools/aui/yui_gtk.py | 4 ++++ manatools/aui/yui_qt.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 147b9d4..464f6eb 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -240,6 +240,10 @@ def setApplicationIcon(self, Icon): def applicationIcon(self): """Get the application title.""" return self.__icon + + def isTextMode(self) -> bool: + """Indicate that this is a text-mode (ncurses) application.""" + return True # --- Internal helpers for ncurses file/directory chooser --- def _parse_filter_patterns(self, filter_str: str): diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 89c5a56..662c4fa 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -247,6 +247,10 @@ def setLogo(self, logo_path: str): def logo(self) -> str: return self._logo or "" + + def isTextMode(self) -> bool: + """Indicate that this is not a text-mode (GTK) application.""" + return False def _create_gtk4_filters(self, filter_str: str) -> List[Gtk.FileFilter]: """ diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 3a9bf86..2806ac1 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -194,6 +194,10 @@ def setLogo(self, logo_path: str): def logo(self) -> str: return self._logo or "" + + def isTextMode(self) -> bool: + """Indicate that this is not a text-mode (Qt) application.""" + return False def askForExistingDirectory(self, startDir: str, headline: str): """ From 3182e87d9fbd7a7d67cd07a68169ba08741d41bb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 10 Jan 2026 20:38:09 +0100 Subject: [PATCH 401/523] removed duplicate definition --- manatools/aui/yui_qt.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 2806ac1..e3766d4 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -324,10 +324,7 @@ def createMultiSelectionBox(self, parent, label): def createProgressBar(self, parent, label, max_value=100): return YProgressBarQt(parent, label, max_value) - - def createComboBox(self, parent, label, editable=False): - return YComboBoxQt(parent, label, editable) - + # Alignment helpers def createLeft(self, parent): return YAlignmentQt(parent, horAlign=YAlignmentType.YAlignBegin, vertAlign=YAlignmentType.YAlignUnchanged) From bafaf8ac0a10a39d8e361ab69c420df4ede524c8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 10 Jan 2026 20:40:11 +0100 Subject: [PATCH 402/523] Added deleteAllDialogs() --- manatools/aui/backends/curses/dialogcurses.py | 19 +++++++++++++++++++ manatools/aui/backends/gtk/dialoggtk.py | 19 +++++++++++++++++++ manatools/aui/backends/qt/dialogqt.py | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index 204b3de..527d7b0 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -70,6 +70,25 @@ def isTopmostDialog(self): '''Return whether this dialog is the topmost open dialog.''' return YDialogCurses._open_dialogs[-1] == self if YDialogCurses._open_dialogs else False + @classmethod + def deleteAllDialogs(cls, doThrow=True): + """Delete all open dialogs (best-effort).""" + ok = True + try: + while cls._open_dialogs: + try: + dlg = cls._open_dialogs[-1] + dlg.destroy(doThrow) + except Exception: + ok = False + try: + cls._open_dialogs.pop() + except Exception: + break + except Exception: + ok = False + return ok + def open(self): if not self._window: self._create_backend_widget() diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index bafe254..25e498e 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -202,6 +202,25 @@ def deleteTopmostDialog(cls, doThrow=True): return dialog.destroy(doThrow) return False + @classmethod + def deleteAllDialogs(cls, doThrow=True): + """Delete all open dialogs (best-effort).""" + ok = True + try: + while cls._open_dialogs: + try: + dlg = cls._open_dialogs[-1] + dlg.destroy(doThrow) + except Exception: + ok = False + try: + cls._open_dialogs.pop() + except Exception: + break + except Exception: + ok = False + return ok + @classmethod def currentDialog(cls, doThrow=True): if not cls._open_dialogs: diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index ba27eb1..5272765 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -92,6 +92,25 @@ def deleteTopmostDialog(cls, doThrow=True): dialog = cls._open_dialogs[-1] return dialog.destroy(doThrow) return False + + @classmethod + def deleteAllDialogs(cls, doThrow=True): + """Delete all open dialogs (best-effort).""" + ok = True + try: + while cls._open_dialogs: + try: + dlg = cls._open_dialogs[-1] + dlg.destroy(doThrow) + except Exception: + ok = False + try: + cls._open_dialogs.pop() + except Exception: + break + except Exception: + ok = False + return ok @classmethod def currentDialog(cls, doThrow=True): From 417fcf727afb3703c308485e70cc98da3c5e6d65 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 10 Jan 2026 21:45:25 +0100 Subject: [PATCH 403/523] Added alias to value/setValue as isChecked setChecked --- manatools/aui/backends/curses/checkboxcurses.py | 12 ++++++++++++ manatools/aui/backends/gtk/checkboxgtk.py | 14 +++++++++++++- manatools/aui/backends/qt/checkboxqt.py | 12 ++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/curses/checkboxcurses.py b/manatools/aui/backends/curses/checkboxcurses.py index b19fa0d..178514b 100644 --- a/manatools/aui/backends/curses/checkboxcurses.py +++ b/manatools/aui/backends/curses/checkboxcurses.py @@ -49,6 +49,18 @@ def value(self): def setValue(self, checked): self._is_checked = checked + + def isChecked(self): + ''' + Simplified access to value(): Return 'true' if the CheckBox is checked. + ''' + return self.value() + + def setChecked(self, checked: bool = True): + ''' + Simplified access to setValue(): Set the CheckBox to 'checked' state if 'checked' is true. + ''' + self.setValue(checked) def label(self): return self._label diff --git a/manatools/aui/backends/gtk/checkboxgtk.py b/manatools/aui/backends/gtk/checkboxgtk.py index d9f5d2c..aab1bdd 100644 --- a/manatools/aui/backends/gtk/checkboxgtk.py +++ b/manatools/aui/backends/gtk/checkboxgtk.py @@ -41,7 +41,19 @@ def setValue(self, checked): self._backend_widget.set_active(checked) except Exception: pass - + + def isChecked(self): + ''' + Simplified access to value(): Return 'true' if the CheckBox is checked. + ''' + return self.value() + + def setChecked(self, checked: bool = True): + ''' + Simplified access to setValue(): Set the CheckBox to 'checked' state if 'checked' is true. + ''' + self.setValue(checked) + def label(self): return self._label diff --git a/manatools/aui/backends/qt/checkboxqt.py b/manatools/aui/backends/qt/checkboxqt.py index 24fdf5a..cf90893 100644 --- a/manatools/aui/backends/qt/checkboxqt.py +++ b/manatools/aui/backends/qt/checkboxqt.py @@ -38,6 +38,18 @@ def setValue(self, checked): self._backend_widget.blockSignals(False) except Exception: pass + + def isChecked(self): + ''' + Simplified access to value(): Return 'true' if the CheckBox is checked. + ''' + return self.value() + + def setChecked(self, checked: bool = True): + ''' + Simplified access to setValue(): Set the CheckBox to 'checked' state if 'checked' is true. + ''' + self.setValue(checked) def label(self): return self._label From bc7e5d0e2602ab22bdf2db7007a2f1bc8b300033 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 10 Jan 2026 23:54:21 +0100 Subject: [PATCH 404/523] Managed stretching --- manatools/aui/backends/gtk/selectionboxgtk.py | 25 +-- manatools/aui/backends/qt/checkboxframeqt.py | 146 +++++++----------- manatools/aui/backends/qt/frameqt.py | 42 ++++- 3 files changed, 117 insertions(+), 96 deletions(-) diff --git a/manatools/aui/backends/gtk/selectionboxgtk.py b/manatools/aui/backends/gtk/selectionboxgtk.py index 3a0dc67..fae1d61 100644 --- a/manatools/aui/backends/gtk/selectionboxgtk.py +++ b/manatools/aui/backends/gtk/selectionboxgtk.py @@ -158,10 +158,18 @@ def _create_backend_widget(self): # Use Gtk.ListBox inside a ScrolledWindow for Gtk4 listbox = Gtk.ListBox() listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE if self._multi_selection else Gtk.SelectionMode.SINGLE) - # allow listbox to expand if parent allocates more space + # allow listbox to expand if parent allocates more space (only when logical stretch/weight allows) try: - listbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - listbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + try: + vexpand_flag = bool(self.stretchable(YUIDimension.YD_VERT)) or bool(int(self.weight(YUIDimension.YD_VERT))) + except Exception: + vexpand_flag = bool(self.stretchable(YUIDimension.YD_VERT)) + try: + hexpand_flag = bool(self.stretchable(YUIDimension.YD_HORIZ)) or bool(int(self.weight(YUIDimension.YD_HORIZ))) + except Exception: + hexpand_flag = bool(self.stretchable(YUIDimension.YD_HORIZ)) + listbox.set_vexpand(vexpand_flag) + listbox.set_hexpand(hexpand_flag) except Exception: pass # populate rows @@ -308,15 +316,14 @@ def _create_backend_widget(self): pass sw = Gtk.ScrolledWindow() - # allow scrolled window to expand vertically and horizontally + # allow scrolled window to expand vertically and horizontally only when allowed try: - sw.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - sw.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + sw.set_vexpand(vexpand_flag) + sw.set_hexpand(hexpand_flag) # give a reasonable minimum content height so layout initially shows several rows; # Gtk4 expects pixels — try a conservative estimate (rows * ~20px) min_h = int(getattr(self, "_preferred_rows", 6) * 20) try: - # some Gtk4 bindings expose set_min_content_height sw.set_min_content_height(min_h) except Exception: pass @@ -333,8 +340,8 @@ def _create_backend_widget(self): # also request vexpand on the outer vbox so parent layout sees it can grow try: - vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) - vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) + vbox.set_vexpand(vexpand_flag) + vbox.set_hexpand(hexpand_flag) except Exception: pass diff --git a/manatools/aui/backends/qt/checkboxframeqt.py b/manatools/aui/backends/qt/checkboxframeqt.py index 41c34c9..ea251cc 100644 --- a/manatools/aui/backends/qt/checkboxframeqt.py +++ b/manatools/aui/backends/qt/checkboxframeqt.py @@ -157,14 +157,17 @@ def _attach_child_backend(self): """Attach child's backend widget into content area.""" if not (self._backend_widget and self._content_layout and self.child()): return + + # Safely clear existing content layout try: - # clear content layout while self._content_layout.count(): it = self._content_layout.takeAt(0) if it and it.widget(): it.widget().setParent(None) except Exception: pass + + # Try to obtain/create child's backend widget and insert it try: child = self.child() w = None @@ -172,110 +175,81 @@ def _attach_child_backend(self): w = child.get_backend_widget() except Exception: w = None + if w is None: - # if backend not created, attempt to create it try: child._create_backend_widget() w = child.get_backend_widget() except Exception: w = None + if w is not None: + # determine stretch factor from child's weight()/stretchable() try: - self._content_layout.addWidget(w) + weight = int(child.weight(YUIDimension.YD_VERT)) except Exception: + weight = 0 + try: + stretchable_vert = bool(child.stretchable(YUIDimension.YD_VERT)) + except Exception: + stretchable_vert = False + stretch = weight if weight > 0 else (1 if stretchable_vert else 0) + + # set child's Qt size policy according to logical stretchable flags + try: + sp = w.sizePolicy() try: - w.setParent(self._content_widget) + horiz = QtWidgets.QSizePolicy.Expanding if bool(child.stretchable(YUIDimension.YD_HORIZ)) else QtWidgets.QSizePolicy.Fixed + except Exception: + horiz = QtWidgets.QSizePolicy.Fixed + try: + vert = QtWidgets.QSizePolicy.Expanding if stretch > 0 else QtWidgets.QSizePolicy.Fixed + except Exception: + vert = QtWidgets.QSizePolicy.Fixed + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + # When autoscale/height-for-width semantics are used by the child, + # preserve its existing height-for-width capability. + try: + if hasattr(sp, "setHeightForWidth"): + sp.setHeightForWidth(bool(getattr(child, "_auto_scale", False))) except Exception: pass - # apply current enablement state - self.handleChildrenEnablement(self.value()) - except Exception: - pass - - def _on_checkbox_toggled(self, checked): - try: - self._checked = bool(checked) - if self._auto_enable: - self.handleChildrenEnablement(self._checked) - # notify logical selection change through YWidgetEvent if needed - try: - if self.notify(): - dlg = self.findDialog() - if dlg: - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - except Exception: - pass - except Exception: - pass - - def handleChildrenEnablement(self, isChecked: bool): - """Enable/disable child widgets according to autoEnable and invertAutoEnable.""" - try: - if not self._auto_enable: - return - state = bool(isChecked) - if self._invert_auto: - state = not state - # propagate to logical child(s) - try: - child = self.child() - child.setEnabled(state) - except Exception: - pass - except Exception: - pass - - def addChild(self, child): - super().addChild(child) - self._attach_child_backend() + w.setSizePolicy(sp) + except Exception: + pass - def _set_backend_enabled(self, enabled: bool): - try: - if self._backend_widget is not None: + # add widget with stretch factor if supported + added = False try: - self._backend_widget.setEnabled(bool(enabled)) + # Some PySide/PyQt bindings accept (widget, stretch) + self._content_layout.addWidget(w, stretch) + added = True + except TypeError: + added = False except Exception: - pass - except Exception: - pass - # propagate to logical child(s) - try: - child = self.child() - if child is not None: - child.setEnabled(enabled) - except Exception: - pass + added = False - def setProperty(self, propertyName, val): - try: - if propertyName == "label": - self.setLabel(str(val)) - return True - if propertyName == "value" or propertyName == "checked": - self.setValue(bool(val)) - return True - except Exception: - pass - return False + if not added: + try: + self._content_layout.addWidget(w) + added = True + except Exception: + added = False - def getProperty(self, propertyName): - try: - if propertyName == "label": - return self.label() - if propertyName == "value" or propertyName == "checked": - return self.value() + if not added: + # final fallback: parent the widget to content container + try: + w.setParent(self._content_widget) + added = True + except Exception: + added = False except Exception: + # top-level protection; do not raise to caller pass - return None - def propertySet(self): + # apply current enablement state (outside try that manipulates the layout) try: - props = YPropertySet() - try: - props.add(YProperty("label", YPropertyType.YStringProperty)) - props.add(YProperty("value", YPropertyType.YBoolProperty)) - except Exception: - pass - return props + self.handleChildrenEnablement(self.value()) except Exception: - return None + pass diff --git a/manatools/aui/backends/qt/frameqt.py b/manatools/aui/backends/qt/frameqt.py index cfa3d44..dce88e7 100644 --- a/manatools/aui/backends/qt/frameqt.py +++ b/manatools/aui/backends/qt/frameqt.py @@ -76,7 +76,47 @@ def _attach_child_backend(self): it.widget().setParent(None) except Exception: pass - self._group_layout.addWidget(w) + + # determine stretch factor from child's weight()/stretchable() + try: + try: + weight = int(self.child().weight(YUIDimension.YD_VERT)) + except Exception: + weight = 0 + try: + stretchable_vert = bool(self.child().stretchable(YUIDimension.YD_VERT)) + except Exception: + stretchable_vert = False + stretch = weight if weight > 0 else (1 if stretchable_vert else 0) + except Exception: + stretch = 0 + + # set child's Qt size policy according to logical stretchable flags + try: + sp = w.sizePolicy() + try: + horiz_expand = QtWidgets.QSizePolicy.Expanding if bool(self.child().stretchable(YUIDimension.YD_HORIZ)) else QtWidgets.QSizePolicy.Fixed + except Exception: + horiz_expand = QtWidgets.QSizePolicy.Fixed + try: + vert_expand = QtWidgets.QSizePolicy.Expanding if stretch > 0 else QtWidgets.QSizePolicy.Fixed + except Exception: + vert_expand = QtWidgets.QSizePolicy.Fixed + sp.setHorizontalPolicy(horiz_expand) + sp.setVerticalPolicy(vert_expand) + w.setSizePolicy(sp) + except Exception: + pass + + # add with layout stretch factor so parent layout distributes extra space correctly + try: + self._group_layout.addWidget(w, stretch) + except Exception: + # fallback if addWidget signature doesn't accept stretch in this binding + try: + self._group_layout.addWidget(w) + except Exception: + pass except Exception: pass From e2093d5c60766fc5acf5ea9b559d35ee07c097b0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 10 Jan 2026 23:55:05 +0100 Subject: [PATCH 405/523] Improved stretching and autoscale --- manatools/aui/backends/qt/imageqt.py | 226 +++++++++++++++++++++------ 1 file changed, 175 insertions(+), 51 deletions(-) diff --git a/manatools/aui/backends/qt/imageqt.py b/manatools/aui/backends/qt/imageqt.py index 90db21a..5fe8cf2 100644 --- a/manatools/aui/backends/qt/imageqt.py +++ b/manatools/aui/backends/qt/imageqt.py @@ -23,6 +23,12 @@ def __init__(self, parent=None, imageFileName=""): self._auto_scale = False self._zero_size = {YUIDimension.YD_HORIZ: False, YUIDimension.YD_VERT: False} self._pixmap = None + self._qicon = None + # minimal visible size guard + self._min_w = 64 + self._min_h = 32 + # aspect ratio tracking (w/h). default 1.0 + self._aspect_ratio = 1.0 self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") self._logger.debug("%s.__init__ file=%s", self.__class__.__name__, imageFileName) @@ -43,10 +49,20 @@ def setImage(self, imageFileName): ico = None if ico is not None: try: - # store QIcon (clear any stored QPixmap) and let - # _apply_pixmap pick an appropriate size self._qicon = ico self._pixmap = None + # update aspect ratio from icon's largest available size + try: + sizes = ico.availableSizes() + if sizes: + s = max(sizes, key=lambda sz: sz.width()*sz.height()) + self._aspect_ratio = max(0.0001, float(s.width()) / float(s.height() or 1)) + else: + self._aspect_ratio = 1.0 + except Exception: + self._aspect_ratio = 1.0 + # re-apply constraints and redraw + self._apply_size_policy() self._apply_pixmap() return except Exception: @@ -56,6 +72,16 @@ def setImage(self, imageFileName): if os.path.exists(imageFileName): try: self._pixmap = QtGui.QPixmap(imageFileName) + # update aspect ratio from pixmap + try: + w = self._pixmap.width() + h = self._pixmap.height() + if w > 0 and h > 0: + self._aspect_ratio = max(0.0001, float(w) / float(h)) + except Exception: + pass + # re-apply constraints and redraw + self._apply_size_policy() self._apply_pixmap() except Exception: self._logger.exception("setImage: failed to load QPixmap %s", imageFileName) @@ -88,13 +114,27 @@ def setZeroSize(self, dim, zeroSize=True): def _create_backend_widget(self): try: - # Use a QLabel subclass that notifies this owner on resize so - # we can re-apply scaled pixmaps when the widget changes size. + # Use a QLabel subclass with height-for-width to keep aspect ratio. class _ImageLabel(QtWidgets.QLabel): def __init__(self, owner, *args, **kwargs): super().__init__(*args, **kwargs) self._owner = owner + #def hasHeightForWidth(self): + # # Enable height-for-width only when autoscale is ON + # try: + # return bool(getattr(self._owner, "_auto_scale", False)) + # except Exception: + # return False +# + #def heightForWidth(self, w): + # try: + # ratio = max(0.0001, float(getattr(self._owner, "_aspect_ratio", 1.0))) + # h = int(max(self._owner._min_h, float(w) / ratio)) + # return h + # except Exception: + # return super().heightForWidth(w) +# def resizeEvent(self, ev): super().resizeEvent(ev) try: @@ -102,30 +142,48 @@ def resizeEvent(self, ev): except Exception: pass - def sizeHint(self): - try: - # When autoscale is enabled prefer to give a small size hint - # so layouts can shrink the widget; actual pixmap will be - # scaled on resizeEvent. - if getattr(self._owner, '_auto_scale', False): - return QtCore.QSize(0, 0) - except Exception: - pass - return super().sizeHint() + def sizeHint(self) -> QtCore.QSize: + """ + Return the recommended size for the widget. + + This considers zero-size settings - if a dimension has zero size + enabled, it returns 0 for that dimension. + """ + if self._owner._auto_scale: + # When auto-scaling, size hint depends on parent + return super().sizeHint() + + base_hint = super().sizeHint() - def minimumSizeHint(self): - try: - if getattr(self._owner, '_auto_scale', False): - return QtCore.QSize(0, 0) - except Exception: - pass - return super().minimumSizeHint() + width = 0 if self._owner.stretchable(YUIDimension.YD_HORIZ) else base_hint.width() + height = 0 if self._owner.stretchable(YUIDimension.YD_VERT) else base_hint.height() + return QtCore.QSize(width, height) + + def minimumSizeHint(self) -> QtCore.QSize: + """ + Return the minimum recommended size for the widget. + + This considers zero-size settings. + """ + if self._owner._auto_scale: + # When auto-scaling, can shrink to 0 + return QtCore.QSize(0, 0) + + base_hint = super().minimumSizeHint() + + width = 0 if self._owner.stretchable(YUIDimension.YD_HORIZ) else base_hint.width() + height = 0 if self._owner.stretchable(YUIDimension.YD_VERT) else base_hint.height() + + return QtCore.QSize(width, height) self._backend_widget = _ImageLabel(self) self._backend_widget.setAlignment(QtCore.Qt.AlignCenter) + # enforce a small minimum so it never vanishes + try: + self._backend_widget.setMinimumSize(self._min_w, self._min_h) + except Exception: + pass if self._imageFileName: - # Use setImage which will attempt to resolve theme icons via - # commonqt._resolve_icon and fall back to filesystem loading. try: self.setImage(self._imageFileName) except Exception: @@ -133,6 +191,7 @@ def minimumSizeHint(self): try: if os.path.exists(self._imageFileName): self._pixmap = QtGui.QPixmap(self._imageFileName) + self._apply_size_policy() self._apply_pixmap() except Exception: pass @@ -145,15 +204,56 @@ def _apply_size_policy(self): try: if getattr(self, '_backend_widget', None) is None: return - horiz = QtWidgets.QSizePolicy.Policy.Expanding if self._auto_scale or self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Policy.Fixed - vert = QtWidgets.QSizePolicy.Policy.Expanding if self._auto_scale or self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Policy.Fixed - try: - sp = self._backend_widget.sizePolicy() - sp.setHorizontalPolicy(horiz) - sp.setVerticalPolicy(vert) - self._backend_widget.setSizePolicy(sp) - except Exception: - pass + + if self._auto_scale: + self._backend_widget.setScaledContents(False) + # When auto-scaling, maintain aspect ratio by default + self._backend_widget.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding) + else: + self._backend_widget.setScaledContents(True) + # Respect stretch flags; in autoscale allow height-for-width by not fixing vertical + horiz = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_HORIZ) else QtWidgets.QSizePolicy.Fixed + vert = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed + try: + sp = self._backend_widget.sizePolicy() + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + self._backend_widget.setSizePolicy(sp) + except Exception: + self._logger.exception("_apply_size_policy: setSizePolicy failed") + + # Reset caps: in autoscale mode allow full growth in both axes. + try: + QWIDGETSIZE_MAX = getattr(QtWidgets, "QWIDGETSIZE_MAX", 16777215) + if self._auto_scale: + self._backend_widget.setMaximumWidth(QWIDGETSIZE_MAX) + self._backend_widget.setMaximumHeight(QWIDGETSIZE_MAX) + else: + # Non-autoscale: cap non-stretch axes to source size or a sane default + pm_w = pm_h = None + if isinstance(getattr(self, "_pixmap", None), QtGui.QPixmap) and not self._pixmap.isNull(): + pm_w, pm_h = self._pixmap.width(), self._pixmap.height() + elif getattr(self, "_qicon", None) is not None: + pm_w, pm_h = 64, 64 + if self.stretchable(YUIDimension.YD_HORIZ): + self._backend_widget.setMaximumWidth(QWIDGETSIZE_MAX) + else: + if pm_w: + self._backend_widget.setMaximumWidth(max(self._backend_widget.maximumWidth(), pm_w)) + if self.stretchable(YUIDimension.YD_VERT): + self._backend_widget.setMaximumHeight(QWIDGETSIZE_MAX) + else: + if pm_h: + self._backend_widget.setMaximumHeight(max(self._backend_widget.maximumHeight(), pm_h)) + except Exception: + self._logger.exception("_apply_size_policy: max size tuning failed") + + # Keep a small minimum visible size + try: + self._backend_widget.setMinimumSize(self._min_w, self._min_h) + except Exception: + pass except Exception: self._logger.exception("_apply_size_policy failed") @@ -161,30 +261,53 @@ def _apply_pixmap(self): try: if getattr(self, '_backend_widget', None) is None: return - # If neither a QPixmap nor a QIcon is available, clear the widget. - if getattr(self, '_qicon', None) is None and not getattr(self, '_pixmap', None): - self._backend_widget.clear() - return - # If we have a QIcon (resolved from theme/name) prefer it as it can provide - # appropriately scaled pixmaps. Otherwise use the stored QPixmap. - try: - if getattr(self, '_qicon', None) is not None: - if self._auto_scale and self._backend_widget.size().isValid(): - pm = self._qicon.pixmap(self._backend_widget.size()) - else: - pm = self._qicon.pixmap(64, 64) + size = self._backend_widget.size() + if not size.isValid(): + size = QtCore.QSize(self._min_w, self._min_h) + + src_icon = self._qicon + src_pm = self._pixmap if isinstance(self._pixmap, QtGui.QPixmap) and not self._pixmap.isNull() else None + + # AutoScale ON => keep aspect ratio to widget size (height-for-width will grow height as width grows) + if self._auto_scale: + # compute target size from current width and aspect ratio; clamp to widget size + try: + ratio = max(0.0001, float(self._aspect_ratio)) + except Exception: + ratio = 1.0 + target_w = max(1, size.width()) + target_h = int(max(self._min_h, target_w / ratio)) + target_h = min(target_h, size.height()) + + if src_icon is not None: + pm = src_icon.pixmap(QtCore.QSize(target_w, target_h)) if pm and not pm.isNull(): self._backend_widget.setPixmap(pm) return - except Exception: - pass - - if self._pixmap: - if self._auto_scale and self._backend_widget.width() > 1 and self._backend_widget.height() > 1: - scaled = self._pixmap.scaled(self._backend_widget.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + if src_pm is not None: + scaled = src_pm.scaled(QtCore.QSize(target_w, target_h), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self._backend_widget.setPixmap(scaled) else: - self._backend_widget.setPixmap(self._pixmap) + self._backend_widget.clear() + return + + # AutoScale OFF => stretch along selected dimensions, allow deformation + target_w = size.width() if self.stretchable(YUIDimension.YD_HORIZ) else (src_pm.width() if src_pm else self._min_w) + target_h = size.height() if self.stretchable(YUIDimension.YD_VERT) else (src_pm.height() if src_pm else self._min_h) + target_w = max(1, target_w) + target_h = max(1, target_h) + + if src_icon is not None: + pm = src_icon.pixmap(QtCore.QSize(target_w, target_h)) + if pm and not pm.isNull(): + self._backend_widget.setPixmap(pm) + return + + if src_pm is not None: + scaled = src_pm.scaled(target_w, target_h, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) + self._backend_widget.setPixmap(scaled) + else: + self._backend_widget.clear() except Exception: self._logger.exception("_apply_pixmap failed") @@ -195,5 +318,6 @@ def setStretchable(self, dim, new_stretch): pass try: self._apply_size_policy() + self._apply_pixmap() except Exception: pass From 23399bcdc7c3144835c00294ea135630aa96d84f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 00:28:46 +0100 Subject: [PATCH 406/523] Fixed addItem --- manatools/aui/backends/qt/comboboxqt.py | 153 ++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 11 deletions(-) diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index 63105e0..8f08bf4 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -9,9 +9,10 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets +from PySide6 import QtWidgets, QtGui import logging from ...yui_common import * +from .commonqt import _resolve_icon as _qt_resolve_icon class YComboBoxQt(YSelectionWidget): def __init__(self, parent=None, label="", editable=False): @@ -21,6 +22,7 @@ def __init__(self, parent=None, label="", editable=False): self._value = "" self._selected_items = [] self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._combo_widget = None def widgetClass(self): return "YComboBox" @@ -42,7 +44,16 @@ def setValue(self, text): if item.label() == text: self._selected_items.append(item) break - + try: + # ensure selected flags consistent: only one selected + for it in self._items: + try: + it.setSelected(it.label() == text) + except Exception: + pass + except Exception: + pass + def editable(self): return self._editable @@ -63,11 +74,52 @@ def _create_backend_widget(self): # Add items to combo box for item in self._items: - combo.addItem(item.label()) + try: + icon_name = None + try: + fn = getattr(item, 'iconName', None) + icon_name = fn() if callable(fn) else fn + except Exception: + icon_name = None + qicon = None + if icon_name: + try: + qicon = _qt_resolve_icon(icon_name) + except Exception: + qicon = None + if qicon is not None: + combo.addItem(qicon, item.label()) + else: + combo.addItem(item.label()) + except Exception: + try: + combo.addItem(item.label()) + except Exception: + pass + combo.currentTextChanged.connect(self._on_text_changed) # also handle index change (safer for some input methods) combo.currentIndexChanged.connect(lambda idx: self._on_text_changed(combo.currentText())) + + # pick initial selection: first item marked selected + try: + selected_idx = -1 + for idx, it in enumerate(self._items): + try: + if it.selected(): + selected_idx = idx + break + except Exception: + pass + if selected_idx >= 0: + combo.setCurrentIndex(selected_idx) + self._value = self._items[selected_idx].label() + self._selected_items = [self._items[selected_idx]] + else: + self._selected_items = [] + except Exception: + pass layout.addWidget(combo) self._backend_widget = container @@ -97,16 +149,95 @@ def _set_backend_enabled(self, enabled): except Exception: pass + # New: add single item at runtime + def addItem(self, item): + super().addItem(item) + new_item = self._items[-1] + + # update backend widget if present + if getattr(self, "_combo_widget", None): + try: + icon_name = None + try: + fn = getattr(new_item, 'iconName', None) + icon_name = fn() if callable(fn) else fn + except Exception: + icon_name = None + qicon = None + if icon_name: + try: + qicon = _qt_resolve_icon(icon_name) + except Exception: + qicon = None + if qicon is not None: + self._combo_widget.addItem(qicon, new_item.label()) + else: + self._combo_widget.addItem(new_item.label()) + # reflect selection state + try: + if new_item.selected(): + # ensure only one is selected + for it in self._items: + try: + it.setSelected(False) + except Exception: + pass + new_item.setSelected(True) + idx = len(self._items) - 1 + self._combo_widget.setCurrentIndex(idx) + self._selected_items = [new_item] + self._value = new_item.label() + except Exception: + pass + except Exception: + pass + + # New: delete all items at runtime + def deleteAllItems(self): + try: + self._items = [] + self._selected_items = [] + self._value = "" + except Exception: + pass + if getattr(self, "_combo_widget", None): + try: + self._combo_widget.clear() + except Exception: + # fallback: recreate widget if clear not supported + try: + parent = self._combo_widget.parent() + layout = self._combo_widget.parent().layout() if parent is not None else None + if layout is not None: + try: + layout.removeWidget(self._combo_widget) + self._combo_widget.deleteLater() + except Exception: + pass + except Exception: + pass + def _on_text_changed(self, text): - self._value = text - # Update selected items - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break + # keep previous behaviour, but update model selection flags robustly + try: + self._value = text + except Exception: + self._value = text + # update selected items: only one selected in combo + try: + self._selected_items = [] + for it in self._items: + try: + was = (it.label() == text) + it.setSelected(was) + if was: + self._selected_items.append(it) + except Exception: + pass + except Exception: + self._selected_items = [] + # notify if self.notify(): - # Post selection-changed event to containing dialog try: dlg = self.findDialog() if dlg is not None: From 67f13ed4609593615905bdddaae8090df64b8879 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 00:35:54 +0100 Subject: [PATCH 407/523] remove old selection --- manatools/aui/backends/qt/comboboxqt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index 8f08bf4..81c2482 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -225,6 +225,9 @@ def _on_text_changed(self, text): self._value = text # update selected items: only one selected in combo try: + old_item = self._selected_items[0] if self._selected_items else None + if old_item: + old_item.setSelected( False ) self._selected_items = [] for it in self._items: try: From e559c1bb6e0ec88e5edc53c6b2610c409dc17747 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 00:52:51 +0100 Subject: [PATCH 408/523] FIxed addItem and added deleteAllItems --- .../aui/backends/curses/comboboxcurses.py | 58 ++++- manatools/aui/backends/gtk/comboboxgtk.py | 230 +++++++++++++----- 2 files changed, 219 insertions(+), 69 deletions(-) diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py index a3405ca..78b6a78 100644 --- a/manatools/aui/backends/curses/comboboxcurses.py +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -55,13 +55,20 @@ def value(self): def setValue(self, text): self._value = text - # Update selected items - self._selected_items = [] - for item in self._items: - if item.label() == text: - self._selected_items.append(item) - break - + # Update selected items and ensure only one item selected + try: + self._selected_items = [] + for it in self._items: + try: + sel = (it.label() == text) + it.setSelected(sel) + if sel: + self._selected_items.append(it) + except Exception: + pass + except Exception: + self._selected_items = [] + def editable(self): return self._editable @@ -272,3 +279,40 @@ def _handle_key(self, key): handled = False return handled + + # New: addItem at runtime + def addItem(self, item): + try: + super().addItem(item) + except Exception: + try: + if isinstance(item, str): + super().addItem(item) + else: + self._items.append(item) + except Exception: + return + try: + new_item = self._items[-1] + new_item.setIndex(len(self._items) - 1) + except Exception: + pass + + # enforce single-selection semantics + try: + if new_item.selected(): + for it in self._items[:-1]: + try: + it.setSelected(False) + except Exception: + pass + self._value = new_item.label() + self._selected_items = [new_item] + except Exception: + pass + + def deleteAllItems(self): + super().deleteAllItems() + self._value = "" + self._expanded = False + self._hover_index = 0 diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index 10b4cce..f364291 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -19,6 +19,7 @@ import os import logging from ...yui_common import * +from .commongtk import _resolve_icon class YComboBoxGtk(YSelectionWidget): @@ -91,21 +92,37 @@ def _create_backend_widget(self): # Build a simple Gtk.DropDown backed by a Gtk.StringList (if available) try: if hasattr(Gtk, "StringList") and hasattr(Gtk, "DropDown"): - model = Gtk.StringList() + # keep model reference for runtime updates + self._string_list_model = Gtk.StringList() for it in self._items: - model.append(it.label()) - dropdown = Gtk.DropDown.new(model, None) - # select initial value - if self._value: - for idx, it in enumerate(self._items): - if it.label() == self._value: - dropdown.set_selected(idx) + self._string_list_model.append(it.label()) + dropdown = Gtk.DropDown.new(self._string_list_model, None) + # prefer explicit selected item flag in model; only one allowed + selected_idx = -1 + for idx, it in enumerate(self._items): + try: + if it.selected(): + selected_idx = idx break + except Exception: + pass + if selected_idx >= 0: + dropdown.set_selected(selected_idx) + self._value = self._items[selected_idx].label() + self._selected_items = [self._items[selected_idx]] + else: + # fallback to explicit value string if provided + if self._value: + for idx, it in enumerate(self._items): + if it.label() == self._value: + dropdown.set_selected(idx) + self._selected_items = [it] + break dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) self._combo_widget = dropdown hbox.append(dropdown) else: - # fallback: simple Gtk.Button that cycles items on click (very simple) + # fallback: simple Gtk.Button that cycles items on click (very simple) btn = Gtk.Button(label=self._value or (self._items[0].label() if self._items else "")) btn.connect("clicked", self._on_fallback_button_clicked) self._combo_widget = btn @@ -178,65 +195,70 @@ def _on_text_changed(self, entry): dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) def _on_changed_dropdown(self, dropdown): + # Prefer using the selected index to get a reliable label + idx = None try: - # Prefer using the selected index to get a reliable label + idx = dropdown.get_selected() + except Exception: idx = None + + if isinstance(idx, int) and 0 <= idx < len(self._items): + self._value = self._items[idx].label() + else: + # Fallback: try to extract text from the selected-item object + val = None try: - idx = dropdown.get_selected() + val = dropdown.get_selected_item() except Exception: - idx = None - - if isinstance(idx, int) and 0 <= idx < len(self._items): - self._value = self._items[idx].label() - else: - # Fallback: try to extract text from the selected-item object val = None - try: - val = dropdown.get_selected_item() - except Exception: - val = None - - self._value = "" - if isinstance(val, str): - self._value = val - elif val is not None: - # Try common accessor names that GTK objects may expose - for meth in ("get_string", "get_text", "get_value", "get_label", "get_name", "to_string"): - try: - fn = getattr(val, meth, None) - if callable(fn): - v = fn() - if isinstance(v, str) and v: - self._value = v - break - except Exception: - continue - # Try properties if available - if not self._value: - try: - props = getattr(val, "props", None) - if props: - for attr in ("string", "value", "label", "name", "text"): - try: - pv = getattr(props, attr) - if isinstance(pv, str) and pv: - self._value = pv - break - except Exception: - pass - except Exception: - pass - # final fallback to str() - if not self._value: - try: - self._value = str(val) - except Exception: - self._value = "" - # update selected_items using reliable labels - self._selected_items = [it for it in self._items if it.label() == self._value][:1] - except Exception: - pass + self._value = "" + if isinstance(val, str): + self._value = val + elif val is not None: + # Try common accessor names that GTK objects may expose + for meth in ("get_string", "get_text", "get_value", "get_label", "get_name", "to_string"): + try: + fn = getattr(val, meth, None) + if callable(fn): + v = fn() + if isinstance(v, str) and v: + self._value = v + break + except Exception: + continue + # Try properties if available + if not self._value: + try: + props = getattr(val, "props", None) + if props: + for attr in ("string", "value", "label", "name", "text"): + try: + pv = getattr(props, attr) + if isinstance(pv, str) and pv: + self._value = pv + break + except Exception: + pass + except Exception: + pass + # final fallback to str() + if not self._value: + try: + self._value = str(val) + except Exception: + self._value = "" + + # update selected_items and ensure only one item is selected in model + self._selected_items = [] + for it in self._items: + try: + sel = (it.label() == self._value) + it.setSelected(sel) + if sel: + self._selected_items.append(it) + except Exception: + pass if self.notify(): dlg = self.findDialog() @@ -246,3 +268,87 @@ def _on_changed_dropdown(self, dropdown): self._logger.debug("_on_changed_dropdown: value=%s selected_items=%s", self._value, [it.label() for it in self._selected_items]) except Exception: pass + + # Runtime: add a single item (model + view) + def addItem(self, item): + try: + super().addItem(item) + except Exception: + # fall back if super fails + try: + if isinstance(item, str): + super().addItem(item) + else: + self._items.append(item) + except Exception: + return + try: + new_item = self._items[-1] + new_item.setIndex(len(self._items) - 1) + except Exception: + return + + # ensure only one selected in combo semantics + try: + if new_item.selected(): + for it in self._items[:-1]: + try: + it.setSelected(False) + except Exception: + pass + new_item.setSelected(True) + self._value = new_item.label() + self._selected_items = [new_item] + except Exception: + pass + + # update GTK backing widget + if getattr(self, "_combo_widget", None): + try: + if isinstance(self._combo_widget, Gtk.Entry): + # nothing special: entries are freeform + pass + elif hasattr(self, "_string_list_model") and isinstance(self._string_list_model, Gtk.StringList): + try: + self._string_list_model.append(new_item.label()) + # if the new item was selected, update dropdown selection + if new_item.selected(): + self._combo_widget.set_selected(len(self._string_list_model) - 1) + except Exception: + pass + else: + # fallback: update button label if single item and selected + try: + if getattr(self._combo_widget, "set_label", None) and new_item.selected(): + self._combo_widget.set_label(new_item.label()) + except Exception: + pass + except Exception: + pass + + def deleteAllItems(self): + super().deleteAllItems() + self._value = "" + # update GTK widgets + if getattr(self, "_combo_widget", None): + try: + if hasattr(self, "_string_list_model") and isinstance(self._string_list_model, Gtk.StringList): + try: + # recreate model + self._string_list_model = Gtk.StringList() + self._combo_widget.set_model(self._string_list_model) + except Exception: + pass + elif isinstance(self._combo_widget, Gtk.Entry): + try: + self._combo_widget.set_text("") + except Exception: + pass + else: + try: + if getattr(self._combo_widget, "set_label", None): + self._combo_widget.set_label("") + except Exception: + pass + except Exception: + pass From 2366512b78cdec496a7626266d3e03b0309923cc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 12:37:45 +0100 Subject: [PATCH 409/523] missing symbols --- manatools/aui/yui.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/manatools/aui/yui.py b/manatools/aui/yui.py index 35810b6..1c7cae7 100644 --- a/manatools/aui/yui.py +++ b/manatools/aui/yui.py @@ -130,11 +130,13 @@ def YUI_ensureUICreated(): YWidget, YSingleChildContainerWidget, YSelectionWidget, YSimpleInputField, YItem, YTreeItem, YTableHeader, YTableItem, YTableCell, # Events - YEvent, YWidgetEvent, YKeyEvent, YMenuEvent, YCancelEvent, + YEvent, YWidgetEvent, YKeyEvent, YMenuEvent, YTimeoutEvent, YCancelEvent, # Exceptions - YUIException, YUIWidgetNotFoundException, YUINoDialogException, - # Other common classes - YProperty, YPropertyValue, YPropertySet, YShortcut + YUIException, YUIWidgetNotFoundException, YUINoDialogException, YUIInvalidWidgetException, + # Menu model + YMenuItem, + # Property system + YPropertyType, YProperty, YPropertyValue, YPropertySet, YShortcut ) # Re-export everything for easy importing @@ -144,8 +146,9 @@ def YUI_ensureUICreated(): 'YEventType', 'YEventReason', 'YCheckBoxState', 'YButtonRole', 'YWidget', 'YSingleChildContainerWidget', 'YSelectionWidget', 'YSimpleInputField', 'YItem', 'YTreeItem', 'YTableHeader', 'YTableItem', 'YTableCell', - 'YEvent', 'YWidgetEvent', 'YKeyEvent', 'YMenuEvent', 'YCancelEvent', - 'YUIException', 'YUIWidgetNotFoundException', 'YUINoDialogException', - 'YProperty', 'YPropertyValue', 'YPropertySet', 'YShortcut', + 'YEvent', 'YWidgetEvent', 'YKeyEvent', 'YMenuEvent', 'YTimeoutEvent', 'YCancelEvent', + 'YUIException', 'YUIWidgetNotFoundException', 'YUINoDialogException', 'YUIInvalidWidgetException', + 'YMenuItem', + 'YPropertyType', 'YProperty', 'YPropertyValue', 'YPropertySet', 'YShortcut', 'Backend' ] \ No newline at end of file From 8828334dce39bcb9138bd6160cc9cefcdc28d77c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 12:38:28 +0100 Subject: [PATCH 410/523] Exception logging --- manatools/aui/backends/gtk/comboboxgtk.py | 209 ++++++++++++++++------ 1 file changed, 150 insertions(+), 59 deletions(-) diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index f364291..3208ed0 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -23,7 +23,18 @@ class YComboBoxGtk(YSelectionWidget): + """ + GTK4 backend for a simple combo/select widget. + Provides editable and non-editable variants using Gtk.Entry, Gtk.DropDown or a simple cycling Gtk.Button. + Uses logging extensively to surface runtime issues and fallbacks. + """ def __init__(self, parent=None, label="", editable=False): + """ + Initialize backend combobox. + - parent: parent widget + - label: optional label text + - editable: if True use an Entry, otherwise use DropDown or fallback + """ super().__init__(parent) self._label = label self._editable = editable @@ -31,7 +42,7 @@ def __init__(self, parent=None, label="", editable=False): self._selected_items = [] self._combo_widget = None self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") - + def widgetClass(self): return "YComboBox" @@ -39,6 +50,10 @@ def value(self): return self._value def setValue(self, text): + """ + Set the current displayed value (tries to update backing GTK widget). + Logs any exceptions encountered while updating the GTK widget. + """ self._value = text if self._combo_widget: try: @@ -57,12 +72,16 @@ def setValue(self, text): # update selected_items self._selected_items = [it for it in self._items if it.label() == text][:1] except Exception: - pass + self._logger.exception("setValue: failed to update backend widget with text=%r", text) def editable(self): return self._editable def _create_backend_widget(self): + """ + Create GTK widgets backing the combo: label + entry/dropdown/button. + Uses conservative fallbacks and logs failures for diagnostics. + """ hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) if self._label: @@ -71,11 +90,14 @@ def _create_backend_widget(self): if hasattr(label, "set_xalign"): label.set_xalign(0.0) except Exception: - pass + self._logger.exception("_create_backend_widget: label.set_xalign failed") try: hbox.append(label) except Exception: - hbox.add(label) + try: + hbox.add(label) + except Exception: + self._logger.exception("_create_backend_widget: failed to add label to hbox") # For Gtk4 there is no ComboBoxText; try DropDown for non-editable, # and Entry for editable combos (simple fallback). @@ -87,7 +109,10 @@ def _create_backend_widget(self): try: hbox.append(entry) except Exception: - hbox.add(entry) + try: + hbox.add(entry) + except Exception: + self._logger.exception("_create_backend_widget: failed to add editable entry to hbox") else: # Build a simple Gtk.DropDown backed by a Gtk.StringList (if available) try: @@ -105,45 +130,71 @@ def _create_backend_widget(self): selected_idx = idx break except Exception: - pass + self._logger.exception("_create_backend_widget: error checking item.selected() for index %d", idx) if selected_idx >= 0: - dropdown.set_selected(selected_idx) - self._value = self._items[selected_idx].label() - self._selected_items = [self._items[selected_idx]] + try: + dropdown.set_selected(selected_idx) + self._value = self._items[selected_idx].label() + self._selected_items = [self._items[selected_idx]] + except Exception: + self._logger.exception("_create_backend_widget: failed to set selected index %d", selected_idx) else: # fallback to explicit value string if provided if self._value: for idx, it in enumerate(self._items): - if it.label() == self._value: - dropdown.set_selected(idx) - self._selected_items = [it] - break - dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) - self._combo_widget = dropdown - hbox.append(dropdown) + try: + if it.label() == self._value: + dropdown.set_selected(idx) + self._selected_items = [it] + break + except Exception: + self._logger.exception("_create_backend_widget: matching value to items failed at index %d", idx) + try: + dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) + self._combo_widget = dropdown + hbox.append(dropdown) + except Exception: + self._logger.exception("_create_backend_widget: failed to connect/append dropdown") else: # fallback: simple Gtk.Button that cycles items on click (very simple) btn = Gtk.Button(label=self._value or (self._items[0].label() if self._items else "")) btn.connect("clicked", self._on_fallback_button_clicked) self._combo_widget = btn - hbox.append(btn) + try: + hbox.append(btn) + except Exception: + try: + hbox.add(btn) + except Exception: + self._logger.exception("_create_backend_widget: failed to add fallback button") except Exception: # final fallback: entry + self._logger.exception("_create_backend_widget: unexpected failure building non-editable control; falling back to Entry") entry = Gtk.Entry() entry.set_text(self._value) entry.connect("changed", self._on_text_changed) self._combo_widget = entry - hbox.append(entry) + try: + hbox.append(entry) + except Exception: + try: + hbox.add(entry) + except Exception: + self._logger.exception("_create_backend_widget: failed to add fallback entry") self._backend_widget = hbox - self._backend_widget.set_sensitive(self._enabled) try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + self._backend_widget.set_sensitive(self._enabled) except Exception: - pass + self._logger.exception("_create_backend_widget: failed to set backend widget sensitivity") + # Allow logger to raise if misconfigured so failures are visible during debugging + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) def _set_backend_enabled(self, enabled): - """Enable/disable the combobox/backing widget and its entry/dropdown.""" + """ + Enable/disable the combobox/backing widget and its entry/dropdown. + Logs problems when setting sensitivity fails. + """ try: # prefer to enable the primary control if present ctl = getattr(self, "_combo_widget", None) @@ -151,20 +202,23 @@ def _set_backend_enabled(self, enabled): try: ctl.set_sensitive(enabled) except Exception: - pass + self._logger.exception("_set_backend_enabled: failed to set_sensitive on primary control") except Exception: - pass + self._logger.exception("_set_backend_enabled: error accessing primary control") try: if self._backend_widget is not None: try: self._backend_widget.set_sensitive(enabled) except Exception: - pass + self._logger.exception("_set_backend_enabled: failed to set_sensitive on backend widget") except Exception: - pass + self._logger.exception("_set_backend_enabled: error accessing backend widget") def _on_fallback_button_clicked(self, btn): - # naive cycle through items + """ + Cycle through items when using the fallback button control. + Logs unexpected issues. + """ if not self._items: return current = btn.get_label() @@ -175,35 +229,57 @@ def _on_fallback_button_clicked(self, btn): except Exception: idx = 0 new = labels[idx] - btn.set_label(new) - self.setValue(new) - if self.notify(): - dlg = self.findDialog() - if dlg: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + try: + btn.set_label(new) + self.setValue(new) + if self.notify(): + dlg = self.findDialog() + if dlg: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + self._logger.exception("_on_fallback_button_clicked: failed to cycle/set selection") def _on_text_changed(self, entry): + """ + Handler for editable text changes. Updates internal value and notifies dialog. + """ try: text = entry.get_text() except Exception: text = "" + self._logger.exception("_on_text_changed: failed to read entry text") self._value = text - self._selected_items = [it for it in self._items if it.label() == self._value][:1] + try: + self._selected_items = [it for it in self._items if it.label() == self._value][:1] + except Exception: + self._logger.exception("_on_text_changed: error updating selected_items") + self._selected_items = [] if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + self._logger.exception("_on_text_changed: failed to notify dialog") def _on_changed_dropdown(self, dropdown): + """ + Handler for Gtk.DropDown selection changes. Attempts robust extraction of selected label. + """ # Prefer using the selected index to get a reliable label idx = None try: idx = dropdown.get_selected() except Exception: idx = None + self._logger.exception("_on_changed_dropdown: failed to get_selected() from dropdown") if isinstance(idx, int) and 0 <= idx < len(self._items): - self._value = self._items[idx].label() + try: + self._value = self._items[idx].label() + except Exception: + self._logger.exception("_on_changed_dropdown: failed to read label from items at index %d", idx) + self._value = "" else: # Fallback: try to extract text from the selected-item object val = None @@ -211,6 +287,7 @@ def _on_changed_dropdown(self, dropdown): val = dropdown.get_selected_item() except Exception: val = None + self._logger.exception("_on_changed_dropdown: failed to get_selected_item()") self._value = "" if isinstance(val, str): @@ -226,6 +303,7 @@ def _on_changed_dropdown(self, dropdown): self._value = v break except Exception: + self._logger.debug("_on_changed_dropdown: accessor %s failed on selected item", meth) continue # Try properties if available if not self._value: @@ -239,15 +317,16 @@ def _on_changed_dropdown(self, dropdown): self._value = pv break except Exception: - pass + self._logger.debug("_on_changed_dropdown: props accessor %s failed", attr) except Exception: - pass + self._logger.exception("_on_changed_dropdown: error inspecting props on selected item") # final fallback to str() if not self._value: try: self._value = str(val) except Exception: self._value = "" + self._logger.exception("_on_changed_dropdown: str(selected_item) failed") # update selected_items and ensure only one item is selected in model self._selected_items = [] @@ -258,34 +337,40 @@ def _on_changed_dropdown(self, dropdown): if sel: self._selected_items.append(it) except Exception: - pass + self._logger.exception("_on_changed_dropdown: failed updating selection for an item") if self.notify(): - dlg = self.findDialog() - if dlg is not None: - dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) - try: - self._logger.debug("_on_changed_dropdown: value=%s selected_items=%s", self._value, [it.label() for it in self._selected_items]) - except Exception: - pass + try: + dlg = self.findDialog() + if dlg is not None: + dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) + except Exception: + self._logger.exception("_on_changed_dropdown: failed to notify dialog") + # Allow logger to raise if misconfigured so failures are visible during debugging + self._logger.debug("_on_changed_dropdown: value=%s selected_items=%s", self._value, [it.label() for it in self._selected_items]) # Runtime: add a single item (model + view) def addItem(self, item): + """ + Add an item at runtime. Updates internal state and GTK backing model when possible. + """ try: super().addItem(item) except Exception: - # fall back if super fails try: if isinstance(item, str): super().addItem(item) else: self._items.append(item) + self._logger.debug("addItem: fallback appended item %r", item) except Exception: + self._logger.exception("addItem: failed to add item %r", item) return try: new_item = self._items[-1] new_item.setIndex(len(self._items) - 1) except Exception: + self._logger.exception("addItem: failed to set index on new item") return # ensure only one selected in combo semantics @@ -295,12 +380,12 @@ def addItem(self, item): try: it.setSelected(False) except Exception: - pass + self._logger.exception("addItem: failed to clear selection on existing item") new_item.setSelected(True) self._value = new_item.label() self._selected_items = [new_item] except Exception: - pass + self._logger.exception("addItem: failed while enforcing single-selection semantics") # update GTK backing widget if getattr(self, "_combo_widget", None): @@ -315,19 +400,25 @@ def addItem(self, item): if new_item.selected(): self._combo_widget.set_selected(len(self._string_list_model) - 1) except Exception: - pass + self._logger.exception("addItem: failed to update string_list_model with new item") else: # fallback: update button label if single item and selected try: if getattr(self._combo_widget, "set_label", None) and new_item.selected(): self._combo_widget.set_label(new_item.label()) except Exception: - pass + self._logger.exception("addItem: failed to update fallback combo widget label") except Exception: - pass + self._logger.exception("addItem: unexpected error while updating backend widget") def deleteAllItems(self): - super().deleteAllItems() + """ + Remove all items and reset backend widgets. Logs any issues so runtime problems are visible. + """ + try: + super().deleteAllItems() + except Exception: + self._logger.exception("deleteAllItems: super().deleteAllItems() failed") self._value = "" # update GTK widgets if getattr(self, "_combo_widget", None): @@ -338,17 +429,17 @@ def deleteAllItems(self): self._string_list_model = Gtk.StringList() self._combo_widget.set_model(self._string_list_model) except Exception: - pass + self._logger.exception("deleteAllItems: failed to recreate string_list_model") elif isinstance(self._combo_widget, Gtk.Entry): try: self._combo_widget.set_text("") except Exception: - pass + self._logger.exception("deleteAllItems: failed to clear entry text") else: try: if getattr(self._combo_widget, "set_label", None): self._combo_widget.set_label("") except Exception: - pass + self._logger.exception("deleteAllItems: failed to clear fallback widget label") except Exception: - pass + self._logger.exception("deleteAllItems: unexpected error while updating backend widget") From 75bcbe9ea4592ccdd3ce9946f25423fe2ce53625 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 13:15:38 +0100 Subject: [PATCH 411/523] Fixed setLabel at runtime --- manatools/aui/backends/gtk/comboboxgtk.py | 39 ++++++++++++++++++++--- manatools/aui/backends/qt/comboboxqt.py | 29 ++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index 3208ed0..a7f8b86 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -42,6 +42,8 @@ def __init__(self, parent=None, label="", editable=False): self._selected_items = [] self._combo_widget = None self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") + # reference to the visible label widget (if any) + self._label_widget = None def widgetClass(self): return "YComboBox" @@ -86,6 +88,7 @@ def _create_backend_widget(self): if self._label: label = Gtk.Label(label=self._label) + self._label_widget = label try: if hasattr(label, "set_xalign"): label.set_xalign(0.0) @@ -94,10 +97,7 @@ def _create_backend_widget(self): try: hbox.append(label) except Exception: - try: - hbox.add(label) - except Exception: - self._logger.exception("_create_backend_widget: failed to add label to hbox") + self._logger.exception("_create_backend_widget: failed to add label to hbox") # For Gtk4 there is no ComboBoxText; try DropDown for non-editable, # and Entry for editable combos (simple fallback). @@ -189,6 +189,37 @@ def _create_backend_widget(self): self._logger.exception("_create_backend_widget: failed to set backend widget sensitivity") # Allow logger to raise if misconfigured so failures are visible during debugging self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + + def setLabel(self, new_label: str): + """Set logical label and update/create the visual Gtk.Label in the box.""" + try: + super().setLabel(new_label) + if self._label_widget is not None: + self._label_widget.set_text(new_label) + else: + # create and prepend label to the hbox + if getattr(self, "_backend_widget", None) is not None: + try: + new_lbl = Gtk.Label(label=new_label) + try: + if hasattr(new_lbl, "set_xalign"): + new_lbl.set_xalign(0.0) + except Exception: + pass + # prepend so label appears before the combo control + try: + self._backend_widget.prepend(new_lbl) + except Exception: + # fallback: append and hope layout is acceptable + try: + self._backend_widget.append(new_lbl) + except Exception: + self._logger.exception("setLabel: failed to add new Gtk.Label to backend box") + self._label_widget = new_lbl + except Exception: + self._logger.exception("setLabel: error creating/inserting Gtk.Label") + except Exception: + self._logger.exception("setLabel: error updating label=%r", new_label) def _set_backend_enabled(self, enabled): """ diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index 81c2482..ce34593 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -23,6 +23,8 @@ def __init__(self, parent=None, label="", editable=False): self._selected_items = [] self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") self._combo_widget = None + # reference to the visual label widget (if any) + self._label_widget = None def widgetClass(self): return "YComboBox" @@ -57,6 +59,26 @@ def setValue(self, text): def editable(self): return self._editable + def setLabel(self, new_label: str): + """Set logical label and update/create the visual QLabel in the container.""" + try: + super().setLabel(new_label) + if self._label_widget is not None: + self._label_widget.setText(new_label) + else: + # create and insert label before combo in layout + if getattr(self, "_backend_widget", None) is not None and getattr(self, "_combo_widget", None) is not None: + try: + layout = self._backend_widget.layout() + if layout is not None: + label = QtWidgets.QLabel(new_label) + layout.insertWidget(0, label) + self._label_widget = label + except Exception: + self._logger.exception("setLabel: failed to insert new QLabel") + except Exception: + self._logger.exception("setLabel: error updating label=%r", new_label) + def _create_backend_widget(self): container = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(container) @@ -64,6 +86,7 @@ def _create_backend_widget(self): if self._label: label = QtWidgets.QLabel(self._label) + self._label_widget = label layout.addWidget(label) if self._editable: @@ -125,10 +148,8 @@ def _create_backend_widget(self): self._backend_widget = container self._combo_widget = combo self._backend_widget.setEnabled(bool(self._enabled)) - try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) - except Exception: - pass + # allow logger to raise if misconfigured + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) def _set_backend_enabled(self, enabled): """Enable/disable the combobox and its container.""" From 5ab879ef0d8ebb76434dde66adb019c3cced2343 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 13:55:59 +0100 Subject: [PATCH 412/523] removed chatti debug log --- manatools/aui/backends/curses/hboxcurses.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py index d1924e6..8cc8a1b 100644 --- a/manatools/aui/backends/curses/hboxcurses.py +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -212,8 +212,8 @@ def _draw(self, window, y, x, width, height): except Exception: wv = 0 details.append((i, lbl, min_reserved[i], pref_reserved[i], sw, wv)) - self._logger.debug("HBox allocation inputs: available=%d spacing=%d details=%s", - available, spacing, details) + #self._logger.debug("HBox allocation inputs: available=%d spacing=%d details=%s", + # available, spacing, details) except Exception: pass @@ -275,10 +275,7 @@ def _draw(self, window, y, x, width, height): overflow -= take # Final debug of allocated widths - try: - self._logger.debug("HBox allocated widths=%s total=%d (available=%d)", widths, sum(widths), available) - except Exception: - pass + #self._logger.debug("HBox allocated widths=%s total=%d (available=%d)", widths, sum(widths), available) # Ensure containers get at least the width required by their children def _required_width_for(widget): @@ -354,12 +351,9 @@ def _required_width_for(widget): else: ch = min(height, max(1, getattr(child, "_height", 1))) if hasattr(child, "_draw"): - try: - self._logger.debug("HBox drawing child %d: lbl=%s alloc_w=%d x=%d height=%d ch_h=%d", i, - (child.debugLabel() if hasattr(child, 'debugLabel') else f'child_{i}'), - w, cx, height, ch) - except Exception: - pass + #self._logger.debug("HBox drawing child %d: lbl=%s alloc_w=%d x=%d height=%d ch_h=%d", i, + # (child.debugLabel() if hasattr(child, 'debugLabel') else f'child_{i}'), + # w, cx, height, ch) child._draw(window, y, cx, w, ch) cx += w if i < num_children - 1: From e91ca88b0938d46e49684fa412aeadafc94d6010 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 14:14:41 +0100 Subject: [PATCH 413/523] tested deleteAllItems and addItems --- test/test_combobox.py | 92 +++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/test/test_combobox.py b/test/test_combobox.py index cc1a6a1..bc8661f 100644 --- a/test/test_combobox.py +++ b/test/test_combobox.py @@ -2,10 +2,32 @@ import os import sys +import logging # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + def test_combobox(backend_name=None): """Test ComboBox widget specifically""" if backend_name: @@ -15,15 +37,14 @@ def test_combobox(backend_name=None): print("Using auto-detection") try: - from manatools.aui.yui import YUI, YUI_ui - import manatools.aui.yui_common as yui + #from manatools.aui.yui import YUI, YUI_ui + #import manatools.aui.yui_common as yui + import manatools.aui.yui as MUI - # Force re-detection - YUI._instance = None - YUI._backend = None - - backend = YUI.backend() + backend = MUI.YUI.backend() print(f"Using backend: {backend.value}") + logger = logging.getLogger("test.test_combobox") + logger.info(f"Testing ComboBox with backend: {backend.value}") if backend.value == 'ncurses': print("\nNCurses ComboBox Instructions:") @@ -34,7 +55,7 @@ def test_combobox(backend_name=None): print("5. Selected value should be displayed") print("6. Press F10 or Q to quit") - ui = YUI_ui() + ui = MUI.YUI_ui() factory = ui.widgetFactory() # Create dialog focused on ComboBox testing @@ -48,26 +69,29 @@ def test_combobox(backend_name=None): # Test ComboBox with initial selection factory.createLabel(vbox, "") hbox = factory.createHBox(vbox) - combo = factory.createComboBox(hbox, "Choose fruit:", False) - combo.addItem("Apple") - combo.addItem("Banana") - combo.addItem("Orange") - combo.addItem("Grape") - combo.addItem("Mango") - + label1 = "Select a fruit:" + combo = factory.createComboBox(hbox, label1, False) + fruits = [ + MUI.YItem("Apple"), MUI.YItem("Banana"), MUI.YItem("Orange", selected=True), MUI.YItem("Grape"), MUI.YItem("Mango")] + combo.addItems(fruits) + # Set initial value to test display combo.setValue("Banana") - factory.createLabel(hbox, " - ") - combo1 = factory.createComboBox(hbox, "Choose option:", False) - combo1.addItem("Option 1") - combo1.addItem("Option 2") - combo1.addItem("Option 3") - + factory.createLabel(hbox, " - ") + label2 = "Select an option:" + combo1 = factory.createComboBox(hbox, label2, False) + options = [ + MUI.YItem("Option 1"), MUI.YItem("Option 2", selected=True, icon_name="dialog-warning"), MUI.YItem("Option 3"), MUI.YItem("Option 4")] + combo1.addItems(options) + + labels = [label1, label2] + combo_items = [fruits, options] + first_combo_info = 0 # Simple buttons selected = factory.createLabel(vbox, "") hbox = factory.createHBox(vbox) - ok_button = factory.createPushButton(hbox, "OK") + swap_button = factory.createPushButton(hbox, "Swap ComboBoxes") cancel_button = factory.createPushButton(hbox, "Cancel") print("\nOpening ComboBox test dialog...") @@ -75,20 +99,36 @@ def test_combobox(backend_name=None): while True: event = dialog.waitForEvent() typ = event.eventType() - if typ == yui.YEventType.CancelEvent: + if typ == MUI.YEventType.CancelEvent: dialog.destroy() break - elif typ == yui.YEventType.WidgetEvent: + elif typ == MUI.YEventType.WidgetEvent: wdg = event.widget() if wdg == cancel_button: dialog.destroy() break elif wdg == combo: selected.setText(f"Selected: '{combo.value()}'") + for it in combo._items: + if it.selected(): + logger.debug(f" - Item: '{it.label()}' Selected: {it.selected()} Icon: {it.iconName()}") elif wdg == combo1: selected.setText(f"Selected: '{combo1.value()}'") - elif wdg == ok_button: - selected.setText(f"OK clicked.") + for it in combo1._items: + if it.selected(): + logger.debug(f" - Item: '{it.label()}' Selected: {it.selected()} Icon: {it.iconName()}") + + elif wdg == swap_button: + selected.setText(f"Swap clicked.") + # Swap combo boxes + old = first_combo_info + first_combo_info = (first_combo_info + 1) % 2 + combo.deleteAllItems() + combo1.deleteAllItems() + combo.setLabel(labels[first_combo_info]) + combo.addItems(combo_items[first_combo_info]) + combo1.setLabel(labels[(first_combo_info + 1) % 2]) + combo1.addItems(combo_items[old]) # Show final result print(f"\nFinal ComboBox value: '{combo.value()}' {combo1.value()}") From bd30f984a9d15eac42afe4aee3a3a2c036503418 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 15:19:17 +0100 Subject: [PATCH 414/523] removed chatty log --- manatools/aui/backends/curses/dialogcurses.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index 527d7b0..947db0e 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -177,10 +177,7 @@ def _draw_dialog(self): try: height, width = self._backend_widget.getmaxyx() - try: - self._logger.debug("Dialog window size: height=%d width=%d", height, width) - except Exception: - pass + #self._logger.debug("Dialog window size: height=%d width=%d", height, width) # Clear screen self._backend_widget.clear() @@ -207,10 +204,7 @@ def _draw_dialog(self): content_width = width - 4 content_y = 2 content_x = 2 - try: - self._logger.debug("Dialog content area: y=%d x=%d h=%d w=%d", content_y, content_x, content_height, content_width) - except Exception: - pass + #self._logger.debug("Dialog content area: y=%d x=%d h=%d w=%d", content_y, content_x, content_height, content_width) # Draw child content if self.hasChildren(): From bd195996d4e598e29e040e97b90b109924371aa4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 15:30:54 +0100 Subject: [PATCH 415/523] managed stretching --- manatools/aui/backends/gtk/comboboxgtk.py | 142 +++++++++++++++------- manatools/aui/backends/qt/comboboxqt.py | 55 ++++++++- 2 files changed, 150 insertions(+), 47 deletions(-) diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index a7f8b86..976e101 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -80,66 +80,88 @@ def editable(self): return self._editable def _create_backend_widget(self): - """ - Create GTK widgets backing the combo: label + entry/dropdown/button. - Uses conservative fallbacks and logs failures for diagnostics. - """ hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) if self._label: label = Gtk.Label(label=self._label) - self._label_widget = label try: if hasattr(label, "set_xalign"): label.set_xalign(0.0) except Exception: - self._logger.exception("_create_backend_widget: label.set_xalign failed") + pass try: hbox.append(label) except Exception: - self._logger.exception("_create_backend_widget: failed to add label to hbox") + hbox.add(label) + + # Determine expansion flags from logical widget before creating the control + try: + try: + vexpand_flag = bool(self.stretchable(YUIDimension.YD_VERT)) or bool(int(self.weight(YUIDimension.YD_VERT))) + except Exception: + vexpand_flag = bool(self.stretchable(YUIDimension.YD_VERT)) + try: + hexpand_flag = bool(self.stretchable(YUIDimension.YD_HORIZ)) or bool(int(self.weight(YUIDimension.YD_HORIZ))) + except Exception: + hexpand_flag = bool(self.stretchable(YUIDimension.YD_HORIZ)) + except Exception: + vexpand_flag = False + hexpand_flag = False # For Gtk4 there is no ComboBoxText; try DropDown for non-editable, # and Entry for editable combos (simple fallback). if self._editable: entry = Gtk.Entry() - entry.set_text(self._value) - entry.connect("changed", self._on_text_changed) + try: + entry.set_text(self._value) + except Exception: + pass + # apply expansion policies + try: + entry.set_hexpand(hexpand_flag) + except Exception: + pass + try: + entry.set_vexpand(vexpand_flag) + except Exception: + pass + try: + entry.connect("changed", self._on_text_changed) + except Exception: + pass self._combo_widget = entry try: hbox.append(entry) except Exception: - try: - hbox.add(entry) - except Exception: - self._logger.exception("_create_backend_widget: failed to add editable entry to hbox") + hbox.add(entry) else: # Build a simple Gtk.DropDown backed by a Gtk.StringList (if available) try: if hasattr(Gtk, "StringList") and hasattr(Gtk, "DropDown"): - # keep model reference for runtime updates self._string_list_model = Gtk.StringList() for it in self._items: - self._string_list_model.append(it.label()) + try: + self._string_list_model.append(it.label()) + except Exception: + pass dropdown = Gtk.DropDown.new(self._string_list_model, None) - # prefer explicit selected item flag in model; only one allowed - selected_idx = -1 + # select initial value (prefer explicit selected() flag) + sel_idx = -1 for idx, it in enumerate(self._items): try: if it.selected(): - selected_idx = idx + sel_idx = idx break except Exception: - self._logger.exception("_create_backend_widget: error checking item.selected() for index %d", idx) - if selected_idx >= 0: + pass + if sel_idx >= 0: try: - dropdown.set_selected(selected_idx) - self._value = self._items[selected_idx].label() - self._selected_items = [self._items[selected_idx]] + dropdown.set_selected(sel_idx) + self._value = self._items[sel_idx].label() + self._selected_items = [self._items[sel_idx]] except Exception: - self._logger.exception("_create_backend_widget: failed to set selected index %d", selected_idx) + pass else: - # fallback to explicit value string if provided if self._value: for idx, it in enumerate(self._items): try: @@ -148,47 +170,77 @@ def _create_backend_widget(self): self._selected_items = [it] break except Exception: - self._logger.exception("_create_backend_widget: matching value to items failed at index %d", idx) + pass try: dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) - self._combo_widget = dropdown + except Exception: + pass + # apply expansion policies + try: + dropdown.set_hexpand(hexpand_flag) + except Exception: + pass + try: + dropdown.set_vexpand(vexpand_flag) + except Exception: + pass + self._combo_widget = dropdown + try: hbox.append(dropdown) except Exception: - self._logger.exception("_create_backend_widget: failed to connect/append dropdown") + hbox.add(dropdown) else: - # fallback: simple Gtk.Button that cycles items on click (very simple) + # fallback: simple Gtk.Button that cycles items on click (very simple) btn = Gtk.Button(label=self._value or (self._items[0].label() if self._items else "")) - btn.connect("clicked", self._on_fallback_button_clicked) + try: + btn.connect("clicked", self._on_fallback_button_clicked) + except Exception: + pass + # apply expansion policies to button as best-effort + try: + btn.set_hexpand(hexpand_flag) + except Exception: + pass + try: + btn.set_vexpand(vexpand_flag) + except Exception: + pass self._combo_widget = btn try: hbox.append(btn) except Exception: - try: - hbox.add(btn) - except Exception: - self._logger.exception("_create_backend_widget: failed to add fallback button") + hbox.add(btn) except Exception: # final fallback: entry - self._logger.exception("_create_backend_widget: unexpected failure building non-editable control; falling back to Entry") entry = Gtk.Entry() - entry.set_text(self._value) - entry.connect("changed", self._on_text_changed) + try: + entry.set_text(self._value or "") + except Exception: + pass + try: + entry.connect("changed", self._on_text_changed) + except Exception: + pass + try: + entry.set_hexpand(hexpand_flag) + except Exception: + pass + try: + entry.set_vexpand(vexpand_flag) + except Exception: + pass self._combo_widget = entry try: hbox.append(entry) except Exception: - try: - hbox.add(entry) - except Exception: - self._logger.exception("_create_backend_widget: failed to add fallback entry") + hbox.add(entry) self._backend_widget = hbox + self._backend_widget.set_sensitive(self._enabled) try: - self._backend_widget.set_sensitive(self._enabled) + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: - self._logger.exception("_create_backend_widget: failed to set backend widget sensitivity") - # Allow logger to raise if misconfigured so failures are visible during debugging - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + pass def setLabel(self, new_label: str): """Set logical label and update/create the visual Gtk.Label in the box.""" diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index ce34593..8ba2bc1 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -143,8 +143,59 @@ def _create_backend_widget(self): self._selected_items = [] except Exception: pass - layout.addWidget(combo) - + # Honor logical stretch/weight: set QSizePolicy and add with layout stretch factor + try: + try: + weight_v = int(self.weight(YUIDimension.YD_VERT)) + except Exception: + weight_v = 0 + try: + stretchable_v = bool(self.stretchable(YUIDimension.YD_VERT)) + except Exception: + stretchable_v = False + stretch = weight_v if weight_v > 0 else (1 if stretchable_v else 0) + + # set QSizePolicy according to logical flags + try: + sp = combo.sizePolicy() + try: + horiz = QtWidgets.QSizePolicy.Expanding if bool(self.stretchable(YUIDimension.YD_HORIZ)) else QtWidgets.QSizePolicy.Fixed + except Exception: + horiz = QtWidgets.QSizePolicy.Fixed + try: + vert = QtWidgets.QSizePolicy.Expanding if stretch > 0 else QtWidgets.QSizePolicy.Fixed + except Exception: + vert = QtWidgets.QSizePolicy.Fixed + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) + combo.setSizePolicy(sp) + except Exception: + pass + + # add to layout with stretch factor when supported + added = False + try: + layout.addWidget(combo, stretch) + added = True + except TypeError: + added = False + except Exception: + added = False + + if not added: + try: + layout.addWidget(combo) + except Exception: + try: + combo.setParent(container) + except Exception: + pass + except Exception: + try: + layout.addWidget(combo) + except Exception: + pass + self._backend_widget = container self._combo_widget = combo self._backend_widget.setEnabled(bool(self._enabled)) From 7581c2ffd5b95332c4deb79d9361aee4ed03e641 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 15:43:41 +0100 Subject: [PATCH 416/523] created label only once --- manatools/aui/backends/gtk/comboboxgtk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index 976e101..4dbd8c3 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -84,6 +84,7 @@ def _create_backend_widget(self): if self._label: label = Gtk.Label(label=self._label) + self._label_widget = label try: if hasattr(label, "set_xalign"): label.set_xalign(0.0) From 2664877aade8694f01e081430e41d226c15402ba Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 16:20:16 +0100 Subject: [PATCH 417/523] Caption label in upper position --- .../aui/backends/curses/comboboxcurses.py | 85 ++++++++++--------- manatools/aui/backends/gtk/comboboxgtk.py | 12 ++- manatools/aui/backends/qt/comboboxqt.py | 7 +- 3 files changed, 59 insertions(+), 45 deletions(-) diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py index 78b6a78..69d2dcd 100644 --- a/manatools/aui/backends/curses/comboboxcurses.py +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -34,7 +34,8 @@ def __init__(self, parent=None, label="", editable=False): self._value = "" self._focused = False self._can_focus = True - self._height = 1 + # Reserve two lines: one for the label (caption) and one for the control + self._height = 2 self._expanded = False self._hover_index = 0 self._combo_x = 0 @@ -112,49 +113,56 @@ def _set_backend_enabled(self, enabled): def _draw(self, window, y, x, width, height): # Store position and dimensions for dropdown drawing - self._combo_y = y + # Label is drawn on row `y`, combo control on row `y+1`. + self._combo_y = y + 1 self._combo_x = x self._combo_width = width - try: - # Calculate available space for combo box - label_space = len(self._label) + 1 if self._label else 0 - combo_space = width - label_space + # require at least two rows (label + control) + if height < 2: + return + try: + # Calculate available space for combo box (full width, label is above) + combo_space = width if combo_space <= 3: return - # Draw label + # Draw label on top row if self._label: label_text = self._label - if len(label_text) > label_space - 1: - label_text = label_text[:label_space - 1] + # clip label if too long for width + if len(label_text) > width: + label_text = label_text[:max(0, width - 3)] + "..." lbl_attr = curses.A_NORMAL if not self.isEnabled(): lbl_attr |= curses.A_DIM - window.addstr(y, x, label_text, lbl_attr) - x += len(label_text) + 1 + try: + window.addstr(y, x, label_text, lbl_attr) + except curses.error: + pass - # Prepare display value + # Prepare display value and draw combo on next row display_value = self._value if self._value else "Select..." max_display_width = combo_space - 3 if len(display_value) > max_display_width: display_value = display_value[:max_display_width] + "..." - # Draw combo box background + # Draw combo box background on the control row if not self.isEnabled(): attr = curses.A_DIM else: attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - combo_bg = " " * combo_space - window.addstr(y, x, combo_bg, attr) - - combo_text = f" {display_value} ▼" - if len(combo_text) > combo_space: - combo_text = combo_text[:combo_space] - - window.addstr(y, x, combo_text, attr) + try: + combo_bg = " " * combo_space + window.addstr(self._combo_y, x, combo_bg, attr) + combo_text = f" {display_value} ▼" + if len(combo_text) > combo_space: + combo_text = combo_text[:combo_space] + window.addstr(self._combo_y, x, combo_text, attr) + except curses.error: + pass # Draw expanded list if active and enabled if self._expanded and self.isEnabled(): @@ -165,61 +173,60 @@ def _draw(self, window, y, x, width, height): except Exception: _mod_logger.error("_draw curses.error: %s", e, exc_info=True) - def _draw_expanded_list(self, window): """Draw the expanded dropdown list at correct position""" if not self._expanded or not self._items: return - + try: # Make sure we don't draw outside screen screen_height, screen_width = window.getmaxyx() list_height = min(len(self._items), screen_height) - - # Calculate dropdown position - right below the combo box + + # Calculate dropdown position - right below the combo control row dropdown_y = self._combo_y + 1 - dropdown_x = self._combo_x + (len(self._label) + 1 if self._label else 0) - dropdown_width = self._combo_width - (len(self._label) + 1 if self._label else 0) - + dropdown_x = self._combo_x + dropdown_width = self._combo_width + # If not enough space below, draw above if dropdown_y + list_height >= screen_height: dropdown_y = max(1, self._combo_y - list_height - 1) - + # Ensure dropdown doesn't go beyond right edge if dropdown_x + dropdown_width >= screen_width: dropdown_width = screen_width - dropdown_x - 1 - + if dropdown_width <= 5: # Need reasonable width return - + # Draw dropdown background for each item for i in range(list_height): if i >= len(self._items): break - + item = self._items[i] item_text = item.label() if len(item_text) > dropdown_width - 2: item_text = item_text[:dropdown_width - 2] + "..." - + # Highlight hovered item attr = curses.A_REVERSE if i == self._hover_index else curses.A_NORMAL - + # Create background for the item bg_text = " " + item_text.ljust(dropdown_width - 2) if len(bg_text) > dropdown_width: bg_text = bg_text[:dropdown_width] - + # Ensure we don't write beyond screen bounds - if (dropdown_y + i < screen_height and - dropdown_x < screen_width and + if (dropdown_y + i < screen_height and + dropdown_x < screen_width and dropdown_x + len(bg_text) <= screen_width): try: window.addstr(dropdown_y + i, dropdown_x, bg_text, attr) except curses.error: - pass # Ignore out-of-bounds errors - + pass + except curses.error as e: try: self._logger.error("_draw_expanded_list curses.error: %s", e, exc_info=True) diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index 4dbd8c3..bb09793 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -80,7 +80,8 @@ def editable(self): return self._editable def _create_backend_widget(self): - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + # use vertical box so label is above the control + hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) if self._label: label = Gtk.Label(label=self._label) @@ -90,6 +91,8 @@ def _create_backend_widget(self): label.set_xalign(0.0) except Exception: pass + # store the label widget so setLabel() can update it later + self._label_widget = label try: hbox.append(label) except Exception: @@ -176,7 +179,7 @@ def _create_backend_widget(self): dropdown.connect("notify::selected", lambda w, pspec: self._on_changed_dropdown(w)) except Exception: pass - # apply expansion policies + # apply expansion policies so the control grows according to widget settings try: dropdown.set_hexpand(hexpand_flag) except Exception: @@ -237,7 +240,10 @@ def _create_backend_widget(self): hbox.add(entry) self._backend_widget = hbox - self._backend_widget.set_sensitive(self._enabled) + try: + self._backend_widget.set_sensitive(self._enabled) + except Exception: + pass try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index 8ba2bc1..40f37ce 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -81,7 +81,8 @@ def setLabel(self, new_label: str): def _create_backend_widget(self): container = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(container) + # use vertical layout so label sits above the combo control + layout = QtWidgets.QVBoxLayout(container) layout.setContentsMargins(0, 0, 0, 0) if self._label: @@ -172,7 +173,7 @@ def _create_backend_widget(self): except Exception: pass - # add to layout with stretch factor when supported + # add to layout with stretch factor when supported (vertical layout: label above, combo below) added = False try: layout.addWidget(combo, stretch) @@ -195,7 +196,7 @@ def _create_backend_widget(self): layout.addWidget(combo) except Exception: pass - + self._backend_widget = container self._combo_widget = combo self._backend_widget.setEnabled(bool(self._enabled)) From 4d88555dc7ae8c1d7bc01b3a7a2207f84cf62755 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 20:09:07 +0100 Subject: [PATCH 418/523] Added normal/busy cursor and createIconButton alias --- manatools/aui/yui_qt.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index e3766d4..20d3246 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -199,6 +199,20 @@ def isTextMode(self) -> bool: """Indicate that this is not a text-mode (Qt) application.""" return False + def busyCursor(self): + """Set busy cursor (Qt backend).""" + try: + QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + except Exception: + pass + + def normalCursor(self): + """Set normal cursor (Qt backend).""" + try: + QtWidgets.QApplication.restoreOverrideCursor() + except Exception: + pass + def askForExistingDirectory(self, startDir: str, headline: str): """ Prompt user to select an existing directory. @@ -291,6 +305,11 @@ def createHBox(self, parent): def createPushButton(self, parent, label): return YPushButtonQt(parent, label) + def createIconButton(self, parent, iconName, fallbackTextLabel): + btn = YPushButtonQt(parent, fallbackTextLabel) + btn.setIcon(iconName) + return btn + def createLabel(self, parent, text, isHeading=False, isOutputField=False): return YLabelQt(parent, text, isHeading, isOutputField) From 712febe6dbd97bbe204277df71e7ade9b4999705 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 20:10:01 +0100 Subject: [PATCH 419/523] preventing getting text child --- manatools/aui/yui_common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index 8c04b8e..dbbdc5d 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -553,8 +553,11 @@ def childrenEnd(self): return iter([]) def addChild(self, item): + if isinstance(item, str): + item = YTreeItem(item) self._children.append(item) item._parent_item = self + return item def isOpen(self): return self._is_open From dcafe75596143d18fb67c70db56b88283bf131e3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 20:10:55 +0100 Subject: [PATCH 420/523] Added createIconButton --- manatools/aui/yui_curses.py | 5 +++++ manatools/aui/yui_gtk.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 464f6eb..b7935b2 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -504,6 +504,11 @@ def createMultiLineEdit(self, parent, label): def createPushButton(self, parent, label): return YPushButtonCurses(parent, label) + + def createIconButton(self, parent, iconName, fallbackTextLabel): + btn = YPushButtonCurses(parent, fallbackTextLabel) + btn.setIcon(iconName) + return btn def createCheckBox(self, parent, label, is_checked=False): return YCheckBoxCurses(parent, label, is_checked) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 662c4fa..912c017 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -684,6 +684,11 @@ def createHBox(self, parent): def createPushButton(self, parent, label): return YPushButtonGtk(parent, label) + + def createIconButton(self, parent, iconName, fallbackTextLabel): + btn = YPushButtonGtk(parent, fallbackTextLabel) + btn.setIcon(iconName) + return btn def createLabel(self, parent, text, isHeading=False, isOutputField=False): return YLabelGtk(parent, text, isHeading, isOutputField) From 9a4a25ee2d599af6c4d872fc34bfde561d669940 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 11 Jan 2026 20:15:58 +0100 Subject: [PATCH 421/523] Added busy and norma cursor implementation --- manatools/aui/yui_curses.py | 8 ++++++++ manatools/aui/yui_gtk.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index b7935b2..58a2c2b 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -245,6 +245,14 @@ def isTextMode(self) -> bool: """Indicate that this is a text-mode (ncurses) application.""" return True + def busyCursor(self): + """Set busy cursor (not applicable in ncurses).""" + pass + + def normalCursor(self): + """Set normal cursor (not applicable in ncurses).""" + pass + # --- Internal helpers for ncurses file/directory chooser --- def _parse_filter_patterns(self, filter_str: str): try: diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 912c017..171c2a0 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -251,6 +251,41 @@ def logo(self) -> str: def isTextMode(self) -> bool: """Indicate that this is not a text-mode (GTK) application.""" return False + + def busyCursor(self): + """Set busy cursor (GTK implementation).""" + display = Gdk.Display.get_default() + if display is None: + return + seat = display.get_default_seat() + if seat is None: + return + pointer = seat.get_pointer() + if pointer is None: + return + window = pointer.get_window() + if window is None: + return + cursor = Gdk.Cursor.new_from_name(display, "wait") + if cursor is None: + return + window.set_cursor(cursor) + + def normalCursor(self): + """Set normal cursor (GTK implementation).""" + display = Gdk.Display.get_default() + if display is None: + return + seat = display.get_default_seat() + if seat is None: + return + pointer = seat.get_pointer() + if pointer is None: + return + window = pointer.get_window() + if window is None: + return + window.set_cursor(None) def _create_gtk4_filters(self, filter_str: str) -> List[Gtk.FileFilter]: """ From 8f95b2ae95018e59fb5d60a68fe5952369c3f7ac Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 12 Jan 2026 19:44:09 +0100 Subject: [PATCH 422/523] less chatty --- manatools/aui/backends/qt/treeqt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/qt/treeqt.py b/manatools/aui/backends/qt/treeqt.py index 1449c2d..19e96ce 100644 --- a/manatools/aui/backends/qt/treeqt.py +++ b/manatools/aui/backends/qt/treeqt.py @@ -145,7 +145,7 @@ def _add_recursive(parent_qitem, item): ico = _resolve_icon(icon_name) if ico is not None: try: - self._logger.debug("Column count for item %d", qitem.columnCount()) + #self._logger.debug("Column count for item %d", qitem.columnCount()) qitem.setIcon(0, ico) except Exception: pass From d4c092bbed825eb7b4280fe6f0780e8282679911 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 13:38:32 +0100 Subject: [PATCH 423/523] Added layout size constraint to get the children size hint, let's hope not to slow down all --- manatools/aui/backends/qt/dialogqt.py | 1 + manatools/aui/backends/qt/frameqt.py | 187 ++++++++++++++++++-------- manatools/aui/backends/qt/hboxqt.py | 5 + manatools/aui/backends/qt/vboxqt.py | 3 + 4 files changed, 139 insertions(+), 57 deletions(-) diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index 5272765..73c60ca 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -170,6 +170,7 @@ def _create_backend_widget(self): if self.child(): layout = QtWidgets.QVBoxLayout(central_widget) + layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) layout.addWidget(self.child().get_backend_widget()) # If the child is a layout box with a menubar as first child, Qt can display QMenuBar inline. # Alternatively, backends may add YMenuBarQt directly to layout. diff --git a/manatools/aui/backends/qt/frameqt.py b/manatools/aui/backends/qt/frameqt.py index dce88e7..6e62087 100644 --- a/manatools/aui/backends/qt/frameqt.py +++ b/manatools/aui/backends/qt/frameqt.py @@ -6,6 +6,7 @@ import logging from ...yui_common import * + class YFrameQt(YSingleChildContainerWidget): """ Qt backend implementation of YFrame. @@ -13,22 +14,22 @@ class YFrameQt(YSingleChildContainerWidget): - Single child is placed inside the group's layout. - Exposes simple property support for 'label'. """ + def __init__(self, parent=None, label=""): super().__init__(parent) self._label = label self._backend_widget = None self._group_layout = None - self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") + self._logger = logging.getLogger( + f"manatools.aui.qt.{self.__class__.__name__}" + ) def widgetClass(self): return "YFrame" def stretchable(self, dim: YUIDimension): - """Return True if the frame should stretch in given dimension. - The frame is stretchable when its child is stretchable or has a layout weight. - """ + """Return True if the frame should stretch in given dimension.""" try: - # prefer explicit single child child = self.child() if child is None: return False @@ -55,68 +56,96 @@ def setLabel(self, newLabel): self._label = newLabel if getattr(self, "_backend_widget", None) is not None: try: - self._backend_widget.setTitle(self._label) + # QGroupBox uses setTitle + if hasattr(self._backend_widget, "setTitle"): + self._backend_widget.setTitle(self._label) except Exception: pass except Exception: pass + def _apply_child_policy_and_stretch(self, child_widget): + """Compute child's stretch and apply Qt size policy and return stretch.""" + stretch = 0 + try: + try: + weight = int(self.child().weight(YUIDimension.YD_VERT)) + except Exception: + weight = 0 + try: + stretchable_vert = bool(self.child().stretchable(YUIDimension.YD_VERT)) + except Exception: + stretchable_vert = False + stretch = weight if weight > 0 else (1 if stretchable_vert else 0) + except Exception: + stretch = 0 + + # set child's QSizePolicy based on logical flags + try: + sp = child_widget.sizePolicy() + try: + horiz_expand = ( + QtWidgets.QSizePolicy.Expanding + if bool(self.child().stretchable(YUIDimension.YD_HORIZ)) + else QtWidgets.QSizePolicy.Fixed + ) + except Exception: + horiz_expand = QtWidgets.QSizePolicy.Fixed + try: + vert_expand = ( + QtWidgets.QSizePolicy.Expanding if stretch > 0 else QtWidgets.QSizePolicy.Preferred + ) + except Exception: + vert_expand = QtWidgets.QSizePolicy.Fixed + sp.setHorizontalPolicy(horiz_expand) + sp.setVerticalPolicy(vert_expand) + child_widget.setSizePolicy(sp) + except Exception: + pass + + return stretch + def _attach_child_backend(self): """Attach existing child backend widget to the groupbox layout.""" if not (self._backend_widget and self._group_layout and self.child()): return try: w = self.child().get_backend_widget() - if w: - # clear any existing widgets in layout (defensive) - try: - while self._group_layout.count(): - it = self._group_layout.takeAt(0) - if it and it.widget(): - it.widget().setParent(None) - except Exception: - pass + if not w: + return - # determine stretch factor from child's weight()/stretchable() - try: - try: - weight = int(self.child().weight(YUIDimension.YD_VERT)) - except Exception: - weight = 0 - try: - stretchable_vert = bool(self.child().stretchable(YUIDimension.YD_VERT)) - except Exception: - stretchable_vert = False - stretch = weight if weight > 0 else (1 if stretchable_vert else 0) - except Exception: - stretch = 0 + # clear existing widgets + try: + while self._group_layout.count(): + it = self._group_layout.takeAt(0) + if it and it.widget(): + it.widget().setParent(None) + except Exception: + pass - # set child's Qt size policy according to logical stretchable flags - try: - sp = w.sizePolicy() - try: - horiz_expand = QtWidgets.QSizePolicy.Expanding if bool(self.child().stretchable(YUIDimension.YD_HORIZ)) else QtWidgets.QSizePolicy.Fixed - except Exception: - horiz_expand = QtWidgets.QSizePolicy.Fixed - try: - vert_expand = QtWidgets.QSizePolicy.Expanding if stretch > 0 else QtWidgets.QSizePolicy.Fixed - except Exception: - vert_expand = QtWidgets.QSizePolicy.Fixed - sp.setHorizontalPolicy(horiz_expand) - sp.setVerticalPolicy(vert_expand) - w.setSizePolicy(sp) - except Exception: - pass + # compute stretch and apply policy + stretch = self._apply_child_policy_and_stretch(w) - # add with layout stretch factor so parent layout distributes extra space correctly + # set group size policy to reflect child's desire for expansion + try: + sp_grp = self._backend_widget.sizePolicy() + grp_vert_expand = ( + QtWidgets.QSizePolicy.Expanding if stretch > 0 else QtWidgets.QSizePolicy.Preferred + ) + sp_grp.setVerticalPolicy(grp_vert_expand) + sp_grp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + self._backend_widget.setSizePolicy(sp_grp) + except Exception: + pass + + # add widget with stretch factor (if supported) + try: + self._group_layout.addWidget(w, stretch) + except Exception: try: - self._group_layout.addWidget(w, stretch) + self._group_layout.addWidget(w) except Exception: - # fallback if addWidget signature doesn't accept stretch in this binding - try: - self._group_layout.addWidget(w) - except Exception: - pass + pass except Exception: pass @@ -130,29 +159,64 @@ def _create_backend_widget(self): try: grp = QtWidgets.QGroupBox(self._label) layout = QtWidgets.QVBoxLayout(grp) + # Ensure the group's widget minimum size follows the layout's minimumSizeHint. + # This prevents Qt from compressing the groupbox contents to zero when nested + # inside other layouts. We keep this local to frame/groupbox layouts. + try: + layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + except Exception: + pass layout.setContentsMargins(6, 6, 6, 6) layout.setSpacing(4) self._backend_widget = grp self._group_layout = layout self._backend_widget.setEnabled(bool(self._enabled)) - # attach child widget if already set + # set group's size policy according to logical stretch of this frame + try: + frame_stretchable = bool(self.stretchable(YUIDimension.YD_VERT)) + except Exception: + frame_stretchable = False + try: + sp_grp = self._backend_widget.sizePolicy() + sp_grp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) + sp_grp.setVerticalPolicy( + QtWidgets.QSizePolicy.Expanding if frame_stretchable else QtWidgets.QSizePolicy.Preferred + ) + self._backend_widget.setSizePolicy(sp_grp) + except Exception: + pass + + # attach child if present (apply same policy/stretches) if self.child(): try: w = self.child().get_backend_widget() if w: - layout.addWidget(w) + stretch = self._apply_child_policy_and_stretch(w) + try: + layout.addWidget(w, stretch) + except Exception: + try: + layout.addWidget(w) + except Exception: + pass except Exception: pass try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass + return except Exception: - # fallback to a plain QWidget container if QGroupBox creation fails + # fallback to a plain QWidget container try: container = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(container) + # Keep fallback container constrained as well (same rationale). + try: + layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + except Exception: + pass layout.setContentsMargins(6, 6, 6, 6) layout.setSpacing(4) self._backend_widget = container @@ -161,16 +225,25 @@ def _create_backend_widget(self): try: w = self.child().get_backend_widget() if w: - layout.addWidget(w) + stretch = self._apply_child_policy_and_stretch(w) + try: + layout.addWidget(w, stretch) + except Exception: + try: + layout.addWidget(w) + except Exception: + pass except Exception: pass try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + self._logger.debug("_create_backend_widget (container): <%s>", self.debugLabel()) except Exception: pass + return except Exception: self._backend_widget = None self._group_layout = None + return def _set_backend_enabled(self, enabled): """Enable/disable the frame and propagate state to the child.""" @@ -221,4 +294,4 @@ def propertySet(self): pass return props except Exception: - return None \ No newline at end of file + return None diff --git a/manatools/aui/backends/qt/hboxqt.py b/manatools/aui/backends/qt/hboxqt.py index 26e885d..ba9965c 100644 --- a/manatools/aui/backends/qt/hboxqt.py +++ b/manatools/aui/backends/qt/hboxqt.py @@ -39,6 +39,11 @@ def _create_backend_widget(self): self._backend_widget = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(self._backend_widget) layout.setContentsMargins(10, 10, 10, 10) + # Keep the layout constrained to its minimum sizeHint so children are not + # compressed to invisible sizes by parent layouts. This is required in our + # UI because many child widgets use Preferred/Fixed policies and rely on + # the layout's minimumSizeHint to be respected. + layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) layout.setSpacing(5) # Map YWidget weights and stretchable flags to Qt layout stretch factors. diff --git a/manatools/aui/backends/qt/vboxqt.py b/manatools/aui/backends/qt/vboxqt.py index f389c86..b2adb60 100644 --- a/manatools/aui/backends/qt/vboxqt.py +++ b/manatools/aui/backends/qt/vboxqt.py @@ -39,6 +39,9 @@ def _create_backend_widget(self): self._backend_widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(self._backend_widget) layout.setContentsMargins(10, 10, 10, 10) + # Keep the layout constrained to its minimum sizeHint so children are not + # compressed to invisible sizes by parent layouts. This matches HBox/Frame behavior. + layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) layout.setSpacing(5) # Map YWidget weights and stretchable flags to Qt layout stretch factors. From 71361a595a3d59c03364f7e95aa10c9fa42081d6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 13:40:23 +0100 Subject: [PATCH 424/523] removed pollEvent by now --- manatools/ui/basedialog.py | 61 +++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/manatools/ui/basedialog.py b/manatools/ui/basedialog.py index dcb339d..3980a8a 100644 --- a/manatools/ui/basedialog.py +++ b/manatools/ui/basedialog.py @@ -64,6 +64,7 @@ def __init__(self, title, icon="", dialogType=DialogType.MAIN, minWidth=-1, minH @param minWidth > 0 min width size in pixels @param minHeight > 0 min height size in pixels ''' + print(f"BaseDialog init title={title} icon={icon} dialogType={dialogType} minWidth={minWidth} minHeight={minHeight}") self._dialogType = dialogType self._icon = icon self._title = title @@ -130,10 +131,10 @@ def run(self): #restore old application title yui.YUI.app().setApplicationTitle(self.backupTitle) if self._icon: - yui.YUI.app().setApplicationTitle(backupIcon) - - self.dialog.destroy() - self.dialog = None + yui.YUI.app().setApplicationIcon(backupIcon) + if self.dialog is not None: + self.dialog.destroy() + self.dialog = None @property def eventManager(self): @@ -161,11 +162,11 @@ def _setupUI(self): vbox = self.factory.createVBox(parent) self.UIlayout(vbox) - def pollEvent(self): - ''' - perform yui pollEvent - ''' - return self.dialog.pollEvent() + #def pollEvent(self): + # ''' + # perform yui pollEvent + # ''' + # return self.dialog.pollEvent() def _handleEvents(self): ''' @@ -174,26 +175,30 @@ def _handleEvents(self): while self._running == True: event = self.dialog.waitForEvent(self.timeout) - eventType = event.eventType() - - rebuild_package_list = False - group = None - #event type checking - if (eventType == yui.YEventType.WidgetEvent) : - # widget selected - widget = event.widget() - self.eventManager.widgetEvent(widget, event) - elif (eventType == yui.YEventType.MenuEvent) : - ### MENU ### - item = event.item() - self.eventManager.menuEvent(item, event) - elif (eventType == yui.YEventType.CancelEvent) : - self.eventManager.cancelEvent() - break - elif (eventType == yui.YEventType.TimeoutEvent) : - self.eventManager.timeoutEvent() + if event is not None: + eventType = event.eventType() + + rebuild_package_list = False + group = None + #event type checking + if (eventType == yui.YEventType.WidgetEvent) : + # widget selected + widget = event.widget() + self.eventManager.widgetEvent(widget, event) + elif (eventType == yui.YEventType.MenuEvent) : + ### MENU ### + item = event.item() + self.eventManager.menuEvent(item, event) + elif (eventType == yui.YEventType.CancelEvent) : + self.eventManager.cancelEvent() + break + elif (eventType == yui.YEventType.TimeoutEvent) : + self.eventManager.timeoutEvent() + else: + print(f"Unmanaged event type {eventType}") else: - print(f"Unmanaged event type {eventType}") + #TODO logging + pass self.doSomethingIntoLoop() From 051301e31917f824074221134c7017a5b8368eae Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 16:30:41 +0100 Subject: [PATCH 425/523] Non need to fix menu if widget has not been created yet --- manatools/aui/backends/qt/menubarqt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index 07b1585..4db0657 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -79,6 +79,8 @@ def _emit_activation(self, item: YMenuItem): pass def _ensure_menu_rendered(self, menu: YMenuItem): + if self._backend_widget is None: + return # skip invisible top-level menus if not menu.visible(): return From 34bbc2b666c87956650416c7b78eea777881d695 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 17:08:31 +0100 Subject: [PATCH 426/523] Sped up addItems --- manatools/aui/backends/qt/tableqt.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/qt/tableqt.py b/manatools/aui/backends/qt/tableqt.py index ffb7827..fee5a3e 100644 --- a/manatools/aui/backends/qt/tableqt.py +++ b/manatools/aui/backends/qt/tableqt.py @@ -462,15 +462,25 @@ def addItem(self, item): else: self._logger.error("YTable.addItem: invalid item type %s", type(item)) raise TypeError("YTable.addItem expects a YTableItem or string label") - try: + + item.setIndex(len(self._items) - 1) + if getattr(self, '_table', None) is not None: + self.rebuildTable() + + def addItems(self, items): + '''add multiple items to the table. This is more efficient than calling addItem repeatedly.''' + for item in items: + if isinstance(item, str): + item = YTableItem(item) + super().addItem(item) + elif isinstance(item, YTableItem): + super().addItem(item) + else: + self._logger.error("YTable.addItem: invalid item type %s", type(item)) + raise TypeError("YTable.addItem expects a YTableItem or string label") item.setIndex(len(self._items) - 1) - except Exception: - pass - try: - if getattr(self, '_table', None) is not None: - self.rebuildTable() - except Exception: - pass + if getattr(self, '_table', None) is not None: + self.rebuildTable() def selectItem(self, item, selected=True): # update model and view From 5cc0606393731d52377744c078d3c2180457be98 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 17:24:29 +0100 Subject: [PATCH 427/523] Sped up addItems --- manatools/aui/backends/gtk/tablegtk.py | 28 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index 604aada..9684814 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -382,19 +382,25 @@ def addItem(self, item): if not isinstance(item, YTableItem): raise TypeError("YTableGtk.addItem expects a YTableItem or string label") super().addItem(item) - try: - item.setIndex(len(self._items) - 1) - except Exception: - pass - try: - if getattr(self, '_listbox', None) is not None: - self.rebuildTable() - except Exception: - pass + item.setIndex(len(self._items) - 1) + if getattr(self, '_listbox', None) is not None: + self.rebuildTable() def addItems(self, items): - for it in items: - self.addItem(it) + '''add multiple items to the table. This is more efficient than calling addItem repeatedly.''' + for item in items: + if isinstance(item, str): + item = YTableItem(item) + super().addItem(item) + elif isinstance(item, YTableItem): + super().addItem(item) + else: + self._logger.error("YTable.addItem: invalid item type %s", type(item)) + raise TypeError("YTableGtk.addItem expects a YTableItem or string label") + item.setIndex(len(self._items) - 1) + if getattr(self, '_listbox', None) is not None: + self.rebuildTable() + def selectItem(self, item, selected=True): try: From efea319ae23564a9cf2d3539f21a0fc105a33e65 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 17:25:21 +0100 Subject: [PATCH 428/523] disable cursor management by now --- manatools/aui/yui_gtk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 171c2a0..53c5bae 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -254,6 +254,7 @@ def isTextMode(self) -> bool: def busyCursor(self): """Set busy cursor (GTK implementation).""" + return display = Gdk.Display.get_default() if display is None: return @@ -273,6 +274,7 @@ def busyCursor(self): def normalCursor(self): """Set normal cursor (GTK implementation).""" + return display = Gdk.Display.get_default() if display is None: return From 44b1de8935c8aef261e99eeeb47888195b239ab5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 17:26:56 +0100 Subject: [PATCH 429/523] fix some module import issues --- manatools/aui/backends/__init__.py | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 manatools/aui/backends/__init__.py diff --git a/manatools/aui/backends/__init__.py b/manatools/aui/backends/__init__.py new file mode 100644 index 0000000..5dd4929 --- /dev/null +++ b/manatools/aui/backends/__init__.py @@ -0,0 +1,36 @@ +""" +manatools.aui.backends package initializer. + +Provides a small, lazy-loading shim so callers can import submodules +(e.g. 'from manatools.aui.backends.gtk import ...') while keeping the +package import lightweight and well-logged. +""" +import importlib +import logging +from types import ModuleType + +_logger = logging.getLogger("manatools.aui.backends") + +# advertised subpackages (may be absent if not installed) +__all__ = ["gtk", "qt", "ncurses"] + +def __getattr__(name: str) -> ModuleType: + """ + Lazy-import backend submodules on attribute access (PEP 562). + Raises AttributeError if submodule cannot be imported. + """ + if name in __all__: + full = f"{__name__}.{name}" + try: + mod = importlib.import_module(full) + globals()[name] = mod + return mod + except Exception: + _logger.debug("Failed to import backend submodule %s", full, exc_info=True) + # re-raise as AttributeError to match import semantics + raise AttributeError(f"cannot import name {name!r} from {__name__}") from None + raise AttributeError(f"module {__name__} has no attribute {name!r}") + +def __dir__(): + # expose advertised backends plus any already imported names + return sorted(list(__all__) + [k for k in globals().keys() if not k.startswith("_")]) From afeefa0ba54dc6a2fa36dd357a81f634393eabf8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Thu, 15 Jan 2026 23:21:24 +0100 Subject: [PATCH 430/523] wrong indentation --- manatools/aui/backends/gtk/tablegtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index 9684814..281db49 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -397,7 +397,7 @@ def addItems(self, items): else: self._logger.error("YTable.addItem: invalid item type %s", type(item)) raise TypeError("YTableGtk.addItem expects a YTableItem or string label") - item.setIndex(len(self._items) - 1) + item.setIndex(len(self._items) - 1) if getattr(self, '_listbox', None) is not None: self.rebuildTable() From 4312c61e0937380ffd650d999708da7f027710c7 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 16:00:49 +0100 Subject: [PATCH 431/523] Added a starting handbook documentation for developers --- docs/manatools_aui_api.md | 198 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/manatools_aui_api.md diff --git a/docs/manatools_aui_api.md b/docs/manatools_aui_api.md new file mode 100644 index 0000000..8a8f237 --- /dev/null +++ b/docs/manatools_aui_api.md @@ -0,0 +1,198 @@ +# manatools AUI - Application API (YUI.application) + +Overview +-------- +The object returned by `YUI.app()` / `YUI.application()` / `YUI.yApp()` is the backend-specific Application object (Qt, GTK, or NCurses). Obtain it via: + +```python +from manatools.aui.yui import YUI +app = YUI.app() # backend-specific application object +# aliases: YUI.application(), YUI.yApp() +``` + +This document lists the common methods that application code should use in a backend-agnostic way and highlights differences and missing documentation. + +Common public methods (backend-agnostic) +--------------------------------------- +These methods are implemented by backend application classes (Qt, GTK, NCurses). Signatures below are the expected API the application code should rely on. + +- setApplicationTitle(title: str) + - Set the application title. Backends attempt to propagate it to windows/dialogs (Qt: QApplication; GTK: active windows; NCurses: terminal window title escape codes). + +- applicationTitle() -> str + - Return current application title. + +- setApplicationIcon(icon_spec: str) + - Set application icon specification (theme name or file path). Backends attempt to resolve and apply the icon where possible. Behavior varies by backend. + +- applicationIcon() -> str + - Return the currently configured icon specification. + +- setIconBasePath(path: str) / iconBasePath() + - Provide a base path used when resolving icon file names prior to theme lookup. + +- setProductName(name: str) / productName() -> str + - Set/read product name metadata used by dialogs or platform integration. + +- setApplicationName(name: str) / applicationName() -> str +- setVersion(version: str) / version() -> str +- setAuthors(authors: str) / authors() -> str +- setDescription(desc: str) / description() -> str +- setLicense(text: str) / license() -> str +- setCredits(credits: str) / credits() -> str +- setInformation(info: str) / information() -> str +- setLogo(path: str) / logo() -> str + - About dialog and metadata setters/getters. + +- isTextMode() -> bool + - Returns True for text-mode (NCurses) backend. Use to adapt UI or behavior. + +- busyCursor() / normalCursor() + - Show/hide busy cursor. Implementations differ (Qt uses override cursor; GTK and NCurses may be no-op or best-effort). + +File chooser helpers (common usage) +----------------------------------- +These functions present file/directory selection dialogs. Implementations differ across backends; calls should be treated as blocking helpers that return the selected path or an empty string when canceled. + +- askForExistingDirectory(startDir: str, headline: str) -> str +- askForExistingFile(startWith: str, filter: str, headline: str) -> str +- askForSaveFileName(startWith: str, filter: str, headline: str) -> str + +Notes: +- `filter` is typically a semicolon-separated list of patterns like `"*.txt;*.md"`. +- Qt uses QFileDialog (synchronous). +- GTK attempts Gtk.FileDialog (GTK4.10+) and supports portals/fallbacks. +- NCurses provides an in-UI browsing overlay; behavior and filter parsing are implemented in Python. + +Practical examples +------------------ +Set title and icon (backend-agnostic): + +```python +app = YUI.app() +app.setApplicationTitle("MyApp") +app.setIconBasePath("/usr/share/myapp/icons") +app.setApplicationIcon("myapp-icon") # theme name or absolute path +``` + +Open a file chooser: + +```python +fn = app.askForExistingFile("/home/user", "*.iso;*.img", "Open image") +if fn: + print("Selected:", fn) +``` + +## Factory createXXX methods + +The widget factory (returned by `YUI.ui().widgetFactory()` / `YUI_ui().widgetFactory()`) provides unified constructors for UI widgets across backends. Use the factory to create dialogs, layout containers and widgets in a backend-agnostic way. + +General pattern: +- Call `factory.createXxx(parent, ...)` to create widget X. +- The returned object is a YWidget subclass; call widget methods (setValue, setLabel, addItem, etc.) and use `dialog.waitForEvent()` loop to handle events. + +Common factory methods (signatures and brief notes) + +- createMainDialog(color_mode=YDialogColorMode.YDialogNormalColor) + - Returns a main dialog (blocking UI container). +- createPopupDialog(color_mode=YDialogColorMode.YDialogNormalColor) + - Returns a popup dialog. + +Layout containers +- createVBox(parent) +- createHBox(parent) + - Vertical / horizontal layout containers (parent is a dialog or another container). + +Common leaf widgets +- createPushButton(parent, label) + - Button widget. Use `setNotify()`, `setIcon()`, `setStretchable()`. +- createIconButton(parent, iconName, fallbackTextLabel) + - Convenience: pushbutton with icon. +- createLabel(parent, text, isHeading=False, isOutputField=False) +- createHeading(parent, label) + - Label / heading above controls. + +Input and selection +- createInputField(parent, label, password_mode=False) + - Single-line text input. +- createMultiLineEdit(parent, label) +- createIntField(parent, label, minVal, maxVal, initialVal) +- createCheckBox(parent, label, is_checked=False) +- createPasswordField(parent, label) +- createComboBox(parent, label, editable=False) + - Combo/select widget. Supports addItem/addItems/deleteAllItems and icons where supported. +- createSelectionBox(parent, label) +- createMultiSelectionBox(parent, label) + - Single- or multi-selection lists. Use `addItem`, `addItems`, `deleteAllItems`, `selectItem`, `selectedItems()`. + +Progress/visual widgets +- createProgressBar(parent, label, max_value=100) +- createImage(parent, imageFileName) + - Backends may support autoscale/stretch; icon/image loading depends on backend. +- createTree(parent, label, multiselection=False, recursiveselection=False) +- createTable(parent, header: YTableHeader, multiSelection=False) +- createRichText(parent, text="", plainTextMode=False) +- createLogView(parent, label, visibleLines, storedLines=0) + +Grouping / frames / special widgets +- createFrame(parent, label="") +- createCheckBoxFrame(parent, label="", checked=False) +- createRadioButton(parent, label="", isChecked=False) +- createReplacePoint(parent) +- createDumbTab(parent) + +Layout helpers +- createSpacing(parent, dim: YUIDimension, stretchable: bool = False, size_px: int = 0) +- createHStretch(parent), createVStretch(parent) +- createHSpacing(parent, size_px=8), createVSpacing(parent, size_px=16) + +Misc +- createMenuBar(parent) +- createSlider(parent, label, minVal, maxVal, initialVal) +- createDateField(parent, label) +- createTimeField(parent, label) + +Notes and backend differences +- All `createXXX` methods return backend-specific widget objects implementing the YWidget API. Call generic methods (setLabel, setValue, addItem, deleteAllItems, setStretchable, setWeight) on those objects. +- Not all backends support icons, autoscaling or the same filter semantics; the factory hides creation differences but some features are best-effort per backend. +- For selection widgets (ComboBox / SelectionBox / Tree / Table): + - Use addItem/addItems/deleteAllItems at runtime to keep the widget and backend in sync. + - Only one item should be selected in single-selection widgets; when selecting programmatically ensure previous selection is cleared (most backends handle this if you use widget.setValue or item.setSelected()). + - Icons, if provided via YItem.iconName(), are displayed on GUI backends (GTK/Qt) when supported; ncurses ignores icons. + +Minimal example +```python +# create a dialog with a combo and a button (backend-agnostic) +ui = YUI_ui() +factory = ui.widgetFactory() +dlg = factory.createMainDialog() +vbox = factory.createVBox(dlg) +combo = factory.createComboBox(vbox, "Choose:", editable=False) +combo.addItems([YItem("One"), YItem("Two", selected=True), YItem("Three")]) +btn = factory.createPushButton(vbox, "OK") + +# basic event loop +while True: + ev = dlg.waitForEvent() + if ev.eventType() == YEventType.CancelEvent: + dlg.destroy() + break + if ev.eventType() == YEventType.WidgetEvent and ev.widget() == btn: + print("Selected:", combo.value()) + dlg.destroy() + break +``` + +Backend differences and caveats +------------------------------- +- Icon resolution: backends first try icon base path, then theme lookup. Icons may not be available or may be treated differently (GTK uses GdkPixbuf, Qt uses QIcon, NCurses ignores icons). +- File dialogs: + - GTK uses Gtk.FileDialog on modern GTK4; availability depends on GTK version and platform (portal fallbacks exist). + - NCurses dialog is implemented with an internal overlay; features (filters, navigation) differ from GUI backends. +- Cursor APIs and certain visual behaviors are backend-specific and may be no-op in text mode. +- Methods that interact with windows/views (e.g., applying an icon to open dialogs) perform best-effort updates and may silently fail if no dialog/window is available. + + +License and contribution +------------------------ +This document is provided to help developers use the cross-backend Application API. Please update the code docstrings and this document when API changes occur. From e5c5fd666f8ba5445d4b18eade8cbf8627160e01 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 16:04:56 +0100 Subject: [PATCH 432/523] Updated with missing tasks --- sow/TODO.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sow/TODO.md b/sow/TODO.md index a80b7fc..696bc6f 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -77,3 +77,21 @@ Skipped widgets: [-] YEmpty (not ported) [-] YSquash / createSquash (not ported) [-] YMenuButton (legacy menus) + +Documentation gaps and recommendations +-------------------------------------- +During review of backend implementations, several simple accessors and setters lack explicit docstrings. To improve developer experience: + +1. Add concise docstrings to all public getters/setters in: + - yui_qt.py (e.g., iconBasePath, setIconBasePath, productName) + - yui_gtk.py (same setters/getters, note GTK version dependencies) + - yui_curses.py (document NCurses-specific behaviors and filter parsing) +2. Document file chooser semantics and supported filter syntax for NCurses and GTK fallbacks. +3. Add a short README or reference page (this file) in the repository to explain expected cross-backend behavior and caveats. + +Checklist for maintainers +------------------------- +- [ ] Add one-line docstrings to all public methods in YApplication* classes. +- [ ] Document minimum runtime dependencies and GTK/Qt version notes. +- [ ] Provide examples for common tasks (file chooser, setting icon/title) in README or docs. +- [ ] Ensure unit tests or integration tests cover file chooser fallbacks. From 5f249933dbdb35b2bbc4351f358b2e4ad787bc0c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 16:34:44 +0100 Subject: [PATCH 433/523] removed chatty log --- manatools/aui/backends/curses/alignmentcurses.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/curses/alignmentcurses.py b/manatools/aui/backends/curses/alignmentcurses.py index 5e52448..0e1f94a 100644 --- a/manatools/aui/backends/curses/alignmentcurses.py +++ b/manatools/aui/backends/curses/alignmentcurses.py @@ -167,14 +167,12 @@ def _draw(self, window, y, x, width, height): pass # give the computed width to the child (at least 1 char) final_w = max(1, child_w) - try: - self._logger.debug("Alignment draw: child=%s halign=%s valign=%s container=(%d,%d) size=(%d,%d) child_min=%d child_pref=%s child_w=%d cx=%d cy=%d", - self.child().debugLabel() if hasattr(self.child(), 'debugLabel') else '', - self._halign_spec, self._valign_spec, - x, y, width, height, - ch_min_w, getattr(self.child(), '_width', None), final_w, cx, cy) - except Exception: - pass + + #self._logger.debug("Alignment draw: child=%s halign=%s valign=%s container=(%d,%d) size=(%d,%d) child_min=%d child_pref=%s child_w=%d cx=%d cy=%d", + # self.child().debugLabel() if hasattr(self.child(), 'debugLabel') else '', + # self._halign_spec, self._valign_spec, + # x, y, width, height, + # ch_min_w, getattr(self.child(), '_width', None), final_w, cx, cy) self.child()._draw(window, cy, cx, final_w, min(height, ch_height)) except Exception: pass From 9442676b483a27efc8d7708cbf5c92780656155c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 16:35:02 +0100 Subject: [PATCH 434/523] improved addItems and deprecated rebuildTree --- manatools/aui/backends/curses/treecurses.py | 34 +++++++++++++++++--- manatools/aui/backends/gtk/treegtk.py | 31 +++++++++++++++--- manatools/aui/backends/qt/treeqt.py | 35 ++++++++++++++++----- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index dcd1e86..8186e90 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -88,7 +88,7 @@ def _create_backend_widget(self): # associate placeholder backend widget to avoid None self._backend_widget = self self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) - self.rebuildTree() + self._rebuildTree() except Exception as e: try: self._logger.critical("_create_backend_widget critical error: %s", e, exc_info=True) @@ -109,7 +109,26 @@ def addItem(self, item): finally: try: # mark rebuild so new items are visible without waiting for external trigger - self.rebuildTree() + self._rebuildTree() + except Exception: + pass + + def addItems(self, items): + '''Add multiple items to the table. This is more efficient than calling addItem repeatedly.''' + try: + for item in items: + # prefer base implementation if present + try: + super().addItem(item) + except Exception: + # fallback: append to _items list used by this backend + if not hasattr(self, "_items") or self._items is None: + self._items = [] + self._items.append(item) + finally: + try: + # mark rebuild so new items are visible without waiting for external trigger + self._rebuildTree() except Exception: pass @@ -126,7 +145,7 @@ def removeItem(self, item): pass finally: try: - self.rebuildTree() + self._rebuildTree() except Exception: pass @@ -218,6 +237,11 @@ def _visit(nodes, depth=0): _visit(roots, 0) def rebuildTree(self): + """RebuildTree to maintain compatibility.""" + self._logger.warning("rebuildTree is deprecated and should not be needed anymore") + self._rebuildTree() + + def _rebuildTree(self): """Recompute visible items and restore selection from item.selected() or last_selected_ids. Ensures ancestors of selected items are opened so selections are visible. @@ -420,7 +444,7 @@ def _toggle_expand(self, item): self._last_selected_ids = set(id(i) for i in getattr(self, "_selected_items", []) or []) except Exception: self._last_selected_ids = set() - self.rebuildTree() + self._rebuildTree() finally: try: self._suppress_selection_handler = False @@ -737,7 +761,7 @@ def selectItem(self, item, selected=True): self._last_selected_ids = set() # after programmatic selection, rebuild visible list to reflect opened parents try: - self.rebuildTree() + self._rebuildTree() except Exception: pass except Exception: diff --git a/manatools/aui/backends/gtk/treegtk.py b/manatools/aui/backends/gtk/treegtk.py index 7556e17..bfa9eb4 100644 --- a/manatools/aui/backends/gtk/treegtk.py +++ b/manatools/aui/backends/gtk/treegtk.py @@ -126,7 +126,7 @@ def _create_backend_widget(self): # populate if items already exist try: if getattr(self, "_items", None): - self.rebuildTree() + self._rebuildTree() except Exception: try: self._logger.error("rebuildTree failed during _create_backend_widget", exc_info=True) @@ -228,7 +228,7 @@ def _on_expanded_changed(expander, pspec, target_item=item): except Exception: self._last_selected_ids = set() try: - self.rebuildTree() + self._rebuildTree() except Exception: pass finally: @@ -342,7 +342,7 @@ def _on_toggle_clicked(self, item): except Exception: self._last_selected_ids = set() try: - self.rebuildTree() + self._rebuildTree() except Exception: pass finally: @@ -373,6 +373,11 @@ def _collect_all_descendants(self, item): return out def rebuildTree(self): + """RebuildTree to maintain compatibility.""" + self._logger.warning("rebuildTree is deprecated and should not be needed anymore") + self._rebuildTree() + + def _rebuildTree(self): """Flatten visible items according to _is_open and populate the ListBox.""" _suppress_selection_handler = True if self._backend_widget is None or self._listbox is None: @@ -992,12 +997,28 @@ def addItem(self, item): try: if getattr(self, '_listbox', None) is not None: try: - self.rebuildTree() + self._rebuildTree() except Exception: pass except Exception: pass + def addItems(self, items): + '''Add multiple items to the table. This is more efficient than calling addItem repeatedly.''' + for item in items: + if isinstance(item, str): + item = YTreeItem(item) + super().addItem(item) + else: + super().addItem(item) + item.setIndex(len(self._items) - 1) + try: + if getattr(self, '_listbox', None) is not None: + self._rebuildTree() + except Exception: + pass + + def selectItem(self, item, selected=True): """Select/deselect a logical YTreeItem and reflect changes in the Gtk.ListBox.""" try: @@ -1021,7 +1042,7 @@ def selectItem(self, item, selected=True): if getattr(self, '_listbox', None) is None: return # ensure mapping exists - self.rebuildTree() + self._rebuildTree() except Exception: pass diff --git a/manatools/aui/backends/qt/treeqt.py b/manatools/aui/backends/qt/treeqt.py index 19e96ce..11bc5a6 100644 --- a/manatools/aui/backends/qt/treeqt.py +++ b/manatools/aui/backends/qt/treeqt.py @@ -70,12 +70,16 @@ def _create_backend_widget(self): self._backend_widget.setEnabled(bool(self._enabled)) # populate if items already present try: - self.rebuildTree() + self._rebuildTree() except Exception: self._logger.error("rebuildTree failed during _create_backend_widget", exc_info=True) - def rebuildTree(self): + """RebuildTree to maintain compatibility.""" + self._logger.warning("rebuildTree is deprecated and should not be needed anymore") + self._rebuildTree() + + def _rebuildTree(self): """Rebuild the QTreeWidget from self._items (calls helper recursively).""" self._logger.debug("rebuildTree: rebuilding tree with %d items", len(self._items) if self._items else 0) self._suppress_selection_handler = True @@ -509,10 +513,27 @@ def addItem(self, item): # if backend exists, refresh tree to reflect new item (including icon/selection) try: if getattr(self, '_tree_widget', None) is not None: - try: - self.rebuildTree() - except Exception: - pass + self._rebuildTree() + except Exception: + pass + + def addItems(self, items): + '''Add multiple items to the table. This is more efficient than calling addItem repeatedly.''' + for item in items: + if isinstance(item, str): + item = YTreeItem(item) + super().addItem(item) + elif isinstance(item, YTreeItem): + super().addItem(item) + else: + self._logger.error("YTree.addItem: invalid item type %s", type(item)) + raise TypeError("YTree.addItem expects a YTreeItem or string label") + # ensure index set + item.setIndex(len(self._items) - 1) + # if backend exists, refresh tree to reflect new item (including icon/selection) + try: + if getattr(self, '_tree_widget', None) is not None: + self._rebuildTree() except Exception: pass @@ -584,7 +605,7 @@ def selectItem(self, item, selected=True): if qit is None: # item may be newly added — rebuild tree and retry try: - self.rebuildTree() + self._rebuildTree() qit = self._item_to_qitem.get(item, None) except Exception: qit = None From 43f203f929853a01a8e953a8994d876691cdda0f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 16:47:19 +0100 Subject: [PATCH 435/523] updated --- setup.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 098363f..3681317 100644 --- a/setup.py +++ b/setup.py @@ -23,11 +23,12 @@ author='Angelo Naselli', author_email='anaselli@linux.it', packages=[ - 'manatools', - 'manatools.aui', - 'manatools.aui.backends.qt', - 'manatools.aui.backends.gtk', - 'manatools.aui.backends.curses', + 'manatools', + 'manatools.aui', + 'manatools.aui.backends', + 'manatools.aui.backends.qt', + 'manatools.aui.backends.gtk', + 'manatools.aui.backends.curses', 'manatools.ui' ], include_package_data=True, From e4b8fc3c43e5a9269ecd53e9546f790108e0c4f4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 17:03:10 +0100 Subject: [PATCH 436/523] scrolling symbol as in Tree widget --- manatools/aui/backends/curses/tablecurses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/tablecurses.py b/manatools/aui/backends/curses/tablecurses.py index 1e9ef7b..bfca616 100644 --- a/manatools/aui/backends/curses/tablecurses.py +++ b/manatools/aui/backends/curses/tablecurses.py @@ -262,9 +262,9 @@ def _draw(self, window, y, x, width, height): if self._focused and len(self._items) > visible and width > 0 and self.isEnabled(): try: if self._scroll_offset > 0: - window.addch(y + 1, x + width - 1, '^') + window.addch(y + 1, x + width - 1, '↑', curses.A_REVERSE) if (self._scroll_offset + visible) < len(self._items): - window.addch(y + visible, x + width - 1, 'v') + window.addch(y + visible, x + width - 1, '↓', curses.A_REVERSE) except curses.error: pass except curses.error: From c67a8c254c54bb127947c08de3256a30b52983b8 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 18:24:37 +0100 Subject: [PATCH 437/523] hide label if not passed --- manatools/aui/backends/qt/inputfieldqt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manatools/aui/backends/qt/inputfieldqt.py b/manatools/aui/backends/qt/inputfieldqt.py index 26f5e5a..243fb97 100644 --- a/manatools/aui/backends/qt/inputfieldqt.py +++ b/manatools/aui/backends/qt/inputfieldqt.py @@ -42,6 +42,8 @@ def _create_backend_widget(self): self._qlbl = QtWidgets.QLabel(self._label) layout.addWidget(self._qlbl) + if not self._label: + self._qlbl.hide() entry = QtWidgets.QLineEdit() if self._password_mode: @@ -99,6 +101,10 @@ def setLabel(self, label): try: if hasattr(self, '_qlbl') and self._qlbl is not None: self._qlbl.setText(str(label)) + if not label: + self._qlbl.hide() + else: + self._qlbl.show() except Exception: pass From b808762ee364b7ee508cae0553fb35e8ed0ed6ee Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 17 Jan 2026 19:43:22 +0100 Subject: [PATCH 438/523] forced to bool --- manatools/aui/yui_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index dbbdc5d..f253b66 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -257,7 +257,7 @@ def stretchable(self, dim): else: return self._stretchable_vert - def setStretchable(self, dim, new_stretch): + def setStretchable(self, dim, new_stretch: bool): if dim == YUIDimension.YD_HORIZ: self._stretchable_horiz = new_stretch else: From 77de0b5b1c15c82d5e3a27d5cd15a545ef68a126 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 00:10:07 +0100 Subject: [PATCH 439/523] Added visibility attribute to YWidget to be managed in any widgets --- manatools/aui/yui_common.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index f253b66..ab57a70 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -135,6 +135,7 @@ def __init__(self, parent=None): self._parent = parent self._children = [] self._enabled = True + self._visible = True self._help_text = "" self._backend_widget = None self._stretchable_horiz = False @@ -165,9 +166,11 @@ def debugLabel(self): return f"{self.widgetClass()}({self._id})" def helpText(self): + ''' Return the help text (tooltip) for this widget. ''' return self._help_text def setHelpText(self, help_text): + ''' Set the help text (tooltip) for this widget. ''' self._help_text = help_text def hasChildren(self): @@ -246,11 +249,17 @@ def setEnabled(self, enabled=True): self._set_backend_enabled(enabled) def setDisabled(self): + ''' Disenable the widget. ''' self.setEnabled(False) def isEnabled(self): + ''' Return whether the widget is enabled. ''' return self._enabled + def setVisible(self, visible=True): + ''' Set the visibility of the widget. Backend-specific implementation required. ''' + pass + def stretchable(self, dim): if dim == YUIDimension.YD_HORIZ: return self._stretchable_horiz From fbe6818d0c48950225ffccd8c5be641d8fc61ebf Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 00:11:13 +0100 Subject: [PATCH 440/523] Added visibility management and icon button only --- .../aui/backends/curses/pushbuttoncurses.py | 11 +- manatools/aui/backends/gtk/pushbuttongtk.py | 148 ++++++++++-------- manatools/aui/backends/qt/pushbuttonqt.py | 39 ++++- manatools/aui/yui_curses.py | 7 +- manatools/aui/yui_gtk.py | 8 +- manatools/aui/yui_qt.py | 4 +- 6 files changed, 136 insertions(+), 81 deletions(-) diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index d508cb6..6748666 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -28,12 +28,13 @@ class YPushButtonCurses(YWidget): - def __init__(self, parent=None, label=""): + def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, icon_only: Optional[bool]=False): super().__init__(parent) self._label = label self._focused = False self._can_focus = True - self._icon_name = None + self._icon_name = icon_name + self._icon_only = bool(icon_only) self._height = 1 # Fixed height - buttons are always one line self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") # derive mnemonic and cleaned label if present @@ -91,6 +92,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: # Center the button label within available width, show underline for mnemonic clean = getattr(self, "_clean_label", None) or self._label @@ -169,3 +172,7 @@ def setIcon(self, icon_name: str): self._icon_name = icon_name except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + self._can_focus = visible \ No newline at end of file diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index cf3b92a..fdc532b 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -18,15 +18,17 @@ import threading import os import logging +from typing import Optional from ...yui_common import * from .commongtk import _resolve_icon, _convert_mnemonic_to_gtk class YPushButtonGtk(YWidget): - def __init__(self, parent=None, label=""): + def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, icon_only: Optional[bool]=False): super().__init__(parent) self._label = _convert_mnemonic_to_gtk(label) - self._icon_name = None + self._icon_name = icon_name + self._icon_only = bool(icon_only) self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): @@ -36,53 +38,54 @@ def label(self): return self._label def setLabel(self, label): - self._label = label + self._label = _convert_mnemonic_to_gtk(label) if self._backend_widget: try: - self._backend_widget.set_label(label) + self._backend_widget.set_label(self._label) except Exception: pass def _create_backend_widget(self): - self._backend_widget = Gtk.Button(label=self._label) - self._backend_widget.set_use_underline(True) - # apply icon if previously set - try: - if getattr(self, "_icon_name", None): - try: + if self._icon_only: + self._logger.info(f"Creating icon-only button '{self._label}'") + self._backend_widget = Gtk.Button() + self._backend_widget.set_use_underline(True) + if self._icon_name: + self._backend_widget.set_icon_name(self._icon_name) + else: + self._logger.info(f"Creating button with icon and label '{self._label}'") + try: + self._backend_widget = Gtk.Button(label=self._label) + self._backend_widget.set_use_underline(True) + hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + if self._icon_name: img = _resolve_icon(self._icon_name) + if img is not None: + hb.append(img) + lbl = Gtk.Label(label=self._label, use_underline=True) + # center contents inside the box so button label appears centered + try: + hb.set_halign(Gtk.Align.CENTER) + hb.set_valign(Gtk.Align.CENTER) + hb.set_hexpand(False) except Exception: - img = None - if img is not None: - try: - # Prefer set_icon if available - try: - self._backend_widget.set_icon(img.get_paintable()) - except Exception: - # Fallback: set a composite child with image + label - try: - hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - hb.append(img) - lbl = Gtk.Label(label=self._label) - # center contents inside the box so button label appears centered - try: - hb.set_halign(Gtk.Align.CENTER) - hb.set_valign(Gtk.Align.CENTER) - hb.set_hexpand(False) - except Exception: - pass - try: - lbl.set_halign(Gtk.Align.CENTER) - except Exception: - pass - hb.append(lbl) - self._backend_widget.set_child(hb) - except Exception: - pass - except Exception: - pass - except Exception: - pass + pass + try: + lbl.set_halign(Gtk.Align.CENTER) + except Exception: + pass + hb.append(lbl) + self._backend_widget.set_child(hb) + except Exception: + self._logger.exception("Failed to create button with icon and label", exc_info=True) + raise RuntimeError("Failed to create button with icon and label") + + if self._help_text: + try: + self._backend_widget.set_tooltip_text(self._help_text) + except Exception: + self._logger.exception("Failed to set tooltip text", exc_info=True) + # Prevent button from being stretched horizontally by default. try: self._backend_widget.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) @@ -131,6 +134,10 @@ def setIcon(self, icon_name: str): self._icon_name = icon_name if getattr(self, "_backend_widget", None) is None: return + if self._icon_only: + self._backend_widget.set_icon_name(icon_name) + return + # not icon_only: try to set icon + label img = None try: img = _resolve_icon(icon_name) @@ -138,30 +145,26 @@ def setIcon(self, icon_name: str): img = None if img is not None: try: + # Set composite child with image + label (centered) try: - self._backend_widget.set_icon(img.get_paintable()) - return - except Exception: - # Fallback: set composite child with image + label (centered) + hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + hb.append(img) + lbl = Gtk.Label(label=self._label) try: - hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - hb.append(img) - lbl = Gtk.Label(label=self._label) - try: - hb.set_halign(Gtk.Align.CENTER) - hb.set_valign(Gtk.Align.CENTER) - hb.set_hexpand(False) - except Exception: - pass - try: - lbl.set_halign(Gtk.Align.CENTER) - except Exception: - pass - hb.append(lbl) - self._backend_widget.set_child(hb) - return + hb.set_halign(Gtk.Align.CENTER) + hb.set_valign(Gtk.Align.CENTER) + hb.set_hexpand(False) except Exception: pass + try: + lbl.set_halign(Gtk.Align.CENTER) + except Exception: + pass + hb.append(lbl) + self._backend_widget.set_child(hb) + return + except Exception: + pass except Exception: pass # If we reach here, clear any icon and ensure label is present @@ -169,6 +172,7 @@ def setIcon(self, icon_name: str): # Reset to simple label-only button try: self._backend_widget.set_label(self._label) + self._backend_widget.set_use_underline(True) except Exception: try: # If set_label not available, set child to a label @@ -183,3 +187,25 @@ def setIcon(self, icon_name: str): self._logger.exception("setIcon failed") except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.set_visible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + except Exception: + pass + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.set_tooltip_text(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) + except Exception: + pass \ No newline at end of file diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index b182845..1c7d331 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -11,15 +11,17 @@ ''' from PySide6 import QtWidgets, QtGui import logging +from typing import Optional from ...yui_common import * from .commonqt import _resolve_icon class YPushButtonQt(YWidget): - def __init__(self, parent=None, label=""): + def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, icon_only: Optional[bool]=False): super().__init__(parent) self._label = label - self._icon_name = None + self._icon_name = icon_name + self._icon_only = bool(icon_only) self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): @@ -30,11 +32,16 @@ def label(self): def setLabel(self, label): self._label = label - if self._backend_widget: + if self._backend_widget and self._icon_only is False: self._backend_widget.setText(label) def _create_backend_widget(self): - self._backend_widget = QtWidgets.QPushButton(self._label) + if self._icon_only: + self._backend_widget = QtWidgets.QPushButton() + else: + self._backend_widget = QtWidgets.QPushButton(self._label) + if self._help_text: + self._backend_widget.setToolTip(self._help_text) # apply icon if previously set try: if getattr(self, "_icon_name", None): @@ -93,10 +100,7 @@ def _on_clicked(self): dlg._post_event(YWidgetEvent(self, YEventReason.Activated)) else: # fallback logging for now - try: - self._logger.warning("Button clicked (no dialog found): %s", self._label) - except Exception: - pass + self._logger.warning("Button clicked (no dialog found): %s", self._label) def setIcon(self, icon_name: str): """Set/clear the icon for this pushbutton (icon_name may be theme name or path).""" @@ -121,3 +125,22 @@ def setIcon(self, icon_name: str): self._logger.exception("setIcon failed") except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + if visible: + self._backend_widget.show() + else: + self._backend_widget.hide() + except Exception: + self._logger.exception("setVisible failed") + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setToolTip(help_text) + except Exception: + self._logger.exception("setHelpText failed") diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index 58a2c2b..c469fa6 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -505,8 +505,10 @@ def createHeading(self, parent, label): def createInputField(self, parent, label, password_mode=False): return YInputFieldCurses(parent, label, password_mode) + def createIntField(self, parent, label, minVal, maxVal, initialVal): return YIntFieldCurses(parent, label, minVal, maxVal, initialVal) + def createMultiLineEdit(self, parent, label): return YMultiLineEditCurses(parent, label) @@ -514,9 +516,8 @@ def createPushButton(self, parent, label): return YPushButtonCurses(parent, label) def createIconButton(self, parent, iconName, fallbackTextLabel): - btn = YPushButtonCurses(parent, fallbackTextLabel) - btn.setIcon(iconName) - return btn + ''' create a button with a fallback text label in ncurses''' + return YPushButtonCurses(parent, label=fallbackTextLabel, icon_name=iconName, icon_only=True) def createCheckBox(self, parent, label, is_checked=False): return YCheckBoxCurses(parent, label, is_checked) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 53c5bae..7f35502 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -723,10 +723,8 @@ def createPushButton(self, parent, label): return YPushButtonGtk(parent, label) def createIconButton(self, parent, iconName, fallbackTextLabel): - btn = YPushButtonGtk(parent, fallbackTextLabel) - btn.setIcon(iconName) - return btn - + return YPushButtonGtk(parent, label=fallbackTextLabel, icon_name=iconName, icon_only=True) + def createLabel(self, parent, text, isHeading=False, isOutputField=False): return YLabelGtk(parent, text, isHeading, isOutputField) @@ -735,8 +733,10 @@ def createHeading(self, parent, label): def createInputField(self, parent, label, password_mode=False): return YInputFieldGtk(parent, label, password_mode) + def createMultiLineEdit(self, parent, label): return YMultiLineEditGtk(parent, label) + def createIntField(self, parent, label, minVal, maxVal, initialVal): return YIntFieldGtk(parent, label, minVal, maxVal, initialVal) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 20d3246..e6b60db 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -306,9 +306,7 @@ def createPushButton(self, parent, label): return YPushButtonQt(parent, label) def createIconButton(self, parent, iconName, fallbackTextLabel): - btn = YPushButtonQt(parent, fallbackTextLabel) - btn.setIcon(iconName) - return btn + return YPushButtonQt(parent, label=fallbackTextLabel, icon_name=iconName, icon_only=True) def createLabel(self, parent, text, isHeading=False, isOutputField=False): return YLabelQt(parent, text, isHeading, isOutputField) From fc39b93bbe629c9fa01be04e410eece0fc6707cc Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 15:10:47 +0100 Subject: [PATCH 441/523] Forgot to set visibility property --- manatools/aui/yui_common.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/manatools/aui/yui_common.py b/manatools/aui/yui_common.py index ab57a70..e65067e 100644 --- a/manatools/aui/yui_common.py +++ b/manatools/aui/yui_common.py @@ -255,10 +255,14 @@ def setDisabled(self): def isEnabled(self): ''' Return whether the widget is enabled. ''' return self._enabled + + def visible(self) -> bool: + ''' Retunr the visibility of the widget. ''' + return bool(self._visible) - def setVisible(self, visible=True): + def setVisible(self, visible:bool=True): ''' Set the visibility of the widget. Backend-specific implementation required. ''' - pass + self._visible = bool(visible) def stretchable(self, dim): if dim == YUIDimension.YD_HORIZ: @@ -823,4 +827,4 @@ def conflict(self): return self._conflict def setConflict(self, conflict=True): - self._conflict = conflict \ No newline at end of file + self._conflict = conflict From 498346b91f531d78b8f4e2480c9f36f4095bf9f1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 15:11:14 +0100 Subject: [PATCH 442/523] Manged visibility on widget creation --- manatools/aui/backends/gtk/pushbuttongtk.py | 6 +++++- manatools/aui/backends/qt/pushbuttonqt.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index fdc532b..19fbdbd 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -85,6 +85,10 @@ def _create_backend_widget(self): self._backend_widget.set_tooltip_text(self._help_text) except Exception: self._logger.exception("Failed to set tooltip text", exc_info=True) + try: + self._backend_widget.set_visible(self.visible()) + except Exception: + self._logger.exception("Failed to set widget visibility", exc_info=True) # Prevent button from being stretched horizontally by default. try: @@ -208,4 +212,4 @@ def setHelpText(self, help_text: str): except Exception: self._logger.exception("setHelpText failed", exc_info=True) except Exception: - pass \ No newline at end of file + pass diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index 1c7d331..e2d0e07 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -42,6 +42,10 @@ def _create_backend_widget(self): self._backend_widget = QtWidgets.QPushButton(self._label) if self._help_text: self._backend_widget.setToolTip(self._help_text) + if self.visible(): + self._backend_widget.show() + else: + self._backend_widget.hide() # apply icon if previously set try: if getattr(self, "_icon_name", None): From 04fbea6137152b386f61b2ac0a59d0b19bdb79d6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 22:19:40 +0100 Subject: [PATCH 443/523] discard not visible children --- manatools/aui/backends/curses/hboxcurses.py | 9 +++++---- manatools/aui/backends/curses/vboxcurses.py | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/curses/hboxcurses.py b/manatools/aui/backends/curses/hboxcurses.py index 8cc8a1b..ac2ee19 100644 --- a/manatools/aui/backends/curses/hboxcurses.py +++ b/manatools/aui/backends/curses/hboxcurses.py @@ -150,6 +150,8 @@ def _draw(self, window, y, x, width, height): pref_reserved = [0] * num_children child_weights = [0] * num_children for i, child in enumerate(self._children): + if child.visible() is False: + continue # compute each child's minimal width (best-effort) m = self._child_min_width(child, available) min_reserved[i] = max(1, m) @@ -311,6 +313,8 @@ def _required_width_for(widget): # try to satisfy required widths by borrowing from others for i, child in enumerate(self._children): + if child.visible() is False: + continue req = _required_width_for(child) if widths[i] < req: need = req - widths[i] @@ -336,7 +340,7 @@ def _required_width_for(widget): cx = x for i, child in enumerate(self._children): w = widths[i] - if w <= 0: + if w <= 0 or child.visible() is False: continue # Give full container height to vertically-stretchable children # and to nested VBoxes so their internal layout can use the @@ -351,9 +355,6 @@ def _required_width_for(widget): else: ch = min(height, max(1, getattr(child, "_height", 1))) if hasattr(child, "_draw"): - #self._logger.debug("HBox drawing child %d: lbl=%s alloc_w=%d x=%d height=%d ch_h=%d", i, - # (child.debugLabel() if hasattr(child, 'debugLabel') else f'child_{i}'), - # w, cx, height, ch) child._draw(window, y, cx, w, ch) cx += w if i < num_children - 1: diff --git a/manatools/aui/backends/curses/vboxcurses.py b/manatools/aui/backends/curses/vboxcurses.py index f6f16ef..d341f24 100644 --- a/manatools/aui/backends/curses/vboxcurses.py +++ b/manatools/aui/backends/curses/vboxcurses.py @@ -94,6 +94,10 @@ def _draw(self, window, y, x, width, height): fixed_height_total = 0 for i, child in enumerate(self._children): + if child.visible() is False: + child_min_heights.append(0) + child_pref_heights.append(0) + continue # Minimal height (hard lower bound) min_h = max(1, _curses_recursive_min_height(child)) # Preferred/requested height (may be larger than min_h) @@ -223,6 +227,8 @@ def _draw(self, window, y, x, width, height): cy = y gaps_allowed = spacing_allowed for i, child in enumerate(self._children): + if child.visible() is False: + continue ch = allocated[i] if ch <= 0: continue From 3d14a56a7225ca173b3f7b3ca1a9fe8a53c82007 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 22:20:20 +0100 Subject: [PATCH 444/523] using setVisible instead of hide and show --- manatools/aui/backends/qt/pushbuttonqt.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index e2d0e07..20d15e9 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -134,10 +134,7 @@ def setVisible(self, visible=True): super().setVisible(visible) try: if getattr(self, "_backend_widget", None) is not None: - if visible: - self._backend_widget.show() - else: - self._backend_widget.hide() + self._backend_widget.setVisible(visible) except Exception: self._logger.exception("setVisible failed") From fa470f7fdd1989ba936c450e54ddcd240899a352 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 22:22:05 +0100 Subject: [PATCH 445/523] Managed visibility and label occupation if not present --- .../aui/backends/curses/comboboxcurses.py | 18 +++- manatools/aui/backends/gtk/comboboxgtk.py | 87 ++++++++++--------- manatools/aui/backends/qt/comboboxqt.py | 54 ++++++++---- 3 files changed, 99 insertions(+), 60 deletions(-) diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py index 69d2dcd..90480ba 100644 --- a/manatools/aui/backends/curses/comboboxcurses.py +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -35,7 +35,7 @@ def __init__(self, parent=None, label="", editable=False): self._focused = False self._can_focus = True # Reserve two lines: one for the label (caption) and one for the control - self._height = 2 + self._height = 2 if self._label else 1 self._expanded = False self._hover_index = 0 self._combo_x = 0 @@ -111,15 +111,21 @@ def _set_backend_enabled(self, enabled): except Exception: pass + def setLabel(self, new_label): + super().setLabel(new_label) + self._height = 2 if self._label else 1 + def _draw(self, window, y, x, width, height): + if self._visible is False: + return # Store position and dimensions for dropdown drawing # Label is drawn on row `y`, combo control on row `y+1`. - self._combo_y = y + 1 + self._combo_y = y + 1 if self._label else y self._combo_x = x self._combo_width = width # require at least two rows (label + control) - if height < 2: + if height < self._height: return try: @@ -144,7 +150,7 @@ def _draw(self, window, y, x, width, height): # Prepare display value and draw combo on next row display_value = self._value if self._value else "Select..." - max_display_width = combo_space - 3 + max_display_width = combo_space - 6 # account for " ▼" and padding if len(display_value) > max_display_width: display_value = display_value[:max_display_width] + "..." @@ -323,3 +329,7 @@ def deleteAllItems(self): self._value = "" self._expanded = False self._hover_index = 0 + + def setVisible(self, visible=True): + super().setVisible(visible) + self._can_focus = visible \ No newline at end of file diff --git a/manatools/aui/backends/gtk/comboboxgtk.py b/manatools/aui/backends/gtk/comboboxgtk.py index bb09793..1093802 100644 --- a/manatools/aui/backends/gtk/comboboxgtk.py +++ b/manatools/aui/backends/gtk/comboboxgtk.py @@ -83,21 +83,22 @@ def _create_backend_widget(self): # use vertical box so label is above the control hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + self._label_widget = Gtk.Label() + try: + hbox.append(self._label_widget) + except Exception: + self._logger.error("Failed to append label to box", exc_info=True) + hbox.add(self._label_widget) if self._label: - label = Gtk.Label(label=self._label) - self._label_widget = label + self._label_widget.set_text(self._label) try: - if hasattr(label, "set_xalign"): - label.set_xalign(0.0) + if hasattr(self._label_widget, "set_xalign"): + self._label_widget.set_xalign(0.0) except Exception: - pass - # store the label widget so setLabel() can update it later - self._label_widget = label - try: - hbox.append(label) - except Exception: - hbox.add(label) - + self._logger.error("Failed to set xalign on label", exc_info=True) + self._label_widget.set_visible(True) + else: + self._label_widget.set_visible(False) # Determine expansion flags from logical widget before creating the control try: try: @@ -240,43 +241,33 @@ def _create_backend_widget(self): hbox.add(entry) self._backend_widget = hbox + if self._help_text: + try: + self._backend_widget.set_tooltip_text(self._help_text) + except Exception: + self._logger.error("Failed to set tooltip text on backend widget", exc_info=True) try: - self._backend_widget.set_sensitive(self._enabled) + self._backend_widget.set_visible(self.visible()) except Exception: - pass + self._logger.error("Failed to set backend widget visible", exc_info=True) try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + self._backend_widget.set_sensitive(self._enabled) except Exception: - pass - + self._logger.exception("Failed to set sensitivity on backend widget") + + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + def setLabel(self, new_label: str): - """Set logical label and update/create the visual Gtk.Label in the box.""" + """Set logical label and update the visual Gtk.Label in the box.""" try: super().setLabel(new_label) - if self._label_widget is not None: + if self._backend_widget is None: + return + if new_label: self._label_widget.set_text(new_label) + self._label_widget.set_visible(True) else: - # create and prepend label to the hbox - if getattr(self, "_backend_widget", None) is not None: - try: - new_lbl = Gtk.Label(label=new_label) - try: - if hasattr(new_lbl, "set_xalign"): - new_lbl.set_xalign(0.0) - except Exception: - pass - # prepend so label appears before the combo control - try: - self._backend_widget.prepend(new_lbl) - except Exception: - # fallback: append and hope layout is acceptable - try: - self._backend_widget.append(new_lbl) - except Exception: - self._logger.exception("setLabel: failed to add new Gtk.Label to backend box") - self._label_widget = new_lbl - except Exception: - self._logger.exception("setLabel: error creating/inserting Gtk.Label") + self._label_widget.set_visible(False) except Exception: self._logger.exception("setLabel: error updating label=%r", new_label) @@ -533,3 +524,19 @@ def deleteAllItems(self): self._logger.exception("deleteAllItems: failed to clear fallback widget label") except Exception: self._logger.exception("deleteAllItems: unexpected error while updating backend widget") + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_visible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_tooltip_text(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) diff --git a/manatools/aui/backends/qt/comboboxqt.py b/manatools/aui/backends/qt/comboboxqt.py index 40f37ce..4c40883 100644 --- a/manatools/aui/backends/qt/comboboxqt.py +++ b/manatools/aui/backends/qt/comboboxqt.py @@ -61,21 +61,15 @@ def editable(self): def setLabel(self, new_label: str): """Set logical label and update/create the visual QLabel in the container.""" + super().setLabel(new_label) try: - super().setLabel(new_label) - if self._label_widget is not None: + if self._backend_widget is None: + return + if new_label: self._label_widget.setText(new_label) + self._label_widget.setVisible(True) else: - # create and insert label before combo in layout - if getattr(self, "_backend_widget", None) is not None and getattr(self, "_combo_widget", None) is not None: - try: - layout = self._backend_widget.layout() - if layout is not None: - label = QtWidgets.QLabel(new_label) - layout.insertWidget(0, label) - self._label_widget = label - except Exception: - self._logger.exception("setLabel: failed to insert new QLabel") + self._label_widget.setVisible(False) except Exception: self._logger.exception("setLabel: error updating label=%r", new_label) @@ -85,11 +79,14 @@ def _create_backend_widget(self): layout = QtWidgets.QVBoxLayout(container) layout.setContentsMargins(0, 0, 0, 0) + self._label_widget = QtWidgets.QLabel() + layout.addWidget(self._label_widget) if self._label: - label = QtWidgets.QLabel(self._label) - self._label_widget = label - layout.addWidget(label) - + self._label_widget.setText(self._label) + self._label_widget.setVisible(True) + else: + self._label_widget.setVisible(False) + if self._editable: combo = QtWidgets.QComboBox() combo.setEditable(True) @@ -200,6 +197,15 @@ def _create_backend_widget(self): self._backend_widget = container self._combo_widget = combo self._backend_widget.setEnabled(bool(self._enabled)) + try: + if self._help_text: + self._backend_widget.setToolTip(self._help_text) + except Exception: + self._logger.exception("Failed to set tooltip text", exc_info=True) + try: + self._backend_widget.setVisible(self.visible()) + except Exception: + self._logger.exception("Failed to set widget visibility", exc_info=True) # allow logger to raise if misconfigured self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) @@ -320,3 +326,19 @@ def _on_text_changed(self, text): dlg._post_event(YWidgetEvent(self, YEventReason.SelectionChanged)) except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setVisible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setToolTip(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) \ No newline at end of file From 9e21efb666100a884f7922e946d25260bd83967c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 22:43:56 +0100 Subject: [PATCH 446/523] show all the item text if there is enough room when expanded --- .../aui/backends/curses/comboboxcurses.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py index 90480ba..98b3716 100644 --- a/manatools/aui/backends/curses/comboboxcurses.py +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -193,9 +193,24 @@ def _draw_expanded_list(self, window): # Calculate dropdown position - right below the combo control row dropdown_y = self._combo_y + 1 dropdown_x = self._combo_x - dropdown_width = self._combo_width + try: + # longest item label + max_label_len = max((len(i.label()) for i in self._items), default=0) + except Exception: + max_label_len = 0 + # desired width includes small padding + desired_width = max_label_len + 2 + # never exceed screen usable width (leave 1 column margin) + max_allowed = max(5, screen_width - 2) + dropdown_width = min(desired_width, max_allowed) + # if popup would overflow to the right, shift it left so full text is visible when possible + if dropdown_x + dropdown_width >= screen_width: + shift = (dropdown_x + dropdown_width) - (screen_width - 1) + dropdown_x = max(0, dropdown_x - shift) + # ensure reasonable minimum + if dropdown_width < 5: + dropdown_width = 5 - # If not enough space below, draw above if dropdown_y + list_height >= screen_height: dropdown_y = max(1, self._combo_y - list_height - 1) @@ -203,9 +218,6 @@ def _draw_expanded_list(self, window): if dropdown_x + dropdown_width >= screen_width: dropdown_width = screen_width - dropdown_x - 1 - if dropdown_width <= 5: # Need reasonable width - return - # Draw dropdown background for each item for i in range(list_height): if i >= len(self._items): From b4618f95c4a29d8b2448993f09bbe3cb5add1f9d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 23:00:02 +0100 Subject: [PATCH 447/523] get 2 more chars available --- manatools/aui/backends/curses/checkboxcurses.py | 2 +- manatools/aui/backends/curses/checkboxframecurses.py | 2 +- manatools/aui/backends/curses/comboboxcurses.py | 12 ++++++------ manatools/aui/backends/curses/framecurses.py | 2 +- manatools/aui/backends/curses/radiobuttoncurses.py | 2 +- manatools/aui/backends/curses/selectionboxcurses.py | 2 +- manatools/aui/backends/curses/treecurses.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/manatools/aui/backends/curses/checkboxcurses.py b/manatools/aui/backends/curses/checkboxcurses.py index 178514b..4bdaf5e 100644 --- a/manatools/aui/backends/curses/checkboxcurses.py +++ b/manatools/aui/backends/curses/checkboxcurses.py @@ -102,7 +102,7 @@ def _draw(self, window, y, x, width, height): checkbox_symbol = "[X]" if self._is_checked else "[ ]" text = f"{checkbox_symbol} {self._label}" if len(text) > width: - text = text[:max(0, width - 3)] + "..." + text = text[:max(0, width - 1)] + "…" if self._focused and self.isEnabled(): window.attron(curses.A_REVERSE) diff --git a/manatools/aui/backends/curses/checkboxframecurses.py b/manatools/aui/backends/curses/checkboxframecurses.py index 5af5cb2..19f3afa 100644 --- a/manatools/aui/backends/curses/checkboxframecurses.py +++ b/manatools/aui/backends/curses/checkboxframecurses.py @@ -307,7 +307,7 @@ def _draw(self, window, y, x, width, height): title_body = f"[{chk}] {self._label}" if self._label else f"[{chk}]" max_title_len = max(0, width - 4) if len(title_body) > max_title_len: - title_body = title_body[:max(0, max_title_len - 3)] + "..." + title_body = title_body[:max(0, max_title_len - 1)] + "…" start_x = x + max(1, (width - len(title_body)) // 2) # choose attributes depending on focus/enable state attr = curses.A_BOLD diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py index 98b3716..704dc3b 100644 --- a/manatools/aui/backends/curses/comboboxcurses.py +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -139,7 +139,7 @@ def _draw(self, window, y, x, width, height): label_text = self._label # clip label if too long for width if len(label_text) > width: - label_text = label_text[:max(0, width - 3)] + "..." + label_text = label_text[:max(0, width - 1)] + "…" lbl_attr = curses.A_NORMAL if not self.isEnabled(): lbl_attr |= curses.A_DIM @@ -149,10 +149,10 @@ def _draw(self, window, y, x, width, height): pass # Prepare display value and draw combo on next row - display_value = self._value if self._value else "Select..." - max_display_width = combo_space - 6 # account for " ▼" and padding - if len(display_value) > max_display_width: - display_value = display_value[:max_display_width] + "..." + display_value = self._value if self._value else "Select…" + max_display_width = combo_space - 4 # account for " ▼" and padding + if len(display_value) -1 > max_display_width: + display_value = display_value[:max_display_width] + "…" # Draw combo box background on the control row if not self.isEnabled(): @@ -226,7 +226,7 @@ def _draw_expanded_list(self, window): item = self._items[i] item_text = item.label() if len(item_text) > dropdown_width - 2: - item_text = item_text[:dropdown_width - 2] + "..." + item_text = item_text[:dropdown_width - 2] + "…" # Highlight hovered item attr = curses.A_REVERSE if i == self._hover_index else curses.A_NORMAL diff --git a/manatools/aui/backends/curses/framecurses.py b/manatools/aui/backends/curses/framecurses.py index fc1edd5..d1cf863 100644 --- a/manatools/aui/backends/curses/framecurses.py +++ b/manatools/aui/backends/curses/framecurses.py @@ -180,7 +180,7 @@ def _draw(self, window, y, x, width, height): title = f" {self._label} " max_title_len = max(0, width - 4) if len(title) > max_title_len: - title = title[:max(0, max_title_len - 3)] + "..." + title = title[:max(0, max_title_len - 1)] + "…" start_x = x + max(1, (width - len(title)) // 2) # overwrite part of top border with title text window.addstr(y, start_x, title, curses.A_BOLD) diff --git a/manatools/aui/backends/curses/radiobuttoncurses.py b/manatools/aui/backends/curses/radiobuttoncurses.py index 722f5f3..411ede5 100644 --- a/manatools/aui/backends/curses/radiobuttoncurses.py +++ b/manatools/aui/backends/curses/radiobuttoncurses.py @@ -113,7 +113,7 @@ def _draw(self, window, y, x, width, height): radio_symbol = "(*)" if self._is_checked else "( )" text = f"{radio_symbol} {self._label}" if len(text) > width: - text = text[:max(0, width - 3)] + "..." + text = text[:max(0, width - 1)] + "…" attr_on = False if self._focused and self.isEnabled(): diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index cc99255..c796005 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -238,7 +238,7 @@ def _draw(self, window, y, x, width, height): checkbox = "*" if item in self._selected_items else " " display = f"[{checkbox}] {text}" if len(display) > width: - display = display[:max(0, width - 3)] + "..." + display = display[:max(0, width - 1)] + "…" attr = curses.A_NORMAL if not self.isEnabled(): attr |= curses.A_DIM diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index 8186e90..42034d3 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -592,7 +592,7 @@ def _draw(self, window, y, x, width, height): indent = " " * (depth * 2) text = f"{indent}{exp} [{checkbox}] {itm.label()}" if len(text) > width: - text = text[:max(0, width - 3)] + "..." + text = text[:max(0, width - 1)] + "…" attr = curses.A_REVERSE if (self._focused and idx == self._hover_index and self.isEnabled()) else curses.A_NORMAL if not self.isEnabled(): attr |= curses.A_DIM From ce2db58143062b2788a5ebece422893b97e1112f Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 23:37:54 +0100 Subject: [PATCH 448/523] Added a kind of tooltip management by pressing F1 --- manatools/aui/backends/curses/dialogcurses.py | 134 +++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index 947db0e..e5f3aab 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -49,6 +49,10 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM # Debounce for resize handling (avoid flicker) self._resize_pending_until = 0.0 self._last_term_size = (0, 0) # (h, w) + # Transient help overlay (triggered by F1) + self._help_overlay_text = None + self._help_overlay_until = 0.0 + self._help_overlay_pos = None # (y, x) where to draw the overlay; None = auto fallback YDialogCurses._open_dialogs.append(self) def widgetClass(self): @@ -225,10 +229,87 @@ def _draw_dialog(self): focus_text = f" Focus: {lbl} " if len(focus_text) < width: self._backend_widget.addstr(height - 1, 2, focus_text, curses.A_REVERSE) - #if the focused widget has an expnded list (menus, combos,...), draw it on top + #ifthe focused widget has an expnded list (menus, combos,...), draw it on top if hasattr(self._focused_widget, "_draw_expanded_list"): self._focused_widget._draw_expanded_list(self._backend_widget) + # Draw help overlay if active and not expired + try: + now = time.time() + if getattr(self, "_help_overlay_text", None) and getattr(self, "_help_overlay_until", 0) > now: + help_txt = str(self._help_overlay_text) + # Prefer to draw the help text inside the focused widget's area when possible. + overlay_y = None + overlay_x = None + overlay_width = None + try: + h, w = self._backend_widget.getmaxyx() + except Exception: + h, w = 24, 80 + # 1) explicit suggested position (from when F1 was pressed) + if getattr(self, "_help_overlay_pos", None): + overlay_y, overlay_x = self._help_overlay_pos + else: + fw = self._focused_widget + if fw is not None: + # Try a number of heuristic attributes that widgets commonly expose + try: + if getattr(fw, "_combo_y", None) is not None and getattr(fw, "_combo_x", None) is not None: + overlay_y, overlay_x = fw._combo_y, fw._combo_x + elif getattr(fw, "_y", None) is not None and getattr(fw, "_x", None) is not None: + overlay_y, overlay_x = fw._y, fw._x + elif getattr(fw, "_widget_y", None) is not None and getattr(fw, "_widget_x", None) is not None: + overlay_y, overlay_x = fw._widget_y, fw._widget_x + elif getattr(fw, "_draw_y", None) is not None and getattr(fw, "_draw_x", None) is not None: + overlay_y, overlay_x = fw._draw_y, fw._draw_x + except Exception: + overlay_y = overlay_x = None + # Attempt to obtain widget width if provided to limit overlay width to widget area + try: + overlay_width = getattr(fw, "_combo_width", None) or getattr(fw, "_width", None) or getattr(fw, "_w", None) + except Exception: + overlay_width = None + # Fallback to safe location near footer/content if we couldn't determine widget area + if overlay_y is None: + overlay_y = max(1, h - 3) + if overlay_x is None: + overlay_x = 2 + # Compute remaining / available width + try: + max_allowed = max(5, w - overlay_x - 1) + except Exception: + max_allowed = 10 + # If widget-specific width was provided, prefer it (but clamp to available) + if overlay_width: + try: + overlay_width = int(overlay_width) + except Exception: + overlay_width = None + remain = overlay_width if overlay_width and overlay_width > 0 else max_allowed + remain = min(remain, max_allowed) + if remain < 5: + remain = 5 + # Clip/truncate help text to fit + display = help_txt + if len(display) > remain: + display = display[: max(0, remain - 1)] + "…" + # Draw overlay inside widget area (standout) + try: + self._backend_widget.addnstr(overlay_y, overlay_x, display, remain, curses.A_STANDOUT) + except Exception: + try: + self._backend_widget.addnstr(overlay_y, overlay_x, display, remain) + except Exception: + pass + else: + # overlay expired -> cleanup fields + if getattr(self, "_help_overlay_text", None): + self._help_overlay_text = None + self._help_overlay_until = 0.0 + self._help_overlay_pos = None + except Exception: + pass + # Refresh main window first self._backend_widget.refresh() @@ -363,7 +444,11 @@ def waitForEvent(self, timeout_millisec=0): break time.sleep(0.01) continue - + + #any key received, clear help overlay if active + self._help_overlay_text = None + self._help_overlay_until = 0.0 + self._help_overlay_pos = None # Global keys if key == curses.KEY_F10: self._post_event(YCancelEvent()) @@ -390,7 +475,52 @@ def waitForEvent(self, timeout_millisec=0): continue # Dispatch key to focused widget + # Handle transient help overlay dismissal: any key (other than F1) should remove it and continue + if getattr(self, "_help_overlay_text", None) and key != curses.KEY_F1: + # clear overlay immediately and force redraw, then proceed to normal handling + try: + self._help_overlay_text = None + self._help_overlay_until = 0.0 + self._help_overlay_pos = None + self._last_draw_time = 0 + except Exception: + pass handled = False + # F1: show help overlay if possible + if key == curses.KEY_F1: + try: + fw = self._focused_widget + if fw is not None: + ht = None + try: + ht = fw.helpText() if callable(getattr(fw, "helpText", None)) else None + except Exception: + ht = None + if ht: + # compute suggested overlay position (try widget coords) + pos = None + try: + y = getattr(fw, "_combo_y", None) + x = getattr(fw, "_combo_x", None) + if y is not None and x is not None: + pos = (y, x) + except Exception: + pos = None + self._help_overlay_text = str(ht) + self._help_overlay_pos = pos + self._help_overlay_until = time.time() + 5.0 + self._last_draw_time = 0 + # F1 handled + handled = True + # force immediate redraw next loop iteration + except Exception: + pass + + # If F1 handled we skip further dispatch to widgets + if handled: + continue + + # Dispatch key to focused widget if self._focused_widget and hasattr(self._focused_widget, '_handle_key'): handled = self._focused_widget._handle_key(key) if handled: From ecbb4efc1d3416ad8c883ba515432e760e6e97b1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 23:47:02 +0100 Subject: [PATCH 449/523] Fixing position for tool tip --- manatools/aui/backends/curses/pushbuttoncurses.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index 6748666..430961f 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -35,6 +35,8 @@ def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, ic self._can_focus = True self._icon_name = icon_name self._icon_only = bool(icon_only) + self._x = 0 + self._y = 0 self._height = 1 # Fixed height - buttons are always one line self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") # derive mnemonic and cleaned label if present @@ -95,6 +97,8 @@ def _draw(self, window, y, x, width, height): if self._visible is False: return try: + self._x = x + self._y = y # Center the button label within available width, show underline for mnemonic clean = getattr(self, "_clean_label", None) or self._label button_text = f"[ {clean} ]" From 37bc96429bcf025c343f2ff229215bc022880126 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 18 Jan 2026 23:55:59 +0100 Subject: [PATCH 450/523] fixed position when label is missing and added visibility --- .../aui/backends/curses/inputfieldcurses.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/manatools/aui/backends/curses/inputfieldcurses.py b/manatools/aui/backends/curses/inputfieldcurses.py index d2f7247..0d3195d 100644 --- a/manatools/aui/backends/curses/inputfieldcurses.py +++ b/manatools/aui/backends/curses/inputfieldcurses.py @@ -36,7 +36,9 @@ def __init__(self, parent=None, label="", password_mode=False): self._focused = False self._can_focus = True # one row for field + optional label row on top - self._height = 1 + (1 if bool(self._label) else 0) + self._height = 2 if self._label else 1 + self._x = 0 + self._y = 0 # per-instance logger self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") if not self._logger.handlers and not logging.getLogger().handlers: @@ -59,10 +61,7 @@ def label(self): def setLabel(self, label): self._label = label - try: - self._height = 1 + (1 if bool(self._label) else 0) - except Exception: - pass + self._height = 2 if self._label else 1 def _create_backend_widget(self): try: @@ -100,7 +99,11 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: + self._x = x + self._y = y line = y # Draw label on its own row above field if self._label: @@ -202,6 +205,10 @@ def minWidth(self): def _desired_height_for_width(self, width: int) -> int: try: - return max(1, 1 + (1 if bool(self._label) else 0)) + return max(1, 2 if self._label else 1) except Exception: return 1 + + def setVisible(self, visible=True): + super().setVisible(visible) + self._can_focus = visible \ No newline at end of file From 1555ed7a0e04151e4bfb2af6b3e5126eb34988a0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 19 Jan 2026 00:13:20 +0100 Subject: [PATCH 451/523] Added visibility and fixed label occupation --- manatools/aui/backends/gtk/inputfieldgtk.py | 40 +++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/inputfieldgtk.py b/manatools/aui/backends/gtk/inputfieldgtk.py index e7b959c..d0716b3 100644 --- a/manatools/aui/backends/gtk/inputfieldgtk.py +++ b/manatools/aui/backends/gtk/inputfieldgtk.py @@ -47,7 +47,7 @@ def label(self): return self._label def _create_backend_widget(self): - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) self._lbl = Gtk.Label(label=self._label) try: @@ -58,7 +58,11 @@ def _create_backend_widget(self): try: vbox.append(self._lbl) except Exception: + self._logger.error("Failed to append label to vbox, trying add()", exc_info=True) vbox.add(self._lbl) + + if not self._label: + self._lbl.set_visible(False) entry = Gtk.Entry() if self._password_mode: @@ -76,10 +80,20 @@ def _create_backend_widget(self): try: vbox.append(entry) except Exception: + self._logger.error("Failed to append entry to vbox, trying add()", exc_info=True) vbox.add(entry) self._backend_widget = vbox self._entry_widget = entry + try: + if self._help_text: + self._backend_widget.set_tooltip_text(self._help_text) + except Exception: + self._logger.error("Failed to set tooltip text on backend widget", exc_info=True) + try: + self._backend_widget.set_visible(self.visible()) + except Exception: + self._logger.error("Failed to set backend widget visible", exc_info=True) self._backend_widget.set_sensitive(self._enabled) try: # initial stretch policy @@ -125,11 +139,17 @@ def _set_backend_enabled(self, enabled): def setLabel(self, label): self._label = label + if self._backend_widget is None: + return try: if hasattr(self, '_lbl') and self._lbl is not None: self._lbl.set_text(str(label)) + if not label: + self._lbl.set_visible(False) + else: + self._lbl.set_visible(True) except Exception: - pass + self._logger.error("setLabel failed", exc_info=True) def setStretchable(self, dim, new_stretch): try: @@ -203,3 +223,19 @@ def _apply_stretch_policy(self): pass except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_visible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_tooltip_text(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) \ No newline at end of file From 2a51ac9d082f04009e5600a85ad8b3448d7c2547 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 24 Jan 2026 19:30:24 +0100 Subject: [PATCH 452/523] Fixed inputfield position if label is not present --- manatools/aui/backends/gtk/inputfieldgtk.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/gtk/inputfieldgtk.py b/manatools/aui/backends/gtk/inputfieldgtk.py index d0716b3..b3847ea 100644 --- a/manatools/aui/backends/gtk/inputfieldgtk.py +++ b/manatools/aui/backends/gtk/inputfieldgtk.py @@ -187,13 +187,16 @@ def _apply_stretch_policy(self): line_h = 18 except Exception: char_w, line_h = 8, 18 + self._logger.exception("Failed to get entry char size", exc_info=True) + lbl_h = 0 try: - lbl_layout = self._lbl.create_pango_layout("M") - _, lbl_h = lbl_layout.get_pixel_size() - if not lbl_h: - lbl_h = 20 + if self._lbl.get_visible(): + lbl_layout = self._lbl.create_pango_layout("M") + _, lbl_h = lbl_layout.get_pixel_size() + if not lbl_h: + lbl_h = 20 except Exception: - lbl_h = 20 + self._logger.exception("Failed to get label height", exc_info=True) desired_chars = 20 w_px = int(char_w * desired_chars) + 12 h_px = int(line_h) + lbl_h + 8 From 18d6fc74e01f1d8e5fd344776d4a85698d1120e1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 24 Jan 2026 19:36:19 +0100 Subject: [PATCH 453/523] minimun length not 0 --- manatools/aui/backends/qt/inputfieldqt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/qt/inputfieldqt.py b/manatools/aui/backends/qt/inputfieldqt.py index 243fb97..efc9e6b 100644 --- a/manatools/aui/backends/qt/inputfieldqt.py +++ b/manatools/aui/backends/qt/inputfieldqt.py @@ -157,7 +157,7 @@ def _apply_stretch_policy(self): if not horiz: self._backend_widget.setFixedWidth(w_px) else: - self._backend_widget.setMinimumWidth(0) + self._backend_widget.setMinimumWidth(w_px) self._backend_widget.setMaximumWidth(16777215) except Exception: pass From cd8f1ebccea0a52f66c6a92373896484259003df Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 24 Jan 2026 19:47:32 +0100 Subject: [PATCH 454/523] fixed missing definitions --- manatools/aui/backends/qt/menubarqt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/qt/menubarqt.py b/manatools/aui/backends/qt/menubarqt.py index 4db0657..5e43396 100644 --- a/manatools/aui/backends/qt/menubarqt.py +++ b/manatools/aui/backends/qt/menubarqt.py @@ -5,7 +5,7 @@ ''' from PySide6 import QtWidgets, QtCore, QtGui import logging -from ...yui_common import YWidget, YMenuEvent, YMenuItem +from ...yui_common import * from .commonqt import _resolve_icon From 518d94711fcf1a1274f1d61c096a983e7fd8177b Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 25 Jan 2026 16:38:51 +0100 Subject: [PATCH 455/523] tooltip position really on pushbutton --- manatools/aui/backends/curses/pushbuttoncurses.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index 430961f..5fca306 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -97,8 +97,6 @@ def _draw(self, window, y, x, width, height): if self._visible is False: return try: - self._x = x - self._y = y # Center the button label within available width, show underline for mnemonic clean = getattr(self, "_clean_label", None) or self._label button_text = f"[ {clean} ]" @@ -120,6 +118,9 @@ def _draw(self, window, y, x, width, height): if self._focused: attr |= curses.A_BOLD + self._x = text_x + self._y = y + try: window.addstr(y, text_x, draw_text, attr) # underline mnemonic if visible From 62532e281a1f45f2b8b368b0cc7cc8cd1a544492 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 25 Jan 2026 16:49:57 +0100 Subject: [PATCH 456/523] Added visibility and tooltip --- manatools/aui/backends/curses/tablecurses.py | 24 ++++++++++++++ manatools/aui/backends/gtk/tablegtk.py | 34 +++++++++++++++++++- manatools/aui/backends/qt/tableqt.py | 32 ++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/curses/tablecurses.py b/manatools/aui/backends/curses/tablecurses.py index bfca616..9794a81 100644 --- a/manatools/aui/backends/curses/tablecurses.py +++ b/manatools/aui/backends/curses/tablecurses.py @@ -60,6 +60,9 @@ def __init__(self, parent=None, header: YTableHeader = None, multiSelection: boo self._selected_items = [] self._changed_item = None self._current_visible_rows = None + # widget position + self._x = 0 + self._y = 0 self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) @@ -79,6 +82,11 @@ def _create_backend_widget(self): # Associate backend with self, compute minimal height, and reflect model selection. self._backend_widget = self self._height = max(3, 1 + min(len(self._items), 6)) + # ensure visibility affects focusability on creation + try: + self._can_focus = bool(self._visible) + except Exception: + pass # Build internal selection list from item flags sel = [] try: @@ -168,6 +176,8 @@ def _align_text(self, text, width, align: YAlignmentType): return s.ljust(width) def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: line = y # Header @@ -190,6 +200,8 @@ def _draw(self, window, y, x, width, height): use_selection_marker = False if use_selection_marker: header_line = " " + header_line + self._x = x + self._y = line try: window.addstr(line, x, header_line[:width], curses.A_BOLD) except curses.error: @@ -461,3 +473,15 @@ def deleteAllItems(self): def changedItem(self): return getattr(self, "_changed_item", None) + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + try: + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) + except Exception: + pass + + def setHelpText(self, help_text: str): + # store help text; curses has no native tooltip but other logic (dialog overlay) may use it + super().setHelpText(help_text) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index 281db49..464ee49 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -130,7 +130,20 @@ def _create_backend_widget(self): self._listbox.set_sensitive(bool(getattr(self, "_enabled", True))) except Exception: pass - + # apply help text (tooltip) and initial visibility + try: + if getattr(self, "_help_text", None): + try: + self._backend_widget.set_tooltip_text(self._help_text) + except Exception: + pass + except Exception: + pass + try: + if hasattr(self._backend_widget, "set_visible"): + self._backend_widget.set_visible(self.visible()) + except Exception: + pass # connect selection handlers if self._multi: try: @@ -480,3 +493,22 @@ def _set_backend_enabled(self, enabled): self._listbox.set_sensitive(bool(enabled)) except Exception: pass + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None and hasattr(self._backend_widget, "set_visible"): + self._backend_widget.set_visible(bool(visible)) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.set_tooltip_text(help_text) + except Exception: + pass + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) diff --git a/manatools/aui/backends/qt/tableqt.py b/manatools/aui/backends/qt/tableqt.py index fee5a3e..2fb3f85 100644 --- a/manatools/aui/backends/qt/tableqt.py +++ b/manatools/aui/backends/qt/tableqt.py @@ -82,6 +82,19 @@ def _create_backend_widget(self): self._table.setEnabled(bool(getattr(self, "_enabled", True))) except Exception: pass + # Apply help text (tooltip) and initial visibility if provided + try: + if getattr(self, "_help_text", None): + try: + self._backend_widget.setToolTip(self._help_text) + except Exception: + pass + except Exception: + pass + try: + self._backend_widget.setVisible(self.visible()) + except Exception: + pass # populate if items already present try: self.rebuildTable() @@ -583,3 +596,22 @@ def _set_backend_enabled(self, enabled): self._table.setEnabled(bool(enabled)) except Exception: pass + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setVisible(bool(visible)) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setToolTip(help_text) + except Exception: + pass + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) From 6429bdbb0a29617662b34c18f9cf3f02ac564da4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 25 Jan 2026 16:50:28 +0100 Subject: [PATCH 457/523] removing extra room for this widget --- manatools/aui/backends/qt/hboxqt.py | 4 ++-- manatools/aui/backends/qt/vboxqt.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/qt/hboxqt.py b/manatools/aui/backends/qt/hboxqt.py index ba9965c..ea10a98 100644 --- a/manatools/aui/backends/qt/hboxqt.py +++ b/manatools/aui/backends/qt/hboxqt.py @@ -38,13 +38,13 @@ def stretchable(self, dim): def _create_backend_widget(self): self._backend_widget = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(self._backend_widget) - layout.setContentsMargins(10, 10, 10, 10) + layout.setContentsMargins(1, 1, 1, 1) # Keep the layout constrained to its minimum sizeHint so children are not # compressed to invisible sizes by parent layouts. This is required in our # UI because many child widgets use Preferred/Fixed policies and rely on # the layout's minimumSizeHint to be respected. layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) - layout.setSpacing(5) + #layout.setSpacing(1) # Map YWidget weights and stretchable flags to Qt layout stretch factors. # Weight semantics: if any child has a positive weight (>0) use those diff --git a/manatools/aui/backends/qt/vboxqt.py b/manatools/aui/backends/qt/vboxqt.py index b2adb60..35e8ded 100644 --- a/manatools/aui/backends/qt/vboxqt.py +++ b/manatools/aui/backends/qt/vboxqt.py @@ -38,11 +38,11 @@ def stretchable(self, dim): def _create_backend_widget(self): self._backend_widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(self._backend_widget) - layout.setContentsMargins(10, 10, 10, 10) + layout.setContentsMargins(1, 1, 1, 1) # Keep the layout constrained to its minimum sizeHint so children are not # compressed to invisible sizes by parent layouts. This matches HBox/Frame behavior. layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) - layout.setSpacing(5) + #layout.setSpacing(1) # Map YWidget weights and stretchable flags to Qt layout stretch factors. # Weight semantics: if any child has a positive weight (>0) use those From 14366f7b606983a717e0402e9a6be85a5a48f199 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 25 Jan 2026 17:13:22 +0100 Subject: [PATCH 458/523] added some logging in case of exceptions --- manatools/aui/backends/gtk/tablegtk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index 464ee49..bf23cb3 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -74,7 +74,7 @@ def _create_backend_widget(self): else: lbl.set_xalign(0.0) except Exception: - pass + self._logger.exception("Failed to set alignment for header column %d", col, exc_info=True) header_grid.attach(lbl, col, 0, 1, 1) # ListBox inside ScrolledWindow @@ -83,7 +83,7 @@ def _create_backend_widget(self): mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE listbox.set_selection_mode(mode) except Exception: - pass + self._logger.exception("Failed to set selection mode on ListBox", exc_info=True) sw = Gtk.ScrolledWindow() try: @@ -150,12 +150,12 @@ def _create_backend_widget(self): self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) self._listbox.connect("row-activated", lambda lb, row: self._on_row_selected_for_multi(lb, row)) except Exception: - pass + self._logger.exception("Failed to connect multi-selection handlers", exc_info=True) else: try: self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) except Exception: - pass + self._logger.exception("Failed to connect single-selection handler", exc_info=True) self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) From 0219495a40232053833165bac33243c1918d0bfd Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 26 Jan 2026 20:07:57 +0100 Subject: [PATCH 459/523] Fixing table layout --- manatools/aui/backends/gtk/tablegtk.py | 825 +++++++++++++++++++------ 1 file changed, 631 insertions(+), 194 deletions(-) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index bf23cb3..9f8f080 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -9,25 +9,39 @@ - Checkbox columns declared via `YTableHeader.isCheckboxColumn()` - Selection driven by `YTableItem.selected()`; emits SelectionChanged on change - Checkbox toggles emit ValueChanged and update `YTableCell.checked()` +- Resizable columns with drag handles between headers +- Column alignment between header and rows Sorting UI is not implemented; if needed we can add clickable headers. """ import gi gi.require_version('Gtk', '4.0') -from gi.repository import Gtk, GLib +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk, GLib, Gdk, Pango import logging from ...yui_common import * class YTableGtk(YSelectionWidget): + """ + GTK4 implementation of YTable with resizable columns and proper alignment. + + Features: + - Column resizing via draggable header separators + - Uniform column widths across header and all rows + - Checkbox column support with proper alignment + - Single/Multi selection modes + """ + def __init__(self, parent=None, header: YTableHeader = None, multiSelection=False): super().__init__(parent) if header is None: raise ValueError("YTableGtk requires a YTableHeader") self._header = header self._multi = bool(multiSelection) - # force single-selection if any checkbox columns present + + # Force single-selection if any checkbox columns present try: for c_idx in range(self._header.columns()): if self._header.isCheckboxColumn(c_idx): @@ -35,8 +49,9 @@ def __init__(self, parent=None, header: YTableHeader = None, multiSelection=Fals break except Exception: pass + self._backend_widget = None - self._header_grid = None + self._header_box = None self._listbox = None self._row_to_item = {} self._item_to_row = {} @@ -44,64 +59,61 @@ def __init__(self, parent=None, header: YTableHeader = None, multiSelection=Fals self._suppress_selection_handler = False self._suppress_item_change = False self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") - self._old_selected_items = [] # for change detection + self._old_selected_items = [] self._changed_item = None + + # Column width management + self._column_widths = [] + self._min_column_width = 50 + self._default_column_width = 100 + self._drag_data = None + self._header_cells = [] + self._header_labels = [] # Store header labels separately + + # Initialize column widths + self._init_column_widths() def widgetClass(self): return "YTable" def _create_backend_widget(self): - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - - # Header grid - header_grid = Gtk.Grid(column_spacing=12, row_spacing=0) - try: - cols = self._header.columns() - except Exception: - cols = 0 - for col in range(cols): - try: - txt = self._header.header(col) - except Exception: - txt = "" - lbl = Gtk.Label(label=txt) - try: - align = self._header.alignment(col) - if align == YAlignmentType.YAlignCenter: - lbl.set_xalign(0.5) - elif align == YAlignmentType.YAlignEnd: - lbl.set_xalign(1.0) - else: - lbl.set_xalign(0.0) - except Exception: - self._logger.exception("Failed to set alignment for header column %d", col, exc_info=True) - header_grid.attach(lbl, col, 0, 1, 1) - - # ListBox inside ScrolledWindow - listbox = Gtk.ListBox() - try: - mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE - listbox.set_selection_mode(mode) - except Exception: - self._logger.exception("Failed to set selection mode on ListBox", exc_info=True) - + """ + Create the GTK widget hierarchy for the table. + + Structure: + - Main vertical box (vbox) + - Header box with column labels and resize handles + - Separator + - ScrolledWindow with ListBox for rows + """ + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + # Create header with resizable columns + self._create_header() + + # Horizontal separator between header and content + separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + + # ListBox inside ScrolledWindow for rows + self._create_listbox() + sw = Gtk.ScrolledWindow() try: - sw.set_child(listbox) + sw.set_child(self._listbox) except Exception: try: - sw.add(listbox) + sw.add(self._listbox) except Exception: pass - # Make expand according to parent stretching + # Set expansion properties try: vbox.set_hexpand(self.stretchable(YUIDimension.YD_HORIZ)) vbox.set_vexpand(self.stretchable(YUIDimension.YD_VERT)) vbox.set_valign(Gtk.Align.FILL) - listbox.set_hexpand(True) - listbox.set_vexpand(True) - listbox.set_valign(Gtk.Align.FILL) + self._listbox.set_hexpand(True) + self._listbox.set_vexpand(True) + self._listbox.set_valign(Gtk.Align.FILL) sw.set_hexpand(True) sw.set_vexpand(True) sw.set_valign(Gtk.Align.FILL) @@ -109,20 +121,221 @@ def _create_backend_widget(self): pass self._backend_widget = vbox - self._header_grid = header_grid - self._listbox = listbox + # Assemble the widget try: - vbox.append(header_grid) + vbox.append(self._header_box) + vbox.append(separator) vbox.append(sw) except Exception: try: - vbox.add(header_grid) + vbox.add(self._header_box) + vbox.add(separator) vbox.add(sw) except Exception: pass - # respect initial enabled state + # Apply initial state + self._apply_initial_state() + + # Connect selection handlers + self._connect_selection_handlers() + + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + + # Populate if items exist + try: + if getattr(self, "_items", None): + self.rebuildTable() + except Exception: + self._logger.exception("rebuildTable failed during _create_backend_widget") + + def _init_column_widths(self): + """Initialize column widths based on header content.""" + try: + cols = self._header.columns() + self._column_widths = [self._default_column_width] * cols + + # Adjust based on header text length + for col in range(cols): + try: + header_text = self._header.header(col) + if header_text: + # Estimate width based on text length + text_width = len(header_text) * 8 + 20 # Rough estimate + self._column_widths[col] = max(text_width, self._min_column_width) + except Exception: + pass + except Exception: + self._column_widths = [] + + def _create_header(self): + """ + Create header with column labels and resizable separators. + + Each column has: + - Label with proper alignment + - Resizable separator (except last column) + """ + self._header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + self._header_cells = [] + self._header_labels = [] + + try: + cols = self._header.columns() + except Exception: + cols = 0 + + for col in range(cols): + # Create container for this column header + col_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + col_box.set_hexpand(False) + + # Apply column width + width = self._get_column_width(col) + col_box.set_size_request(width, -1) + + # Create label with proper alignment + try: + txt = self._header.header(col) + except Exception: + txt = "" + + lbl = Gtk.Label(label=txt) + lbl.set_halign(Gtk.Align.START) + lbl.set_margin_start(5) + lbl.set_margin_end(5) + lbl.set_hexpand(True) + + # Apply alignment to header label + try: + align = self._header.alignment(col) + if align == YAlignmentType.YAlignCenter: + lbl.set_xalign(0.5) + elif align == YAlignmentType.YAlignEnd: + lbl.set_xalign(1.0) + else: + lbl.set_xalign(0.0) + except Exception: + lbl.set_xalign(0.0) + + self._header_labels.append(lbl) + + # Add separator for resizing (except last column) + if col < cols - 1: + # Create separator container + sep_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + # Create draggable handle + sep_handle = Gtk.Box() + sep_handle.set_size_request(8, -1) + sep_handle.get_style_context().add_class("col-sep-handle") + + # Add event controllers for dragging + motion_ctrl = Gtk.EventControllerMotion.new() + drag_ctrl = Gtk.GestureDrag.new() + + # Store column index + sep_handle.column_index = col + sep_container.column_index = col + + # Connect signals + motion_ctrl.connect("enter", self._on_separator_enter) + motion_ctrl.connect("leave", self._on_separator_leave) + drag_ctrl.connect("drag-begin", self._on_drag_begin) + drag_ctrl.connect("drag-update", self._on_drag_update) + drag_ctrl.connect("drag-end", self._on_drag_end) + + sep_handle.add_controller(motion_ctrl) + sep_handle.add_controller(drag_ctrl) + + # Create visual separator + sep = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) + sep.set_margin_end(5) + + sep_container.append(sep_handle) + sep_container.append(sep) + + col_box.append(lbl) + col_box.append(sep_container) + else: + col_box.append(lbl) + + self._header_box.append(col_box) + self._header_cells.append(col_box) + + # Setup CSS for styling + self._setup_header_css() + + def _setup_header_css(self): + """Setup CSS for header styling.""" + css_provider = Gtk.CssProvider() + css = """ + /* Style for column separator handles */ + .col-sep-handle { + background-color: transparent; + min-width: 8px; + } + + .col-sep-handle:hover { + background-color: alpha(@theme_fg_color, 0.2); + } + + /* Style for header */ + .y-table-header { + padding: 4px 0px; + background-color: @theme_bg_color; + border-bottom: 1px solid @borders; + } + + .y-table-header label { + padding: 4px 8px; + font-weight: bold; + } + + /* Style for table rows */ + .y-table-row { + border-bottom: 1px solid alpha(@theme_fg_color, 0.1); + } + + .y-table-row:hover { + background-color: alpha(@theme_selected_bg_color, 0.1); + } + """ + + try: + try: + css_provider.load_from_data(css, -1) + except TypeError: + css_provider.load_from_data(css.encode()) + + # Add CSS class to header + ctx = self._header_box.get_style_context() + ctx.add_class("y-table-header") + + # Apply provider + display = Gdk.Display.get_default() + if display: + Gtk.StyleContext.add_provider_for_display( + display, + css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + except Exception as e: + self._logger.debug("CSS setup failed: %s", str(e)) + + def _create_listbox(self): + """Create the ListBox for table rows.""" + self._listbox = Gtk.ListBox() + try: + mode = Gtk.SelectionMode.MULTIPLE if self._multi else Gtk.SelectionMode.SINGLE + self._listbox.set_selection_mode(mode) + except Exception: + self._logger.exception("Failed to set selection mode on ListBox") + + def _apply_initial_state(self): + """Apply initial widget state (enabled, tooltip, visibility).""" + # Enabled state try: if hasattr(self._backend_widget, "set_sensitive"): self._backend_widget.set_sensitive(bool(getattr(self, "_enabled", True))) @@ -130,7 +343,8 @@ def _create_backend_widget(self): self._listbox.set_sensitive(bool(getattr(self, "_enabled", True))) except Exception: pass - # apply help text (tooltip) and initial visibility + + # Help text (tooltip) try: if getattr(self, "_help_text", None): try: @@ -139,177 +353,406 @@ def _create_backend_widget(self): pass except Exception: pass + + # Visibility try: if hasattr(self._backend_widget, "set_visible"): self._backend_widget.set_visible(self.visible()) except Exception: pass - # connect selection handlers + + def _connect_selection_handlers(self): + """Connect selection event handlers.""" if self._multi: try: - self._listbox.connect("selected-rows-changed", lambda lb: self._on_selected_rows_changed(lb)) - self._listbox.connect("row-activated", lambda lb, row: self._on_row_selected_for_multi(lb, row)) - except Exception: - self._logger.exception("Failed to connect multi-selection handlers", exc_info=True) + self._listbox.connect("selected-rows-changed", + lambda lb: self._on_selected_rows_changed(lb)) + self._listbox.connect("row-activated", + lambda lb, row: self._on_row_selected_for_multi(lb, row)) + except Exception: + self._logger.exception("Failed to connect multi-selection handlers") else: try: - self._listbox.connect("row-selected", lambda lb, row: self._on_row_selected(lb, row)) + self._listbox.connect("row-selected", + lambda lb, row: self._on_row_selected(lb, row)) except Exception: - self._logger.exception("Failed to connect single-selection handler", exc_info=True) + self._logger.exception("Failed to connect single-selection handler") - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) - - # populate if items exist - try: - if getattr(self, "_items", None): - self.rebuildTable() - except Exception: - self._logger.exception("rebuildTable failed during _create_backend_widget") + def _get_column_width(self, col): + """Get width for specified column.""" + if col < len(self._column_widths): + width = self._column_widths[col] + return max(width, self._min_column_width) if width > 0 else self._default_column_width + return self._default_column_width def _header_is_checkbox(self, col): + """Check if column is a checkbox column.""" try: return bool(self._header.isCheckboxColumn(col)) except Exception: return False def rebuildTable(self): + """ + Rebuild the entire table from scratch. + + This ensures column alignment between header and all rows. + """ self._logger.debug("rebuildTable: %d items", len(self._items) if self._items else 0) if self._backend_widget is None or self._listbox is None: self._create_backend_widget() - # clear rows + # Clear existing rows + self._clear_rows() + + # Build new rows + try: + cols = self._header.columns() + except Exception: + cols = 0 + if cols <= 0: + cols = 1 + + for row_idx, it in enumerate(list(getattr(self, '_items', []) or [])): + try: + row = self._create_row(it, cols) + if row: + self._listbox.append(row) + self._row_to_item[row] = it + self._item_to_row[it] = row + self._rows.append(row) + except Exception: + self._logger.exception("Failed to create row %d", row_idx) + + # Apply selection from model + self._apply_model_selection() + + def _clear_rows(self): + """Clear all rows from the table.""" try: self._row_to_item.clear() self._item_to_row.clear() except Exception: - pass + self._logger.exception("Failed to clear row-item mappings") + try: - for row in list(self._rows): + for row in self._rows: try: self._listbox.remove(row) except Exception: - pass + self._logger.exception("Failed to remove row during clear_rows") self._rows = [] except Exception: - pass + self._logger.exception("Failed to clear rows") - # build rows + def _create_row(self, item, cols): + """ + Create a single table row with proper column alignment. + + Args: + item: YTableItem for this row + cols: Number of columns + + Returns: + Gtk.ListBoxRow or None on error + """ try: - cols = self._header.columns() + row = Gtk.ListBoxRow() + ctx = row.get_style_context() + ctx.add_class("y-table-row") + + # Use Grid instead of Box for better column control + grid = Gtk.Grid() + grid.set_column_spacing(0) + grid.set_row_spacing(0) + grid.set_hexpand(True) + + # Create cells for each column with exact widths + for col in range(cols): + cell_widget = self._create_cell_widget(item, col) + if cell_widget: + # Attach cell to grid at column position + grid.attach(cell_widget, col, 0, 1, 1) + + row.set_child(grid) + return row except Exception: - cols = 0 - if cols <= 0: - cols = 1 + self._logger.exception("Failed to create row") + return None - for row_idx, it in enumerate(list(getattr(self, '_items', []) or [])): + def _create_cell_widget(self, item, col): + """ + Create widget for a single cell with proper width and alignment. + + Args: + item: YTableItem containing cell data + col: Column index + + Returns: + Gtk.Widget for the cell + """ + try: + # Get cell data + cell = item.cell(col) if hasattr(item, 'cell') else None + is_cb = self._header_is_checkbox(col) + + # Get alignment for this column try: - row = Gtk.ListBoxRow() - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + align_t = self._header.alignment(col) + except Exception: + align_t = YAlignmentType.YAlignBegin + + # Create container with exact column width + cell_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + cell_container.set_hexpand(False) + + # Apply exact column width from header + width = self._get_column_width(col) + cell_container.set_size_request(width, -1) + + # Create cell content based on type + if is_cb: + content = self._create_checkbox_content(cell, align_t, item, col) + else: + content = self._create_label_content(cell, align_t) + + if content: + cell_container.append(content) + + return cell_container + except Exception: + self._logger.exception("Failed to create cell widget for column %d", col) + # Return empty container with correct width + empty = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + empty.set_size_request(self._get_column_width(col), -1) + return empty - for col in range(cols): - cell = it.cell(col) if hasattr(it, 'cell') else None - is_cb = self._header_is_checkbox(col) - # alignment for this column - try: - align_t = self._header.alignment(col) - except Exception: - align_t = YAlignmentType.YAlignBegin - - if is_cb: - # render a checkbox honoring alignment - try: - chk = Gtk.CheckButton() - try: - chk.set_active(cell.checked() if cell is not None else False) - except Exception: - chk.set_active(False) - # place inside a box to honor alignment - cell_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - if align_t == YAlignmentType.YAlignCenter: - cell_box.set_halign(Gtk.Align.CENTER) - elif align_t == YAlignmentType.YAlignEnd: - cell_box.set_halign(Gtk.Align.END) - else: - cell_box.set_halign(Gtk.Align.START) - cell_box.append(chk) - hbox.append(cell_box) - # connect toggle - def _on_toggled(btn, item=it, cindex=col): - try: - c = item.cell(cindex) - if c is not None: - c.setChecked(bool(btn.get_active())) - # track changed item - self._changed_item = item - # emit value changed - dlg = self.findDialog() - if dlg is not None and self.notify(): - dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) - except Exception: - pass - chk.connect("toggled", _on_toggled) - except Exception: - hbox.append(Gtk.Label(label="")) - else: - # render text label honoring alignment - txt = "" - try: - txt = cell.label() if cell is not None else "" - except Exception: - txt = "" - lbl = Gtk.Label(label=txt) - try: - if align_t == YAlignmentType.YAlignCenter: - lbl.set_xalign(0.5) - elif align_t == YAlignmentType.YAlignEnd: - lbl.set_xalign(1.0) - else: - lbl.set_xalign(0.0) - except Exception: - pass - hbox.append(lbl) - - row.set_child(hbox) - self._listbox.append(row) - self._row_to_item[row] = it - self._item_to_row[it] = row - self._rows.append(row) + def _create_checkbox_content(self, cell, align_t, item, col): + """ + Create checkbox cell content with proper alignment. + + Args: + cell: YTableCell or None + align_t: Alignment type + item: Parent YTableItem + col: Column index + + Returns: + Gtk.Widget for checkbox cell + """ + try: + # Create checkbox + chk = Gtk.CheckButton() + try: + chk.set_active(cell.checked() if cell is not None else False) except Exception: - pass + chk.set_active(False) + + # Apply alignment to checkbox + if align_t == YAlignmentType.YAlignCenter: + chk.set_halign(Gtk.Align.CENTER) + chk.set_margin_start(0) + chk.set_margin_end(0) + elif align_t == YAlignmentType.YAlignEnd: + chk.set_halign(Gtk.Align.END) + chk.set_margin_start(0) + chk.set_margin_end(10) + else: + chk.set_halign(Gtk.Align.START) + chk.set_margin_start(10) + chk.set_margin_end(0) + + chk.set_valign(Gtk.Align.CENTER) + + # Connect toggle handler + def _on_toggled(btn, item=item, cindex=col): + try: + c = item.cell(cindex) + if c is not None: + c.setChecked(bool(btn.get_active())) + # Track changed item + self._changed_item = item + # Emit value changed + dlg = self.findDialog() + if dlg is not None and self.notify(): + dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) + except Exception: + self._logger.exception("Checkbox toggle failed") + + chk.connect("toggled", _on_toggled) + + return chk + except Exception: + self._logger.exception("Failed to create checkbox content") + return Gtk.Label(label="") + + def _create_label_content(self, cell, align_t): + """ + Create label cell content with proper alignment. + + Args: + cell: YTableCell or None + align_t: Alignment type + + Returns: + Gtk.Label widget + """ + try: + # Get text + txt = cell.label() if cell is not None else "" + lbl = Gtk.Label(label=txt) + lbl.set_halign(Gtk.Align.FILL) + lbl.set_margin_start(5) + lbl.set_margin_end(5) + lbl.set_hexpand(True) + lbl.set_ellipsize(Pango.EllipsizeMode.END) + + # Apply alignment + if align_t == YAlignmentType.YAlignCenter: + lbl.set_xalign(0.5) + elif align_t == YAlignmentType.YAlignEnd: + lbl.set_xalign(1.0) + else: + lbl.set_xalign(0.0) + + return lbl + except Exception: + self._logger.exception("Failed to create label content") + lbl = Gtk.Label(label="") + lbl.set_xalign(0.0) + return lbl - # apply selection from model + def _apply_model_selection(self): + """Apply selection state from model to UI.""" try: self._suppress_selection_handler = True - if not self._multi: - # single: select the first selected item - for it in list(getattr(self, '_items', []) or []): - if hasattr(it, 'selected') and it.selected(): - try: - row = self._item_to_row.get(it) - if row is not None: - self._listbox.select_row(row) - break - except Exception: - pass - else: - # multi: select all selected items - for it in list(getattr(self, '_items', []) or []): - if hasattr(it, 'selected') and it.selected(): - try: - row = self._item_to_row.get(it) - if row is not None: - self._listbox.select_row(row) - except Exception: - pass + + # Clear all selections first + try: + self._listbox.unselect_all() + except Exception: + pass + + # Apply selection from model + selected_items = [] + for it in list(getattr(self, '_items', []) or []): + if hasattr(it, 'selected') and it.selected(): + selected_items.append(it) + + # Select items in UI + for it in selected_items: + try: + row = self._item_to_row.get(it) + if row is not None: + self._listbox.select_row(row) + except Exception: + pass + + # For single selection, ensure only one is selected + if not self._multi and len(selected_items) > 1: + for it in selected_items[1:]: + try: + row = self._item_to_row.get(it) + if row is not None: + self._listbox.unselect_row(row) + except Exception: + pass + finally: self._suppress_selection_handler = False - # selection handlers + # Column resizing handlers + def _on_separator_enter(self, controller, x, y): + """Change cursor to col-resize when hovering over separator.""" + try: + widget = controller.get_widget() + display = widget.get_display() + cursor = Gdk.Cursor.new_from_name(display, "col-resize") + if cursor: + widget.get_root().set_cursor(cursor) + except Exception: + pass + + def _on_separator_leave(self, controller): + """Reset cursor when leaving separator.""" + try: + widget = controller.get_widget() + widget.get_root().set_cursor(None) + except Exception: + pass + + def _on_drag_begin(self, gesture, start_x, start_y): + """Begin column resize drag operation.""" + try: + widget = gesture.get_widget() + col_index = widget.column_index + + if col_index < len(self._column_widths): + current_width = self._column_widths[col_index] + self._drag_data = { + 'column_index': col_index, + 'start_width': current_width, + 'start_x': start_x + } + except Exception: + self._drag_data = None + + def _on_drag_update(self, gesture, offset_x, offset_y): + """Update column width during drag.""" + if not self._drag_data: + return + + try: + col_index = self._drag_data['column_index'] + new_width = max(self._min_column_width, + self._drag_data['start_width'] + offset_x) + + # Update column width + self._update_column_width(col_index, new_width) + + self._drag_data['current_width'] = new_width + except Exception: + self._logger.exception("Drag update failed") + + def _on_drag_end(self, gesture, offset_x, offset_y): + """End column resize drag operation.""" + self._drag_data = None + + def _update_column_width(self, col_index, new_width): + """ + Update width for a specific column in header and all rows. + """ + if col_index >= len(self._column_widths): + return + + # Update stored width + self._column_widths[col_index] = new_width + + # Update header cell + if col_index < len(self._header_cells): + try: + self._header_cells[col_index].set_size_request(new_width, -1) + except Exception: + pass + + # Update all rows + for row in self._rows: + try: + grid = row.get_child() + if grid and isinstance(grid, Gtk.Grid): + # Get the cell container at this column index + cell_widget = grid.get_child_at(col_index, 0) + if cell_widget: + cell_widget.set_size_request(new_width, -1) + except Exception: + pass + + # Selection handlers (maintained from original) def _on_row_selected(self, listbox, row): if self._suppress_selection_handler: return try: - # update selected flags + # Update selected flags for it in list(getattr(self, '_items', []) or []): try: it.setSelected(False) @@ -354,7 +797,6 @@ def _on_selected_rows_changed(self, listbox): it.setSelected(True) except Exception: pass - # clamp single-selection just in case if not self._multi and len(new_selected) > 1: new_selected = [new_selected[-1]] @@ -382,14 +824,15 @@ def _on_row_selected_for_multi(self, listbox, row): it = self._row_to_item.get(row, None) if it is not None: if it in self._old_selected_items: - self._listbox.unselect_row( row ) - it.setSelected( False ) + self._listbox.unselect_row(row) + it.setSelected(False) self._on_selected_rows_changed(listbox) else: self._old_selected_items = self._selected_items - # API + # API methods def addItem(self, item): + """Add a single item to the table.""" if isinstance(item, str): item = YTableItem(item) if not isinstance(item, YTableItem): @@ -400,7 +843,7 @@ def addItem(self, item): self.rebuildTable() def addItems(self, items): - '''add multiple items to the table. This is more efficient than calling addItem repeatedly.''' + """Add multiple items to the table efficiently.""" for item in items: if isinstance(item, str): item = YTableItem(item) @@ -414,14 +857,14 @@ def addItems(self, items): if getattr(self, '_listbox', None) is not None: self.rebuildTable() - def selectItem(self, item, selected=True): + """Select or deselect an item.""" try: item.setSelected(bool(selected)) except Exception: pass + if getattr(self, '_listbox', None) is None: - # only update model if selected: if item not in self._selected_items: self._selected_items.append(item) @@ -432,6 +875,7 @@ def selectItem(self, item, selected=True): except Exception: pass return + try: row = self._item_to_row.get(item) if row is None: @@ -439,11 +883,11 @@ def selectItem(self, item, selected=True): row = self._item_to_row.get(item) if row is None: return + self._suppress_selection_handler = True if selected: if not self._multi: try: - # GTK4 ListBox does not have clearSelection; manually unselect others for r in list(self._listbox.get_selected_rows() or []): self._listbox.unselect_row(r) except Exception: @@ -458,27 +902,18 @@ def selectItem(self, item, selected=True): self._suppress_selection_handler = False def deleteAllItems(self): + """Delete all items from the table.""" try: super().deleteAllItems() except Exception: self._items = [] self._selected_items = [] self._changed_item = None - try: - self._row_to_item.clear() - self._item_to_row.clear() - except Exception: - pass - try: - for row in list(self._listbox.get_children() or []): - try: - self._listbox.remove(row) - except Exception: - pass - except Exception: - pass + + self._clear_rows() def changedItem(self): + """Get the item that was most recently changed.""" return getattr(self, "_changed_item", None) def _set_backend_enabled(self, enabled): @@ -495,14 +930,16 @@ def _set_backend_enabled(self, enabled): pass def setVisible(self, visible: bool = True): + """Set widget visibility.""" super().setVisible(visible) try: if getattr(self, "_backend_widget", None) is not None and hasattr(self._backend_widget, "set_visible"): self._backend_widget.set_visible(bool(visible)) except Exception: - self._logger.exception("setVisible failed", exc_info=True) + self._logger.exception("setVisible failed") def setHelpText(self, help_text: str): + """Set help text (tooltip) for the widget.""" super().setHelpText(help_text) try: if getattr(self, "_backend_widget", None) is not None: @@ -511,4 +948,4 @@ def setHelpText(self, help_text: str): except Exception: pass except Exception: - self._logger.exception("setHelpText failed", exc_info=True) + self._logger.exception("setHelpText failed") From 1b0d1e2aa5b65ea3db4f1adb6b4da21ac8682b56 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 11:30:25 +0100 Subject: [PATCH 460/523] do not show useless space if label is not set --- manatools/aui/backends/qt/progressbarqt.py | 72 +++++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/manatools/aui/backends/qt/progressbarqt.py b/manatools/aui/backends/qt/progressbarqt.py index a62a211..f32f404 100644 --- a/manatools/aui/backends/qt/progressbarqt.py +++ b/manatools/aui/backends/qt/progressbarqt.py @@ -14,6 +14,10 @@ from ...yui_common import * class YProgressBarQt(YWidget): + """Qt6 (PySide6) progress bar with optional label above it. + Provides value/max management and respects visibility and tooltip (help text). + """ + def __init__(self, parent=None, label="", maxValue=100): super().__init__(parent) self._label = label @@ -32,14 +36,20 @@ def label(self): def setLabel(self, newLabel): try: - self._label = str(newLabel) + self._label = str(newLabel) if isinstance(newLabel, str) else newLabel if getattr(self, "_label_widget", None) is not None: try: - self._label_widget.setText(self._label) + is_valid = isinstance(self._label, str) and bool(self._label.strip()) + if is_valid: + self._label_widget.setText(self._label) + self._label_widget.setVisible(True) + else: + # hide the label if not a valid string + self._label_widget.setVisible(False) except Exception: - pass + self._logger.exception("setLabel: failed to update QLabel") except Exception: - pass + self._logger.exception("setLabel: unexpected error") def maxValue(self): return int(self._max_value) @@ -75,11 +85,20 @@ def _create_backend_widget(self): v_policy = QtWidgets.QSizePolicy.Expanding if self.stretchable(YUIDimension.YD_VERT) else QtWidgets.QSizePolicy.Fixed container.setSizePolicy(h_policy, v_policy) - # Place label above the progress bar with no spacing so they remain attached - lbl = QtWidgets.QLabel(self._label) if self._label else None - if lbl is not None: - lbl.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - layout.addWidget(lbl) + # Place label above the progress bar; always create then show/hide based on validity + lbl = QtWidgets.QLabel() + lbl.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + layout.addWidget(lbl) + try: + is_valid = isinstance(self._label, str) and bool(self._label.strip()) + if is_valid: + lbl.setText(self._label) + lbl.setVisible(True) + else: + lbl.setVisible(False) + except Exception: + # be safe: hide if anything goes wrong + lbl.setVisible(False) prog = QtWidgets.QProgressBar() prog.setRange(0, max(1, int(self._max_value))) @@ -94,6 +113,19 @@ def _create_backend_widget(self): self._label_widget = lbl self._progress_widget = prog self._backend_widget.setEnabled(bool(self._enabled)) + # Apply initial tooltip and visibility like other Qt widgets (e.g., combobox) + try: + if getattr(self, "_help_text", None): + try: + self._backend_widget.setToolTip(self._help_text) + except Exception: + self._logger.exception("Failed to set tooltip on progressbar") + except Exception: + self._logger.exception("Tooltip setup error on progressbar") + try: + self._backend_widget.setVisible(self.visible()) + except Exception: + self._logger.exception("Failed to set initial visibility on progressbar") try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -102,6 +134,7 @@ def _create_backend_widget(self): self._backend_widget = None self._label_widget = None self._progress_widget = None + self._logger.exception("_create_backend_widget failed") def _set_backend_enabled(self, enabled): try: @@ -165,3 +198,24 @@ def stretchable(self, dim: YUIDimension): return False except Exception: return False + + def setVisible(self, visible=True): + """Set widget visibility and propagate it to the Qt backend widget.""" + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setVisible(bool(visible)) + except Exception: + self._logger.exception("setVisible failed") + + def setHelpText(self, help_text: str): + """Set help text (tooltip) and propagate it to the Qt backend widget.""" + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.setToolTip(help_text) + except Exception: + self._logger.exception("Failed to apply tooltip to backend widget") + except Exception: + self._logger.exception("setHelpText failed") From 4ed5e3e86ecc817a23b7ef61a5eb7b482cddbf15 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 12:04:44 +0100 Subject: [PATCH 461/523] manage visibility and tooltip --- manatools/aui/backends/gtk/progressbargtk.py | 61 +++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/manatools/aui/backends/gtk/progressbargtk.py b/manatools/aui/backends/gtk/progressbargtk.py index 177bdb0..1d9ad69 100644 --- a/manatools/aui/backends/gtk/progressbargtk.py +++ b/manatools/aui/backends/gtk/progressbargtk.py @@ -39,14 +39,19 @@ def label(self): def setLabel(self, newLabel): try: - self._label = str(newLabel) + self._label = str(newLabel) if isinstance(newLabel, str) else newLabel if getattr(self, "_label_widget", None) is not None: try: - self._label_widget.set_text(self._label) + is_valid = isinstance(self._label, str) and bool(self._label.strip()) + if is_valid: + self._label_widget.set_text(self._label) + self._label_widget.set_visible(True) + else: + self._label_widget.set_visible(False) except Exception: - pass + self._logger.exception("setLabel: failed to update Gtk.Label") except Exception: - pass + self._logger.exception("setLabel: unexpected error") def maxValue(self): return int(self._max_value) @@ -76,9 +81,19 @@ def _create_backend_widget(self): container.set_halign(Gtk.Align.FILL) # Label - self._label_widget = Gtk.Label(label=self._label) + self._label_widget = Gtk.Label() self._label_widget.set_halign(Gtk.Align.START) container.append(self._label_widget) + try: + is_valid = isinstance(self._label, str) and bool(self._label.strip()) + if is_valid: + self._label_widget.set_text(self._label) + self._label_widget.set_visible(True) + else: + self._label_widget.set_visible(False) + except Exception: + # be safe: hide if anything goes wrong + self._label_widget.set_visible(False) # Progress Bar self._progress_widget = Gtk.ProgressBar() @@ -93,7 +108,39 @@ def _create_backend_widget(self): self._backend_widget.set_sensitive(self._enabled) except Exception: pass + if self._help_text: + try: + self._backend_widget.set_tooltip_text(self._help_text) + except Exception: + self._logger.error("Failed to set tooltip text on backend widget", exc_info=True) try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + self._backend_widget.set_visible(self.visible()) + except Exception: + self._logger.error("Failed to set backend widget visible", exc_info=True) + try: + self._backend_widget.set_sensitive(self._enabled) + except Exception: + self._logger.exception("Failed to set sensitivity on backend widget") + + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + + def setVisible(self, visible: bool = True): + """Set widget visibility and propagate it to the GTK backend widget.""" + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_visible(bool(visible)) + except Exception: + self._logger.exception("setVisible failed") + + def setHelpText(self, help_text: str): + """Set help text (tooltip) and propagate it to the GTK backend widget.""" + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + try: + self._backend_widget.set_tooltip_text(help_text) + except Exception: + self._logger.exception("Failed to apply tooltip to backend widget") except Exception: - pass \ No newline at end of file + self._logger.exception("setHelpText failed") \ No newline at end of file From 34d8c1cea279d7601cd45bcbe03d5880faaa3611 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 12:08:39 +0100 Subject: [PATCH 462/523] added visibility --- manatools/aui/backends/curses/progressbarcurses.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/curses/progressbarcurses.py b/manatools/aui/backends/curses/progressbarcurses.py index 49461ee..815b8b2 100644 --- a/manatools/aui/backends/curses/progressbarcurses.py +++ b/manatools/aui/backends/curses/progressbarcurses.py @@ -162,4 +162,8 @@ def _draw(self, window, y, x, width, height): try: self._logger.error("_draw error: %s", e, exc_info=True) except Exception: - _mod_logger.error("_draw error: %s", e, exc_info=True) \ No newline at end of file + _mod_logger.error("_draw error: %s", e, exc_info=True) + + def setVisible(self, visible=True): + super().setVisible(visible) + self._can_focus = visible \ No newline at end of file From 1f1cec54f8c81eaa14db8cdea438ee451357ea16 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 12:23:33 +0100 Subject: [PATCH 463/523] tooltip and fixing visibility mangement --- manatools/aui/backends/curses/progressbarcurses.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/manatools/aui/backends/curses/progressbarcurses.py b/manatools/aui/backends/curses/progressbarcurses.py index 815b8b2..52d2011 100644 --- a/manatools/aui/backends/curses/progressbarcurses.py +++ b/manatools/aui/backends/curses/progressbarcurses.py @@ -31,6 +31,8 @@ def __init__(self, parent=None, label="", maxValue=100): self._label = label self._max_value = int(maxValue) if maxValue is not None else 100 self._value = 0 + self._x = 0 + self._y = 0 # progress bar occupies 2 rows when label present, otherwise 1 self._height = 2 if self._label else 1 self._backend_widget = None @@ -100,6 +102,8 @@ def _create_backend_widget(self): _mod_logger.error("_create_backend_widget error: %s", e, exc_info=True) def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: if width <= 0 or height <= 0: return @@ -143,6 +147,9 @@ def _draw(self, window, y, x, width, height): bar_attr = curses.A_REVERSE if self.isEnabled() else curses.A_DIM perc_attr = curses.A_BOLD if self.isEnabled() else curses.A_DIM + # Tooltip positioning + self._x = x + self._y = bar_y # Draw base bar try: window.addstr(bar_y, x, bar_str[:width], bar_attr) From 37a4b84a673d645384a5d0c50ce34959853e3c40 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 12:32:39 +0100 Subject: [PATCH 464/523] Added visibility and tooltip --- manatools/aui/backends/curses/labelcurses.py | 2 ++ manatools/aui/backends/gtk/labelgtk.py | 31 ++++++++++++++++++-- manatools/aui/backends/qt/labelqt.py | 27 +++++++++++++++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index 3ec723b..f73d53e 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -130,6 +130,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: attr = 0 if self._is_heading: diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py index 37dc067..7a93f55 100644 --- a/manatools/aui/backends/gtk/labelgtk.py +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -107,11 +107,20 @@ def _create_backend_widget(self): self._backend_widget.set_markup(markup) except Exception: pass - self._backend_widget.set_sensitive(self._enabled) + if self._help_text: + try: + self._backend_widget.set_tooltip_text(self._help_text) + except Exception: + self._logger.error("Failed to set tooltip text on backend widget", exc_info=True) try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + self._backend_widget.set_visible(self.visible()) except Exception: - pass + self._logger.error("Failed to set backend widget visible", exc_info=True) + try: + self._backend_widget.set_sensitive(self._enabled) + except Exception: + self._logger.exception("Failed to set sensitivity on backend widget") + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) try: # apply initial size policy according to any stretch hints self._apply_size_policy() @@ -163,3 +172,19 @@ def setStretchable(self, dim, new_stretch): self._apply_size_policy() except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_visible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_tooltip_text(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) diff --git a/manatools/aui/backends/qt/labelqt.py b/manatools/aui/backends/qt/labelqt.py index f5400f2..9bc2e2e 100644 --- a/manatools/aui/backends/qt/labelqt.py +++ b/manatools/aui/backends/qt/labelqt.py @@ -89,9 +89,16 @@ def _create_backend_widget(self): self._backend_widget.setFont(font) self._backend_widget.setEnabled(bool(self._enabled)) try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + if self._help_text: + self._backend_widget.setToolTip(self._help_text) except Exception: - pass + self._logger.exception("Failed to set tooltip text", exc_info=True) + try: + self._backend_widget.setVisible(self.visible()) + except Exception: + self._logger.exception("Failed to set widget visibility", exc_info=True) + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + try: # apply initial size policy according to any stretch hints self._apply_size_policy() @@ -153,3 +160,19 @@ def setStretchable(self, dim, new_stretch): self._apply_size_policy() except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setVisible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setToolTip(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) From 45426d1837a414bcb53c2f754b1e157edd572f14 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 14:41:38 +0100 Subject: [PATCH 465/523] Added visibility and tooltip --- .../aui/backends/curses/richtextcurses.py | 8 ++++ manatools/aui/backends/gtk/richtextgtk.py | 38 ++++++++++++++--- manatools/aui/backends/qt/richtextqt.py | 41 +++++++++++++++---- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index a2905f0..39a35ee 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -42,6 +42,9 @@ def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): self._parsed_lines = None self._named_color_pairs = {} self._next_color_pid = 20 + #tooltip support + self._x = 0 + self._y = 0 self.setStretchable(YUIDimension.YD_HORIZ, True) self.setStretchable(YUIDimension.YD_VERT, True) self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") @@ -310,6 +313,8 @@ def _ensure_hover_visible(self): self._scroll_offset = self._hover_line - visible + 1 def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: # draw border try: @@ -320,6 +325,8 @@ def _draw(self, window, y, x, width, height): inner_x = x + 1 inner_y = y + 1 + self._x = inner_x + self._y = inner_y inner_w = max(1, width - 2) inner_h = max(1, height - 2) @@ -694,3 +701,4 @@ def _get_named_color_pair(self, name: str): return None except Exception: return None + diff --git a/manatools/aui/backends/gtk/richtextgtk.py b/manatools/aui/backends/gtk/richtextgtk.py index c96facb..9590cbd 100644 --- a/manatools/aui/backends/gtk/richtextgtk.py +++ b/manatools/aui/backends/gtk/richtextgtk.py @@ -196,18 +196,28 @@ def _create_backend_widget(self): try: self._backend_widget.set_sensitive(bool(self._enabled)) except Exception: - pass + self._logger.error("Failed to set backend widget sensitive state", exc_info=True) + if self._help_text: + try: + self._backend_widget.set_tooltip_text(self._help_text) + except Exception: + self._logger.error("Failed to set tooltip text on backend widget", exc_info=True) try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + self._backend_widget.set_visible(self.visible()) except Exception: - pass - + self._logger.error("Failed to set backend widget visible", exc_info=True) + try: + self._backend_widget.set_sensitive(self._enabled) + except Exception: + self._logger.exception("Failed to set sensitivity on backend widget") + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + def _set_backend_enabled(self, enabled): try: if getattr(self, "_backend_widget", None) is not None: self._backend_widget.set_sensitive(bool(enabled)) except Exception: - pass + self._logger.exception("Failed to set enabled state", exc_info=True) def _html_to_pango_markup(self, s: str) -> str: """Convert a limited subset of HTML into GTK/Pango markup. @@ -228,7 +238,7 @@ def _html_to_pango_markup(self, s: str) -> str: t = re.sub(r"", "\n", t, flags=re.IGNORECASE) t = re.sub(r"", "• ", t, flags=re.IGNORECASE) t = re.sub(r"", "\n", t, flags=re.IGNORECASE) - # Headings -> bold span with size + # Headings -> bold span with size sizes = {1: "xx-large", 2: "x-large", 3: "large", 4: "medium", 5: "small", 6: "x-small"} for n, sz in sizes.items(): t = re.sub(fr"", f"", t, flags=re.IGNORECASE) @@ -236,3 +246,19 @@ def _html_to_pango_markup(self, s: str) -> str: # Allow basic formatting tags and ; strip all other tags t = re.sub(r"]*>", "", t) return t + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_visible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.set_tooltip_text(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) diff --git a/manatools/aui/backends/qt/richtextqt.py b/manatools/aui/backends/qt/richtextqt.py index d2ba6de..0a2f248 100644 --- a/manatools/aui/backends/qt/richtextqt.py +++ b/manatools/aui/backends/qt/richtextqt.py @@ -150,9 +150,14 @@ def _on_anchor_clicked(url: QtCore.QUrl): try: sp = tb.sizePolicy() try: - sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) - sp.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Expanding) + weight_v = int(self.weight(YUIDimension.YD_VERT)) + weight_h = int(self.weight(YUIDimension.YD_HORIZ)) + horiz = QtWidgets.QSizePolicy.Expanding if bool(self.stretchable(YUIDimension.YD_HORIZ) or weight_h) else QtWidgets.QSizePolicy.Fixed + vert = QtWidgets.QSizePolicy.Expanding if bool(self.stretchable(YUIDimension.YD_VERT) or weight_v) else QtWidgets.QSizePolicy.Fixed + sp.setHorizontalPolicy(horiz) + sp.setVerticalPolicy(vert) except Exception: + self._logger.exception("Error setting size policy weights", exc_info=True) try: sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding) sp.setVerticalPolicy(QtWidgets.QSizePolicy.Expanding) @@ -166,15 +171,37 @@ def _on_anchor_clicked(url: QtCore.QUrl): try: self._backend_widget.setEnabled(bool(self._enabled)) except Exception: - pass + self._logger.exception("Failed to set enabled state", exc_info=True) try: - self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + if self._help_text: + self._backend_widget.setToolTip(self._help_text) except Exception: - pass - + self._logger.exception("Failed to set tooltip text", exc_info=True) + try: + self._backend_widget.setVisible(self.visible()) + except Exception: + self._logger.exception("Failed to set widget visibility", exc_info=True) + self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) + def _set_backend_enabled(self, enabled): try: if getattr(self, "_backend_widget", None) is not None: self._backend_widget.setEnabled(bool(enabled)) except Exception: - pass + self._logger.exception("Failed to set enabled state", exc_info=True) + + def setVisible(self, visible=True): + super().setVisible(visible) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setVisible(visible) + except Exception: + self._logger.exception("setVisible failed", exc_info=True) + + def setHelpText(self, help_text: str): + super().setHelpText(help_text) + try: + if getattr(self, "_backend_widget", None) is not None: + self._backend_widget.setToolTip(help_text) + except Exception: + self._logger.exception("setHelpText failed", exc_info=True) \ No newline at end of file From 6b1445b02f80393ce8f29175f6878ac940687e5a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 16:24:51 +0100 Subject: [PATCH 466/523] used not deprecated widget getter and added logging on exceptions capture --- manatools/aui/backends/gtk/hboxgtk.py | 12 ++++++------ manatools/aui/backends/gtk/vboxgtk.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py index 97b4ee5..86f6495 100644 --- a/manatools/aui/backends/gtk/hboxgtk.py +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -90,7 +90,7 @@ def _create_backend_widget(self): def _apply_weights(*args): try: - alloc = self._backend_widget.get_allocated_width() + alloc = self._backend_widget.get_width() if not alloc or alloc <= 0: return True # subtract spacing and margins conservatively @@ -107,7 +107,7 @@ def _apply_weights(*args): except Exception: pass except Exception: - pass + self._logger.exception("_apply_weights: failed", exc_info=True) # remove idle after first successful sizing; keep size-allocate return False @@ -125,10 +125,10 @@ def _on_size_allocate(widget, allocation): try: _apply_weights() except Exception: - pass + self._logger.exception("_on_size_allocate: failed", exc_info=True) self._backend_widget.connect('size-allocate', _on_size_allocate) except Exception: - pass + self._logger.exception("_create_backend_widget: failed to connect size-allocate", exc_info=True) except Exception: pass try: @@ -143,9 +143,9 @@ def _set_backend_enabled(self, enabled): try: self._backend_widget.set_sensitive(enabled) except Exception: - pass + self._logger.exception("_set_backend_enabled: failed to set_sensitive", exc_info=True) except Exception: - pass + self._logger.exception("_set_backend_enabled: failed", exc_info=True) try: for c in list(getattr(self, "_children", []) or []): try: diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index d581a1e..c6a05aa 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -92,7 +92,7 @@ def _create_backend_widget(self): def _apply_vweights(*args): try: - alloc = self._backend_widget.get_allocated_height() + alloc = self._backend_widget.get_height() if not alloc or alloc <= 0: return True spacing = getattr(self._backend_widget, 'get_spacing', lambda: 5)() @@ -108,7 +108,7 @@ def _apply_vweights(*args): except Exception: pass except Exception: - pass + self._logger.exception("_apply_vweights: failed", exc_info=True) return False try: @@ -138,9 +138,9 @@ def _set_backend_enabled(self, enabled): try: self._backend_widget.set_sensitive(enabled) except Exception: - pass + self._logger.exception("_set_backend_enabled: failed to set_sensitive", exc_info=True) except Exception: - pass + self._logger.exception("_set_backend_enabled: failed", exc_info=True) # propagate logical enabled state to child widgets so they update their backends try: for c in list(getattr(self, "_children", []) or []): From 22218d7c4f3763abc74ef333b307cbf3e6122a37 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 16:26:18 +0100 Subject: [PATCH 467/523] added _apply_size_policy to manage stretching properties --- manatools/aui/backends/gtk/progressbargtk.py | 74 +++++++++++++++ manatools/aui/backends/gtk/richtextgtk.py | 96 ++++++++++++++++++-- 2 files changed, 162 insertions(+), 8 deletions(-) diff --git a/manatools/aui/backends/gtk/progressbargtk.py b/manatools/aui/backends/gtk/progressbargtk.py index 1d9ad69..40f82bd 100644 --- a/manatools/aui/backends/gtk/progressbargtk.py +++ b/manatools/aui/backends/gtk/progressbargtk.py @@ -29,11 +29,78 @@ def __init__(self, parent=None, label="", maxValue=100): self._backend_widget = None self._label_widget = None self._progress_widget = None + #default stretchable in horizontal direction + self.setStretchable(YUIDimension.YD_HORIZ, True) self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): return "YProgressBar" + # --- size policy helpers --- + def setStretchable(self, dimension, stretchable): + """Override stretchable to re-apply size policy on change using YUI base attributes.""" + try: + super().setStretchable(dimension, stretchable) + except Exception: + self._logger.exception("setStretchable: base implementation failed") + # Do not cache locally; read from base each time + self._apply_size_policy() + + def _apply_size_policy(self): + """Apply GTK4 expand/alignment to container, label and progress bar using YUI stretch/weight.""" + if self._backend_widget is None: + return + try: + # Read stretch flags from base YWidget + h_stretch = bool(self.stretchable(YUIDimension.YD_HORIZ)) + v_stretch = bool(self.stretchable(YUIDimension.YD_VERT)) + + # Read weights from base YWidget; default to 0.0 if not provided + try: + w_h = float(self.weight(YUIDimension.YD_HORIZ)) + except Exception: + w_h = 0.0 + try: + w_v = float(self.weight(YUIDimension.YD_VERT)) + except Exception: + w_v = 0.0 + + eff_h = bool(h_stretch or (w_h > 0.0)) + eff_v = bool(v_stretch or (w_v > 0.0)) + + targets = [] + if getattr(self, "_backend_widget", None) is not None: + targets.append(self._backend_widget) + if getattr(self, "_label_widget", None) is not None: + targets.append(self._label_widget) + if getattr(self, "_progress_widget", None) is not None: + targets.append(self._progress_widget) + + for w in targets: + try: + w.set_hexpand(eff_h) + except Exception: + self._logger.debug("set_hexpand failed on %s", type(w), exc_info=True) + try: + w.set_halign(Gtk.Align.FILL if eff_h else Gtk.Align.START) + except Exception: + self._logger.debug("set_halign failed on %s", type(w), exc_info=True) + try: + w.set_vexpand(eff_v) + except Exception: + self._logger.debug("set_vexpand failed on %s", type(w), exc_info=True) + try: + w.set_valign(Gtk.Align.FILL if eff_v else Gtk.Align.START) + except Exception: + self._logger.debug("set_valign failed on %s", type(w), exc_info=True) + + self._logger.debug( + "_apply_size_policy: h_stretch=%s v_stretch=%s w_h=%s w_v=%s eff_h=%s eff_v=%s", + h_stretch, v_stretch, w_h, w_v, eff_h, eff_v + ) + except Exception: + self._logger.exception("_apply_size_policy: unexpected failure") + def label(self): return self._label @@ -77,8 +144,11 @@ def setValue(self, newValue): def _create_backend_widget(self): container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + # Start with sane defaults; final behavior comes from _apply_size_policy container.set_hexpand(True) container.set_halign(Gtk.Align.FILL) + container.set_valign(Gtk.Align.FILL) + # container.set_homogeneous(False) # default; keep for clarity # Label self._label_widget = Gtk.Label() @@ -104,6 +174,10 @@ def _create_backend_widget(self): container.append(self._progress_widget) self._backend_widget = container + + # Apply consistent size policy to avoid centered layout + self._apply_size_policy() + try: self._backend_widget.set_sensitive(self._enabled) except Exception: diff --git a/manatools/aui/backends/gtk/richtextgtk.py b/manatools/aui/backends/gtk/richtextgtk.py index 9590cbd..c0d5a4d 100644 --- a/manatools/aui/backends/gtk/richtextgtk.py +++ b/manatools/aui/backends/gtk/richtextgtk.py @@ -37,6 +37,82 @@ def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): def widgetClass(self): return "YRichText" + # --- size policy helpers --- + def setStretchable(self, dimension, stretchable): + """Override stretchable to immediately re-apply the GTK size policy.""" + try: + # Call parent implementation first + super().setStretchable(dimension, stretchable) + except Exception: + self._logger.exception("setStretchable: base implementation failed") + # Apply to current backend/content + self._apply_size_policy() + + def _apply_size_policy(self): + """Apply GTK4 expand and alignment based on stretch flags and weights. + + - Stretch or positive weight => expand True and FILL. + - Otherwise => expand False and START. + - Applies to the Gtk.ScrolledWindow and content widget (Label/TextView). + """ + try: + h_stretch = bool(self.stretchable(YUIDimension.YD_HORIZ)) + v_stretch = bool(self.stretchable(YUIDimension.YD_VERT)) + try: + w_h = float(self.weight(YUIDimension.YD_HORIZ)) + except Exception: + w_h = 0.0 + try: + w_v = float(self.weight(YUIDimension.YD_VERT)) + except Exception: + w_v = 0.0 + + eff_h = bool(h_stretch or (w_h > 0.0)) + eff_v = bool(v_stretch or (w_v > 0.0)) + + targets = [] + if getattr(self, "_backend_widget", None) is not None: + targets.append(self._backend_widget) + if getattr(self, "_content_widget", None) is not None: + targets.append(self._content_widget) + + for ww in targets: + try: + ww.set_hexpand(eff_h) + except Exception: + self._logger.debug("set_hexpand failed on %s", type(ww), exc_info=True) + try: + ww.set_halign(Gtk.Align.FILL if eff_h else Gtk.Align.START) + except Exception: + self._logger.debug("set_halign failed on %s", type(ww), exc_info=True) + try: + ww.set_vexpand(eff_v) + except Exception: + self._logger.debug("set_vexpand failed on %s", type(ww), exc_info=True) + try: + ww.set_valign(Gtk.Align.FILL if eff_v else Gtk.Align.START) + except Exception: + self._logger.debug("set_valign failed on %s", type(ww), exc_info=True) + + # Enforce left/top content alignment for Label/TextView + try: + cw = getattr(self, "_content_widget", None) + if isinstance(cw, Gtk.Label): + cw.set_justify(Gtk.Justification.LEFT) + cw.set_xalign(0.0) + elif isinstance(cw, Gtk.TextView): + cw.set_monospace(False) + cw.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + except Exception: + self._logger.debug("additional content alignment failed", exc_info=True) + + self._logger.debug( + "_apply_size_policy: h_stretch=%s v_stretch=%s w_h=%s w_v=%s eff_h=%s eff_v=%s", + h_stretch, v_stretch, w_h, w_v, eff_h, eff_v + ) + except Exception: + self._logger.exception("_apply_size_policy: unexpected failure") + def setValue(self, newValue: str): self._text = newValue or "" w = getattr(self, "_content_widget", None) @@ -84,6 +160,7 @@ def setPlainTextMode(self, on: bool = True): # rebuild content widget to reflect mode if getattr(self, "_backend_widget", None) is not None: self._create_content() + self._apply_size_policy() self.setValue(self._text) def autoScrollDown(self) -> bool: @@ -140,13 +217,14 @@ def _create_content(self): except Exception: lbl.set_text(converted) try: - lbl.set_selectable(False) + lbl.set_selectable(False) # keep it non-selectable; avoids odd sizing in some themes except Exception: pass try: lbl.set_wrap(True) lbl.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) lbl.set_xalign(0.0) + lbl.set_justify(Gtk.Justification.LEFT) except Exception: pass # connect link activation @@ -173,15 +251,13 @@ def _on_activate_link(label, uri): def _create_backend_widget(self): sw = Gtk.ScrolledWindow() try: + # let size policy decide final expand/align; start with sane defaults sw.set_hexpand(True) sw.set_vexpand(True) - try: - sw.set_halign(Gtk.Align.FILL) - sw.set_valign(Gtk.Align.FILL) - except Exception: - pass + sw.set_halign(Gtk.Align.FILL) + sw.set_valign(Gtk.Align.FILL) except Exception: - pass + self._logger.debug("Failed to set initial expand/align on scrolled window", exc_info=True) self._create_content() try: @@ -190,8 +266,12 @@ def _create_backend_widget(self): try: sw.add(self._content_widget) except Exception: - pass + self._logger.debug("Failed to attach content to scrolled window", exc_info=True) self._backend_widget = sw + + # Apply consistent size policy after backend and content exist + self._apply_size_policy() + # respect initial enabled state try: self._backend_widget.set_sensitive(bool(self._enabled)) From 1cfb15ab768a94ec6a1f596c08269eb7ee7195ea Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 16:35:30 +0100 Subject: [PATCH 468/523] added log --- manatools/aui/backends/curses/richtextcurses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index 39a35ee..ce2f28f 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -660,6 +660,7 @@ def _get_named_color_pair(self, name: str): if not name: return None if not curses.has_colors(): + self._logger.debug("_get_named_color_pair: terminal has no color support") return None # normalize name nm = str(name).strip().lower() From ded07d80d22456fe45aad7e7fd6ee77ea8f89ef6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 16:47:12 +0100 Subject: [PATCH 469/523] No foxus if not visible --- manatools/aui/backends/curses/labelcurses.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index f73d53e..9555e6a 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -169,3 +169,7 @@ def _draw(self, window, y, x, width, height): self._logger.error("_draw curses.error: %s", e, exc_info=True) except Exception: _mod_logger.error("_draw curses.error: %s", e, exc_info=True) + + def setVisible(self, visible=True): + super().setVisible(visible) + self._can_focus = visible \ No newline at end of file From e2f583ec0785208d3bd2ebe26c06da8392de1700 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 16:52:19 +0100 Subject: [PATCH 470/523] Horizontal stretching by default --- manatools/aui/backends/curses/progressbarcurses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manatools/aui/backends/curses/progressbarcurses.py b/manatools/aui/backends/curses/progressbarcurses.py index 52d2011..5739e58 100644 --- a/manatools/aui/backends/curses/progressbarcurses.py +++ b/manatools/aui/backends/curses/progressbarcurses.py @@ -36,6 +36,8 @@ def __init__(self, parent=None, label="", maxValue=100): # progress bar occupies 2 rows when label present, otherwise 1 self._height = 2 if self._label else 1 self._backend_widget = None + #default stretchable in horizontal direction + self.setStretchable(YUIDimension.YD_HORIZ, True) self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") if not self._logger.handlers and not logging.getLogger().handlers: for h in _mod_logger.handlers: From 2cef0bda79100314038b5c1020562f3304575d0e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 16:52:47 +0100 Subject: [PATCH 471/523] No focus on widget if invisible --- manatools/aui/backends/curses/richtextcurses.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index ce2f28f..03432ee 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -703,3 +703,6 @@ def _get_named_color_pair(self, name: str): except Exception: return None + def setVisible(self, visible=True): + super().setVisible(visible) + self._can_focus = visible \ No newline at end of file From 9e9fd2fe36481bea672ced05a0fab92b93dbd5c3 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 17:32:45 +0100 Subject: [PATCH 472/523] better richtext layout evaluation --- .../aui/backends/curses/richtextcurses.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index 03432ee..3ff9130 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -323,22 +323,24 @@ def _draw(self, window, y, x, width, height): except curses.error: pass - inner_x = x + 1 - inner_y = y + 1 + inner_x = x + inner_y = y self._x = inner_x self._y = inner_y - inner_w = max(1, width - 2) - inner_h = max(1, height - 2) + inner_w = max(1, width) + inner_h = max(1, height) + + # obtain lines (prefer parsed rich lines if available) + lines = self._parsed_lines if (not self._plain and getattr(self, '_parsed_lines', None)) else self._lines() + total_rows = len(lines) + max_row_len = max(len(row) for row in lines) if lines else 0 # reserve rightmost column for vertical scrollbar, bottom row for horizontal scrollbar - bar_w = 1 if inner_w > 2 else 0 + bar_w = 1 if inner_w > 2 and max_row_len > inner_w else 0 content_w = inner_w - bar_w - bar_h_row = 1 if inner_h > 2 else 0 + bar_h_row = 1 if inner_h > 2 and total_rows > inner_h else 0 content_h = inner_h - bar_h_row - # obtain lines (prefer parsed rich lines if available) - lines = self._parsed_lines if (not self._plain and getattr(self, '_parsed_lines', None)) else self._lines() - total_rows = len(lines) visible = min(total_rows, max(1, content_h)) # remember for visibility calculations self._last_content_w = content_w From faed57c83ea5b28c941cf184d688aa3636c1720a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 17:39:38 +0100 Subject: [PATCH 473/523] added _preferred_rows to manage later --- manatools/aui/backends/curses/richtextcurses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index 3ff9130..a6b1d50 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -42,6 +42,7 @@ def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): self._parsed_lines = None self._named_color_pairs = {} self._next_color_pid = 20 + self._preferred_rows = 6 #not used by now #tooltip support self._x = 0 self._y = 0 From 1e6f8b744979c03b6cd1606798e62fd454cde717 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 19:03:52 +0100 Subject: [PATCH 474/523] First attempt to provide a paned for Qt --- manatools/aui/backends/qt/__init__.py | 4 +- manatools/aui/backends/qt/panedqt.py | 69 +++++++++++++ manatools/aui/yui_qt.py | 9 ++ test/test_paned.py | 138 ++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/qt/panedqt.py create mode 100644 test/test_paned.py diff --git a/manatools/aui/backends/qt/__init__.py b/manatools/aui/backends/qt/__init__.py index b00540c..599238d 100644 --- a/manatools/aui/backends/qt/__init__.py +++ b/manatools/aui/backends/qt/__init__.py @@ -26,6 +26,7 @@ from .sliderqt import YSliderQt from .logviewqt import YLogViewQt from .timefieldqt import YTimeFieldQt +from .panedqt import YPanedQt __all__ = [ @@ -57,5 +58,6 @@ "YSliderQt", "YLogViewQt", "YTimeFieldQt", - # ... + "YPanedQt", + # ... add new widgets here ... ] diff --git a/manatools/aui/backends/qt/panedqt.py b/manatools/aui/backends/qt/panedqt.py new file mode 100644 index 0000000..46323a7 --- /dev/null +++ b/manatools/aui/backends/qt/panedqt.py @@ -0,0 +1,69 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +YPanedQt: Qt6 Paned widget wrapper. + +- Wraps QSplitter with horizontal or vertical orientation. +- Children are added in order, up to two for parity with Gtk Paned. +""" + +import logging +from ...yui_common import YWidget, YUIDimension + +try: + from PySide6.QtCore import Qt + from PySide6.QtWidgets import QSplitter +except Exception as e: + QSplitter = None + Qt = None + logging.getLogger("manatools.aui.qt.paned").error("Failed to import Qt6: %s", e, exc_info=True) + + +class YPanedQt(YWidget): + """ + Qt6 implementation of YPaned using QSplitter. + """ + + def __init__(self, parent=None, dimension: YUIDimension = YUIDimension.YD_HORIZ): + super().__init__(parent) + self._logger = logging.getLogger("manatools.aui.qt.YPanedQt") + self._orientation = dimension + self._backend_widget = None + self._children = [] + + def widgetClass(self): + return "YPaned" + + def _create_backend_widget(self): + """ + Create the underlying QSplitter with the chosen orientation. + """ + if QSplitter is None or Qt is None: + raise RuntimeError("Qt6 is not available") + orient = Qt.Horizontal if self._orientation == YUIDimension.YD_HORIZ else Qt.Vertical + self._backend_widget = QSplitter(orient) + self._logger.debug("Created QSplitter orientation=%s", "H" if orient == Qt.Horizontal else "V") + for idx, child in enumerate(self._children): + widget = child.get_backend_widget() + if widget is not None: + self._backend_widget.addWidget(widget) + self._logger.debug("Added existing child %d to splitter during creation", idx) + + def addChild(self, child): + """ + Add a child to the splitter, limiting to two children for consistency. + """ + super().addChild(child) + if getattr(self, "_backend_widget", None) is None: + return + + try: + if len(self._children) >= 2: + self._logger.warning("YPanedQt can only manage two children; ignoring extra child") + return + if getattr(child, "_backend_widget", None) is not None: + self._backend_widget.addWidget(child._backend_widget) + self._children.append(child) + self._logger.debug("Added child to splitter: %s", getattr(child, "debugLabel", lambda: repr(child))()) + except Exception as e: + self._logger.error("addChild error: %s", e, exc_info=True) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index e6b60db..5e926c1 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -482,4 +482,13 @@ def createTimeField(self, parent, label): return YTimeFieldQt(parent, label) except Exception as e: logging.getLogger(__name__).exception("Failed to create YTimeFieldQt: %s", e) + raise + + def createPaned(self, parent, dimension: YUIDimension = YUIDimension.YD_HORIZ): + """Create a Paned widget (Qt backend).""" + from .backends.qt import YPanedQt + try: + return YPanedQt(parent, dimension) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YPanedQt: %s", e) raise \ No newline at end of file diff --git a/test/test_paned.py b/test/test_paned.py new file mode 100644 index 0000000..d3c7876 --- /dev/null +++ b/test/test_paned.py @@ -0,0 +1,138 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +Test script for YPaned using the YUI abstraction (inspired by test_frame.py). + +- Horizontal paned: Tree widget + Table. +- Vertical paned: RichText + Table. +- Dialog includes a push button to quit. + +No backend-specific fallback; if it fails, fix the widget implementation. +""" + +import os +import sys +import logging + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + +def test_paned(backend_name=None): + """Interactive test showcasing YDumbTab with three tabs and a ReplacePoint.""" + if backend_name: + print(f"Setting backend to: {backend_name}") + os.environ['YUI_BACKEND'] = backend_name + else: + print("Using auto-detection") + + try: + from manatools.aui.yui import YUI, YUI_ui + import manatools.aui.yui_common as yui + + # Force re-detection + YUI._instance = None + YUI._backend = None + + backend = YUI.backend() + print(f"Using backend: {backend.value}") + + # Basic logging for diagnosis + import logging + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(name)s: %(message)s") + + ui = YUI_ui() + factory = ui.widgetFactory() + dialog = factory.createMainDialog() + mainVbox = factory.createVBox( dialog ) + paned_h = factory.createPaned(mainVbox, yui.YUIDimension.YD_HORIZ) + tree = factory.createTree(paned_h, "Test Tree") + items = [] + for i in range(1, 6): + itm = yui.YTreeItem(f"Item {i}", is_open=(i == 1)) + for j in range(1, 4): + sub = yui.YTreeItem(f"SubItem {i}.{j}", parent=itm) + for k in range(1, 3): + yui.YTreeItem(f"SubItem {i}.{j}.{k}", parent=sub) + items.append(itm) + tree.addItem(itm) + header = yui.YTableHeader() + header.addColumn('date', alignment=yui.YAlignmentType.YAlignEnd) + header.addColumn('document', alignment=yui.YAlignmentType.YAlignBegin) + table_h = factory.createTable(paned_h, header) + paned_v = factory.createPaned(mainVbox, yui.YUIDimension.YD_VERT) + rich = factory.createRichText( + paned_v, + "

RichText sample

  • Line 1
  • Line 2
Visit: https://example.org", + ) + header = yui.YTableHeader() + header.addColumn('num.', alignment=yui.YAlignmentType.YAlignEnd) + header.addColumn('info', alignment=yui.YAlignmentType.YAlignBegin) + header.addColumn('', checkBox=True, alignment=yui.YAlignmentType.YAlignCenter) + table_v = factory.createTable(paned_v, header) + items = [] + for i in range(1, 7): + itm = yui.YTableItem(f"Row {i}") + itm.addCell(str(i)) + itm.addCell(f"test {i}") + # third column is checkbox column + itm.addCell(False if i % 2 == 0 else True) + items.append(itm) + table_v.addItem(itm) + btn_quit = factory.createPushButton(mainVbox, "Quit") + + # + # Event loop + # + #valueField.setText( "???" ) + while True: + event = dialog.waitForEvent() + if not event: + print("Empty") + next + typ = event.eventType() + if typ == yui.YEventType.CancelEvent: + dialog.destroy() + break + elif typ == yui.YEventType.WidgetEvent: + wdg = event.widget() + if wdg == btn_quit: + dialog.destroy() + break + if wdg == tree: + sel = tree.selectedItem() + if sel: + root_logger.debug(f"Tree selection changed: {sel.label()}") + else: + print(f"Unhandled event type: {typ}") + + except Exception as e: + print(f"Error testing Paned with backend {backend_name}: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + if len(sys.argv) > 1: + test_paned(sys.argv[1]) + else: + test_paned() \ No newline at end of file From 52c352457e6b1b9d6f2410225887a7679fc8f648 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 19:25:53 +0100 Subject: [PATCH 475/523] First attempt to have dynamic panel on Gtk --- manatools/aui/backends/gtk/__init__.py | 2 + manatools/aui/backends/gtk/panedgtk.py | 85 ++++++++++++++++++++++++++ manatools/aui/yui_gtk.py | 9 +++ 3 files changed, 96 insertions(+) create mode 100644 manatools/aui/backends/gtk/panedgtk.py diff --git a/manatools/aui/backends/gtk/__init__.py b/manatools/aui/backends/gtk/__init__.py index 4f4164a..680abd3 100644 --- a/manatools/aui/backends/gtk/__init__.py +++ b/manatools/aui/backends/gtk/__init__.py @@ -26,6 +26,7 @@ from .slidergtk import YSliderGtk from .logviewgtk import YLogViewGtk from .timefieldgtk import YTimeFieldGtk +from .panedgtk import YPanedGtk __all__ = [ "YDialogGtk", @@ -56,5 +57,6 @@ "YSliderGtk", "YLogViewGtk", "YTimeFieldGtk", + "YPanedGtk", # ... ] diff --git a/manatools/aui/backends/gtk/panedgtk.py b/manatools/aui/backends/gtk/panedgtk.py new file mode 100644 index 0000000..2cd9086 --- /dev/null +++ b/manatools/aui/backends/gtk/panedgtk.py @@ -0,0 +1,85 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +""" +YPanedGtk: GTK4 Paned widget wrapper. + +- Wraps Gtk.Paned with horizontal or vertical orientation. +- Accepts up to two children; first child goes to "start", second to "end". +- Behavior similar to HBox/VBox but using native Gtk.Paned. +""" + +import logging +from ...yui_common import YWidget, YUIDimension + +try: + import gi + gi.require_version("Gtk", "4.0") + from gi.repository import Gtk +except Exception as e: + Gtk = None # Allow import in non-GTK environments + logging.getLogger("manatools.aui.gtk.paned").error("Failed to import GTK4: %s", e, exc_info=True) + + +class YPanedGtk(YWidget): + """ + GTK4 implementation of YPaned using Gtk.Paned. + """ + + def __init__(self, parent=None, dimension: YUIDimension = YUIDimension.YD_HORIZ): + super().__init__(parent) + self._logger = logging.getLogger("manatools.aui.gtk.YPanedGtk") + self._orientation = dimension + self._backend_widget = None + + def widgetClass(self): + return "YPaned" + + def _create_backend_widget(self): + """ + Create the underlying Gtk.Paned with the chosen orientation. + """ + if Gtk is None: + raise RuntimeError("GTK4 is not available") + orient = Gtk.Orientation.HORIZONTAL if self._orientation == YUIDimension.YD_HORIZ else Gtk.Orientation.VERTICAL + self._backend_widget = Gtk.Paned.new(orient) + self._logger.debug("Created Gtk.Paned orientation=%s", "H" if orient == Gtk.Orientation.HORIZONTAL else "V") + # Collect children first so we can apply weight-based heuristics + children = list(self._children) + + for idx, child in enumerate(self._children): + self._logger.debug("Paned child: %s", child.debugLabel()) + widget = child.get_backend_widget() + if idx == 0: + if widget is not None: + self._backend_widget.set_start_child(widget) + self._logger.debug("Set start child: %s", child.debugLabel()) + elif idx == 1: + if widget is not None: + self._backend_widget.set_end_child(widget) + self._logger.debug("Set end child: %s", child.debugLabel()) + else: + self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") + + def addChild(self, child: YWidget): + """ + Add a child to the paned: first goes to 'start', second to 'end'. + """ + if len(self._children) == 2: + self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") + return + super().addChild(child) + if self._backend_widget is None: + return + try: + if child == self._children[0]: + if getattr(child, "_backend_widget", None) is not None: + self._backend_widget.set_start_child(child._backend_widget) + self._logger.debug("Set start child: %s", getattr(child, "debugLabel", lambda: repr(child))()) + elif len(self._children) > 1 and child == self._children[1]: + if getattr(child, "_backend_widget", None) is not None: + self._backend_widget.set_end_child(child._backend_widget) + self._logger.debug("Set end child: %s", getattr(child, "debugLabel", lambda: repr(child))()) + else: + self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") + except Exception as e: + self._logger.error("addChild error: %s", e, exc_info=True) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index 7f35502..fd48f56 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -907,3 +907,12 @@ def createTimeField(self, parent, label): def createFrame(self, parent, label: str=""): """Create a Frame widget.""" return YFrameGtk(parent, label) + + def createPaned(self, parent, dimension: YUIDimension = YUIDimension.YD_HORIZ): + """Create a Paned widget (GTK backend).""" + from .backends.gtk import YPanedGtk + try: + return YPanedGtk(parent, dimension) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YPanedGtk: %s", e) + raise \ No newline at end of file From b5d9b5260c7c6f0bfc92624dfc8589ea747441c9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 19:28:09 +0100 Subject: [PATCH 476/523] header file --- manatools/aui/backends/gtk/panedgtk.py | 9 +++++++++ manatools/aui/backends/qt/panedqt.py | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/manatools/aui/backends/gtk/panedgtk.py b/manatools/aui/backends/gtk/panedgtk.py index 2cd9086..4e9db43 100644 --- a/manatools/aui/backends/gtk/panedgtk.py +++ b/manatools/aui/backends/gtk/panedgtk.py @@ -1,5 +1,14 @@ # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.gtk contains all GTK backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.gtk +''' """ YPanedGtk: GTK4 Paned widget wrapper. diff --git a/manatools/aui/backends/qt/panedqt.py b/manatools/aui/backends/qt/panedqt.py index 46323a7..97008cc 100644 --- a/manatools/aui/backends/qt/panedqt.py +++ b/manatools/aui/backends/qt/panedqt.py @@ -1,5 +1,15 @@ # vim: set fileencoding=utf-8 : # vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.qt contains all Qt backend classes + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.qt +''' + """ YPanedQt: Qt6 Paned widget wrapper. From 69a2f42996951c6888a6e9f213a499e2ce90f354 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 19:50:18 +0100 Subject: [PATCH 477/523] first attempt to have a simplified (show/hide) dynamic pane for ncurses --- manatools/aui/backends/curses/__init__.py | 4 +- manatools/aui/backends/curses/panedcurses.py | 211 +++++++++++++++++++ manatools/aui/yui_curses.py | 9 + 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 manatools/aui/backends/curses/panedcurses.py diff --git a/manatools/aui/backends/curses/__init__.py b/manatools/aui/backends/curses/__init__.py index 57be6d0..9827afc 100644 --- a/manatools/aui/backends/curses/__init__.py +++ b/manatools/aui/backends/curses/__init__.py @@ -26,6 +26,7 @@ from .slidercurses import YSliderCurses from .logviewcurses import YLogViewCurses from .timefieldcurses import YTimeFieldCurses +from .panedcurses import YPanedCurses __all__ = [ "YDialogCurses", @@ -56,5 +57,6 @@ "YSliderCurses", "YLogViewCurses", "YTimeFieldCurses", - # ... + "YPanedCurses", + # ... add new widgets here ... ] diff --git a/manatools/aui/backends/curses/panedcurses.py b/manatools/aui/backends/curses/panedcurses.py new file mode 100644 index 0000000..ca292ff --- /dev/null +++ b/manatools/aui/backends/curses/panedcurses.py @@ -0,0 +1,211 @@ +# vim: set fileencoding=utf-8 : +# vim: set et ts=4 sw=4: +''' +Python manatools.aui.backends.curses contains curses backend for YMultiLineEdit + +License: LGPLv2+ + +Author: Angelo Naselli + +@package manatools.aui.backends.curses + +YPanedCurses: Curses Paned widget. + +- Splits area horizontally or vertically into up to two panes. +- Draws only visible children; hidden ones are not drawn and do not receive keys. +- Special keys when a child has focus: + '+' => make focused child visible + '-' => make focused child invisible +- Hidden children remain focus-reachable via parent navigation so '+' can restore visibility. +''' + +import curses +import logging +from ...yui_common import YWidget, YUIDimension +from .commoncurses import _curses_recursive_min_height + +class YPanedCurses(YWidget): + """ + ncurses implementation of YPaned with two child panes. + """ + + def __init__(self, parent=None, dimension: YUIDimension = YUIDimension.YD_HORIZ): + super().__init__(parent) + self._logger = logging.getLogger("manatools.aui.curses.YPanedCurses") + self._orientation = dimension + self._backend_widget = self # self-managed drawing + self._hidden = [False, False] # visibility flags per child index + # Minimum height will be computed from children + self._height = 1 + self._logger.debug("YPanedCurses created orientation=%s", "H" if dimension == YUIDimension.YD_HORIZ else "V") + + def widgetClass(self): + return "YPaned" + + def _create_backend_widget(self): + """ + No external backend widget required; drawing is managed in _draw. + """ + self._backend_widget = self + self._logger.debug("_create_backend_widget: using self-managed backend") + + def _recompute_min_height(self): + """Compute minimal height for this horizontal box as the tallest child's minimum.""" + try: + if not self._children: + self._height = 1 + return + self._height = max(1, max(_curses_recursive_min_height(c) for c in self._children)) + except Exception: + self._height = 1 + + def addChild(self, child): + """ + Add a child to the paned container. First child is 'start', second is 'end'. + """ + if len(self._children) == 2: + self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") + return + super().addChild(child) + self._recompute_min_height() + self._logger.debug("Added start child: %s", getattr(child, "debugLabel", lambda: repr(child))()) + + def setStartChild(self, child: YWidget): + """Explicitly set the start child.""" + try: + self._children[0] = child + self._hidden[0] = False + self._logger.debug("setStartChild: %s", getattr(child, "debugLabel", lambda: repr(child))()) + except Exception as e: + self._logger.error("setStartChild error: %s", e, exc_info=True) + + def setEndChild(self, child: YWidget): + """Explicitly set the end child.""" + try: + self._children[1] = child + self._hidden[1] = False + self._logger.debug("setEndChild: %s", getattr(child, "debugLabel", lambda: repr(child))()) + except Exception as e: + self._logger.error("setEndChild error: %s", e, exc_info=True) + + def _get_focused_child_index(self): + """ + Try to detect which child currently has focus. Fallback to first present child. + """ + try: + for idx, ch in enumerate(self._children): + if ch is None: + continue + # Heuristic focus detection compatible with existing widgets + if hasattr(ch, "hasFocus") and callable(getattr(ch, "hasFocus")): + if ch.hasFocus(): # type: ignore[attr-defined] + return idx + elif getattr(ch, "_focused", False): + return idx + except Exception as e: + self._logger.debug("focus detection failed: %s", e) + # fallback + return 0 if self._children[0] is not None else 1 + + def _set_child_visible(self, idx: int, visible: bool): + """ + Toggle child visibility without touching the child's own setVisible(), + so it stays focus-reachable even when hidden. + """ + try: + if idx not in (0, 1): + return + if self._children[idx] is None: + return + self._hidden[idx] = not bool(visible) + self._logger.debug("Child %d visibility -> %s", idx, "visible" if visible else "hidden") + except Exception as e: + self._logger.error("_set_child_visible error: %s", e, exc_info=True) + + def _draw(self, window, y, x, width, height): + """ + Draw visible children split by orientation. + - If both visible: split area evenly. + - If one visible: give full area to that child. + - Hidden children are not drawn. + """ + try: + start, end = self._children + start_vis = (start is not None) and (not self._hidden[0]) + end_vis = (end is not None) and (not self._hidden[1]) + + # If neither is visible, fill with spaces and return + if not start_vis and not end_vis: + try: + for r in range(max(0, height)): + window.addstr(y + r, x, " " * max(0, width)) + except curses.error: + pass + return + + if self._orientation == YUIDimension.YD_HORIZ: + # horizontal split: left | right + if start_vis and end_vis: + w1 = max(0, width // 2) + w2 = max(0, width - w1) + if start is not None: + start._draw(window, y, x, w1, height) + if end is not None: + end._draw(window, y, x + w1, w2, height) + elif start_vis: + if start is not None: + start._draw(window, y, x, width, height) + else: + if end is not None: + end._draw(window, y, x, width, height) + else: + # vertical split: top / bottom + if start_vis and end_vis: + h1 = max(0, height // 2) + h2 = max(0, height - h1) + if start is not None: + start._draw(window, y, x, width, h1) + if end is not None: + end._draw(window, y + h1, x, width, h2) + elif start_vis: + if start is not None: + start._draw(window, y, x, width, height) + else: + if end is not None: + end._draw(window, y, x, width, height) + except Exception as e: + try: + self._logger.error("_draw error: %s", e, exc_info=True) + except Exception: + pass + + def _handle_key(self, key): + """ + Intercept '+' and '-' to toggle visibility of the focused child. + - '+' makes focused child visible. + - '-' makes focused child hidden. + Hidden child does not receive keys until made visible again. + """ + try: + idx = self._get_focused_child_index() + child = self._children[idx] + if key in (ord('+'),): + self._set_child_visible(idx, True) + return True + if key in (ord('-'),): + self._set_child_visible(idx, False) + return True + + # Delegate key handling to focused child only if visible + if child is not None and not self._hidden[idx]: + if hasattr(child, "_handle_key"): + try: + return bool(child._handle_key(key)) # type: ignore[attr-defined] + except Exception as e: + self._logger.error("child _handle_key error: %s", e, exc_info=True) + return False + # If hidden (or no child), do not handle other keys + return False + except Exception as e: + self._logger.error("_handle_key error: %s", e, exc_info=True) + return False diff --git a/manatools/aui/yui_curses.py b/manatools/aui/yui_curses.py index c469fa6..5fce18f 100644 --- a/manatools/aui/yui_curses.py +++ b/manatools/aui/yui_curses.py @@ -8,6 +8,7 @@ import os import time import fnmatch +import logging from .yui_common import * from .backends.curses import * @@ -679,3 +680,11 @@ def createTimeField(self, parent, label): logging.getLogger(__name__).exception("Failed to create YTimeFieldCurses: %s", e) raise + def createPaned(self, parent, dimension: YUIDimension = YUIDimension.YD_HORIZ): + """Create a Paned widget (ncurses backend).""" + from .backends.curses import YPanedCurses + try: + return YPanedCurses(parent, dimension) + except Exception as e: + logging.getLogger(__name__).exception("Failed to create YPanedCurses: %s", e) + raise \ No newline at end of file From 6cb13285b50d240736a709c7d45ccbb25ff5aaa5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 19:54:48 +0100 Subject: [PATCH 478/523] Added more items --- test/test_paned.py | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/test/test_paned.py b/test/test_paned.py index d3c7876..086b3b4 100644 --- a/test/test_paned.py +++ b/test/test_paned.py @@ -67,6 +67,8 @@ def test_paned(backend_name=None): mainVbox = factory.createVBox( dialog ) paned_h = factory.createPaned(mainVbox, yui.YUIDimension.YD_HORIZ) tree = factory.createTree(paned_h, "Test Tree") + tree.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tree.setStretchable(yui.YUIDimension.YD_VERT, True) items = [] for i in range(1, 6): itm = yui.YTreeItem(f"Item {i}", is_open=(i == 1)) @@ -77,19 +79,32 @@ def test_paned(backend_name=None): items.append(itm) tree.addItem(itm) header = yui.YTableHeader() - header.addColumn('date', alignment=yui.YAlignmentType.YAlignEnd) + header.addColumn('num.', alignment=yui.YAlignmentType.YAlignEnd) header.addColumn('document', alignment=yui.YAlignmentType.YAlignBegin) table_h = factory.createTable(paned_h, header) + table_h.setStretchable(yui.YUIDimension.YD_HORIZ, True) + table_h.setStretchable(yui.YUIDimension.YD_VERT, True) + items = [] + for i in range(1, 7): + itm = yui.YTableItem(f"Row {i}") + itm.addCell(str(i)) + itm.addCell(f"test_{i}") + items.append(itm) + table_h.addItem(itm) paned_v = factory.createPaned(mainVbox, yui.YUIDimension.YD_VERT) rich = factory.createRichText( paned_v, "

RichText sample

  • Line 1
  • Line 2
Visit: https://example.org", ) + rich.setStretchable(yui.YUIDimension.YD_HORIZ, True) + rich.setStretchable(yui.YUIDimension.YD_VERT, True) header = yui.YTableHeader() header.addColumn('num.', alignment=yui.YAlignmentType.YAlignEnd) header.addColumn('info', alignment=yui.YAlignmentType.YAlignBegin) header.addColumn('', checkBox=True, alignment=yui.YAlignmentType.YAlignCenter) table_v = factory.createTable(paned_v, header) + table_v.setStretchable(yui.YUIDimension.YD_HORIZ, True) + table_v.setStretchable(yui.YUIDimension.YD_VERT, True) items = [] for i in range(1, 7): itm = yui.YTableItem(f"Row {i}") @@ -104,7 +119,6 @@ def test_paned(backend_name=None): # # Event loop # - #valueField.setText( "???" ) while True: event = dialog.waitForEvent() if not event: @@ -115,14 +129,23 @@ def test_paned(backend_name=None): dialog.destroy() break elif typ == yui.YEventType.WidgetEvent: - wdg = event.widget() - if wdg == btn_quit: - dialog.destroy() - break - if wdg == tree: - sel = tree.selectedItem() - if sel: - root_logger.debug(f"Tree selection changed: {sel.label()}") + wdg = event.widget() + if wdg == btn_quit: + dialog.destroy() + break + if wdg == tree: + sel = tree.selectedItem() + if sel: + root_logger.debug(f"Tree selection changed: {sel.label()}") + elif wdg == table_h: + sel = table_h.selectedItem() + if sel: + root_logger.debug(f"Horizontal Table selection changed: {sel.label(0)} {sel.label(1)}") + elif wdg == table_v: + sel = table_v.selectedItem() + if sel: + root_logger.debug(f"Vertical Table selection changed: {sel.label(0)} {sel.label(1)}") + else: print(f"Unhandled event type: {typ}") From 01b78bbb31fbbd13ef16da8314f19d31fa8e6ac4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 20:09:00 +0100 Subject: [PATCH 479/523] improved layout --- manatools/aui/backends/gtk/panedgtk.py | 168 ++++++++++++++++++++++--- 1 file changed, 149 insertions(+), 19 deletions(-) diff --git a/manatools/aui/backends/gtk/panedgtk.py b/manatools/aui/backends/gtk/panedgtk.py index 4e9db43..4cb684b 100644 --- a/manatools/aui/backends/gtk/panedgtk.py +++ b/manatools/aui/backends/gtk/panedgtk.py @@ -32,46 +32,171 @@ class YPanedGtk(YWidget): """ GTK4 implementation of YPaned using Gtk.Paned. + + - Paned is stretchable by default on both directions. + - Children are set to expand and fill, similar to HBox/VBox behavior. """ def __init__(self, parent=None, dimension: YUIDimension = YUIDimension.YD_HORIZ): super().__init__(parent) self._logger = logging.getLogger("manatools.aui.gtk.YPanedGtk") self._orientation = dimension - self._backend_widget = None + self._backend_widget = None + # Make paned stretchable by default so it fills available space + try: + self.setStretchable(YUIDimension.YD_HORIZ, True) + self.setStretchable(YUIDimension.YD_VERT, True) + except Exception: + self._logger.debug("Default stretchable setup failed", exc_info=True) def widgetClass(self): return "YPaned" + # --- size policy helpers --- + def setStretchable(self, dimension, stretchable): + """Override stretchable to re-apply size policy using base attributes.""" + try: + super().setStretchable(dimension, stretchable) + except Exception: + self._logger.exception("setStretchable: base implementation failed") + self._apply_size_policy() + + def _apply_size_policy(self): + """ + Apply GTK4 expand/alignment to the paned and its children using YUI stretch/weights. + Ensures the paned and children fill the allocated space. + """ + if self._backend_widget is None or Gtk is None: + return + try: + # Read stretch flags from base YWidget + h_stretch = bool(self.stretchable(YUIDimension.YD_HORIZ)) + v_stretch = bool(self.stretchable(YUIDimension.YD_VERT)) + # Read weights from base YWidget; default to 0.0 if not provided + try: + w_h = float(self.weight(YUIDimension.YD_HORIZ)) + except Exception: + w_h = 0.0 + try: + w_v = float(self.weight(YUIDimension.YD_VERT)) + except Exception: + w_v = 0.0 + + eff_h = bool(h_stretch or (w_h > 0.0)) + eff_v = bool(v_stretch or (w_v > 0.0)) + + # Paned should expand and fill + try: + self._backend_widget.set_hexpand(eff_h) + except Exception: + self._logger.debug("set_hexpand failed on paned", exc_info=True) + try: + self._backend_widget.set_vexpand(eff_v) + except Exception: + self._logger.debug("set_vexpand failed on paned", exc_info=True) + try: + self._backend_widget.set_halign(Gtk.Align.FILL if eff_h else Gtk.Align.START) + except Exception: + self._logger.debug("set_halign failed on paned", exc_info=True) + try: + self._backend_widget.set_valign(Gtk.Align.FILL if eff_v else Gtk.Align.START) + except Exception: + self._logger.debug("set_valign failed on paned", exc_info=True) + + # Ensure children fill their panes (like HBox/VBox) + for ch in getattr(self, "_children", []): + try: + bw = ch.get_backend_widget() if hasattr(ch, "get_backend_widget") else getattr(ch, "_backend_widget", None) + if bw is None: + continue + bw.set_hexpand(True) + bw.set_vexpand(True) + bw.set_halign(Gtk.Align.FILL) + bw.set_valign(Gtk.Align.FILL) + except Exception: + self._logger.debug("Failed to apply child size policy for %s", getattr(ch, "debugLabel", lambda: repr(ch))(), exc_info=True) + + self._logger.debug( + "_apply_size_policy: h_stretch=%s v_stretch=%s w_h=%s w_v=%s eff_h=%s eff_v=%s", + h_stretch, v_stretch, w_h, w_v, eff_h, eff_v + ) + except Exception: + self._logger.exception("_apply_size_policy: unexpected failure") + + def _apply_child_props(self, child_widget): + """ + Ensure a child widget expands and fills its allocated pane area. + """ + if child_widget is None or Gtk is None: + return + try: + child_widget.set_hexpand(True) + except Exception: + self._logger.debug("child.set_hexpand failed", exc_info=True) + try: + child_widget.set_vexpand(True) + except Exception: + self._logger.debug("child.set_vexpand failed", exc_info=True) + try: + child_widget.set_halign(Gtk.Align.FILL) + except Exception: + self._logger.debug("child.set_halign failed", exc_info=True) + try: + child_widget.set_valign(Gtk.Align.FILL) + except Exception: + self._logger.debug("child.set_valign failed", exc_info=True) + def _create_backend_widget(self): """ - Create the underlying Gtk.Paned with the chosen orientation. + Create the underlying Gtk.Paned with the chosen orientation and attach existing children. """ if Gtk is None: raise RuntimeError("GTK4 is not available") orient = Gtk.Orientation.HORIZONTAL if self._orientation == YUIDimension.YD_HORIZ else Gtk.Orientation.VERTICAL self._backend_widget = Gtk.Paned.new(orient) self._logger.debug("Created Gtk.Paned orientation=%s", "H" if orient == Gtk.Orientation.HORIZONTAL else "V") - # Collect children first so we can apply weight-based heuristics - children = list(self._children) - for idx, child in enumerate(self._children): - self._logger.debug("Paned child: %s", child.debugLabel()) - widget = child.get_backend_widget() - if idx == 0: - if widget is not None: + # Paned should fill available space + try: + self._backend_widget.set_hexpand(True) + self._backend_widget.set_vexpand(True) + self._backend_widget.set_halign(Gtk.Align.FILL) + self._backend_widget.set_valign(Gtk.Align.FILL) + # Avoid shrinking children below their minimum size + if hasattr(self._backend_widget, "set_shrink_start_child"): + self._backend_widget.set_shrink_start_child(False) + if hasattr(self._backend_widget, "set_shrink_end_child"): + self._backend_widget.set_shrink_end_child(False) + except Exception: + self._logger.debug("Initial paned size setup failed", exc_info=True) + + # Attach already collected children (like HBox/VBox does) + for idx, child in enumerate(getattr(self, "_children", [])): + try: + widget = child.get_backend_widget() if hasattr(child, "get_backend_widget") else getattr(child, "_backend_widget", None) + if widget is None: + self._logger.debug("Child %s has no backend widget yet", getattr(child, "debugLabel", lambda: repr(child))()) + continue + if idx == 0: self._backend_widget.set_start_child(widget) - self._logger.debug("Set start child: %s", child.debugLabel()) - elif idx == 1: - if widget is not None: + self._logger.debug("Set start child: %s", child.debugLabel()) + elif idx == 1: self._backend_widget.set_end_child(widget) - self._logger.debug("Set end child: %s", child.debugLabel()) - else: - self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") + self._logger.debug("Set end child: %s", child.debugLabel()) + else: + self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") + # Ensure child fills + self._apply_child_props(widget) + except Exception as e: + self._logger.error("Attaching child[%d] failed: %s", idx, e, exc_info=True) + + # Apply size policy once backend is ready + self._apply_size_policy() def addChild(self, child: YWidget): """ Add a child to the paned: first goes to 'start', second to 'end'. + Children are set to expand and fill. """ if len(self._children) == 2: self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") @@ -80,15 +205,20 @@ def addChild(self, child: YWidget): if self._backend_widget is None: return try: + widget = child.get_backend_widget() if hasattr(child, "get_backend_widget") else getattr(child, "_backend_widget", None) if child == self._children[0]: - if getattr(child, "_backend_widget", None) is not None: - self._backend_widget.set_start_child(child._backend_widget) + if widget is not None: + self._backend_widget.set_start_child(widget) + self._apply_child_props(widget) self._logger.debug("Set start child: %s", getattr(child, "debugLabel", lambda: repr(child))()) elif len(self._children) > 1 and child == self._children[1]: - if getattr(child, "_backend_widget", None) is not None: - self._backend_widget.set_end_child(child._backend_widget) + if widget is not None: + self._backend_widget.set_end_child(widget) + self._apply_child_props(widget) self._logger.debug("Set end child: %s", getattr(child, "debugLabel", lambda: repr(child))()) else: self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") except Exception as e: self._logger.error("addChild error: %s", e, exc_info=True) + # Re-apply overall size policy + self._apply_size_policy() From 0cc4a7685c77eddc5052f3d2e950a605ad941e54 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 20:13:29 +0100 Subject: [PATCH 480/523] Paned on Gtk --- manatools/aui/backends/gtk/panedgtk.py | 31 +++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/manatools/aui/backends/gtk/panedgtk.py b/manatools/aui/backends/gtk/panedgtk.py index 4cb684b..ef8fb24 100644 --- a/manatools/aui/backends/gtk/panedgtk.py +++ b/manatools/aui/backends/gtk/panedgtk.py @@ -146,6 +146,27 @@ def _apply_child_props(self, child_widget): except Exception: self._logger.debug("child.set_valign failed", exc_info=True) + def _configure_paned_behavior(self): + """ + Configure Gtk.Paned to behave like Qt's QSplitter: + - allow full collapse of either child (shrink = True on both sides) + - let both children participate in resize (resize = True on both sides) + """ + if self._backend_widget is None or Gtk is None: + return + try: + if hasattr(self._backend_widget, "set_shrink_start_child"): + self._backend_widget.set_shrink_start_child(True) + if hasattr(self._backend_widget, "set_shrink_end_child"): + self._backend_widget.set_shrink_end_child(True) + if hasattr(self._backend_widget, "set_resize_start_child"): + self._backend_widget.set_resize_start_child(True) + if hasattr(self._backend_widget, "set_resize_end_child"): + self._backend_widget.set_resize_end_child(True) + self._logger.debug("Paned behavior configured: shrink(start/end)=True, resize(start/end)=True") + except Exception: + self._logger.error("Failed to configure paned behavior", exc_info=True) + def _create_backend_widget(self): """ Create the underlying Gtk.Paned with the chosen orientation and attach existing children. @@ -162,14 +183,12 @@ def _create_backend_widget(self): self._backend_widget.set_vexpand(True) self._backend_widget.set_halign(Gtk.Align.FILL) self._backend_widget.set_valign(Gtk.Align.FILL) - # Avoid shrinking children below their minimum size - if hasattr(self._backend_widget, "set_shrink_start_child"): - self._backend_widget.set_shrink_start_child(False) - if hasattr(self._backend_widget, "set_shrink_end_child"): - self._backend_widget.set_shrink_end_child(False) except Exception: self._logger.debug("Initial paned size setup failed", exc_info=True) + # Ensure splitter can fully collapse either child (Qt-like behavior) + self._configure_paned_behavior() + # Attach already collected children (like HBox/VBox does) for idx, child in enumerate(getattr(self, "_children", [])): try: @@ -220,5 +239,7 @@ def addChild(self, child: YWidget): self._logger.warning("YPanedGtk can only manage two children; ignoring extra child") except Exception as e: self._logger.error("addChild error: %s", e, exc_info=True) + # Keep paned behavior consistent after dynamic changes + self._configure_paned_behavior() # Re-apply overall size policy self._apply_size_policy() From cce7055f2d8bd00dc6cf4b6897973d3b856dd726 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 20:30:04 +0100 Subject: [PATCH 481/523] managing +/- to show/hide dynamic panel children --- manatools/aui/backends/curses/dialogcurses.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index e5f3aab..f213b78 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -464,6 +464,16 @@ def waitForEvent(self, timeout_millisec=0): self._resize_pending_until = time.time() + 0.15 continue + # Handle hide/show for paned via focused child + if key in (ord('+'), ord('-')): + try: + if self._bubble_key_to_paned(key): + # handled by a YPanedCurses ancestor; force redraw + self._last_draw_time = 0 + continue + except Exception: + pass + # Focus navigation if key == ord('\t'): self._cycle_focus(forward=True) @@ -585,3 +595,24 @@ def visit(widget): except Exception: pass return False + + def _bubble_key_to_paned(self, key) -> bool: + """Send '+' or '-' to the nearest YPanedCurses ancestor of the focused widget.""" + try: + fw = self._focused_widget + w = fw + while w is not None: + try: + if hasattr(w, "widgetClass") and w.widgetClass() == "YPaned": + if hasattr(w, "_handle_key"): + return bool(w._handle_key(key)) + return False + except Exception: + pass + try: + w = w.parent() if hasattr(w, "parent") else getattr(w, "_parent", None) + except Exception: + w = getattr(w, "_parent", None) + return False + except Exception: + return False From 050758cf89879974dbf8efe45cd9256255139590 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 31 Jan 2026 20:57:07 +0100 Subject: [PATCH 482/523] Added visibility --- manatools/aui/backends/curses/checkboxcurses.py | 9 ++++++++- manatools/aui/backends/curses/comboboxcurses.py | 2 +- manatools/aui/backends/curses/datefieldcurses.py | 9 ++++++++- manatools/aui/backends/curses/imagecurses.py | 2 ++ manatools/aui/backends/curses/inputfieldcurses.py | 2 +- manatools/aui/backends/curses/intfieldcurses.py | 9 ++++++++- manatools/aui/backends/curses/labelcurses.py | 3 --- manatools/aui/backends/curses/logviewcurses.py | 9 ++++++++- manatools/aui/backends/curses/menubarcurses.py | 9 ++++++++- .../aui/backends/curses/multilineeditcurses.py | 9 ++++++++- manatools/aui/backends/curses/panedcurses.py | 1 + manatools/aui/backends/curses/progressbarcurses.py | 4 ---- manatools/aui/backends/curses/pushbuttoncurses.py | 4 ++-- manatools/aui/backends/curses/radiobuttoncurses.py | 9 ++++++++- manatools/aui/backends/curses/richtextcurses.py | 2 +- manatools/aui/backends/curses/selectionboxcurses.py | 9 ++++++++- manatools/aui/backends/curses/slidercurses.py | 9 ++++++++- manatools/aui/backends/curses/tablecurses.py | 13 +++---------- manatools/aui/backends/curses/timefieldcurses.py | 9 ++++++++- manatools/aui/backends/curses/treecurses.py | 9 ++++++++- 20 files changed, 99 insertions(+), 33 deletions(-) diff --git a/manatools/aui/backends/curses/checkboxcurses.py b/manatools/aui/backends/curses/checkboxcurses.py index 4bdaf5e..2698a42 100644 --- a/manatools/aui/backends/curses/checkboxcurses.py +++ b/manatools/aui/backends/curses/checkboxcurses.py @@ -98,6 +98,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: checkbox_symbol = "[X]" if self._is_checked else "[ ]" text = f"{checkbox_symbol} {self._label}" @@ -123,7 +125,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False # Space or Enter to toggle if key in (ord(' '), ord('\n'), curses.KEY_ENTER): @@ -142,3 +144,8 @@ def _toggle(self): dlg._post_event(YWidgetEvent(self, YEventReason.ValueChanged)) else: print(f"CheckBox toggled (no dialog found): {self._label} = {self._is_checked}") + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/comboboxcurses.py b/manatools/aui/backends/curses/comboboxcurses.py index 704dc3b..96ef363 100644 --- a/manatools/aui/backends/curses/comboboxcurses.py +++ b/manatools/aui/backends/curses/comboboxcurses.py @@ -252,7 +252,7 @@ def _draw_expanded_list(self, window): _mod_logger.error("_draw_expanded_list curses.error: %s", e, exc_info=True) def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True diff --git a/manatools/aui/backends/curses/datefieldcurses.py b/manatools/aui/backends/curses/datefieldcurses.py index bc83359..265b35b 100644 --- a/manatools/aui/backends/curses/datefieldcurses.py +++ b/manatools/aui/backends/curses/datefieldcurses.py @@ -105,6 +105,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: line = y label_to_show = self._label if self._label else (self.debugLabel() if hasattr(self, 'debugLabel') else "unknown") @@ -143,7 +145,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False # Editing @@ -236,3 +238,8 @@ def _commit_edit(self): if self._d > dmax: self._d = dmax self._cancel_edit() + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/imagecurses.py b/manatools/aui/backends/curses/imagecurses.py index d4fd94c..d313484 100644 --- a/manatools/aui/backends/curses/imagecurses.py +++ b/manatools/aui/backends/curses/imagecurses.py @@ -72,6 +72,8 @@ def _create_backend_widget(self): self._logger.exception("_create_backend_widget failed") def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: if width <= 0 or height <= 0: return diff --git a/manatools/aui/backends/curses/inputfieldcurses.py b/manatools/aui/backends/curses/inputfieldcurses.py index 0d3195d..60bd0fc 100644 --- a/manatools/aui/backends/curses/inputfieldcurses.py +++ b/manatools/aui/backends/curses/inputfieldcurses.py @@ -159,7 +159,7 @@ def _draw(self, window, y, x, width, height): _mod_logger.error("_draw curses.error: %s", e, exc_info=True) def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True diff --git a/manatools/aui/backends/curses/intfieldcurses.py b/manatools/aui/backends/curses/intfieldcurses.py index b1c7925..6de3d69 100644 --- a/manatools/aui/backends/curses/intfieldcurses.py +++ b/manatools/aui/backends/curses/intfieldcurses.py @@ -92,6 +92,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: # Draw label on the first line, control on the second line (if available) line_label = y @@ -158,7 +160,7 @@ def _draw(self, window, y, x, width, height): def _handle_key(self, key): """Handle keys for focusable spin-like behaviour: up/down to change value.""" - if not getattr(self, '_focused', False) or not self.isEnabled(): + if not getattr(self, '_focused', False) or not self.isEnabled() or not self.visible(): return False try: # If currently editing, handle editing keys @@ -271,3 +273,8 @@ def _handle_key(self, key): return False return False + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/labelcurses.py b/manatools/aui/backends/curses/labelcurses.py index 9555e6a..680e292 100644 --- a/manatools/aui/backends/curses/labelcurses.py +++ b/manatools/aui/backends/curses/labelcurses.py @@ -170,6 +170,3 @@ def _draw(self, window, y, x, width, height): except Exception: _mod_logger.error("_draw curses.error: %s", e, exc_info=True) - def setVisible(self, visible=True): - super().setVisible(visible) - self._can_focus = visible \ No newline at end of file diff --git a/manatools/aui/backends/curses/logviewcurses.py b/manatools/aui/backends/curses/logviewcurses.py index 39ad82b..c285457 100644 --- a/manatools/aui/backends/curses/logviewcurses.py +++ b/manatools/aui/backends/curses/logviewcurses.py @@ -102,6 +102,8 @@ def _trim_if_needed(self): # curses drawing def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: line = y # label @@ -195,7 +197,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True @@ -227,3 +229,8 @@ def _handle_key(self, key): def _set_backend_enabled(self, enabled): pass + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/menubarcurses.py b/manatools/aui/backends/curses/menubarcurses.py index 1869ff1..7cb5274 100644 --- a/manatools/aui/backends/curses/menubarcurses.py +++ b/manatools/aui/backends/curses/menubarcurses.py @@ -161,6 +161,8 @@ def _next_selectable_index(self, items, start_idx, direction): return None def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: # remember bar area self._bar_y = y @@ -422,7 +424,7 @@ def deleteMenus(self): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True if key in (curses.KEY_LEFT, ord('h')): @@ -650,3 +652,8 @@ def _handle_key(self, key): return True handled = False return handled + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/multilineeditcurses.py b/manatools/aui/backends/curses/multilineeditcurses.py index 7632ea8..79c30da 100644 --- a/manatools/aui/backends/curses/multilineeditcurses.py +++ b/manatools/aui/backends/curses/multilineeditcurses.py @@ -155,6 +155,8 @@ def _desired_height_for_width(self, width: int) -> int: return max(1, getattr(self, '_height', 1)) def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: # Draw label (if present) and up to available lines line = y @@ -241,7 +243,7 @@ def _handle_key(self, key): - PageUp/PageDown scroll the view. - Ctrl-S posts ValueChanged (explicit commit), but we also post on each edit. """ - if not getattr(self, '_focused', False) or not self.isEnabled(): + if not getattr(self, '_focused', False) or not self.isEnabled() or not self.visible(): return False try: @@ -374,3 +376,8 @@ def _handle_key(self, key): return True except Exception: return False + + def setVisible(self, visible: bool = True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/panedcurses.py b/manatools/aui/backends/curses/panedcurses.py index ca292ff..0decfa4 100644 --- a/manatools/aui/backends/curses/panedcurses.py +++ b/manatools/aui/backends/curses/panedcurses.py @@ -118,6 +118,7 @@ def _set_child_visible(self, idx: int, visible: bool): if self._children[idx] is None: return self._hidden[idx] = not bool(visible) + self._children[idx].setVisible(visible) self._logger.debug("Child %d visibility -> %s", idx, "visible" if visible else "hidden") except Exception as e: self._logger.error("_set_child_visible error: %s", e, exc_info=True) diff --git a/manatools/aui/backends/curses/progressbarcurses.py b/manatools/aui/backends/curses/progressbarcurses.py index 5739e58..afe8006 100644 --- a/manatools/aui/backends/curses/progressbarcurses.py +++ b/manatools/aui/backends/curses/progressbarcurses.py @@ -172,7 +172,3 @@ def _draw(self, window, y, x, width, height): self._logger.error("_draw error: %s", e, exc_info=True) except Exception: _mod_logger.error("_draw error: %s", e, exc_info=True) - - def setVisible(self, visible=True): - super().setVisible(visible) - self._can_focus = visible \ No newline at end of file diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index 5fca306..aff1d64 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -141,7 +141,7 @@ def _draw(self, window, y, x, width, height): _mod_logger.error("_draw curses.error: %s", e, exc_info=True) def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False if key == ord('\n') or key == ord(' '): @@ -180,4 +180,4 @@ def setIcon(self, icon_name: str): def setVisible(self, visible=True): super().setVisible(visible) - self._can_focus = visible \ No newline at end of file + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/radiobuttoncurses.py b/manatools/aui/backends/curses/radiobuttoncurses.py index 411ede5..47a9463 100644 --- a/manatools/aui/backends/curses/radiobuttoncurses.py +++ b/manatools/aui/backends/curses/radiobuttoncurses.py @@ -108,6 +108,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: # Use parentheses style for radio: '(*)' if checked else '( )' radio_symbol = "(*)" if self._is_checked else "( )" @@ -140,7 +142,7 @@ def _draw(self, window, y, x, width, height): _mod_logger.error("_draw curses.error: %s", e, exc_info=True) def _handle_key(self, key): - if not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False # Space or Enter to select radio if key in (ord(' '), ord('\n'), curses.KEY_ENTER): @@ -191,3 +193,8 @@ def _select(self): self._logger.error("_select error", exc_info=True) except Exception: _mod_logger.error("_select error", exc_info=True) + + def setVisible(self, visible=True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/richtextcurses.py b/manatools/aui/backends/curses/richtextcurses.py index a6b1d50..8381d55 100644 --- a/manatools/aui/backends/curses/richtextcurses.py +++ b/manatools/aui/backends/curses/richtextcurses.py @@ -496,7 +496,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True lines = self._parsed_lines if (not self._plain and getattr(self, '_parsed_lines', None)) else self._lines() diff --git a/manatools/aui/backends/curses/selectionboxcurses.py b/manatools/aui/backends/curses/selectionboxcurses.py index c796005..e5148ad 100644 --- a/manatools/aui/backends/curses/selectionboxcurses.py +++ b/manatools/aui/backends/curses/selectionboxcurses.py @@ -207,6 +207,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: line = y # draw label if present @@ -261,7 +263,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False self._logger.debug("_handle_key called key=%r focused=%s hover_index=%d", key, self._focused, self._hover_index) handled = True @@ -366,3 +368,8 @@ def addItem(self, item): self._logger.debug("addItem: label=<%s> selected=<%s> value=<%r>", new_item.label(), selected_flag, self._value) except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/slidercurses.py b/manatools/aui/backends/curses/slidercurses.py index 87ac6c1..b8b14f7 100644 --- a/manatools/aui/backends/curses/slidercurses.py +++ b/manatools/aui/backends/curses/slidercurses.py @@ -86,6 +86,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: line = y # label on top if present @@ -167,7 +169,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True @@ -258,3 +260,8 @@ def _commit_edit(self): return self.setValue(v) self._cancel_edit() + + def setVisible(self, visible=True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/tablecurses.py b/manatools/aui/backends/curses/tablecurses.py index 9794a81..50a50ee 100644 --- a/manatools/aui/backends/curses/tablecurses.py +++ b/manatools/aui/backends/curses/tablecurses.py @@ -283,7 +283,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True if key == curses.KEY_UP: @@ -476,12 +476,5 @@ def changedItem(self): def setVisible(self, visible: bool = True): super().setVisible(visible) - try: - # in curses backend visibility controls whether widget can receive focus - self._can_focus = bool(visible) - except Exception: - pass - - def setHelpText(self, help_text: str): - # store help text; curses has no native tooltip but other logic (dialog overlay) may use it - super().setHelpText(help_text) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/timefieldcurses.py b/manatools/aui/backends/curses/timefieldcurses.py index 1cb2e41..2c130cf 100644 --- a/manatools/aui/backends/curses/timefieldcurses.py +++ b/manatools/aui/backends/curses/timefieldcurses.py @@ -63,6 +63,8 @@ def _set_backend_enabled(self, enabled): pass def _draw(self, window, y, x, width, height): + if self._visible is False: + return try: line = y label_to_show = self._label if self._label else (self.debugLabel() if hasattr(self, 'debugLabel') else "unknown") @@ -99,7 +101,7 @@ def _draw(self, window, y, x, width, height): pass def _handle_key(self, key): - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False if self._editing: @@ -173,3 +175,8 @@ def _commit_edit(self): v = max(lo, min(hi, v)) setattr(self, name, v) self._cancel_edit() + + def setVisible(self, visible=True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file diff --git a/manatools/aui/backends/curses/treecurses.py b/manatools/aui/backends/curses/treecurses.py index 42034d3..f863c05 100644 --- a/manatools/aui/backends/curses/treecurses.py +++ b/manatools/aui/backends/curses/treecurses.py @@ -535,6 +535,8 @@ def _handle_selection_action(self, item): def _draw(self, window, y, x, width, height): """Draw tree in provided rectangle. Expects height rows available.""" + if self._visible is False: + return try: # compute drawing area for items (first row may be label) line = y @@ -614,7 +616,7 @@ def _draw(self, window, y, x, width, height): def _handle_key(self, key): """Keyboard handling: navigation, expand (SPACE), select (ENTER).""" - if not self._focused or not self.isEnabled(): + if not self._focused or not self.isEnabled() or not self.visible(): return False handled = True total = len(self._visible_items) @@ -766,3 +768,8 @@ def selectItem(self, item, selected=True): pass except Exception: pass + + def setVisible(self, visible=True): + super().setVisible(visible) + # in curses backend visibility controls whether widget can receive focus + self._can_focus = bool(visible) \ No newline at end of file From 722df6881fdb625afe325df9d273642631ba9613 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 16:20:12 +0100 Subject: [PATCH 483/523] Fixed busy/normal cursor setting --- manatools/aui/yui_gtk.py | 64 +++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/manatools/aui/yui_gtk.py b/manatools/aui/yui_gtk.py index fd48f56..19336ea 100644 --- a/manatools/aui/yui_gtk.py +++ b/manatools/aui/yui_gtk.py @@ -254,40 +254,42 @@ def isTextMode(self) -> bool: def busyCursor(self): """Set busy cursor (GTK implementation).""" - return - display = Gdk.Display.get_default() - if display is None: - return - seat = display.get_default_seat() - if seat is None: - return - pointer = seat.get_pointer() - if pointer is None: - return - window = pointer.get_window() - if window is None: - return - cursor = Gdk.Cursor.new_from_name(display, "wait") - if cursor is None: - return - window.set_cursor(cursor) + current_dialog = YDialogGtk.currentDialog() + if current_dialog is not None: + window = getattr(current_dialog, "_window", None) + if window is None: + return + surface = window.get_surface() + if surface is None: + return + display = surface.get_display() + cursor = Gdk.Cursor.new_from_name("wait", None) + # Try alternatives if "wait" not available + if not cursor: + cursor = Gdk.Cursor.new_from_name("progress", None) + if not cursor: + cursor = Gdk.Cursor.new_from_name("grabbing", None) + if cursor is None: + return + surface.set_cursor(cursor) + display.flush() # Force update def normalCursor(self): """Set normal cursor (GTK implementation).""" - return - display = Gdk.Display.get_default() - if display is None: - return - seat = display.get_default_seat() - if seat is None: - return - pointer = seat.get_pointer() - if pointer is None: - return - window = pointer.get_window() - if window is None: - return - window.set_cursor(None) + current_dialog = YDialogGtk.currentDialog() + if current_dialog is not None: + window = getattr(current_dialog, "_window", None) + if window is None: + return + surface = window.get_surface() + if surface is None: + return + display = surface.get_display() + cursor = Gdk.Cursor.new_from_name("default", None) + if cursor is None: + return + surface.set_cursor(cursor) + display.flush() # Force update def _create_gtk4_filters(self, filter_str: str) -> List[Gtk.FileFilter]: """ From 066cbc98b3e0ead40f5cf9110ccb07f57822d1f0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 16:20:38 +0100 Subject: [PATCH 484/523] Added busy/normal cursor testing --- test/test_progressbar.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_progressbar.py b/test/test_progressbar.py index 971a2a5..4bb5c2c 100644 --- a/test/test_progressbar.py +++ b/test/test_progressbar.py @@ -41,7 +41,7 @@ def test_progressbar(backend_name=None): # then wait 3s before restarting counting. Any non-timeout event (e.g. OK) # will break the loop and close the dialog. value = 0 - timeout_ms = 500 + timeout_ms = 100 phase = 0 # Normal counting while True: ev = dialog.waitForEvent(timeout_ms) @@ -50,12 +50,14 @@ def test_progressbar(backend_name=None): if phase == 0: value = min(100, value + 1) elif phase == 1: + YUI.app().busyCursor() value = 0 timeout_ms = 3000 phase = 3 # Waiting before restart elif phase == 3: + YUI.app().normalCursor() value = 1 - timeout_ms = 500 + timeout_ms = 100 phase = 0 # Normal counting try: pb.setValue(value) From d047746037f6915eac4b0ba392dfbd0dc6c78e07 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 16:21:22 +0100 Subject: [PATCH 485/523] updated --- sow/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sow/TODO.md b/sow/TODO.md index 696bc6f..887d21b 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -28,7 +28,7 @@ Missing Widgets comparing libyui original factory: [X] YReplacePoint [X] YRadioButton [X] YImage - [ ] YBusyIndicator + [X] YBusyIndicator [X] YLogView Optional/special widgets (from `YOptionalWidgetFactory`): From adb929cacc58867c0dc20b2ea2d9b92095203cb6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 16:24:56 +0100 Subject: [PATCH 486/523] added manatools defautl icon --- share/images/manatools.svg | 75 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 share/images/manatools.svg diff --git a/share/images/manatools.svg b/share/images/manatools.svg new file mode 100644 index 0000000..3575b4f --- /dev/null +++ b/share/images/manatools.svg @@ -0,0 +1,75 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + From 4a045bf5895b8d6151d93ff46566d67483218286 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 16:57:14 +0100 Subject: [PATCH 487/523] min image size 64x64 --- manatools/aui/backends/qt/imageqt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manatools/aui/backends/qt/imageqt.py b/manatools/aui/backends/qt/imageqt.py index 5fe8cf2..e96a94c 100644 --- a/manatools/aui/backends/qt/imageqt.py +++ b/manatools/aui/backends/qt/imageqt.py @@ -26,7 +26,7 @@ def __init__(self, parent=None, imageFileName=""): self._qicon = None # minimal visible size guard self._min_w = 64 - self._min_h = 32 + self._min_h = 64 # aspect ratio tracking (w/h). default 1.0 self._aspect_ratio = 1.0 self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") From 0a86fd48a374a0be726d4f2d54b19c7ac190fc1e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 19:04:40 +0100 Subject: [PATCH 488/523] Changed the warning dialog --- manatools/ui/common.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 9ef466d..539528c 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -52,11 +52,21 @@ def warningMsgBox (info) : factory.createHeading(vbox, info.get('title')) text = info.get('text', "") rt = bool(info.get('richtext', False)) + hbox = factory.createHBox(vbox) + align = factory.createTop(hbox) + icon = factory.createImage(align, "dialog-warning") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) if rt: - t = factory.createRichText(vbox, "", False) + t = factory.createRichText(hbox, "", False) t.setValue(text) + t.setStretchable(yui.YUIDimension.YD_HORIZ, True) + t.setStretchable(yui.YUIDimension.YD_VERT, True) else: - factory.createLabel(vbox, text) + t = factory.createLabel(hbox, text) + t.setStretchable(yui.YUIDimension.YD_HORIZ, True) + t.setStretchable(yui.YUIDimension.YD_VERT, True) align = factory.createRight(vbox) ok_btn = factory.createPushButton(align, _("&Ok")) while True: From 3a36f457ce0102035b6ad4ecb3ba99730951072a Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 19:57:05 +0100 Subject: [PATCH 489/523] improving basedialog min size management --- manatools/ui/basedialog.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/manatools/ui/basedialog.py b/manatools/ui/basedialog.py index 3980a8a..2aa5da0 100644 --- a/manatools/ui/basedialog.py +++ b/manatools/ui/basedialog.py @@ -154,19 +154,19 @@ def factory(self): def _setupUI(self): self.dialog = self.factory.createPopupDialog() if self._dialogType == DialogType.POPUP else self.factory.createMainDialog() - - parent = self.dialog + + # If a minimum size is requested, wrap the layout inside a MinSize container. + # IMPORTANT: MinSize is a single-child container -> add a VBox inside it and + # pass that VBox to UIlayout so multiple children can be attached there. + content_vbox = None if self._minSize is not None: - parent = self.factory.createMinSize(self.dialog, self._minSize['minWidth'], self._minSize['minHeight']) - - vbox = self.factory.createVBox(parent) - self.UIlayout(vbox) - - #def pollEvent(self): - # ''' - # perform yui pollEvent - # ''' - # return self.dialog.pollEvent() + min_container = self.factory.createMinSize(self.dialog, self._minSize['minWidth'], self._minSize['minHeight']) + content_vbox = self.factory.createVBox(min_container) + else: + content_vbox = self.factory.createVBox(self.dialog) + layout_parent = content_vbox + # Build the dialog layout using the chosen parent (MinSize->VBox or root VBox) + self.UIlayout(layout_parent) def _handleEvents(self): ''' From a0499f4a4b638a08389fc9f78cbc7c1d458cb529 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 20:43:42 +0100 Subject: [PATCH 490/523] Reviewed Info, Warning and Msg Boxes --- manatools/ui/common.py | 163 +++++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 48 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 539528c..40e1340 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -34,119 +34,186 @@ def destroyUI () : def warningMsgBox (info) : ''' - This function creates an Warning dialog and show the message - passed as input. + This function creates a Warning dialog and shows the message passed as input. @param info: dictionary, information to be passed to the dialog. title => dialog title - text => string to be swhon into the dialog + text => string to be shown into the dialog richtext => True if using rich text ''' - if (not info) : + if not info: return 0 factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() vbox = factory.createVBox(dlg) - if info.get('title'): - factory.createHeading(vbox, info.get('title')) + + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) + + # Content row: icon + text text = info.get('text', "") rt = bool(info.get('richtext', False)) - hbox = factory.createHBox(vbox) - align = factory.createTop(hbox) - icon = factory.createImage(align, "dialog-warning") - icon.setStretchable(yui.YUIDimension.YD_VERT, False) - icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) - icon.setAutoScale(False) + row = factory.createHBox(vbox) + + # Icon (warning) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-warning") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + # If icon creation fails, continue without it + pass + + # Text widget if rt: - t = factory.createRichText(hbox, "", False) - t.setValue(text) - t.setStretchable(yui.YUIDimension.YD_HORIZ, True) - t.setStretchable(yui.YUIDimension.YD_VERT, True) + tw = factory.createRichText(row, "", False) + tw.setValue(text) else: - t = factory.createLabel(hbox, text) - t.setStretchable(yui.YUIDimension.YD_HORIZ, True) - t.setStretchable(yui.YUIDimension.YD_VERT, True) - align = factory.createRight(vbox) - ok_btn = factory.createPushButton(align, _("&Ok")) + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + + # Ok button centered + center = factory.createHVCenter(vbox) + ok_btn = factory.createPushButton(center, _("&Ok")) + + # Event loop while True: ev = dlg.waitForEvent() - if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + et = ev.eventType() + if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): break - if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: break + dlg.destroy() return 1 def infoMsgBox (info) : ''' - This function creates an Info dialog and show the message - passed as input. + This function creates an Info dialog and shows the message passed as input. @param info: dictionary, information to be passed to the dialog. title => dialog title - text => string to be swhon into the dialog + text => string to be shown into the dialog richtext => True if using rich text ''' - if (not info) : + if not info: return 0 factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() vbox = factory.createVBox(dlg) - if info.get('title'): - factory.createHeading(vbox, info.get('title')) + + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) + + # Content row: icon + text text = info.get('text', "") rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) + + # Icon (information) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-information") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + pass + + # Text widget if rt: - t = factory.createRichText(vbox, "", False) - t.setValue(text) + tw = factory.createRichText(row, "", False) + tw.setValue(text) else: - factory.createLabel(vbox, text) - align = factory.createRight(vbox) - ok_btn = factory.createPushButton(align, _("&Ok")) + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + + # Ok button centered + center = factory.createHVCenter(vbox) + ok_btn = factory.createPushButton(center, _("&Ok")) + + # Event loop while True: ev = dlg.waitForEvent() - if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + et = ev.eventType() + if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): break - if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: break + dlg.destroy() return 1 + def msgBox (info) : ''' - This function creates a dialog and show the message passed as input. + This function creates a dialog and shows the message passed as input. @param info: dictionary, information to be passed to the dialog. title => dialog title - text => string to be swhon into the dialog + text => string to be shown into the dialog richtext => True if using rich text ''' - if (not info) : + if not info: return 0 factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() vbox = factory.createVBox(dlg) - if info.get('title'): - factory.createHeading(vbox, info.get('title')) + + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) + + # Content row: text only (no icon) text = info.get('text', "") rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) + + # Text widget if rt: - t = factory.createRichText(vbox, "", False) - t.setValue(text) + tw = factory.createRichText(row, "", False) + tw.setValue(text) else: - factory.createLabel(vbox, text) - align = factory.createRight(vbox) - ok_btn = factory.createPushButton(align, _("&Ok")) + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + + # Ok button centered + center = factory.createHVCenter(vbox) + ok_btn = factory.createPushButton(center, _("&Ok")) + + # Event loop while True: ev = dlg.waitForEvent() - if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + et = ev.eventType() + if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): break - if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: break + dlg.destroy() return 1 From 998a9caf4f195f7efe20e6336e6085c6c70201be Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 21:32:31 +0100 Subject: [PATCH 491/523] improving exception management --- manatools/aui/backends/gtk/richtextgtk.py | 52 +++++++++++++++-------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/manatools/aui/backends/gtk/richtextgtk.py b/manatools/aui/backends/gtk/richtextgtk.py index c0d5a4d..ecdd367 100644 --- a/manatools/aui/backends/gtk/richtextgtk.py +++ b/manatools/aui/backends/gtk/richtextgtk.py @@ -136,18 +136,20 @@ def setValue(self, newValue: str): try: w.set_use_markup(True) except Exception: - pass + self._logger.debug("set_use_markup failed on Gtk.Label", exc_info=True) # Convert common HTML tags to Pango markup supported by Gtk.Label converted = self._html_to_pango_markup(self._text) try: w.set_markup(converted) - except Exception: + except Exception as e: + # If markup fails, log and fallback to plain text + self._logger.error("set_markup failed; falling back to set_text: %s", e, exc_info=True) try: - w.set_text(converted) + w.set_text(re.sub(r"<[^>]+>", "", converted)) except Exception: - pass + self._logger.debug("set_text failed on Gtk.Label", exc_info=True) except Exception: - pass + self._logger.exception("setValue failed", exc_info=True) def value(self) -> str: return self._text @@ -188,45 +190,46 @@ def _create_content(self): try: tv.set_editable(False) except Exception: - pass + self._logger.debug("set_editable failed on Gtk.TextView", exc_info=True) try: tv.set_cursor_visible(False) except Exception: - pass + self._logger.debug("set_cursor_visible failed on Gtk.TextView", exc_info=True) try: tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) except Exception: - pass + self._logger.debug("set_wrap_mode failed on Gtk.TextView", exc_info=True) self._content_widget = tv # set initial text try: buf = tv.get_buffer() buf.set_text(self._text) except Exception: - pass + self._logger.debug("set_text failed on Gtk.TextBuffer", exc_info=True) else: lbl = Gtk.Label() try: lbl.set_use_markup(True) except Exception: - pass + self._logger.debug("set_use_markup failed on Gtk.Label", exc_info=True) # Convert HTML to Pango markup for GTK Label converted = self._html_to_pango_markup(self._text) try: lbl.set_markup(converted) except Exception: + self._logger.exception("set_markup failed on Gtk.Label", exc_info=True) lbl.set_text(converted) try: lbl.set_selectable(False) # keep it non-selectable; avoids odd sizing in some themes except Exception: - pass + self._logger.exception("set_selectable failed on Gtk.Label", exc_info=True) try: lbl.set_wrap(True) - lbl.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + lbl.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) # need Pango.WrapMode lbl.set_xalign(0.0) lbl.set_justify(Gtk.Justification.LEFT) except Exception: - pass + self._logger.debug("set_justify failed on Gtk.Label", exc_info=True) # connect link activation def _on_activate_link(label, uri): try: @@ -236,15 +239,16 @@ def _on_activate_link(label, uri): # emit a MenuEvent for link activation (back-compat) dlg._post_event(YMenuEvent(item=None, id=uri)) except Exception: - pass + self._logger.debug("activate-link handler failed", exc_info=True) # return True to stop default handling return True try: lbl.connect("activate-link", _on_activate_link) except Exception: - pass + self._logger.debug("connect activate-link failed on Gtk.Label", exc_info=True) self._content_widget = lbl except Exception: + self._logger.exception("Failed to create content widget", exc_info=True) # fallback to a simple label self._content_widget = Gtk.Label(label=self._text) @@ -301,9 +305,10 @@ def _set_backend_enabled(self, enabled): def _html_to_pango_markup(self, s: str) -> str: """Convert a limited subset of HTML into GTK/Pango markup. - Supports: h1-h6 (as bold spans with size), b/i/u, a href, p/br, ul/li. + Supports: h1-h6 (as bold spans with size), b/i/em/u, a href, p/br, ul/li. Unknown tags are stripped. """ + self._logger.debug("Converted markup: %s", s) if not s: return "" t = s @@ -318,13 +323,24 @@ def _html_to_pango_markup(self, s: str) -> str: t = re.sub(r"", "\n", t, flags=re.IGNORECASE) t = re.sub(r"", "• ", t, flags=re.IGNORECASE) t = re.sub(r"", "\n", t, flags=re.IGNORECASE) - # Headings -> bold span with size + # Headings -> bold span with size sizes = {1: "xx-large", 2: "x-large", 3: "large", 4: "medium", 5: "small", 6: "x-small"} for n, sz in sizes.items(): t = re.sub(fr"", f"", t, flags=re.IGNORECASE) t = re.sub(fr"", "
\n", t, flags=re.IGNORECASE) + # Explicitly convert formatting tags to Pango span attributes + t = re.sub(r"", "", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) + t = re.sub(r"", "", t, flags=re.IGNORECASE) # Allow basic formatting tags and ; strip all other tags - t = re.sub(r"]*>", "", t) + t = re.sub(r"]*>", "", t) + + self._logger.debug("Converted markup: %s", t) return t def setVisible(self, visible=True): From edf3ead4527fda7c8ba3ddbc83f4e54499fa26d9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 1 Feb 2026 23:08:40 +0100 Subject: [PATCH 492/523] Used Pango.WrapMode --- manatools/aui/backends/gtk/richtextgtk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/gtk/richtextgtk.py b/manatools/aui/backends/gtk/richtextgtk.py index ecdd367..58974f4 100644 --- a/manatools/aui/backends/gtk/richtextgtk.py +++ b/manatools/aui/backends/gtk/richtextgtk.py @@ -11,7 +11,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Gdk', '4.0') -from gi.repository import Gtk, GLib, Gdk +from gi.repository import Gtk, Pango, GLib, Gdk import logging import re from ...yui_common import * @@ -225,7 +225,7 @@ def _create_content(self): self._logger.exception("set_selectable failed on Gtk.Label", exc_info=True) try: lbl.set_wrap(True) - lbl.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) # need Pango.WrapMode + lbl.set_wrap_mode(Pango.WrapMode.WORD_CHAR) # need Pango.WrapMode lbl.set_xalign(0.0) lbl.set_justify(Gtk.Justification.LEFT) except Exception: From 83bb43350266736ef75b7ee33aabfe7a18e403e5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 17:18:13 +0100 Subject: [PATCH 493/523] Worked on AskOkCancel and AskYesOrNo --- manatools/ui/common.py | 82 +++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 40e1340..690f72d 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -239,18 +239,45 @@ def askOkCancel (info) : factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() vbox = factory.createVBox(dlg) - if info.get('title'): - factory.createHeading(vbox, info.get('title')) - text = info.get('text', "") + + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) + + # Content row: icon + text + text = info.get('text', "") or "" rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) + + # Icon (information) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-information") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + pass + + # Text widget if rt: - t = factory.createRichText(vbox, "", False) - t.setValue(text) + tw = factory.createRichText(row, "", False) + tw.setValue(text) else: - factory.createLabel(vbox, text) + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + + # Buttons on the right btns = factory.createHBox(vbox) + factory.createHStretch(btns) ok_btn = factory.createPushButton(btns, _("&Ok")) cancel_btn = factory.createPushButton(btns, _("&Cancel")) + default_ok = bool(info.get('default_button', 0) == 1) # simple default: ignore focusing specifics for now result = False @@ -293,15 +320,40 @@ def askYesOrNo (info) : factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() vbox = factory.createVBox(dlg) - if info.get('title'): - factory.createHeading(vbox, info.get('title')) - text = info.get('text', "") - rt = bool(info.get('richtext', False)) + + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) + + # Content row: icon + text + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) + + # Icon (question) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-question") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + pass + + # Text widget if rt: - t = factory.createRichText(vbox, "", False) - t.setValue(text) + tw = factory.createRichText(row, "", False) + tw.setValue(text) else: - factory.createLabel(vbox, text) + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + + # Handle size if provided if 'size' in info.keys(): try: dims = info['size'] @@ -309,9 +361,13 @@ def askYesOrNo (info) : vbox = parent except Exception: pass + + # Buttons on the right btns = factory.createHBox(vbox) + factory.createHStretch(btns) yes_btn = factory.createPushButton(btns, _("&Yes")) no_btn = factory.createPushButton(btns, _("&No")) + result = False while True: ev = dlg.waitForEvent() From fbf1a1c922bb26ad6e2e30eedd62c3e4a9e31b0e Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 17:38:01 +0100 Subject: [PATCH 494/523] worked also on msg boxes --- manatools/ui/common.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 690f72d..b590b3e 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -54,7 +54,7 @@ def warningMsgBox (info) : factory.createHeading(vbox, title) # Content row: icon + text - text = info.get('text', "") + text = info.get('text', "") or "" rt = bool(info.get('richtext', False)) row = factory.createHBox(vbox) @@ -81,9 +81,10 @@ def warningMsgBox (info) : except Exception: pass - # Ok button centered - center = factory.createHVCenter(vbox) - ok_btn = factory.createPushButton(center, _("&Ok")) + # Ok button on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + ok_btn = factory.createPushButton(btns, _("&Ok")) # Event loop while True: @@ -120,8 +121,8 @@ def infoMsgBox (info) : factory.createHeading(vbox, title) # Content row: icon + text - text = info.get('text', "") - rt = bool(info.get('richtext', False)) + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) row = factory.createHBox(vbox) # Icon (information) @@ -146,9 +147,10 @@ def infoMsgBox (info) : except Exception: pass - # Ok button centered - center = factory.createHVCenter(vbox) - ok_btn = factory.createPushButton(center, _("&Ok")) + # Ok button on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + ok_btn = factory.createPushButton(btns, _("&Ok")) # Event loop while True: @@ -185,8 +187,8 @@ def msgBox (info) : factory.createHeading(vbox, title) # Content row: text only (no icon) - text = info.get('text', "") - rt = bool(info.get('richtext', False)) + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) row = factory.createHBox(vbox) # Text widget @@ -201,9 +203,10 @@ def msgBox (info) : except Exception: pass - # Ok button centered - center = factory.createHVCenter(vbox) - ok_btn = factory.createPushButton(center, _("&Ok")) + # Ok button on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + ok_btn = factory.createPushButton(btns, _("&Ok")) # Event loop while True: From 07898f75483dd95379e81e57d28b910024d8db92 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 17:45:06 +0100 Subject: [PATCH 495/523] Centered ok button --- manatools/ui/common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index b590b3e..fbe01ee 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -85,6 +85,7 @@ def warningMsgBox (info) : btns = factory.createHBox(vbox) factory.createHStretch(btns) ok_btn = factory.createPushButton(btns, _("&Ok")) + factory.createHStretch(btns) # Event loop while True: @@ -151,6 +152,7 @@ def infoMsgBox (info) : btns = factory.createHBox(vbox) factory.createHStretch(btns) ok_btn = factory.createPushButton(btns, _("&Ok")) + factory.createHStretch(btns) # Event loop while True: @@ -207,6 +209,7 @@ def msgBox (info) : btns = factory.createHBox(vbox) factory.createHStretch(btns) ok_btn = factory.createPushButton(btns, _("&Ok")) + factory.createHStretch(btns) # Event loop while True: From 8ab5fb610442a12a3840eb390948816630c68013 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 18:54:36 +0100 Subject: [PATCH 496/523] Still working on dialogs --- manatools/ui/common.py | 773 +++++++++++++++++++++++++---------------- 1 file changed, 470 insertions(+), 303 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index fbe01ee..756fbb4 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -12,6 +12,7 @@ ''' from ..aui import yui +from ..aui.yui_common import YUIDimension from enum import Enum import gettext # https://pymotw.com/3/gettext/#module-localization @@ -32,6 +33,28 @@ def destroyUI () : return +def _push_app_title(new_title): + """Save current application title and optionally set a new one.""" + try: + app = yui.YUI.app() + old_title = app.applicationTitle() + if new_title: + app.setApplicationTitle(str(new_title)) + return app, old_title + except Exception: + return None, None + + +def _restore_app_title(app, old_title): + """Restore the previously saved application title.""" + if app is None: + return + try: + if old_title is not None: + app.setApplicationTitle(old_title) + except Exception: + pass + def warningMsgBox (info) : ''' This function creates a Warning dialog and shows the message passed as input. @@ -44,61 +67,72 @@ def warningMsgBox (info) : if not info: return 0 - factory = yui.YUI.widgetFactory() - dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) - - # Content row: icon + text - text = info.get('text', "") or "" - rt = bool(info.get('richtext', False)) - row = factory.createHBox(vbox) - - # Icon (warning) + app_ctx = _push_app_title(title) + app, previous_title = app_ctx if app_ctx else (None, None) + dlg = None try: - icon_align = factory.createTop(row) - icon = factory.createImage(icon_align, "dialog-warning") - icon.setStretchable(yui.YUIDimension.YD_VERT, False) - icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) - icon.setAutoScale(False) - except Exception: - # If icon creation fails, continue without it - pass + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) - # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) - # Ok button on the right - btns = factory.createHBox(vbox) - factory.createHStretch(btns) - ok_btn = factory.createPushButton(btns, _("&Ok")) - factory.createHStretch(btns) + # Content row: icon + text + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) - # Event loop - while True: - ev = dlg.waitForEvent() - et = ev.eventType() - if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): - break - if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: - break + # Icon (warning) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-warning") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + # If icon creation fails, continue without it + pass - dlg.destroy() - return 1 + # Text widget + if rt: + tw = factory.createRichText(row, "", False) + tw.setValue(text) + else: + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + # Ok button on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + ok_btn = factory.createPushButton(btns, _("&Ok")) + factory.createHStretch(btns) + + # Event loop + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + break + + dlg.destroy() + return 1 + finally: + if dlg is not None: + try: + dlg.destroy() + except Exception: + pass + _restore_app_title(app, previous_title) def infoMsgBox (info) : ''' @@ -112,60 +146,71 @@ def infoMsgBox (info) : if not info: return 0 - factory = yui.YUI.widgetFactory() - dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) - - # Content row: icon + text - text = info.get('text', "") or "" - rt = bool(info.get('richtext', False)) - row = factory.createHBox(vbox) - - # Icon (information) + app_ctx = _push_app_title(title) + app, previous_title = app_ctx if app_ctx else (None, None) + dlg = None try: - icon_align = factory.createTop(row) - icon = factory.createImage(icon_align, "dialog-information") - icon.setStretchable(yui.YUIDimension.YD_VERT, False) - icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) - icon.setAutoScale(False) - except Exception: - pass + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) - # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) + + # Content row: icon + text + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) - # Ok button on the right - btns = factory.createHBox(vbox) - factory.createHStretch(btns) - ok_btn = factory.createPushButton(btns, _("&Ok")) - factory.createHStretch(btns) + # Icon (information) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-information") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + pass - # Event loop - while True: - ev = dlg.waitForEvent() - et = ev.eventType() - if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): - break - if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: - break + # Text widget + if rt: + tw = factory.createRichText(row, "", False) + tw.setValue(text) + else: + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass - dlg.destroy() - return 1 + # Ok button on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + ok_btn = factory.createPushButton(btns, _("&Ok")) + factory.createHStretch(btns) + + # Event loop + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + break + dlg.destroy() + return 1 + finally: + if dlg is not None: + try: + dlg.destroy() + except Exception: + pass + _restore_app_title(app, previous_title) def msgBox (info) : ''' @@ -179,50 +224,61 @@ def msgBox (info) : if not info: return 0 - factory = yui.YUI.widgetFactory() - dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) - - # Content row: text only (no icon) - text = info.get('text', "") or "" - rt = bool(info.get('richtext', False)) - row = factory.createHBox(vbox) - - # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) + app_ctx = _push_app_title(title) + app, previous_title = app_ctx if app_ctx else (None, None) + dlg = None try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass - - # Ok button on the right - btns = factory.createHBox(vbox) - factory.createHStretch(btns) - ok_btn = factory.createPushButton(btns, _("&Ok")) - factory.createHStretch(btns) - - # Event loop - while True: - ev = dlg.waitForEvent() - et = ev.eventType() - if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): - break - if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: - break + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) + + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) + + # Content row: text only (no icon) + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) + + # Text widget + if rt: + tw = factory.createRichText(row, "", False) + tw.setValue(text) + else: + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass - dlg.destroy() - return 1 + # Ok button on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + ok_btn = factory.createPushButton(btns, _("&Ok")) + factory.createHStretch(btns) + + # Event loop + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if et == yui.YEventType.WidgetEvent and ev.widget() == ok_btn and ev.reason() == yui.YEventReason.Activated: + break + dlg.destroy() + return 1 + finally: + if dlg is not None: + try: + dlg.destroy() + except Exception: + pass + _restore_app_title(app, previous_title) def askOkCancel (info) : ''' @@ -242,67 +298,79 @@ def askOkCancel (info) : if (not info) : return False - factory = yui.YUI.widgetFactory() - dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) + app_ctx = _push_app_title(title) + app, previous_title = app_ctx if app_ctx else (None, None) + dlg = None + try: + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) - # Content row: icon + text - text = info.get('text', "") or "" - rt = bool(info.get('richtext', False)) - row = factory.createHBox(vbox) + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) - # Icon (information) - try: - icon_align = factory.createTop(row) - icon = factory.createImage(icon_align, "dialog-information") - icon.setStretchable(yui.YUIDimension.YD_VERT, False) - icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) - icon.setAutoScale(False) - except Exception: - pass + # Content row: icon + text + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) - # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + # Icon (information) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-information") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + pass - # Buttons on the right - btns = factory.createHBox(vbox) - factory.createHStretch(btns) - ok_btn = factory.createPushButton(btns, _("&Ok")) - cancel_btn = factory.createPushButton(btns, _("&Cancel")) - - default_ok = bool(info.get('default_button', 0) == 1) - # simple default: ignore focusing specifics for now - result = False - while True: - ev = dlg.waitForEvent() - et = ev.eventType() - if et == yui.YEventType.CancelEvent: - result = False - break - if et == yui.YEventType.WidgetEvent: - w = ev.widget() - if w == ok_btn and ev.reason() == yui.YEventReason.Activated: - result = True - break - if w == cancel_btn and ev.reason() == yui.YEventReason.Activated: + # Text widget + if rt: + tw = factory.createRichText(row, "", False) + tw.setValue(text) + else: + tw = factory.createLabel(row, text) + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + + # Buttons on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + ok_btn = factory.createPushButton(btns, _("&Ok")) + cancel_btn = factory.createPushButton(btns, _("&Cancel")) + + default_ok = bool(info.get('default_button', 0) == 1) + # simple default: ignore focusing specifics for now + result = False + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: result = False break - dlg.destroy() - return result + if et == yui.YEventType.WidgetEvent: + w = ev.widget() + if w == ok_btn and ev.reason() == yui.YEventReason.Activated: + result = True + break + if w == cancel_btn and ev.reason() == yui.YEventReason.Activated: + result = False + break + dlg.destroy() + return result + finally: + if dlg is not None: + try: + dlg.destroy() + except Exception: + pass + _restore_app_title(app, previous_title) def askYesOrNo (info) : ''' @@ -323,74 +391,86 @@ def askYesOrNo (info) : if (not info) : return False - factory = yui.YUI.widgetFactory() - dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) + app_ctx = _push_app_title(title) + app, previous_title = app_ctx if app_ctx else (None, None) + dlg = None + try: + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) - # Content row: icon + text - text = info.get('text', "") or "" - rt = bool(info.get('richtext', False)) - row = factory.createHBox(vbox) + # Heading + title = info.get('title') + if title: + factory.createHeading(vbox, title) - # Icon (question) - try: - icon_align = factory.createTop(row) - icon = factory.createImage(icon_align, "dialog-question") - icon.setStretchable(yui.YUIDimension.YD_VERT, False) - icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) - icon.setAutoScale(False) - except Exception: - pass + # Content row: icon + text + text = info.get('text', "") or "" + rt = bool(info.get('richtext', False)) + row = factory.createHBox(vbox) - # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + # Icon (question) + try: + icon_align = factory.createTop(row) + icon = factory.createImage(icon_align, "dialog-question") + icon.setStretchable(yui.YUIDimension.YD_VERT, False) + icon.setStretchable(yui.YUIDimension.YD_HORIZ, False) + icon.setAutoScale(False) + except Exception: + pass - # Handle size if provided - if 'size' in info.keys(): + # Text widget + if rt: + tw = factory.createRichText(row, "", False) + tw.setValue(text) + else: + tw = factory.createLabel(row, text) try: - dims = info['size'] - parent = factory.createMinSize(vbox, int(dims[0]), int(dims[1])) - vbox = parent + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, True) except Exception: pass - # Buttons on the right - btns = factory.createHBox(vbox) - factory.createHStretch(btns) - yes_btn = factory.createPushButton(btns, _("&Yes")) - no_btn = factory.createPushButton(btns, _("&No")) - - result = False - while True: - ev = dlg.waitForEvent() - et = ev.eventType() - if et == yui.YEventType.CancelEvent: - result = False - break - if et == yui.YEventType.WidgetEvent: - w = ev.widget() - if w == yes_btn and ev.reason() == yui.YEventReason.Activated: - result = True - break - if w == no_btn and ev.reason() == yui.YEventReason.Activated: + # Handle size if provided + if 'size' in info.keys(): + try: + dims = info['size'] + parent = factory.createMinSize(vbox, int(dims[0]), int(dims[1])) + vbox = parent + except Exception: + pass + + # Buttons on the right + btns = factory.createHBox(vbox) + factory.createHStretch(btns) + yes_btn = factory.createPushButton(btns, _("&Yes")) + no_btn = factory.createPushButton(btns, _("&No")) + + result = False + while True: + ev = dlg.waitForEvent() + et = ev.eventType() + if et == yui.YEventType.CancelEvent: result = False break - dlg.destroy() - return result + if et == yui.YEventType.WidgetEvent: + w = ev.widget() + if w == yes_btn and ev.reason() == yui.YEventReason.Activated: + result = True + break + if w == no_btn and ev.reason() == yui.YEventReason.Activated: + result = False + break + dlg.destroy() + return result + finally: + if dlg is not None: + try: + dlg.destroy() + except Exception: + pass + _restore_app_title(app, previous_title) class AboutDialogMode(Enum): ''' @@ -422,62 +502,149 @@ def AboutDialog (info) : if (not info) : raise ValueError("Missing AboutDialog parameters") - # Build a simple About dialog using AUI widgets - factory = yui.YUI.widgetFactory() - dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - name = info.get('name', "") - version = info.get('version', "") - license_txt = info.get('license', "") - authors = info.get('authors', "") - description = info.get('description', "") - logo = info.get('logo', "") - credits = info.get('credits', "") - information = info.get('information', "") - - title = _("About") + (f" {name}" if name else "") - factory.createHeading(vbox, title) - - # Header block - header = factory.createHBox(vbox) - if logo: + title = info.get('title') + app_ctx = _push_app_title(title) + app, previous_title = app_ctx if app_ctx else (None, None) + dlg = None + try: + factory = yui.YUI.widgetFactory() + dlg = factory.createPopupDialog() + root_vbox = factory.createVBox(dlg) + + # Optional MinSize wrapper (accepts {'column','lines'} like the C++ code) + content_parent = root_vbox + size_hint = info.get('size') or {} try: - factory.createImage(header, logo) - factory.createHSpacing(header, 8) + cols = int(size_hint.get('column', size_hint.get('columns'))) + rows = int(size_hint.get('lines', size_hint.get('rows'))) + content_parent = factory.createMinSize(root_vbox, cols, rows) except Exception: - pass - labels = factory.createVBox(header) - if name: - factory.createLabel(labels, name) - if version: - factory.createLabel(labels, version) - if license_txt: - factory.createLabel(labels, license_txt) - - # Content block - if description: - rt = factory.createRichText(vbox, "", False) - rt.setValue(description) - if authors: - factory.createHeading(vbox, _("Authors")) - ra = factory.createRichText(vbox, "", False) - ra.setValue(authors) - if credits: - factory.createHeading(vbox, _("Credits")) - rc = factory.createRichText(vbox, "", False) - rc.setValue(credits) - if information: - factory.createHeading(vbox, _("Information")) - ri = factory.createRichText(vbox, "", False) - ri.setValue(information) - - align = factory.createRight(vbox) - close_btn = factory.createPushButton(align, _("&Close")) - while True: - ev = dlg.waitForEvent() - if ev.eventType() in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): - break - if ev.eventType() == yui.YEventType.WidgetEvent and ev.widget() == close_btn and ev.reason() == yui.YEventReason.Activated: - break - dlg.destroy() + content_parent = root_vbox + + vbox = factory.createVBox(content_parent) + + name = info.get('name', "") + version = info.get('version', "") + license_txt = info.get('license', "") + authors = info.get('authors', "") + description = info.get('description', "") + logo = info.get('logo', "") + credits = info.get('credits', "") + information = info.get('information', "") + dialog_mode = info.get('dialog_mode', AboutDialogMode.CLASSIC) + + title = _("About") + (f" {name}" if name else "") + factory.createHeading(vbox, title) + + # Header block (logo + labels) + header = factory.createHBox(vbox) + if logo: + try: + factory.createImage(header, logo) + factory.createSpacing(header, 8) + except Exception: + pass + labels = factory.createVBox(header) + if name: + factory.createLabel(labels, name) + if version: + factory.createLabel(labels, version) + if license_txt: + factory.createLabel(labels, license_txt) + + # Credits line (matches C++ layout) + if credits: + credits_box = factory.createHBox(vbox) + factory.createSpacing(credits_box, 1) + factory.createLabel(credits_box, credits) + factory.createSpacing(credits_box, 1) + # ...existing code... + + # Helper to add a RichText block + def _add_richtext(parent, value): + rt = factory.createRichText(parent, "", False) + rt.setValue(value) + try: + rt.setStretchable(YUIDimension.YD_HORIZ, True) + rt.setStretchable(YUIDimension.YD_VERT, True) + except Exception: + pass + return rt + + info_btn = None + credits_btn = None + + # Tabbed layout (Authors / Description / Information) if requested and available + use_tabbed = (dialog_mode == AboutDialogMode.TABBED) + tab_widget = None + if use_tabbed: + try: + tab_widget = factory.createDumbTab(vbox) + except Exception: + tab_widget = None + if tab_widget: + sections_added = False + if authors: + tab_authors = tab_widget.addItem(_("Authors")) + _add_richtext(tab_authors, authors) + sections_added = True + if description: + tab_desc = tab_widget.addItem(_("Description")) + _add_richtext(tab_desc, description) + sections_added = True + if information: + tab_info = tab_widget.addItem(_("Information")) + _add_richtext(tab_info, information) + sections_added = True + if not sections_added: + tab_widget = None + if tab_widget is None: + use_tabbed = False # fallback to classic if tabs unavailable + + if not use_tabbed: + # Classic stacked content + buttons (mirrors C++ behavior) + if description: + factory.createHeading(vbox, _("Description")) + _add_richtext(vbox, description) + if authors: + factory.createHeading(vbox, _("Authors")) + _add_richtext(vbox, authors) + if information: + factory.createHeading(vbox, _("Information")) + _add_richtext(vbox, information) + button_row = factory.createHBox(vbox) + if information: + info_btn = factory.createPushButton(button_row, _("&Info")) + if credits: + credits_btn = factory.createPushButton(button_row, _("&Credits")) + + # Close button aligned to the right, as in the C++ dialog + close_row = factory.createHBox(vbox) + factory.createHStretch(close_row) + close_btn = factory.createPushButton(close_row, _("&Close")) + + while True: + ev = dlg.waitForEvent() + if not ev: + continue + et = ev.eventType() + if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + break + if et != yui.YEventType.WidgetEvent: + continue + widget = ev.widget() + if widget == close_btn: + break + if info_btn and widget == info_btn: + infoMsgBox({"title": _("Information"), "text": information or "", "richtext": True}) + elif credits_btn and widget == credits_btn: + infoMsgBox({"title": _("Credits"), "text": credits or "", "richtext": True}) + + dlg.destroy() + finally: + if dlg is not None: + try: + dlg.destroy() + except Exception: + pass + _restore_app_title(app, previous_title) From 7729f3dff4961aac21d3a56c38be9fb98cf33460 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 19:34:17 +0100 Subject: [PATCH 497/523] Managed title at top most dialog level as for gtk --- manatools/aui/yui_qt.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/manatools/aui/yui_qt.py b/manatools/aui/yui_qt.py index 5e926c1..0c5c316 100644 --- a/manatools/aui/yui_qt.py +++ b/manatools/aui/yui_qt.py @@ -75,20 +75,21 @@ def productName(self): return self._product_name def setApplicationTitle(self, title): - """Set the application title.""" + """Set the application title and try to update dialogs/windows.""" self._application_title = title - # also keep Qt's application name in sync so dialogs can read it without importing YUI try: - app = QtWidgets.QApplication.instance() - if app: - app.setApplicationName(title) - top_level_widgets = app.topLevelWidgets() - - for widget in top_level_widgets: - if isinstance(widget, QtWidgets.QMainWindow): - main_window = widget - main_window.setWindowTitle(title) - break + # update the top most YDialogQt window if available + try: + dlg = YDialogQt.currentDialog(doThrow=False) + if dlg: + win = getattr(dlg, "_qwidget", None) + if win: + try: + win.setWindowTitle(title) + except Exception: + pass + except Exception: + pass except Exception: pass From 47de38a77691122ad8562f501f9f5eac3fe670ea Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 19:34:59 +0100 Subject: [PATCH 498/523] Title must be set after dlg is created so that only top most dialog changes this property --- manatools/ui/common.py | 88 ++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 60 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 756fbb4..78805a0 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -40,18 +40,17 @@ def _push_app_title(new_title): old_title = app.applicationTitle() if new_title: app.setApplicationTitle(str(new_title)) - return app, old_title + return old_title except Exception: - return None, None + return None -def _restore_app_title(app, old_title): - """Restore the previously saved application title.""" - if app is None: - return +def _restore_app_title(old_title): + """Restore the previously saved application title.""" + app = yui.YUI.app() try: if old_title is not None: - app.setApplicationTitle(old_title) + app.setApplicationTitle(str(old_title)) except Exception: pass @@ -67,19 +66,14 @@ def warningMsgBox (info) : if not info: return 0 - title = info.get('title') - app_ctx = _push_app_title(title) - app, previous_title = app_ctx if app_ctx else (None, None) dlg = None try: factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) + old_title = _push_app_title(title) + + vbox = factory.createVBox(dlg) # Content row: icon + text text = info.get('text', "") or "" @@ -132,7 +126,7 @@ def warningMsgBox (info) : dlg.destroy() except Exception: pass - _restore_app_title(app, previous_title) + _restore_app_title(old_title) def infoMsgBox (info) : ''' @@ -146,19 +140,14 @@ def infoMsgBox (info) : if not info: return 0 - title = info.get('title') - app_ctx = _push_app_title(title) - app, previous_title = app_ctx if app_ctx else (None, None) dlg = None try: factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) + old_title = _push_app_title(title) + + vbox = factory.createVBox(dlg) # Content row: icon + text text = info.get('text', "") or "" @@ -210,7 +199,7 @@ def infoMsgBox (info) : dlg.destroy() except Exception: pass - _restore_app_title(app, previous_title) + _restore_app_title(old_title) def msgBox (info) : ''' @@ -224,19 +213,13 @@ def msgBox (info) : if not info: return 0 - title = info.get('title') - app_ctx = _push_app_title(title) - app, previous_title = app_ctx if app_ctx else (None, None) dlg = None try: factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) + old_title = _push_app_title(title) + vbox = factory.createVBox(dlg) # Content row: text only (no icon) text = info.get('text', "") or "" @@ -278,7 +261,7 @@ def msgBox (info) : dlg.destroy() except Exception: pass - _restore_app_title(app, previous_title) + _restore_app_title(old_title) def askOkCancel (info) : ''' @@ -298,19 +281,14 @@ def askOkCancel (info) : if (not info) : return False - title = info.get('title') - app_ctx = _push_app_title(title) - app, previous_title = app_ctx if app_ctx else (None, None) dlg = None try: factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) + old_title = _push_app_title(title) + + vbox = factory.createVBox(dlg) # Content row: icon + text text = info.get('text', "") or "" @@ -370,7 +348,7 @@ def askOkCancel (info) : dlg.destroy() except Exception: pass - _restore_app_title(app, previous_title) + _restore_app_title(old_title) def askYesOrNo (info) : ''' @@ -391,19 +369,13 @@ def askYesOrNo (info) : if (not info) : return False - title = info.get('title') - app_ctx = _push_app_title(title) - app, previous_title = app_ctx if app_ctx else (None, None) dlg = None try: factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() - vbox = factory.createVBox(dlg) - - # Heading title = info.get('title') - if title: - factory.createHeading(vbox, title) + old_title = _push_app_title(title) + vbox = factory.createVBox(dlg) # Content row: icon + text text = info.get('text', "") or "" @@ -470,7 +442,7 @@ def askYesOrNo (info) : dlg.destroy() except Exception: pass - _restore_app_title(app, previous_title) + _restore_app_title(old_title) class AboutDialogMode(Enum): ''' @@ -501,14 +473,13 @@ def AboutDialog (info) : ''' if (not info) : raise ValueError("Missing AboutDialog parameters") - - title = info.get('title') - app_ctx = _push_app_title(title) - app, previous_title = app_ctx if app_ctx else (None, None) + dlg = None try: factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() + title = _("About") + " " + info.get('name', "") + old_title = _push_app_title(title) root_vbox = factory.createVBox(dlg) # Optional MinSize wrapper (accepts {'column','lines'} like the C++ code) @@ -533,9 +504,6 @@ def AboutDialog (info) : information = info.get('information', "") dialog_mode = info.get('dialog_mode', AboutDialogMode.CLASSIC) - title = _("About") + (f" {name}" if name else "") - factory.createHeading(vbox, title) - # Header block (logo + labels) header = factory.createHBox(vbox) if logo: @@ -647,4 +615,4 @@ def _add_richtext(parent, value): dlg.destroy() except Exception: pass - _restore_app_title(app, previous_title) + _restore_app_title(old_title) From 65525a19524918c4480737c73ee6f938777634b6 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 20:33:29 +0100 Subject: [PATCH 499/523] Restored Tabbed About dialog --- manatools/ui/common.py | 161 +++++++++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 55 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 78805a0..c28bc86 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -12,9 +12,10 @@ ''' from ..aui import yui -from ..aui.yui_common import YUIDimension +from ..aui.yui_common import YUIDimension, YItem from enum import Enum import gettext +import logging # https://pymotw.com/3/gettext/#module-localization t = gettext.translation( 'python-manatools', @@ -24,6 +25,8 @@ _ = t.gettext ngettext = t.ngettext +logger = logging.getLogger("manatools.ui.common") + def destroyUI () : ''' Best-effort teardown for AUI dialogs. AUI manages backend lifecycle internally, @@ -360,7 +363,7 @@ def askYesOrNo (info) : text => string to be swhon into the dialog richtext => True if using rich text default_button => optional default button [1 => Yes - any other values => No] - size => [row, coulmn] + size => [width, height] @output: False: No button has been pressed @@ -468,7 +471,7 @@ def AboutDialog (info) : icon => the string providing the file path for the application icon (low-res image) credits => the application credits, they can be html-formatted information => other extra informations, they can be html-formatted - size => libyui dialog minimum size, dictionary containing {column, lines} + size => libyui dialog minimum size, dictionary containing {width, height} dialog_mode => AboutDialogMode.CLASSIC: classic style dialog, any other as tabbed style dialog ''' if (not info) : @@ -482,12 +485,12 @@ def AboutDialog (info) : old_title = _push_app_title(title) root_vbox = factory.createVBox(dlg) - # Optional MinSize wrapper (accepts {'column','lines'} like the C++ code) + # Optional MinSize wrapper (accepts {'width','height'} in pixels) content_parent = root_vbox size_hint = info.get('size') or {} try: - cols = int(size_hint.get('column', size_hint.get('columns'))) - rows = int(size_hint.get('lines', size_hint.get('rows'))) + cols = int(size_hint.get('width', 320)) + rows = int(size_hint.get('height', 240)) content_parent = factory.createMinSize(root_vbox, cols, rows) except Exception: content_parent = root_vbox @@ -504,14 +507,20 @@ def AboutDialog (info) : information = info.get('information', "") dialog_mode = info.get('dialog_mode', AboutDialogMode.CLASSIC) + logger.debug( + "Opening AboutDialog name='%s' mode=%s", + name or "", + getattr(dialog_mode, "name", dialog_mode), + ) + # Header block (logo + labels) header = factory.createHBox(vbox) if logo: try: factory.createImage(header, logo) factory.createSpacing(header, 8) - except Exception: - pass + except Exception as exc: + logger.debug("Unable to load logo '%s': %s", logo, exc) labels = factory.createVBox(header) if name: factory.createLabel(labels, name) @@ -520,14 +529,6 @@ def AboutDialog (info) : if license_txt: factory.createLabel(labels, license_txt) - # Credits line (matches C++ layout) - if credits: - credits_box = factory.createHBox(vbox) - factory.createSpacing(credits_box, 1) - factory.createLabel(credits_box, credits) - factory.createSpacing(credits_box, 1) - # ...existing code... - # Helper to add a RichText block def _add_richtext(parent, value): rt = factory.createRichText(parent, "", False) @@ -541,50 +542,88 @@ def _add_richtext(parent, value): info_btn = None credits_btn = None + tab_widget = None + tab_content_updater = None + + tab_sections = [] + if description: + tab_sections.append((_('Description'), description)) + if authors: + tab_sections.append((_('Authors'), authors)) + if information: + tab_sections.append((_('Information'), information)) + if credits: + tab_sections.append((_('Credits'), credits)) - # Tabbed layout (Authors / Description / Information) if requested and available use_tabbed = (dialog_mode == AboutDialogMode.TABBED) - tab_widget = None + tab_text_widget = None + if use_tabbed: - try: - tab_widget = factory.createDumbTab(vbox) - except Exception: - tab_widget = None - if tab_widget: - sections_added = False - if authors: - tab_authors = tab_widget.addItem(_("Authors")) - _add_richtext(tab_authors, authors) - sections_added = True - if description: - tab_desc = tab_widget.addItem(_("Description")) - _add_richtext(tab_desc, description) - sections_added = True - if information: - tab_info = tab_widget.addItem(_("Information")) - _add_richtext(tab_info, information) - sections_added = True - if not sections_added: + if not tab_sections: + logger.debug("Tabbed mode requested but there are no sections; falling back to classic mode.") + use_tabbed = False + else: + try: + tabs_box = factory.createVBox(vbox) + tab_widget = factory.createDumbTab(tabs_box) + tab_widget.setNotify(True) + content_holder = factory.createReplacePoint(tabs_box) + tab_text_widget = _add_richtext(content_holder, "") + try: + content_holder.showChild() + except Exception as exc: + logger.debug("Unable to show tab content immediately: %s", exc) + added_items = [] + for section_title, section_value in tab_sections: + item = YItem(section_title) + item.setData(section_value) + tab_widget.addItem(item) + added_items.append(item) + if not added_items: + logger.debug("No tab items added; reverting to classic mode.") + use_tabbed = False + tab_widget = None + else: + try: + tab_widget.selectItem(added_items[0], True) + except Exception as exc: + logger.debug("Unable to preselect first tab: %s", exc) + + def _update_tab_content(): + current_item = tab_widget.selectedItem() + payload = "" + if current_item is not None: + payload = current_item.data() or "" + if tab_text_widget is not None: + tab_text_widget.setValue(payload) + + _update_tab_content() + tab_content_updater = _update_tab_content + except Exception as exc: + logger.exception("Failed to initialize tabbed AboutDialog: %s", exc) + use_tabbed = False tab_widget = None - if tab_widget is None: - use_tabbed = False # fallback to classic if tabs unavailable + tab_content_updater = None if not use_tabbed: - # Classic stacked content + buttons (mirrors C++ behavior) - if description: - factory.createHeading(vbox, _("Description")) - _add_richtext(vbox, description) - if authors: - factory.createHeading(vbox, _("Authors")) - _add_richtext(vbox, authors) - if information: - factory.createHeading(vbox, _("Information")) - _add_richtext(vbox, information) - button_row = factory.createHBox(vbox) - if information: - info_btn = factory.createPushButton(button_row, _("&Info")) - if credits: - credits_btn = factory.createPushButton(button_row, _("&Credits")) + inline_sections = [ + (_("Description"), description), + (_("Authors"), authors), + ] + for heading, value in inline_sections: + if not value: + continue + factory.createHeading(vbox, heading) + _add_richtext(vbox, value) + + if information or credits: + button_row = factory.createHBox(vbox) + if information: + info_btn = factory.createPushButton(button_row, _("&Info")) + if credits: + credits_btn = factory.createPushButton(button_row, _("&Credits")) + else: + button_row = None # Close button aligned to the right, as in the C++ dialog close_row = factory.createHBox(vbox) @@ -597,16 +636,28 @@ def _add_richtext(parent, value): continue et = ev.eventType() if et in (yui.YEventType.CancelEvent, yui.YEventType.TimeoutEvent): + logger.debug("AboutDialog closing due to event type %s", et) break if et != yui.YEventType.WidgetEvent: continue widget = ev.widget() if widget == close_btn: + logger.debug("AboutDialog close button activated") break + if tab_widget and widget == tab_widget: + if tab_content_updater: + tab_content_updater() + continue if info_btn and widget == info_btn: + logger.debug("AboutDialog information button activated") infoMsgBox({"title": _("Information"), "text": information or "", "richtext": True}) - elif credits_btn and widget == credits_btn: + continue + if credits_btn and widget == credits_btn: + logger.debug("AboutDialog credits button activated") infoMsgBox({"title": _("Credits"), "text": credits or "", "richtext": True}) + continue + + logger.debug("Unhandled widget event from %s", getattr(widget, 'widgetClass', lambda: 'unknown')()) dlg.destroy() finally: From 3c6d61e8900618fdddd5903d5006fe4e8bc32f79 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sat, 7 Feb 2026 20:33:58 +0100 Subject: [PATCH 500/523] improved testing/example case --- test/testCommon.py | 122 +++++++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 33 deletions(-) diff --git a/test/testCommon.py b/test/testCommon.py index 568b309..4d888cf 100644 --- a/test/testCommon.py +++ b/test/testCommon.py @@ -11,12 +11,21 @@ @package manatools ''' +import os +import sys +import time +import gettext + +# Prefer using the local workspace package when running this test directly +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + import manatools.ui.basedialog as basedialog import manatools.ui.common as common import manatools.version as manatools -import yui -import time -import gettext +from manatools.aui import yui +# allow running from repo root +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging ###################################################################### ## @@ -27,34 +36,60 @@ class TestDialog(basedialog.BaseDialog): def __init__(self): - basedialog.BaseDialog.__init__(self, "Test dialog", "", basedialog.DialogType.POPUP, 80, 10) + basedialog.BaseDialog.__init__(self, "Test dialog", "", basedialog.DialogType.POPUP, 320, 200) + # Configure file logger for this test: write DEBUG logs to '.log' in cwd + try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + self._logger = logging.getLogger() + self._logger.setLevel(logging.DEBUG) + existing = False + for h in list(self._logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + self._logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") + except Exception as _e: + print(f"Failed to configure file logger: {_e}") + def UIlayout(self, layout): ''' layout implementation called in base class to setup UI ''' - # Let's test a Menu widget - hbox = self.factory.createHBox(layout) - menu = self.factory.createMenuButton(self.factory.createLeft(hbox), "&File") - qm = yui.YMenuItem("&Quit") - menu.addItem(qm) - menu.rebuildMenuTree() + # Menu bar at the very top (attach directly to the main vertical layout) + menubar = self.factory.createMenuBar(layout) + file_menu = menubar.addMenu("&File") + qm = menubar.addItem(file_menu, "&Quit") self.eventManager.addMenuEvent(qm, self.onQuitEvent) - menu = self.factory.createMenuButton(self.factory.createRight(hbox), "&Help") - about = yui.YMenuItem("&About") - menu.addItem(about) - menu.rebuildMenuTree() + help_menu = menubar.addMenu("&Help") + about = menubar.addItem(help_menu, "&About") self.eventManager.addMenuEvent(about, self.onAbout) - #let's test some buttons - hbox = self.factory.createHBox(layout) - self.pressButton = self.factory.createPushButton(hbox, "&Warning") - self.eventManager.addWidgetEvent(self.pressButton, self.onPressWarning) + # Let's test some buttons (inside the content area) + self.factory.createVStretch(layout) + hbox = self.factory.createHBox(layout) + self.warnButton = self.factory.createPushButton(hbox, "&Warning") + self.eventManager.addWidgetEvent(self.warnButton, self.onPressWarning) + self.infoButton = self.factory.createPushButton(hbox, "&Information") + self.eventManager.addWidgetEvent(self.infoButton, self.onPressInformation) + self.OkCancelButton = self.factory.createPushButton(hbox, "&Ok/Cancel Dialog") + self.eventManager.addWidgetEvent(self.OkCancelButton, self.onPressOkCancel) + self.factory.createVStretch(layout) + hbox = self.factory.createHBox(layout) + align = self.factory.createRight(hbox) # Let's test a quitbutton (same handle as Quit menu) - self.quitButton = self.factory.createPushButton(layout, "&Quit") + self.quitButton = self.factory.createPushButton(align, "&Quit") self.eventManager.addWidgetEvent(self.quitButton, self.onQuitEvent) # Let's test a cancel event @@ -66,34 +101,51 @@ def onAbout(self): ''' yes = common.askYesOrNo({"title": "Choose About dialog mode", "text": "Do you want a tabbed About dialog?
Yes means Tabbed, No Classic", "richtext" : True, 'default_button': 1 }) if yes: - common.AboutDialog({ 'name' : "Test Tabbed About Dialog", + common.AboutDialog({ 'name' : "Test Dialog", 'dialog_mode' : common.AboutDialogMode.TABBED, 'version' : manatools.__project_version__, - 'credits' :"Copyright (C) 2014-2017 Angelo Naselli", + 'credits' :"Copyright (C) 2014-2026 Angelo Naselli", 'license' : 'GPLv2', 'authors' : 'Angelo Naselli <anaselli@linux.it>', + 'logo' : 'manatools', 'information' : "Tabbed About dialog additional information", - 'description' : "Project description here", - 'size': {'column': 50, 'lines': 6}, + 'description' : "Manatools Test Dialog example", + 'size': {'width': 320, 'height': 240}, }) else : - common.AboutDialog({ 'name' : "Test Classic About Dialog", + common.AboutDialog({ 'name' : "Test Dialog", 'dialog_mode' : common.AboutDialogMode.CLASSIC, 'version' : manatools.__project_version__, - 'credits' :"Copyright (C) 2014-2017 Angelo Naselli", + 'credits' :"Copyright (C) 2014-2026 Angelo Naselli", 'license' : 'GPLv2', 'authors' : 'Angelo Naselli <anaselli@linux.it>', + 'logo' : 'manatools', 'information' : "Classic About dialog additional information", - 'description' : "Project description here", - 'size': {'column': 50, 'lines': 5}, + 'description' : "Manatools Test Dialog example", + 'size': {'width': 320, 'height': 240}, }) def onPressWarning(self) : ''' Warning button call back ''' - print ('Button "Press" pressed') - wd = common.warningMsgBox({"title" : "Warning Dialog", "text": "Warning button has been pressed!", "richtext" : True}) + print ('Button "Warning" pressed') + wd = common.warningMsgBox({"title" : "Warning Dialog", "text": "Warning button has been pressed!", "richtext" : True}) + + def onPressInformation(self) : + ''' + Information button call back + ''' + print ('Button "Information" pressed') + id = common.infoMsgBox({"title" : "Information Dialog", "text": "Information button has been pressed!", "richtext" : True}) + + def onPressOkCancel(self) : + ''' + Ok/Cancel button call back + ''' + print ('Button "Ok/Cancel Dialog" pressed') + ok = common.askOkCancel({"title": "Ok/Cancel Dialog", "text": "To proceed, click OK or Cancel to skip.", "richtext" : True }) + print ("User selected: %s" % ("OK" if ok else "Cancel")) def onCancelEvent(self) : print ("Got a cancel event") @@ -109,12 +161,16 @@ def onQuitEvent(self) : self.ExitLoop() if __name__ == '__main__': - + # Allow selecting backend via argv: e.g. `python3 test/testCommon.py gtk` + if len(sys.argv) > 1: + backend = sys.argv[1].lower() + os.environ['YUI_BACKEND'] = backend + gettext.install('manatools', localedir='/usr/share/locale', names=('ngettext',)) - + td = TestDialog() td.run() common.destroyUI() - - + + From e6d3886178effe667f0e6d97ec7af4afb181eca4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 8 Feb 2026 12:54:45 +0100 Subject: [PATCH 501/523] deprecated info on AboutDialog using app() api to get app information --- manatools/ui/common.py | 158 ++++++++++++++++++++++++++++++----------- test/testCommon.py | 67 ++++++++++------- 2 files changed, 161 insertions(+), 64 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index c28bc86..6ca6cc3 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -16,6 +16,7 @@ from enum import Enum import gettext import logging +import warnings # https://pymotw.com/3/gettext/#module-localization t = gettext.translation( 'python-manatools', @@ -363,7 +364,7 @@ def askYesOrNo (info) : text => string to be swhon into the dialog richtext => True if using rich text default_button => optional default button [1 => Yes - any other values => No] - size => [width, height] + size => [width, height] in pixels, optional dialog minimum size hint @output: False: No button has been pressed @@ -411,7 +412,7 @@ def askYesOrNo (info) : if 'size' in info.keys(): try: dims = info['size'] - parent = factory.createMinSize(vbox, int(dims[0]), int(dims[1])) + parent = factory.createMinSize(vbox, int(dims.get('width', 320)), int(dims.get('height', 240))) vbox = parent except Exception: pass @@ -456,61 +457,138 @@ class AboutDialogMode(Enum): CLASSIC = 1 TABBED = 2 -def AboutDialog (info) : +def AboutDialog(info=None, *, dialog_mode: AboutDialogMode = AboutDialogMode.CLASSIC, size=None): ''' About dialog implementation. AboutDialog can be used by - modules, to show authors, license, credits, etc. - - @param info: dictionary, optional information to be passed to the dialog. - name => the application name - version => the application version - license => the application license, the short length one (e.g. GPLv2, GPLv3, LGPLv2+, etc) - authors => the string providing the list of authors; it could be html-formatted - description => the string providing a brief description of the application - logo => the string providing the file path for the application logo (high-res image) - icon => the string providing the file path for the application icon (low-res image) - credits => the application credits, they can be html-formatted - information => other extra informations, they can be html-formatted - size => libyui dialog minimum size, dictionary containing {width, height} - dialog_mode => AboutDialogMode.CLASSIC: classic style dialog, any other as tabbed style dialog + modules to show authors, license, credits, etc. + + Parameters + ---------- + info: dict | None + Deprecated dictionary that historically provided dialog metadata. + Present values still override backend metadata, but callers should + prefer configuring fields via the application backend. When provided + a DeprecationWarning is emitted. + dialog_mode: AboutDialogMode + Target layout style (classic/tabbed). Defaults to CLASSIC. + size: Mapping | Sequence | None + Optional minimum-size hint. Accepted formats: + - mapping containing 'width'/'height' (or legacy 'column'/'lines') + - tuple/list where the first element is width and the second height ''' - if (not info) : - raise ValueError("Missing AboutDialog parameters") - + + safe_info = {} + if info is not None: + if not isinstance(info, dict): + raise TypeError("info must be a dict when provided") + safe_info = dict(info) + warnings.warn( + "AboutDialog 'info' parameter is deprecated; configure metadata via the application backend.", + DeprecationWarning, + stacklevel=2, + ) + logger.warning("AboutDialog 'info' parameter is deprecated; prefer backend metadata.") + + app = None + try: + app = yui.YUI.app() + except Exception as exc: + logger.debug("Unable to obtain YUI application instance: %s", exc) + + def _fetch_value(info_key, app_attr, fallback=""): + if safe_info.get(info_key): + return safe_info.get(info_key) + if app is None: + return fallback + value = None + attr = getattr(app, app_attr, None) + try: + value = attr() if callable(attr) else attr + except Exception as exc: + logger.debug("Unable to fetch %s via %s: %s", info_key, app_attr, exc) + value = None + if (not value) and info_key == 'name': + try: + value = app.productName() + except Exception: + pass + return value or fallback + + name = _fetch_value('name', 'applicationName') + version = _fetch_value('version', 'version') + license_txt = _fetch_value('license', 'license') + authors = _fetch_value('authors', 'authors') + description = _fetch_value('description', 'description') + logo = _fetch_value('logo', 'logo') + credits = _fetch_value('credits', 'credits') + information = _fetch_value('information', 'information') + + legacy_mode = safe_info.get('dialog_mode') if safe_info else None + effective_mode = dialog_mode or AboutDialogMode.CLASSIC + if legacy_mode is not None: + if isinstance(legacy_mode, AboutDialogMode): + effective_mode = legacy_mode + else: + try: + effective_mode = AboutDialogMode[str(legacy_mode)] + except Exception: + logger.debug("Unsupported legacy dialog_mode value: %s", legacy_mode) + + def _normalize_size(size_spec): + width = 320 + height = 240 + if size_spec is None and safe_info.get('size'): + size_spec = safe_info.get('size') + if isinstance(size_spec, dict): + width = size_spec.get('width', width) + if 'width' not in size_spec: + if 'column' in size_spec: + width = size_spec['column'] + elif 'columns' in size_spec: + width = size_spec['columns'] + height = size_spec.get('height', height) + if 'height' not in size_spec: + if 'lines' in size_spec: + height = size_spec['lines'] + elif 'rows' in size_spec: + height = size_spec['rows'] + elif isinstance(size_spec, (list, tuple)): + if len(size_spec) > 0 and size_spec[0] is not None: + width = size_spec[0] + if len(size_spec) > 1 and size_spec[1] is not None: + height = size_spec[1] + elif size_spec is not None: + logger.debug("Unsupported size specification type: %s", type(size_spec)) + try: + width = int(width) + height = int(height) + except Exception: + width, height = 320, 240 + return width, height + dlg = None try: factory = yui.YUI.widgetFactory() dlg = factory.createPopupDialog() - title = _("About") + " " + info.get('name', "") + title = _("About") + (" " + name if name else "") old_title = _push_app_title(title) root_vbox = factory.createVBox(dlg) # Optional MinSize wrapper (accepts {'width','height'} in pixels) content_parent = root_vbox - size_hint = info.get('size') or {} - try: - cols = int(size_hint.get('width', 320)) - rows = int(size_hint.get('height', 240)) - content_parent = factory.createMinSize(root_vbox, cols, rows) - except Exception: - content_parent = root_vbox + width_hint, height_hint = _normalize_size(size if size is not None else None) + if width_hint > 0 and height_hint > 0: + try: + content_parent = factory.createMinSize(root_vbox, int(width_hint), int(height_hint)) + except Exception as exc: + logger.debug("Unable to apply size hint (%s, %s): %s", width_hint, height_hint, exc) vbox = factory.createVBox(content_parent) - name = info.get('name', "") - version = info.get('version', "") - license_txt = info.get('license', "") - authors = info.get('authors', "") - description = info.get('description', "") - logo = info.get('logo', "") - credits = info.get('credits', "") - information = info.get('information', "") - dialog_mode = info.get('dialog_mode', AboutDialogMode.CLASSIC) - logger.debug( "Opening AboutDialog name='%s' mode=%s", name or "", - getattr(dialog_mode, "name", dialog_mode), + getattr(effective_mode, "name", effective_mode), ) # Header block (logo + labels) @@ -555,7 +633,7 @@ def _add_richtext(parent, value): if credits: tab_sections.append((_('Credits'), credits)) - use_tabbed = (dialog_mode == AboutDialogMode.TABBED) + use_tabbed = (effective_mode == AboutDialogMode.TABBED) tab_text_widget = None if use_tabbed: diff --git a/test/testCommon.py b/test/testCommon.py index 4d888cf..b895b98 100644 --- a/test/testCommon.py +++ b/test/testCommon.py @@ -59,6 +59,45 @@ def __init__(self): except Exception as _e: print(f"Failed to configure file logger: {_e}") + self._tabbed_information = "Tabbed About dialog additional information" + self._about_dialog_size = (320, 240) + self._about_metadata = { + 'setApplicationName': "Test Dialog", + 'setVersion': manatools.__project_version__, + 'setAuthors': 'Angelo Naselli <anaselli@linux.it>
Author 2
Author 3
Author 4
Author 5', + 'setDescription': "Manatools Test Dialog example", + 'setLicense': 'GPLv2', + 'setCredits': "Copyright (C) 2014-2026 Angelo Naselli", + 'setLogo': 'manatools', + 'setInformation': "Classic About dialog additional information", + } + self._apply_about_metadata() + + def _apply_about_metadata(self, **overrides): + ''' + Push application metadata to the active YUI backend so AboutDialog + retrieves consistent information regardless of the selected backend. + ''' + payload = dict(self._about_metadata) + for setter_name, value in overrides.items(): + if value is not None: + payload[setter_name] = value + + try: + app = yui.YUI.app() + except Exception as exc: + logging.getLogger(__name__).debug("Unable to reach YUI app: %s", exc) + return + + for setter_name, value in payload.items(): + setter = getattr(app, setter_name, None) + if not callable(setter): + continue + try: + setter(value) + except Exception as exc: + logging.getLogger(__name__).debug("Failed to apply %s: %s", setter_name, exc) + def UIlayout(self, layout): ''' @@ -100,30 +139,10 @@ def onAbout(self): About menu call back ''' yes = common.askYesOrNo({"title": "Choose About dialog mode", "text": "Do you want a tabbed About dialog?
Yes means Tabbed, No Classic", "richtext" : True, 'default_button': 1 }) - if yes: - common.AboutDialog({ 'name' : "Test Dialog", - 'dialog_mode' : common.AboutDialogMode.TABBED, - 'version' : manatools.__project_version__, - 'credits' :"Copyright (C) 2014-2026 Angelo Naselli", - 'license' : 'GPLv2', - 'authors' : 'Angelo Naselli <anaselli@linux.it>', - 'logo' : 'manatools', - 'information' : "Tabbed About dialog additional information", - 'description' : "Manatools Test Dialog example", - 'size': {'width': 320, 'height': 240}, - }) - else : - common.AboutDialog({ 'name' : "Test Dialog", - 'dialog_mode' : common.AboutDialogMode.CLASSIC, - 'version' : manatools.__project_version__, - 'credits' :"Copyright (C) 2014-2026 Angelo Naselli", - 'license' : 'GPLv2', - 'authors' : 'Angelo Naselli <anaselli@linux.it>', - 'logo' : 'manatools', - 'information' : "Classic About dialog additional information", - 'description' : "Manatools Test Dialog example", - 'size': {'width': 320, 'height': 240}, - }) + selected_mode = common.AboutDialogMode.TABBED if yes else common.AboutDialogMode.CLASSIC + info_text = self._tabbed_information if yes else self._about_metadata.get('setInformation', "") + self._apply_about_metadata(setInformation=info_text) + common.AboutDialog(dialog_mode=selected_mode, size=self._about_dialog_size) def onPressWarning(self) : ''' From 51a7306cf546d360cc81fdce3d557013cc4045c1 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 8 Feb 2026 17:31:55 +0100 Subject: [PATCH 502/523] Removed description caption into classic about dialog and fixed credits/information --- manatools/ui/common.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 6ca6cc3..34937a6 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -684,15 +684,11 @@ def _update_tab_content(): tab_content_updater = None if not use_tabbed: - inline_sections = [ - (_("Description"), description), - (_("Authors"), authors), - ] - for heading, value in inline_sections: - if not value: - continue - factory.createHeading(vbox, heading) - _add_richtext(vbox, value) + if description: + _add_richtext(vbox, description) + if authors: + factory.createHeading(vbox, _("Authors")) + _add_richtext(vbox, authors) if information or credits: button_row = factory.createHBox(vbox) @@ -728,11 +724,11 @@ def _update_tab_content(): continue if info_btn and widget == info_btn: logger.debug("AboutDialog information button activated") - infoMsgBox({"title": _("Information"), "text": information or "", "richtext": True}) + msgBox({"title": _("Information"), "text": information or "", "richtext": True}) continue if credits_btn and widget == credits_btn: logger.debug("AboutDialog credits button activated") - infoMsgBox({"title": _("Credits"), "text": credits or "", "richtext": True}) + msgBox({"title": _("Credits"), "text": credits or "", "richtext": True}) continue logger.debug("Unhandled widget event from %s", getattr(widget, 'widgetClass', lambda: 'unknown')()) From 59ac9fc7dd8711c079b7eec68a88fa023c9f28b0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 8 Feb 2026 17:55:37 +0100 Subject: [PATCH 503/523] manage minimum size into all the common dialogs --- manatools/ui/common.py | 115 +++++++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 34937a6..9373eab 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -58,6 +58,41 @@ def _restore_app_title(old_title): except Exception: pass +def _extract_dialog_size(info): + """Normalize a size specification stored in an info dictionary.""" + if not info: + return None + size_spec = info.get('size') + if not size_spec: + return None + + width = height = None + if isinstance(size_spec, dict): + width = size_spec.get('width') + if width is None: + width = size_spec.get('column') or size_spec.get('columns') + height = size_spec.get('height') + if height is None: + height = size_spec.get('lines') or size_spec.get('rows') + elif isinstance(size_spec, (list, tuple)): + if len(size_spec) > 0: + width = size_spec[0] + if len(size_spec) > 1: + height = size_spec[1] + else: + logger.debug("Unsupported size spec type: %s", type(size_spec)) + return None + + try: + width = int(width) if width is not None else None + height = int(height) if height is not None else None + except Exception: + return None + + if width and height and width > 0 and height > 0: + return (width, height) + return None + def warningMsgBox (info) : ''' This function creates a Warning dialog and shows the message passed as input. @@ -66,6 +101,10 @@ def warningMsgBox (info) : title => dialog title text => string to be shown into the dialog richtext => True if using rich text + size => Mapping | Sequence | None + Optional minimum-size hint. Accepted formats: + - mapping containing 'width'/'height' (or legacy 'column'/'lines') + - tuple/list where the first element is width and the second height ''' if not info: return 0 @@ -77,7 +116,15 @@ def warningMsgBox (info) : title = info.get('title') old_title = _push_app_title(title) - vbox = factory.createVBox(dlg) + root_vbox = factory.createVBox(dlg) + content_parent = root_vbox + size_hint = _extract_dialog_size(info) + if size_hint: + try: + content_parent = factory.createMinSize(root_vbox, size_hint[0], size_hint[1]) + except Exception: + content_parent = root_vbox + vbox = factory.createVBox(content_parent) # Content row: icon + text text = info.get('text', "") or "" @@ -140,6 +187,10 @@ def infoMsgBox (info) : title => dialog title text => string to be shown into the dialog richtext => True if using rich text + size => Mapping | Sequence | None + Optional minimum-size hint. Accepted formats: + - mapping containing 'width'/'height' (or legacy 'column'/'lines') + - tuple/list where the first element is width and the second height ''' if not info: return 0 @@ -151,7 +202,15 @@ def infoMsgBox (info) : title = info.get('title') old_title = _push_app_title(title) - vbox = factory.createVBox(dlg) + root_vbox = factory.createVBox(dlg) + content_parent = root_vbox + size_hint = _extract_dialog_size(info) + if size_hint: + try: + content_parent = factory.createMinSize(root_vbox, size_hint[0], size_hint[1]) + except Exception: + content_parent = root_vbox + vbox = factory.createVBox(content_parent) # Content row: icon + text text = info.get('text', "") or "" @@ -213,6 +272,10 @@ def msgBox (info) : title => dialog title text => string to be shown into the dialog richtext => True if using rich text + size => Mapping | Sequence | None + Optional minimum-size hint. Accepted formats: + - mapping containing 'width'/'height' (or legacy 'column'/'lines') + - tuple/list where the first element is width and the second height ''' if not info: return 0 @@ -223,7 +286,15 @@ def msgBox (info) : dlg = factory.createPopupDialog() title = info.get('title') old_title = _push_app_title(title) - vbox = factory.createVBox(dlg) + root_vbox = factory.createVBox(dlg) + content_parent = root_vbox + size_hint = _extract_dialog_size(info) + if size_hint: + try: + content_parent = factory.createMinSize(root_vbox, size_hint[0], size_hint[1]) + except Exception: + content_parent = root_vbox + vbox = factory.createVBox(content_parent) # Content row: text only (no icon) text = info.get('text', "") or "" @@ -277,6 +348,10 @@ def askOkCancel (info) : text => string to be swhon into the dialog richtext => True if using rich text default_button => optional default button [1 => Ok - any other values => Cancel] + size => Mapping | Sequence | None + Optional minimum-size hint. Accepted formats: + - mapping containing 'width'/'height' (or legacy 'column'/'lines') + - tuple/list where the first element is width and the second height @output: False: Cancel button has been pressed @@ -292,7 +367,15 @@ def askOkCancel (info) : title = info.get('title') old_title = _push_app_title(title) - vbox = factory.createVBox(dlg) + root_vbox = factory.createVBox(dlg) + content_parent = root_vbox + size_hint = _extract_dialog_size(info) + if size_hint: + try: + content_parent = factory.createMinSize(root_vbox, size_hint[0], size_hint[1]) + except Exception: + content_parent = root_vbox + vbox = factory.createVBox(content_parent) # Content row: icon + text text = info.get('text', "") or "" @@ -364,7 +447,10 @@ def askYesOrNo (info) : text => string to be swhon into the dialog richtext => True if using rich text default_button => optional default button [1 => Yes - any other values => No] - size => [width, height] in pixels, optional dialog minimum size hint + size => Mapping | Sequence | None + Optional minimum-size hint. Accepted formats: + - mapping containing 'width'/'height' (or legacy 'column'/'lines') + - tuple/list where the first element is width and the second height @output: False: No button has been pressed @@ -379,7 +465,15 @@ def askYesOrNo (info) : dlg = factory.createPopupDialog() title = info.get('title') old_title = _push_app_title(title) - vbox = factory.createVBox(dlg) + root_vbox = factory.createVBox(dlg) + content_parent = root_vbox + size_hint = _extract_dialog_size(info) + if size_hint: + try: + content_parent = factory.createMinSize(root_vbox, size_hint[0], size_hint[1]) + except Exception: + content_parent = root_vbox + vbox = factory.createVBox(content_parent) # Content row: icon + text text = info.get('text', "") or "" @@ -408,15 +502,6 @@ def askYesOrNo (info) : except Exception: pass - # Handle size if provided - if 'size' in info.keys(): - try: - dims = info['size'] - parent = factory.createMinSize(vbox, int(dims.get('width', 320)), int(dims.get('height', 240))) - vbox = parent - except Exception: - pass - # Buttons on the right btns = factory.createHBox(vbox) factory.createHStretch(btns) From b4d5a3238d295932109dcfa165fcfb903c18fdb0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 8 Feb 2026 19:12:40 +0100 Subject: [PATCH 504/523] first attempt to manage default button --- manatools/aui/backends/curses/dialogcurses.py | 80 +++++++++++++++++ .../aui/backends/curses/pushbuttoncurses.py | 37 +++++++- manatools/aui/backends/gtk/dialoggtk.py | 89 +++++++++++++++++++ manatools/aui/backends/gtk/pushbuttongtk.py | 52 +++++++++++ manatools/aui/backends/qt/dialogqt.py | 53 +++++++++++ manatools/aui/backends/qt/pushbuttonqt.py | 43 +++++++++ manatools/ui/common.py | 4 + 7 files changed, 356 insertions(+), 2 deletions(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index f213b78..869b886 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -27,6 +27,7 @@ _mod_logger.setLevel(logging.INFO) class YDialogCurses(YSingleChildContainerWidget): + """Ncurses dialog container with focus, help, and default button support.""" _open_dialogs = [] _current_dialog = None @@ -53,6 +54,7 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._help_overlay_text = None self._help_overlay_until = 0.0 self._help_overlay_pos = None # (y, x) where to draw the overlay; None = auto fallback + self._default_button = None YDialogCurses._open_dialogs.append(self) def widgetClass(self): @@ -115,6 +117,7 @@ def isOpen(self): return self._is_open def destroy(self, doThrow=True): + self._clear_default_button() self._is_open = False if self in YDialogCurses._open_dialogs: YDialogCurses._open_dialogs.remove(self) @@ -140,6 +143,34 @@ def currentDialog(cls, doThrow=True): raise YUINoDialogException("No dialog open") return None return cls._open_dialogs[-1] + + def setDefaultButton(self, button): + """Set or clear the dialog default push button.""" + if button is None: + self._clear_default_button() + return True + try: + if button.widgetClass() != "YPushButton": + raise ValueError("Default button must be a YPushButton instance") + except Exception: + self._logger.error("Invalid widget passed to setDefaultButton", exc_info=True) + return False + try: + dlg = button.findDialog() if hasattr(button, "findDialog") else None + except Exception: + dlg = None + if dlg not in (None, self): + try: + self._logger.error("Button belongs to another dialog; cannot set as default") + except Exception: + pass + return False + try: + button.setDefault(True) + except Exception: + self._logger.exception("Failed to mark button as default") + return False + return True def _create_backend_widget(self): # Use the main screen @@ -540,6 +571,13 @@ def waitForEvent(self, timeout_millisec=0): if not handled and ((key >= ord('a') and key <= ord('z')) or (key >= ord('A') and key <= ord('Z'))): if self._activate_pushbutton_mnemonic(chr(key)): self._last_draw_time = 0 + handled = True + + # Trigger default button when Enter/Return pressed and no widget handled it + if not handled and key in (curses.KEY_ENTER, ord('\n'), ord('\r')): + if self._activate_default_button(): + self._last_draw_time = 0 + continue except KeyboardInterrupt: self._post_event(YCancelEvent()) @@ -616,3 +654,45 @@ def _bubble_key_to_paned(self, key) -> bool: return False except Exception: return False + + def _register_default_button(self, button): + """Internal: ensure only one default button is tracked for this dialog.""" + if getattr(self, "_default_button", None) == button: + return + if getattr(self, "_default_button", None) is not None: + try: + self._default_button._apply_default_state(False, notify_dialog=False) + except Exception: + self._logger.exception("Failed to clear previous default button") + self._default_button = button + + def _unregister_default_button(self, button): + """Internal: drop reference when button is no longer default.""" + if getattr(self, "_default_button", None) == button: + self._default_button = None + + def _clear_default_button(self): + """Clear the current default button if any.""" + if getattr(self, "_default_button", None) is not None: + try: + self._default_button._apply_default_state(False, notify_dialog=False) + except Exception: + self._logger.exception("Failed to reset default button state") + self._default_button = None + + def _activate_default_button(self): + """Post an activation event for the default button if available.""" + button = getattr(self, "_default_button", None) + if button is None: + return False + try: + if not button.isEnabled() or not button.visible(): + return False + except Exception: + return False + try: + self._post_event(YWidgetEvent(button, YEventReason.Activated)) + return True + except Exception: + self._logger.exception("Failed to activate default button") + return False diff --git a/manatools/aui/backends/curses/pushbuttoncurses.py b/manatools/aui/backends/curses/pushbuttoncurses.py index aff1d64..6a3b134 100644 --- a/manatools/aui/backends/curses/pushbuttoncurses.py +++ b/manatools/aui/backends/curses/pushbuttoncurses.py @@ -15,6 +15,7 @@ import os import time import logging +from typing import Optional from ...yui_common import * from .commoncurses import extract_mnemonic, split_mnemonic @@ -28,6 +29,7 @@ class YPushButtonCurses(YWidget): + """Curses push button widget with mnemonic handling and default state.""" def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, icon_only: Optional[bool]=False): super().__init__(parent) self._label = label @@ -38,6 +40,7 @@ def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, ic self._x = 0 self._y = 0 self._height = 1 # Fixed height - buttons are always one line + self._is_default = False self._logger = logging.getLogger(f"manatools.aui.ncurses.{self.__class__.__name__}") # derive mnemonic and cleaned label if present try: @@ -61,6 +64,14 @@ def setLabel(self, label): self._mnemonic, self._mnemonic_index, self._clean_label = split_mnemonic(self._label) except Exception: self._mnemonic, self._mnemonic_index, self._clean_label = None, None, self._label + + def setDefault(self, default: bool): + """Mark this button as the dialog default (or clear it).""" + self._apply_default_state(bool(default), notify_dialog=True) + + def default(self) -> bool: + """Return True when this button is the dialog default.""" + return bool(getattr(self, "_is_default", False)) def _create_backend_widget(self): try: @@ -115,7 +126,7 @@ def _draw(self, window, y, x, width, height): attr = curses.A_DIM else: attr = curses.A_REVERSE if self._focused else curses.A_NORMAL - if self._focused: + if self._focused or self._is_default: attr |= curses.A_BOLD self._x = text_x @@ -180,4 +191,26 @@ def setIcon(self, icon_name: str): def setVisible(self, visible=True): super().setVisible(visible) - self._can_focus = bool(visible) \ No newline at end of file + self._can_focus = bool(visible) + + def _apply_default_state(self, state: bool, notify_dialog: bool): + """Internal helper to store default state and notify dialog if needed.""" + desired = bool(state) + if getattr(self, "_is_default", False) == desired and not notify_dialog: + return + dlg = self.findDialog() if notify_dialog else None + if notify_dialog and dlg is not None: + try: + if desired: + dlg._register_default_button(self) + else: + dlg._unregister_default_button(self) + except Exception: + self._logger.exception("Failed to synchronize default button with dialog") + self._is_default = desired + self._sync_default_visual() + + def _sync_default_visual(self): + """Best-effort visual cue for default buttons in curses.""" + # curses drawing checks _is_default to add bold attribute; nothing else to do here + return \ No newline at end of file diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index 25e498e..f7ee0f8 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -22,6 +22,7 @@ from ... import yui as yui_mod class YDialogGtk(YSingleChildContainerWidget): + """Gtk4 dialog window with nested-loop event handling and default button support.""" _open_dialogs = [] def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): @@ -32,6 +33,8 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._window = None self._event_result = None self._glib_loop = None + self._default_button = None + self._default_key_controller = None YDialogGtk._open_dialogs.append(self) self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") @@ -73,6 +76,7 @@ def isOpen(self): return self._is_open def destroy(self, doThrow=True): + self._clear_default_button() if self._window: try: self._window.destroy() @@ -228,6 +232,31 @@ def currentDialog(cls, doThrow=True): raise YUINoDialogException("No dialog open") return None return cls._open_dialogs[-1] + + def setDefaultButton(self, button): + """Set or clear the default push button for this dialog.""" + if button is None: + self._clear_default_button() + return True + try: + if button.widgetClass() != "YPushButton": + raise ValueError("Default button must be a YPushButton") + except Exception: + self._logger.error("Invalid widget passed to setDefaultButton", exc_info=True) + return False + try: + dlg = button.findDialog() if hasattr(button, "findDialog") else None + except Exception: + dlg = None + if dlg not in (None, self): + self._logger.error("Refusing to set a default button owned by another dialog") + return False + try: + button.setDefault(True) + except Exception: + self._logger.exception("Failed to activate default button") + return False + return True def _create_backend_widget(self): # Determine window title from YApplicationGtk instance stored on the YUI backend @@ -310,6 +339,14 @@ def _create_backend_widget(self): self._backend_widget = self._window self._backend_widget.set_sensitive(self._enabled) + # Install key controller to trigger the default button with Enter/Return. + try: + controller = Gtk.EventControllerKey() + controller.connect("key-pressed", self._on_default_key_pressed) + self._window.add_controller(controller) + self._default_key_controller = controller + except Exception: + self._logger.exception("Failed to install default button key controller") # Connect destroy/close handlers try: # Gtk4: use 'close-request' if available, otherwise 'destroy' @@ -372,3 +409,55 @@ def _set_backend_enabled(self, enabled): pass except Exception: pass + + def _on_default_key_pressed(self, controller, keyval, keycode, state): + """Activate the default button when Return/Enter is pressed.""" + try: + if keyval in (Gdk.KEY_Return, Gdk.KEY_ISO_Enter, Gdk.KEY_KP_Enter): + if self._activate_default_button(): + return True + except Exception: + self._logger.exception("Default key handler failed") + return False + + def _register_default_button(self, button): + """Ensure this dialog tracks a single default push button.""" + if getattr(self, "_default_button", None) == button: + return + if getattr(self, "_default_button", None) is not None: + try: + self._default_button._apply_default_state(False, notify_dialog=False) + except Exception: + self._logger.exception("Failed to clear previous default button") + self._default_button = button + + def _unregister_default_button(self, button): + """Drop dialog reference when button is no longer default.""" + if getattr(self, "_default_button", None) == button: + self._default_button = None + + def _clear_default_button(self): + """Clear any current default button.""" + if getattr(self, "_default_button", None) is not None: + try: + self._default_button._apply_default_state(False, notify_dialog=False) + except Exception: + self._logger.exception("Failed to reset default button state") + self._default_button = None + + def _activate_default_button(self): + """Invoke the current default button if enabled and visible.""" + button = getattr(self, "_default_button", None) + if button is None: + return False + try: + if not button.isEnabled() or not button.visible(): + return False + except Exception: + return False + try: + self._post_event(YWidgetEvent(button, YEventReason.Activated)) + return True + except Exception: + self._logger.exception("Failed to activate default button") + return False diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index 19fbdbd..d731aec 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -24,11 +24,13 @@ class YPushButtonGtk(YWidget): + """Gtk4 push button wrapper supporting icons, mnemonics, and default state.""" def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, icon_only: Optional[bool]=False): super().__init__(parent) self._label = _convert_mnemonic_to_gtk(label) self._icon_name = icon_name self._icon_only = bool(icon_only) + self._is_default = False self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") def widgetClass(self): @@ -44,6 +46,14 @@ def setLabel(self, label): self._backend_widget.set_label(self._label) except Exception: pass + + def setDefault(self, default: bool): + """Mark this push button as the dialog default.""" + self._apply_default_state(bool(default), notify_dialog=True) + + def default(self) -> bool: + """Return True if this button is currently the default.""" + return bool(getattr(self, "_is_default", False)) def _create_backend_widget(self): if self._icon_only: @@ -106,6 +116,7 @@ def _create_backend_widget(self): self._logger.error("_create_backend_widget setup failed", exc_info=True) except Exception: pass + self._sync_default_visual() try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -213,3 +224,44 @@ def setHelpText(self, help_text: str): self._logger.exception("setHelpText failed", exc_info=True) except Exception: pass + + def _apply_default_state(self, state: bool, notify_dialog: bool): + """Store default flag and notify parent dialog if requested.""" + desired = bool(state) + if getattr(self, "_is_default", False) == desired and not notify_dialog: + return + dlg = self.findDialog() if notify_dialog else None + if notify_dialog and dlg is not None: + try: + if desired: + dlg._register_default_button(self) + else: + dlg._unregister_default_button(self) + except Exception: + self._logger.exception("Failed to synchronize default state with dialog") + self._is_default = desired + self._sync_default_visual() + + def _sync_default_visual(self): + """Apply/remove Gtk suggested-action styling for default buttons.""" + widget = getattr(self, "_backend_widget", None) + if widget is None: + return + try: + if self._is_default: + widget.add_css_class("suggested-action") + else: + widget.remove_css_class("suggested-action") + return + except Exception: + pass + # Fallback for bindings exposing style_context instead of css helpers + try: + ctx = widget.get_style_context() + if ctx: + if self._is_default: + ctx.add_class("suggested-action") + else: + ctx.remove_class("suggested-action") + except Exception: + self._logger.exception("Failed to toggle suggested-action class") diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index 73c60ca..abfda3e 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -20,6 +20,7 @@ import fcntl class YDialogQt(YSingleChildContainerWidget): + """Qt6 main window wrapper that manages dialog state and default buttons.""" _open_dialogs = [] def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorMode.YDialogNormalColor): @@ -36,6 +37,7 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._sigint_notifier = None self._prev_wakeup_fd = None self._prev_sigint_handler = None + self._default_button = None YDialogQt._open_dialogs.append(self) self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") @@ -78,6 +80,7 @@ def isOpen(self): return self._is_open def destroy(self, doThrow=True): + self._clear_default_button() if self._qwidget: self._qwidget.close() self._qwidget = None @@ -119,6 +122,31 @@ def currentDialog(cls, doThrow=True): raise YUINoDialogException("No dialog open") return None return cls._open_dialogs[-1] + + def setDefaultButton(self, button): + """Set or clear the default push button for this dialog.""" + if button is None: + self._clear_default_button() + return True + try: + if button.widgetClass() != "YPushButton": + raise ValueError("Default button must be a YPushButton") + except Exception: + self._logger.error("Invalid widget passed to setDefaultButton", exc_info=True) + return False + try: + dlg = button.findDialog() if hasattr(button, "findDialog") else None + except Exception: + dlg = None + if dlg not in (None, self): + self._logger.error("Refusing to reuse a button owned by a different dialog") + return False + try: + button.setDefault(True) + except Exception: + self._logger.exception("Failed to flag button as default") + return False + return True def _create_backend_widget(self): self._qwidget = QtWidgets.QMainWindow() @@ -401,3 +429,28 @@ def _teardown_sigint_notifier(self): pass except Exception: pass + + def _register_default_button(self, button): + """Ensure only one Qt push button is marked as default.""" + if getattr(self, "_default_button", None) == button: + return + if getattr(self, "_default_button", None) is not None: + try: + self._default_button._apply_default_state(False, notify_dialog=False) + except Exception: + self._logger.exception("Failed to clear previous default button") + self._default_button = button + + def _unregister_default_button(self, button): + """Drop reference when the current default button changes.""" + if getattr(self, "_default_button", None) == button: + self._default_button = None + + def _clear_default_button(self): + """Clear existing default button, if any.""" + if getattr(self, "_default_button", None) is not None: + try: + self._default_button._apply_default_state(False, notify_dialog=False) + except Exception: + self._logger.exception("Failed to reset default button state") + self._default_button = None diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index 20d15e9..bc1470e 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -17,11 +17,13 @@ class YPushButtonQt(YWidget): + """Qt6 push button wrapper honoring icons, help text, and default state.""" def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, icon_only: Optional[bool]=False): super().__init__(parent) self._label = label self._icon_name = icon_name self._icon_only = bool(icon_only) + self._is_default = False self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): @@ -34,6 +36,14 @@ def setLabel(self, label): self._label = label if self._backend_widget and self._icon_only is False: self._backend_widget.setText(label) + + def setDefault(self, default: bool): + """Mark/unmark this push button as the dialog default.""" + self._apply_default_state(bool(default), notify_dialog=True) + + def default(self) -> bool: + """Return True if this button is currently the default.""" + return bool(getattr(self, "_is_default", False)) def _create_backend_widget(self): if self._icon_only: @@ -81,6 +91,7 @@ def _create_backend_widget(self): pass self._backend_widget.setEnabled(bool(self._enabled)) self._backend_widget.clicked.connect(self._on_clicked) + self._sync_default_visual() try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -145,3 +156,35 @@ def setHelpText(self, help_text: str): self._backend_widget.setToolTip(help_text) except Exception: self._logger.exception("setHelpText failed") + + def _apply_default_state(self, state: bool, notify_dialog: bool): + """Persist default flag and coordinate with owning dialog.""" + desired = bool(state) + if getattr(self, "_is_default", False) == desired and not notify_dialog: + return + dlg = self.findDialog() if notify_dialog else None + if notify_dialog and dlg is not None: + try: + if desired: + dlg._register_default_button(self) + else: + dlg._unregister_default_button(self) + except Exception: + self._logger.exception("Failed to sync default state with dialog") + self._is_default = desired + self._sync_default_visual() + + def _sync_default_visual(self): + """Apply Qt default/auto-default flags on the backend widget.""" + widget = getattr(self, "_backend_widget", None) + if widget is None: + return + try: + widget.setDefault(bool(self._is_default)) + except Exception: + self._logger.exception("Failed to set Qt default flag") + try: + widget.setAutoDefault(bool(self._is_default)) + except Exception: + # Some styles/widgets may not expose auto-default; ignore. + pass diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 9373eab..8fc4468 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -411,6 +411,7 @@ def askOkCancel (info) : cancel_btn = factory.createPushButton(btns, _("&Cancel")) default_ok = bool(info.get('default_button', 0) == 1) + dlg.setDefaultButton(ok_btn if default_ok else cancel_btn) # simple default: ignore focusing specifics for now result = False while True: @@ -508,6 +509,9 @@ def askYesOrNo (info) : yes_btn = factory.createPushButton(btns, _("&Yes")) no_btn = factory.createPushButton(btns, _("&No")) + default_yes = bool(info.get('default_button', 0) == 1) + dlg.setDefaultButton(yes_btn if default_yes else no_btn) + result = False while True: ev = dlg.waitForEvent() From aab4fd74a0867abef7489a00bb6f0f10d942e6c9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 9 Feb 2026 19:43:58 +0100 Subject: [PATCH 505/523] Avoid loop on create widget --- manatools/aui/backends/curses/imagecurses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manatools/aui/backends/curses/imagecurses.py b/manatools/aui/backends/curses/imagecurses.py index d313484..908925f 100644 --- a/manatools/aui/backends/curses/imagecurses.py +++ b/manatools/aui/backends/curses/imagecurses.py @@ -66,6 +66,7 @@ def setZeroSize(self, dim, zeroSize=True): def _create_backend_widget(self): try: + self._backend_widget = self # nothing to create for curses; drawing uses _draw self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: From 67bf55a260610f21b45fd67243dc6fedb6cca4b9 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 9 Feb 2026 20:45:55 +0100 Subject: [PATCH 506/523] Fix action on default button if enter is pressed --- manatools/aui/backends/qt/dialogqt.py | 19 +++++-- manatools/aui/backends/qt/pushbuttonqt.py | 60 ++++++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index abfda3e..1f6eeb1 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -11,7 +11,19 @@ ''' from PySide6 import QtWidgets, QtCore, QtGui -from ...yui_common import YSingleChildContainerWidget, YUIDimension, YPropertySet, YProperty, YPropertyType, YUINoDialogException, YDialogType, YDialogColorMode, YEvent, YCancelEvent, YTimeoutEvent +from ...yui_common import ( + YSingleChildContainerWidget, + YUIDimension, + YPropertySet, + YProperty, + YPropertyType, + YUINoDialogException, + YDialogType, + YDialogColorMode, + YEvent, + YCancelEvent, + YTimeoutEvent, +) from .commonqt import _resolve_icon from ... import yui as yui_mod import os @@ -431,7 +443,7 @@ def _teardown_sigint_notifier(self): pass def _register_default_button(self, button): - """Ensure only one Qt push button is marked as default.""" + """Ensure only one Qt push button is tracked as default.""" if getattr(self, "_default_button", None) == button: return if getattr(self, "_default_button", None) is not None: @@ -442,7 +454,7 @@ def _register_default_button(self, button): self._default_button = button def _unregister_default_button(self, button): - """Drop reference when the current default button changes.""" + """Drop reference when the dialog loses its default button.""" if getattr(self, "_default_button", None) == button: self._default_button = None @@ -454,3 +466,4 @@ def _clear_default_button(self): except Exception: self._logger.exception("Failed to reset default button state") self._default_button = None + diff --git a/manatools/aui/backends/qt/pushbuttonqt.py b/manatools/aui/backends/qt/pushbuttonqt.py index bc1470e..fe440e6 100644 --- a/manatools/aui/backends/qt/pushbuttonqt.py +++ b/manatools/aui/backends/qt/pushbuttonqt.py @@ -9,7 +9,7 @@ @package manatools.aui.backends.qt ''' -from PySide6 import QtWidgets, QtGui +from PySide6 import QtWidgets, QtGui, QtCore import logging from typing import Optional from ...yui_common import * @@ -24,6 +24,7 @@ def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, ic self._icon_name = icon_name self._icon_only = bool(icon_only) self._is_default = False + self._default_shortcuts = [] self._logger = logging.getLogger(f"manatools.aui.qt.{self.__class__.__name__}") def widgetClass(self): @@ -92,6 +93,7 @@ def _create_backend_widget(self): self._backend_widget.setEnabled(bool(self._enabled)) self._backend_widget.clicked.connect(self._on_clicked) self._sync_default_visual() + self._refresh_default_shortcuts() try: self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: @@ -173,6 +175,7 @@ def _apply_default_state(self, state: bool, notify_dialog: bool): self._logger.exception("Failed to sync default state with dialog") self._is_default = desired self._sync_default_visual() + self._refresh_default_shortcuts() def _sync_default_visual(self): """Apply Qt default/auto-default flags on the backend widget.""" @@ -188,3 +191,58 @@ def _sync_default_visual(self): except Exception: # Some styles/widgets may not expose auto-default; ignore. pass + + def _refresh_default_shortcuts(self): + """Create or remove shortcuts that emulate Qt default button behavior.""" + self._clear_default_shortcuts() + if not self._is_default: + return + dlg = self.findDialog() + backend_window = getattr(dlg, "_qwidget", None) if dlg else None + backend_button = getattr(self, "_backend_widget", None) + if backend_window is None or backend_button is None: + return + try: + keys = [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space] + for key in keys: + shortcut = QtGui.QShortcut(QtGui.QKeySequence(key), backend_window) + shortcut.setContext(QtCore.Qt.WidgetWithChildrenShortcut) + shortcut.activated.connect(lambda key_code=key: self._shortcut_activate(key_code)) + self._default_shortcuts.append(shortcut) + except Exception: + self._logger.exception("Failed to install default button shortcuts") + + def _clear_default_shortcuts(self): + """Dispose shortcut objects previously created for default activation.""" + for shortcut in self._default_shortcuts: + try: + shortcut.deleteLater() + except Exception: + pass + self._default_shortcuts = [] + + def _shortcut_activate(self, key_code): + """Slot executed when a default shortcut fires (Enter/Space).""" + if not self._is_default: + return + if not self.isEnabled() or not self.visible(): + return + dlg = self.findDialog() + backend_window = getattr(dlg, "_qwidget", None) if dlg else None + backend = getattr(self, "_backend_widget", None) + if backend is None: + return + if key_code == QtCore.Qt.Key_Space and backend_window is not None: + try: + focus_widget = backend_window.focusWidget() + if isinstance(focus_widget, (QtWidgets.QLineEdit, QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit)): + return + except Exception: + pass + try: + backend.animateClick(0) + except Exception: + try: + backend.click() + except Exception: + self._logger.exception("Default shortcut could not activate button") From b3515d19e4c56811754fded263c2cd958b1aaafb Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 9 Feb 2026 21:02:28 +0100 Subject: [PATCH 507/523] Action added on default button --- manatools/aui/backends/curses/dialogcurses.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/curses/dialogcurses.py b/manatools/aui/backends/curses/dialogcurses.py index 869b886..ef292dc 100644 --- a/manatools/aui/backends/curses/dialogcurses.py +++ b/manatools/aui/backends/curses/dialogcurses.py @@ -106,7 +106,7 @@ def open(self): # Find first focusable widget focusable = self._find_focusable_widgets() if focusable: - self._focused_widget = focusable[0] + self._focused_widget = self._default_button if self._default_button else focusable[0] self._focused_widget._focused = True # open() must be non-blocking (finalize and show). Event loop is @@ -170,6 +170,12 @@ def setDefaultButton(self, button): except Exception: self._logger.exception("Failed to mark button as default") return False + # Mirror GTK/Qt behavior: highlight default button and give it focus so + # the user can immediately activate it with space/enter. + try: + self._focus_widget(button) + except Exception: + pass return True def _create_backend_widget(self): @@ -395,6 +401,41 @@ def _cycle_focus(self, forward=True): # Force redraw on focus change self._last_draw_time = 0 + def _focus_widget(self, widget): + """Force focus on a specific widget when possible.""" + if widget is None: + return False + try: + if not getattr(widget, "_can_focus", False): + return False + except Exception: + return False + try: + if not widget.isEnabled() or not widget.visible(): + return False + except Exception: + return False + # Skip updates if the widget already owns focus + if getattr(widget, "_focused", False): + self._focused_widget = widget + return True + # Clear previous focus + if getattr(self, "_focused_widget", None) is not None: + try: + self._focused_widget._focused = False + except Exception: + pass + self._focused_widget = widget + try: + widget._focused = True + except Exception: + pass + try: + self._last_draw_time = 0 + except Exception: + pass + return True + def _find_focusable_widgets(self): """Find all widgets that can receive focus""" focusable = [] From d9ab3efdf5af61bc319a5e10d7d570d3fef42676 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Mon, 9 Feb 2026 21:49:23 +0100 Subject: [PATCH 508/523] reworking on help dialog --- manatools/ui/helpdialog.py | 66 ++++++++++++++++++++++++++++++++------ test/testHelpDialog.py | 59 +++++++++++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/manatools/ui/helpdialog.py b/manatools/ui/helpdialog.py index ff7ec44..94d179e 100644 --- a/manatools/ui/helpdialog.py +++ b/manatools/ui/helpdialog.py @@ -10,6 +10,7 @@ @package manatools.ui.helpdialog ''' +import logging import webbrowser from . import basedialog as basedialog @@ -25,19 +26,40 @@ _ = t.gettext ngettext = t.ngettext +logger = logging.getLogger("manatools.ui.helpdialog") + class HelpDialog(basedialog.BaseDialog): - def __init__(self, info, title=_("Help dialog"), icon="", minWidth=80, minHeight=20): - basedialog.BaseDialog.__init__(self, title, icon, basedialog.DialogType.POPUP, minWidth, minHeight) + """Simple rich-text help browser dialog with internal navigation.""" + def __init__(self, info, title=_("Help dialog"), icon="", minWidth=320, minHeight=200): + self._minWidthHint = self._normalize_dimension(minWidth) + self._minHeightHint = self._normalize_dimension(minHeight) + basedialog.BaseDialog.__init__(self, title, icon, basedialog.DialogType.POPUP, -1, -1) ''' HelpDialog constructor @param title dialog title @param icon dialog icon - @param minWidth > 0 mim width size, see libYui createMinSize - @param minHeight > 0 mim height size, see libYui createMinSize + @param minWidth > 0 mim width size in pixels + @param minHeight > 0 mim height size in pixels ''' if not isinstance(info, helpdata.HelpInfoBase): raise TypeError("info must be a HelpInfoBase instance") self.info = info + logger.debug( + "HelpDialog initialized title=%s icon=%s minWidth=%s minHeight=%s", + title, + icon, + self._minWidthHint, + self._minHeightHint, + ) + + @staticmethod + def _normalize_dimension(value): + """Return positive integer dimension or 0 if invalid.""" + try: + dimension = int(value) + except (TypeError, ValueError): + return 0 + return dimension if dimension > 0 else 0 def UIlayout(self, layout): ''' @@ -45,22 +67,48 @@ def UIlayout(self, layout): ''' # URL events may be sent as MenuEvent by backends that support it self.eventManager.addMenuEvent(None, self.onURLEvent, False) - self.text = self.factory.createRichText(layout, "", False) + content_parent = layout + if self._minWidthHint and self._minHeightHint: + try: + min_container = self.factory.createMinSize(layout, self._minWidthHint, self._minHeightHint) + content_parent = self.factory.createVBox(min_container) + logger.debug( + "Applied min-size container (%s x %s) to HelpDialog", + self._minWidthHint, + self._minHeightHint, + ) + except Exception as exc: + content_parent = layout + logger.debug("Unable to apply min-size hint: %s", exc) + self.text = self.factory.createRichText(content_parent, "", False) + self.text.setStretchable(yui.YUIDimension.YD_HORIZ, True) + self.text.setStretchable(yui.YUIDimension.YD_VERT, True) self.text.setValue(self.info.home()) - align = self.factory.createRight(layout) - self.quitButton = self.factory.createPushButton(align, _("Quit")) + logger.debug("Initial help content loaded") + + button_row = self.factory.createHBox(content_parent) + self.factory.createHStretch(button_row) + self.quitButton = self.factory.createPushButton(button_row, _("Quit")) self.eventManager.addWidgetEvent(self.quitButton, self.onQuitEvent) + logger.debug("Quit button registered") def onQuitEvent(self) : + """Handle Quit button activation and terminate the dialog loop.""" # BaseDialog needs to force to exit the handle event loop + logger.debug("Quit event triggered") self.ExitLoop() def onURLEvent(self, mEvent): + """Handle rich text URL activations and navigate or open links.""" url = mEvent.id() if url: text = self.info.show(url) if text: self.text.setValue(text) + logger.debug("Help content switched to %s", url) else: - print("onURLEvent: running webbrowser", url) - webbrowser.open(url, 2) + logger.debug("Opening external URL %s", url) + try: + webbrowser.open(url, 2) + except Exception as exc: + logger.error("Failed to open URL %s: %s", url, exc) diff --git a/test/testHelpDialog.py b/test/testHelpDialog.py index cbc46b2..5a8d20c 100644 --- a/test/testHelpDialog.py +++ b/test/testHelpDialog.py @@ -11,11 +11,16 @@ @package manatools ''' +import os +import sys import manatools.basehelpinfo as helpdata import manatools.ui.helpdialog as helpdialog import yui -import time +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +import logging + +logger = logging.getLogger("manatools.test.helpdialog") ###################################################################### ## @@ -28,9 +33,53 @@ class HelpInfo(helpdata.HelpInfoBase): def __init__(self): helpdata.HelpInfoBase.__init__(self) + # Configure file logger for this test: write DEBUG logs to '.log' in cwd + try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + self._logger = logging.getLogger() + self._logger.setLevel(logging.DEBUG) + existing = False + for h in list(self._logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + self._logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") + except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + self._logger.debug("Creating HelpInfo contents") index1 = '%s'%self._formatLink("Title 1", 'title1') index2 = '%s'%self._formatLink("Title 2", 'titleindex2') - self.text = { 'home': "This text explain how to use manatools Help Dialog.

%s
%s"%(index1, index2), + index3 = '%s'%self._formatLink("Info", 'info') + html = ( + "

Heading 1

" + "

Heading 2

" + "

Heading 3

" + "

Heading 4

" + "
Heading 5
" + "
Heading 6
" + "
" + "

Welcome to RichText

" + "
" + "

This is a paragraph with bold, italic, and underlined text.

" + "

Click the Manatools or go home link to emit an activation event.

" + "

Colored text:

" + "" + "

Lists:

" + "
  • Alpha
  • Beta
  • Gamma
" + ) + self.text = { 'home': "This text explain how to use manatools Help Dialog.

%s - %s
%s"%(index1, index2, index3), + 'info': html, 'title1': '

Title 1

This is the title 1 really interesting context.
%s'%self._formatLink("Go to index", 'home'), 'titleindex2': '

Title 2

This is the title 2 interesting context.
%s'%self._formatLink("Go to index", 'home'), } @@ -60,10 +109,10 @@ def home(self): return self.text['home'] -if __name__ == '__main__': - +if __name__ == '__main__': info = HelpInfo() - td = helpdialog.HelpDialog(info) + td = helpdialog.HelpDialog(info) td.run() + From 369cdd5d0d7561c41410157a72f867ce9353b5b0 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 13:24:16 +0100 Subject: [PATCH 509/523] Managed Height-for-Width in gtk --- manatools/aui/backends/gtk/dialoggtk.py | 75 +++++++++++- manatools/aui/backends/gtk/hboxgtk.py | 119 +++++++++++++++++-- manatools/aui/backends/gtk/labelgtk.py | 76 ++++++++++++- manatools/aui/backends/gtk/pushbuttongtk.py | 73 +++++++++++- manatools/aui/backends/gtk/richtextgtk.py | 76 ++++++++++++- manatools/aui/backends/gtk/vboxgtk.py | 120 ++++++++++++++++++-- 6 files changed, 510 insertions(+), 29 deletions(-) diff --git a/manatools/aui/backends/gtk/dialoggtk.py b/manatools/aui/backends/gtk/dialoggtk.py index f7ee0f8..59d36f6 100644 --- a/manatools/aui/backends/gtk/dialoggtk.py +++ b/manatools/aui/backends/gtk/dialoggtk.py @@ -21,6 +21,37 @@ from ...yui_common import * from ... import yui as yui_mod + +class _YDialogMeasureWindow(Gtk.Window): + """Gtk.Window subclass delegating size measurement to YDialogGtk.""" + + def __init__(self, owner, title=""): + """Initialize the measuring window. + + Args: + owner: Owning YDialogGtk instance. + title: Initial window title. + """ + super().__init__(title=title) + self._owner = owner + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + try: + return self._owner.do_measure(orientation, for_size) + except Exception: + self._owner._logger.exception("Dialog backend do_measure delegation failed", exc_info=True) + return (0, 0, -1, -1) + + class YDialogGtk(YSingleChildContainerWidget): """Gtk4 dialog window with nested-loop event handling and default button support.""" _open_dialogs = [] @@ -35,6 +66,7 @@ def __init__(self, dialog_type=YDialogType.YMainDialog, color_mode=YDialogColorM self._glib_loop = None self._default_button = None self._default_key_controller = None + self._content_widget = None YDialogGtk._open_dialogs.append(self) self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") @@ -257,6 +289,38 @@ def setDefaultButton(self, button): self._logger.exception("Failed to activate default button") return False return True + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + container = getattr(self, "_content_widget", None) + if container is not None: + try: + cmin, cnat, _bmin, _bnat = container.measure(orientation, for_size) + margin = 20 + minimum_size = int(cmin + margin) + natural_size = int(cnat + margin) + self._logger.debug( + "Dialog do_measure orientation=%s for_size=%s -> min=%s nat=%s", + orientation, + for_size, + minimum_size, + natural_size, + ) + return (minimum_size, natural_size, -1, -1) + except Exception: + self._logger.exception("Dialog content measure failed", exc_info=True) + + if orientation == Gtk.Orientation.HORIZONTAL: + return (420, 600, -1, -1) + return (280, 400, -1, -1) def _create_backend_widget(self): # Determine window title from YApplicationGtk instance stored on the YUI backend @@ -280,7 +344,7 @@ def _create_backend_widget(self): pass # Create Gtk4 Window - self._window = Gtk.Window(title=title) + self._window = _YDialogMeasureWindow(self, title=title) # set window icon if available try: if _resolved_pixbuf is not None: @@ -304,10 +368,10 @@ def _create_backend_widget(self): pass except Exception: pass - try: - self._window.set_default_size(600, 400) - except Exception: - pass + #try: + # self._window.set_default_size(600, 400) + #except Exception: + # pass # Content container with margins (window.set_child used in Gtk4) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) @@ -315,6 +379,7 @@ def _create_backend_widget(self): content.set_margin_end(10) content.set_margin_top(10) content.set_margin_bottom(10) + self._content_widget = content child = self.child() if child: diff --git a/manatools/aui/backends/gtk/hboxgtk.py b/manatools/aui/backends/gtk/hboxgtk.py index 86f6495..73af9d9 100644 --- a/manatools/aui/backends/gtk/hboxgtk.py +++ b/manatools/aui/backends/gtk/hboxgtk.py @@ -21,7 +21,38 @@ from ...yui_common import * +class _YHBoxMeasureBox(Gtk.Box): + """Gtk.Box subclass delegating size requests to YHBoxGtk.""" + + def __init__(self, owner): + """Initialize the measuring box. + + Args: + owner: Owning YHBoxGtk instance. + """ + super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + self._owner = owner + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + try: + return self._owner.do_measure(orientation, for_size) + except Exception: + self._owner._logger.exception("HBox backend do_measure delegation failed", exc_info=True) + return (0, 0, -1, -1) + + class YHBoxGtk(YWidget): + """Horizontal GTK4 container with weight-aware geometry management.""" + def __init__(self, parent=None): super().__init__(parent) self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") @@ -37,8 +68,68 @@ def stretchable(self, dim): return True return False + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + children = list(getattr(self, "_children", []) or []) + if not children: + return (0, 0, -1, -1) + + spacing = 5 + try: + if getattr(self, "_backend_widget", None) is not None and hasattr(self._backend_widget, "get_spacing"): + spacing = int(self._backend_widget.get_spacing()) + except Exception: + self._logger.exception("Failed to read HBox spacing for measure", exc_info=True) + spacing = 5 + + minimum_size = 0 + natural_size = 0 + maxima_minimum = 0 + maxima_natural = 0 + + for child in children: + try: + child_widget = child.get_backend_widget() + cmin, cnat, _cbase_min, _cbase_nat = child_widget.measure(orientation, for_size) + except Exception: + self._logger.exception("HBox measure failed for child %s", getattr(child, "debugLabel", lambda: "")(), exc_info=True) + cmin, cnat = 0, 0 + + if orientation == Gtk.Orientation.HORIZONTAL: + minimum_size += int(cmin) + natural_size += int(cnat) + else: + maxima_minimum = max(maxima_minimum, int(cmin)) + maxima_natural = max(maxima_natural, int(cnat)) + + if orientation == Gtk.Orientation.HORIZONTAL: + gap_total = max(0, len(children) - 1) * spacing + minimum_size += gap_total + natural_size += gap_total + else: + minimum_size = maxima_minimum + natural_size = maxima_natural + + self._logger.debug( + "HBox do_measure orientation=%s for_size=%s -> min=%s nat=%s", + orientation, + for_size, + minimum_size, + natural_size, + ) + return (minimum_size, natural_size, -1, -1) + def _create_backend_widget(self): - self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + """Create backend widget and configure weight/stretch behavior.""" + self._backend_widget = _YHBoxMeasureBox(self) # Collect children first so we can apply weight-based heuristics children = list(self._children) @@ -120,15 +211,23 @@ def _apply_weights(*args): except Exception: pass # keep children proportional on subsequent resizes if possible - try: - def _on_size_allocate(widget, allocation): - try: - _apply_weights() - except Exception: - self._logger.exception("_on_size_allocate: failed", exc_info=True) - self._backend_widget.connect('size-allocate', _on_size_allocate) - except Exception: - self._logger.exception("_create_backend_widget: failed to connect size-allocate", exc_info=True) + def _on_resize_update(*_args): + try: + _apply_weights() + except Exception: + self._logger.exception("_on_resize_update: failed", exc_info=True) + + connected = False + for signal_name in ("size-allocate", "notify::width", "notify::width-request"): + try: + self._backend_widget.connect(signal_name, _on_resize_update) + connected = True + self._logger.debug("Connected HBox resize hook using signal '%s'", signal_name) + break + except Exception: + continue + if not connected: + self._logger.debug("No supported resize signal found for HBox; dynamic weight refresh disabled") except Exception: pass try: diff --git a/manatools/aui/backends/gtk/labelgtk.py b/manatools/aui/backends/gtk/labelgtk.py index 7a93f55..92986da 100644 --- a/manatools/aui/backends/gtk/labelgtk.py +++ b/manatools/aui/backends/gtk/labelgtk.py @@ -21,7 +21,39 @@ from ...yui_common import * +class _YLabelMeasure(Gtk.Label): + """Gtk.Label subclass delegating size measurement to YLabelGtk.""" + + def __init__(self, owner, label=""): + """Initialize the measuring label. + + Args: + owner: Owning YLabelGtk instance. + label: Initial label text. + """ + super().__init__(label=label) + self._owner = owner + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + try: + return self._owner.do_measure(orientation, for_size) + except Exception: + self._owner._logger.exception("Label backend do_measure delegation failed", exc_info=True) + return (0, 0, -1, -1) + + class YLabelGtk(YWidget): + """GTK4 label widget with heading/output-field options and wrapping.""" + def __init__(self, parent=None, text="", isHeading=False, isOutputField=False): super().__init__(parent) self._text = text @@ -79,8 +111,50 @@ def setAutoWrap(self, on: bool = True): except Exception: pass + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + widget = getattr(self, "_backend_widget", None) + if widget is not None: + try: + minimum_size, natural_size, minimum_baseline, natural_baseline = Gtk.Label.do_measure(widget, orientation, for_size) + if orientation == Gtk.Orientation.HORIZONTAL: + minimum_baseline = -1 + natural_baseline = -1 + measured = (minimum_size, natural_size, minimum_baseline, natural_baseline) + self._logger.debug("Label do_measure orientation=%s for_size=%s -> %s", orientation, for_size, measured) + return measured + except Exception: + self._logger.exception("Label base do_measure failed", exc_info=True) + + text = str(getattr(self, "_text", "") or "") + line_count = max(1, text.count("\n") + 1) + longest_line = max((len(line) for line in text.splitlines()), default=len(text)) + if orientation == Gtk.Orientation.HORIZONTAL: + minimum_size = min(80, max(8, longest_line * 5)) + natural_size = max(minimum_size, longest_line * 8) + else: + minimum_size = max(18, line_count * 18) + natural_size = minimum_size + self._logger.debug( + "Label fallback do_measure orientation=%s for_size=%s -> min=%s nat=%s", + orientation, + for_size, + minimum_size, + natural_size, + ) + return (minimum_size, natural_size, -1, -1) + def _create_backend_widget(self): - self._backend_widget = Gtk.Label(label=self._text) + """Create backend label and apply visual and sizing policy.""" + self._backend_widget = _YLabelMeasure(self, label=self._text) try: # alignment API in Gtk4 differs; fall back to setting xalign if available if hasattr(self._backend_widget, "set_xalign"): diff --git a/manatools/aui/backends/gtk/pushbuttongtk.py b/manatools/aui/backends/gtk/pushbuttongtk.py index d731aec..c7a93ea 100644 --- a/manatools/aui/backends/gtk/pushbuttongtk.py +++ b/manatools/aui/backends/gtk/pushbuttongtk.py @@ -23,6 +23,39 @@ from .commongtk import _resolve_icon, _convert_mnemonic_to_gtk +class _YPushButtonMeasure(Gtk.Button): + """Gtk.Button subclass delegating size measurement to YPushButtonGtk.""" + + def __init__(self, owner, label=None): + """Initialize the measuring button. + + Args: + owner: Owning YPushButtonGtk instance. + label: Optional initial label. + """ + if label is None: + super().__init__() + else: + super().__init__(label=label) + self._owner = owner + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + try: + return self._owner.do_measure(orientation, for_size) + except Exception: + self._owner._logger.exception("PushButton backend do_measure delegation failed", exc_info=True) + return (0, 0, -1, -1) + + class YPushButtonGtk(YWidget): """Gtk4 push button wrapper supporting icons, mnemonics, and default state.""" def __init__(self, parent=None, label: str="", icon_name: Optional[str]=None, icon_only: Optional[bool]=False): @@ -54,18 +87,54 @@ def setDefault(self, default: bool): def default(self) -> bool: """Return True if this button is currently the default.""" return bool(getattr(self, "_is_default", False)) + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + widget = getattr(self, "_backend_widget", None) + if widget is not None: + try: + measured = Gtk.Button.do_measure(widget, orientation, for_size) + self._logger.debug("PushButton do_measure orientation=%s for_size=%s -> %s", orientation, for_size, measured) + return measured + except Exception: + self._logger.exception("PushButton base do_measure failed", exc_info=True) + + text = str(getattr(self, "_label", "") or "") + text_len = max(1, len(text.replace("_", ""))) + if orientation == Gtk.Orientation.HORIZONTAL: + minimum_size = max(48, text_len * 6 + 18) + natural_size = max(minimum_size, text_len * 9 + 28) + else: + minimum_size = 30 + natural_size = 34 + self._logger.debug( + "PushButton fallback do_measure orientation=%s for_size=%s -> min=%s nat=%s", + orientation, + for_size, + minimum_size, + natural_size, + ) + return (minimum_size, natural_size, -1, -1) def _create_backend_widget(self): if self._icon_only: self._logger.info(f"Creating icon-only button '{self._label}'") - self._backend_widget = Gtk.Button() + self._backend_widget = _YPushButtonMeasure(self) self._backend_widget.set_use_underline(True) if self._icon_name: self._backend_widget.set_icon_name(self._icon_name) else: self._logger.info(f"Creating button with icon and label '{self._label}'") try: - self._backend_widget = Gtk.Button(label=self._label) + self._backend_widget = _YPushButtonMeasure(self, label=self._label) self._backend_widget.set_use_underline(True) hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) if self._icon_name: diff --git a/manatools/aui/backends/gtk/richtextgtk.py b/manatools/aui/backends/gtk/richtextgtk.py index 58974f4..ff2fdab 100644 --- a/manatools/aui/backends/gtk/richtextgtk.py +++ b/manatools/aui/backends/gtk/richtextgtk.py @@ -17,7 +17,39 @@ from ...yui_common import * + +class _YRichTextMeasureScrolledWindow(Gtk.ScrolledWindow): + """Gtk.ScrolledWindow subclass delegating size measurement to YRichTextGtk.""" + + def __init__(self, owner): + """Initialize the measuring scrolled window. + + Args: + owner: Owning YRichTextGtk instance. + """ + super().__init__() + self._owner = owner + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + try: + return self._owner.do_measure(orientation, for_size) + except Exception: + self._owner._logger.exception("RichText backend do_measure delegation failed", exc_info=True) + return (0, 0, -1, -1) + + class YRichTextGtk(YWidget): + """GTK4 rich text widget with plain/markup rendering and link activation.""" + def __init__(self, parent=None, text: str = "", plainTextMode: bool = False): super().__init__(parent) self._text = text or "" @@ -182,6 +214,47 @@ def setAutoScrollDown(self, on: bool = True): def lastActivatedUrl(self): return self._last_url + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + widget = getattr(self, "_backend_widget", None) + if widget is not None: + try: + minimum_size, natural_size, minimum_baseline, natural_baseline = Gtk.ScrolledWindow.do_measure(widget, orientation, for_size) + if orientation == Gtk.Orientation.HORIZONTAL: + minimum_baseline = -1 + natural_baseline = -1 + measured = (minimum_size, natural_size, minimum_baseline, natural_baseline) + self._logger.debug("RichText do_measure orientation=%s for_size=%s -> %s", orientation, for_size, measured) + return measured + except Exception: + self._logger.exception("RichText base do_measure failed", exc_info=True) + + text = str(getattr(self, "_text", "") or "") + line_count = max(1, text.count("\n") + 1) + longest_line = max((len(line) for line in text.splitlines()), default=len(text)) + if orientation == Gtk.Orientation.HORIZONTAL: + minimum_size = 160 + natural_size = max(minimum_size, min(900, max(220, longest_line * 7))) + else: + minimum_size = max(72, min(240, line_count * 18)) + natural_size = max(minimum_size, min(720, line_count * 22)) + self._logger.debug( + "RichText fallback do_measure orientation=%s for_size=%s -> min=%s nat=%s", + orientation, + for_size, + minimum_size, + natural_size, + ) + return (minimum_size, natural_size, -1, -1) + def _create_content(self): # Create the content widget according to mode try: @@ -253,7 +326,8 @@ def _on_activate_link(label, uri): self._content_widget = Gtk.Label(label=self._text) def _create_backend_widget(self): - sw = Gtk.ScrolledWindow() + """Create scrolled backend and attach rich text content widget.""" + sw = _YRichTextMeasureScrolledWindow(self) try: # let size policy decide final expand/align; start with sane defaults sw.set_hexpand(True) diff --git a/manatools/aui/backends/gtk/vboxgtk.py b/manatools/aui/backends/gtk/vboxgtk.py index c6a05aa..22a8491 100644 --- a/manatools/aui/backends/gtk/vboxgtk.py +++ b/manatools/aui/backends/gtk/vboxgtk.py @@ -20,7 +20,39 @@ import logging from ...yui_common import * + +class _YVBoxMeasureBox(Gtk.Box): + """Gtk.Box subclass delegating size requests to YVBoxGtk.""" + + def __init__(self, owner): + """Initialize the measuring box. + + Args: + owner: Owning YVBoxGtk instance. + """ + super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5) + self._owner = owner + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + try: + return self._owner.do_measure(orientation, for_size) + except Exception: + self._owner._logger.exception("VBox backend do_measure delegation failed", exc_info=True) + return (0, 0, -1, -1) + + class YVBoxGtk(YWidget): + """Vertical GTK4 container with weight-aware geometry management.""" + def __init__(self, parent=None): super().__init__(parent) self._logger = logging.getLogger(f"manatools.aui.gtk.{self.__class__.__name__}") @@ -37,8 +69,68 @@ def stretchable(self, dim): return True return False + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + children = list(getattr(self, "_children", []) or []) + if not children: + return (0, 0, -1, -1) + + spacing = 5 + try: + if getattr(self, "_backend_widget", None) is not None and hasattr(self._backend_widget, "get_spacing"): + spacing = int(self._backend_widget.get_spacing()) + except Exception: + self._logger.exception("Failed to read VBox spacing for measure", exc_info=True) + spacing = 5 + + minimum_size = 0 + natural_size = 0 + maxima_minimum = 0 + maxima_natural = 0 + + for child in children: + try: + child_widget = child.get_backend_widget() + cmin, cnat, _cbase_min, _cbase_nat = child_widget.measure(orientation, for_size) + except Exception: + self._logger.exception("VBox measure failed for child %s", getattr(child, "debugLabel", lambda: "")(), exc_info=True) + cmin, cnat = 0, 0 + + if orientation == Gtk.Orientation.VERTICAL: + minimum_size += int(cmin) + natural_size += int(cnat) + else: + maxima_minimum = max(maxima_minimum, int(cmin)) + maxima_natural = max(maxima_natural, int(cnat)) + + if orientation == Gtk.Orientation.VERTICAL: + gap_total = max(0, len(children) - 1) * spacing + minimum_size += gap_total + natural_size += gap_total + else: + minimum_size = maxima_minimum + natural_size = maxima_natural + + self._logger.debug( + "VBox do_measure orientation=%s for_size=%s -> min=%s nat=%s", + orientation, + for_size, + minimum_size, + natural_size, + ) + return (minimum_size, natural_size, -1, -1) + def _create_backend_widget(self): - self._backend_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + """Create backend widget and configure weight/stretch behavior.""" + self._backend_widget = _YVBoxMeasureBox(self) # Collect children first so we can apply weight-based heuristics children = list(self._children) @@ -118,15 +210,23 @@ def _apply_vweights(*args): _apply_vweights() except Exception: pass - try: - def _on_size_allocate(widget, allocation): - try: - _apply_vweights() - except Exception: - pass - self._backend_widget.connect('size-allocate', _on_size_allocate) - except Exception: - pass + def _on_resize_update(*_args): + try: + _apply_vweights() + except Exception: + self._logger.exception("_on_resize_update: failed", exc_info=True) + + connected = False + for signal_name in ("size-allocate", "notify::height", "notify::height-request"): + try: + self._backend_widget.connect(signal_name, _on_resize_update) + connected = True + self._logger.debug("Connected VBox resize hook using signal '%s'", signal_name) + break + except Exception: + continue + if not connected: + self._logger.debug("No supported resize signal found for VBox; dynamic weight refresh disabled") except Exception: pass From 6be1c62e8c3ffd8957bdf7e9fa3d0febaaaaaf59 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:06:55 +0100 Subject: [PATCH 510/523] better axpect on qt size management --- manatools/ui/common.py | 109 ++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/manatools/ui/common.py b/manatools/ui/common.py index 8fc4468..a1da19c 100644 --- a/manatools/ui/common.py +++ b/manatools/ui/common.py @@ -93,6 +93,60 @@ def _extract_dialog_size(info): return (width, height) return None + +def _create_message_text_widget(factory, parent, text, richtext): + """Create a compact text widget for message dialogs. + + On Qt backend, YRichText is implemented with QTextBrowser whose intrinsic + minimum size is relatively large, causing popup dialogs with small size + hints to grow significantly. For compact message dialogs we prefer a wrapped + label on Qt while preserving rich text markup rendering. + + Args: + factory: Active YUI widget factory. + parent: Parent widget/container. + text: Message text or markup. + richtext: Whether rich text rendering is requested. + + Returns: + YWidget: Created text widget. + """ + backend_name = "" + try: + backend_name = str(getattr(yui.YUI.backend(), "value", "") or "").lower() + except Exception: + backend_name = "" + + if richtext and backend_name == "qt": + tw = factory.createLabel(parent, text) + try: + tw.setAutoWrap(True) + except Exception: + pass + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, False) + except Exception: + pass + return tw + + if richtext: + tw = factory.createRichText(parent, "", False) + tw.setValue(text) + else: + tw = factory.createLabel(parent, text) + try: + tw.setAutoWrap(True) + except Exception: + pass + + try: + tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) + tw.setStretchable(yui.YUIDimension.YD_VERT, False) + except Exception: + pass + return tw + def warningMsgBox (info) : ''' This function creates a Warning dialog and shows the message passed as input. @@ -143,16 +197,7 @@ def warningMsgBox (info) : pass # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + tw = _create_message_text_widget(factory, row, text, rt) # Ok button on the right btns = factory.createHBox(vbox) @@ -228,16 +273,7 @@ def infoMsgBox (info) : pass # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + tw = _create_message_text_widget(factory, row, text, rt) # Ok button on the right btns = factory.createHBox(vbox) @@ -302,16 +338,7 @@ def msgBox (info) : row = factory.createHBox(vbox) # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + tw = _create_message_text_widget(factory, row, text, rt) # Ok button on the right btns = factory.createHBox(vbox) @@ -393,16 +420,7 @@ def askOkCancel (info) : pass # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + tw = _create_message_text_widget(factory, row, text, rt) # Buttons on the right btns = factory.createHBox(vbox) @@ -492,16 +510,7 @@ def askYesOrNo (info) : pass # Text widget - if rt: - tw = factory.createRichText(row, "", False) - tw.setValue(text) - else: - tw = factory.createLabel(row, text) - try: - tw.setStretchable(yui.YUIDimension.YD_HORIZ, True) - tw.setStretchable(yui.YUIDimension.YD_VERT, True) - except Exception: - pass + tw = _create_message_text_widget(factory, row, text, rt) # Buttons on the right btns = factory.createHBox(vbox) From e788a456a517b364ec07a097dd6a2ddf8f127c69 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:07:44 +0100 Subject: [PATCH 511/523] Layout on help dialog --- manatools/ui/helpdialog.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/manatools/ui/helpdialog.py b/manatools/ui/helpdialog.py index 94d179e..2632a91 100644 --- a/manatools/ui/helpdialog.py +++ b/manatools/ui/helpdialog.py @@ -83,12 +83,16 @@ def UIlayout(self, layout): self.text = self.factory.createRichText(content_parent, "", False) self.text.setStretchable(yui.YUIDimension.YD_HORIZ, True) self.text.setStretchable(yui.YUIDimension.YD_VERT, True) + self.text.setWeight(yui.YUIDimension.YD_HORIZ, 1) + self.text.setWeight(yui.YUIDimension.YD_VERT, 1) self.text.setValue(self.info.home()) logger.debug("Initial help content loaded") button_row = self.factory.createHBox(content_parent) - self.factory.createHStretch(button_row) - self.quitButton = self.factory.createPushButton(button_row, _("Quit")) + button_row.setStretchable(yui.YUIDimension.YD_HORIZ, True) + button_row.setWeight(yui.YUIDimension.YD_HORIZ, 1) + right_align = self.factory.createRight(button_row) + self.quitButton = self.factory.createPushButton(right_align, _("Quit")) self.eventManager.addWidgetEvent(self.quitButton, self.onQuitEvent) logger.debug("Quit button registered") From 9339db770f09564cf83a6d551f5e93c20f464c32 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:08:12 +0100 Subject: [PATCH 512/523] Minimum size on dialogs to test it works --- test/testCommon.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/test/testCommon.py b/test/testCommon.py index b895b98..82289e0 100644 --- a/test/testCommon.py +++ b/test/testCommon.py @@ -149,21 +149,34 @@ def onPressWarning(self) : Warning button call back ''' print ('Button "Warning" pressed') - wd = common.warningMsgBox({"title" : "Warning Dialog", "text": "Warning button has been pressed!", "richtext" : True}) + wd = common.warningMsgBox({ + "title" : "Warning Dialog", + "text": "Warning button has been pressed!", + "size": (300, 120), + "richtext" : True}) def onPressInformation(self) : ''' Information button call back ''' print ('Button "Information" pressed') - id = common.infoMsgBox({"title" : "Information Dialog", "text": "Information button has been pressed!", "richtext" : True}) + id = common.infoMsgBox({ + "title" : "Information Dialog", + "text": "Information button has been pressed!", + "size": (300, 120), + "richtext" : True}) def onPressOkCancel(self) : ''' Ok/Cancel button call back ''' print ('Button "Ok/Cancel Dialog" pressed') - ok = common.askOkCancel({"title": "Ok/Cancel Dialog", "text": "To proceed, click OK or Cancel to skip.", "richtext" : True }) + ok = common.askOkCancel({ + "title": "Ok/Cancel Dialog", + "text": "To proceed, click OK or Cancel to skip.", + "size": (300, 120), + "richtext" : True + }) print ("User selected: %s" % ("OK" if ok else "Cancel")) def onCancelEvent(self) : @@ -173,7 +186,11 @@ def onQuitEvent(self) : ''' Quit button call back ''' - ok = common.askYesOrNo({"title": "Quit confirmation", "text": "Do you really want to quit?", "richtext" : True }) + ok = common.askYesOrNo({ + "title": "Quit confirmation", + "text": "Do you really want to quit?", + "size": (300, 120), + "richtext" : True }) print ("Quit button pressed") # BaseDialog needs to force to exit the handle event loop if ok: From 13c1a88d72243a9ad0d648ff247518e4ee984cea Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:08:46 +0100 Subject: [PATCH 513/523] do not force 640x480 size --- manatools/aui/backends/qt/dialogqt.py | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/qt/dialogqt.py b/manatools/aui/backends/qt/dialogqt.py index 1f6eeb1..09d7998 100644 --- a/manatools/aui/backends/qt/dialogqt.py +++ b/manatools/aui/backends/qt/dialogqt.py @@ -161,6 +161,12 @@ def setDefaultButton(self, button): return True def _create_backend_widget(self): + """Create the Qt window and initialize title, icon, content, and initial size. + + Popup dialogs should honor content-driven minimum size hints (e.g. from + createMinSize in BaseDialog) instead of forcing a large default size. + Main dialogs keep a larger default for usability. + """ self._qwidget = QtWidgets.QMainWindow() # Determine window title:from YApplicationQt instance stored on the YUI backend title = "Manatools Qt Dialog" @@ -203,7 +209,6 @@ def _create_backend_widget(self): self._qwidget.setWindowIcon(_resolved_qicon) except Exception: pass - self._qwidget.resize(600, 400) central_widget = QtWidgets.QWidget() self._qwidget.setCentralWidget(central_widget) @@ -214,6 +219,11 @@ def _create_backend_widget(self): layout.addWidget(self.child().get_backend_widget()) # If the child is a layout box with a menubar as first child, Qt can display QMenuBar inline. # Alternatively, backends may add YMenuBarQt directly to layout. + + try: + self._apply_initial_size() + except Exception: + self._logger.exception("Failed to apply initial dialog size", exc_info=True) self._backend_widget = self._qwidget self._qwidget.closeEvent = self._on_close_event @@ -222,6 +232,36 @@ def _create_backend_widget(self): self._logger.debug("_create_backend_widget: <%s>", self.debugLabel()) except Exception: pass + + def _apply_initial_size(self): + """Apply initial size policy based on dialog type and content hints. + + - Main dialogs: keep a generous default size. + - Popup dialogs: size to content/minimum hints from the layout tree. + """ + if getattr(self, "_qwidget", None) is None: + return + + if self._dialog_type == YDialogType.YMainDialog: + try: + self._qwidget.resize(600, 400) + except Exception: + self._logger.exception("Failed to set default main-dialog size", exc_info=True) + return + + # Popup: derive initial size from content to better match GTK behavior. + try: + self._qwidget.adjustSize() + except Exception: + self._logger.exception("adjustSize failed for popup dialog", exc_info=True) + + try: + hint = self._qwidget.sizeHint() + if hint is not None and hint.isValid(): + self._qwidget.resize(hint) + self._logger.debug("Applied popup size from sizeHint: %sx%s", hint.width(), hint.height()) + except Exception: + self._logger.exception("Failed to apply popup sizeHint", exc_info=True) def _set_backend_enabled(self, enabled): """Enable/disable the dialog window and propagate to logical child widgets.""" From 073c502409ccf2dac055e6eb709859852e98c72c Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:13:59 +0100 Subject: [PATCH 514/523] pass-through mode for unchanged/fill alignment --- manatools/aui/backends/gtk/alignmentgtk.py | 79 ++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/manatools/aui/backends/gtk/alignmentgtk.py b/manatools/aui/backends/gtk/alignmentgtk.py index 290515a..e599652 100644 --- a/manatools/aui/backends/gtk/alignmentgtk.py +++ b/manatools/aui/backends/gtk/alignmentgtk.py @@ -44,6 +44,9 @@ def __init__(self, parent=None, horAlign: YAlignmentType=YAlignmentType.YAlignUn self._backend_widget = None # get a reference to the single row container self._row = [] + # pass-through mode for unchanged/fill alignment + self._fill_direct_mode = False + self._direct_child_widget = None # schedule guard for deferred attach self._attach_scheduled = False # Track if we've already attached a child @@ -216,7 +219,83 @@ def _ensure_child_attached(self): hal = self._to_gtk_halign() val = self._to_gtk_valign() + # Pass-through mode: when both alignments are unchanged/fill, this + # container should not center the child but let it fill all available + # space. CenterBox-based placement would visually center content. + if hal == Gtk.Align.FILL and val == Gtk.Align.FILL: + try: + # switch to direct-child mode by removing row containers once + if not self._fill_direct_mode: + for row in list(self._row): + try: + self._backend_widget.remove(row) + except Exception: + pass + self._fill_direct_mode = True + + # replace previous direct child if present + old_direct = getattr(self, "_direct_child_widget", None) + if old_direct is not None and old_direct is not cw: + try: + self._backend_widget.remove(old_direct) + except Exception: + pass + + try: + cw.set_halign(Gtk.Align.FILL) + cw.set_valign(Gtk.Align.FILL) + cw.set_hexpand(True) + cw.set_vexpand(True) + except Exception: + pass + + # enforce minimum size on child if requested + try: + mw = getattr(self, '_min_width_px', 0) + mh = getattr(self, '_min_height_px', 0) + if (mw and mw > 0) or (mh and mh > 0): + w_req = int(mw) if mw and mw > 0 else -1 + h_req = int(mh) if mh and mh > 0 else -1 + try: + cw.set_size_request(w_req, h_req) + except Exception: + pass + except Exception: + pass + + if cw.get_parent() is not self._backend_widget: + try: + self._backend_widget.append(cw) + except Exception: + pass + self._direct_child_widget = cw + self._child_attached = True + self._logger.debug("Successfully attached child %s %s [fill-direct]", child.widgetClass(), child.debugLabel()) + return + except Exception as e: + self._logger.error("Error in fill-direct alignment mode: %s", e, exc_info=True) + try: + # if previously in direct mode, restore row containers + if self._fill_direct_mode: + try: + old_direct = getattr(self, "_direct_child_widget", None) + if old_direct is not None: + try: + self._backend_widget.remove(old_direct) + except Exception: + pass + except Exception: + pass + for row in list(self._row): + try: + if row.get_parent() is None: + self._backend_widget.append(row) + except Exception: + pass + self._fill_direct_mode = False + self._direct_child_widget = None + # Determine row index based on vertical alignment row_index = 0 if val == Gtk.Align.START else 2 if val == Gtk.Align.END else 1 # center default From a6e9efafc6cb4ae7ea376914966eee43934b08f5 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:14:23 +0100 Subject: [PATCH 515/523] Height for width to try improving gtk image no that success though --- manatools/aui/backends/gtk/imagegtk.py | 108 ++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/manatools/aui/backends/gtk/imagegtk.py b/manatools/aui/backends/gtk/imagegtk.py index 2b5d152..79fb255 100644 --- a/manatools/aui/backends/gtk/imagegtk.py +++ b/manatools/aui/backends/gtk/imagegtk.py @@ -19,7 +19,38 @@ from .commongtk import _resolve_icon, _resolve_gicon +class _YImageMeasure(Gtk.Image): + """Gtk.Image subclass delegating size measurement to YImageGtk.""" + + def __init__(self, owner): + """Initialize the measuring image widget. + + Args: + owner: Owning YImageGtk instance. + """ + super().__init__() + self._owner = owner + + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + try: + return self._owner.do_measure(orientation, for_size) + except Exception: + self._owner._logger.exception("Image backend do_measure delegation failed", exc_info=True) + return (0, 0, -1, -1) + + class YImageGtk(YWidget): + """GTK4 image widget with optional autoscale and height-for-width measurement.""" + def __init__(self, parent=None, imageFileName=""): super().__init__(parent) self._imageFileName = imageFileName @@ -39,6 +70,16 @@ def setImage(self, imageFileName): try: self._imageFileName = imageFileName if getattr(self, '_backend_widget', None) is not None: + # Prefer direct filesystem loading for absolute/real paths so we keep + # an explicit pixbuf and reliable intrinsic geometry. + if imageFileName and os.path.exists(imageFileName): + try: + self._pixbuf = GdkPixbuf.Pixbuf.new_from_file(imageFileName) + self._apply_pixbuf() + return + except Exception: + self._logger.exception("failed to load pixbuf from file path") + # Try resolving via common GTK helper (may be theme icon or file) try: resolved = _resolve_icon(imageFileName, size=48) @@ -102,9 +143,74 @@ def _on_size_allocate(self, widget, allocation): except Exception: self._logger.exception("_on_size_allocate failed") + def do_measure(self, orientation, for_size): + """GTK4 virtual method for size measurement. + + Args: + orientation: Gtk.Orientation (HORIZONTAL or VERTICAL) + for_size: Size in the opposite orientation (-1 if not constrained) + + Returns: + tuple: (minimum_size, natural_size, minimum_baseline, natural_baseline) + """ + pixbuf = getattr(self, "_pixbuf", None) + if pixbuf is not None: + try: + pix_w = max(1, int(pixbuf.get_width())) + pix_h = max(1, int(pixbuf.get_height())) + if orientation == Gtk.Orientation.HORIZONTAL: + if for_size is not None and int(for_size) > 0: + natural_size = max(1, int((int(for_size) * pix_w) / pix_h)) + else: + natural_size = pix_w + minimum_size = 0 if self.hasZeroSize(YUIDimension.YD_HORIZ) else natural_size + else: + if for_size is not None and int(for_size) > 0: + natural_size = max(1, int((int(for_size) * pix_h) / pix_w)) + else: + natural_size = pix_h + minimum_size = 0 if self.hasZeroSize(YUIDimension.YD_VERT) else natural_size + self._logger.debug( + "Image fallback do_measure orientation=%s for_size=%s -> min=%s nat=%s (pix=%sx%s)", + orientation, + for_size, + minimum_size, + natural_size, + pix_w, + pix_h, + ) + return (minimum_size, natural_size, -1, -1) + except Exception: + self._logger.exception("Image fallback do_measure from pixbuf failed", exc_info=True) + + widget = getattr(self, "_backend_widget", None) + if widget is not None: + try: + minimum_size, natural_size, _minimum_baseline, _natural_baseline = Gtk.Image.do_measure(widget, orientation, for_size) + measured = (minimum_size, natural_size, -1, -1) + self._logger.debug("Image base do_measure orientation=%s for_size=%s -> %s", orientation, for_size, measured) + return measured + except Exception: + self._logger.exception("Image base do_measure failed", exc_info=True) + + if orientation == Gtk.Orientation.HORIZONTAL: + minimum_size = 0 if self.hasZeroSize(YUIDimension.YD_HORIZ) else 16 + natural_size = max(minimum_size, 32) + else: + minimum_size = 0 if self.hasZeroSize(YUIDimension.YD_VERT) else 16 + natural_size = max(minimum_size, 32) + self._logger.debug( + "Image generic fallback do_measure orientation=%s for_size=%s -> min=%s nat=%s", + orientation, + for_size, + minimum_size, + natural_size, + ) + return (minimum_size, natural_size, -1, -1) + def _create_backend_widget(self): try: - self._backend_widget = Gtk.Image() + self._backend_widget = _YImageMeasure(self) if self._imageFileName: # Use setImage to allow theme icon resolution via commongtk._resolve_icon try: From 647d80bbaf6ef69c8d65381f339cacd2ff8fc3cf Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:15:21 +0100 Subject: [PATCH 516/523] Added logging --- test/test_aligment.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/test_aligment.py b/test/test_aligment.py index f488c4b..182bce9 100644 --- a/test/test_aligment.py +++ b/test/test_aligment.py @@ -2,10 +2,34 @@ import os import sys +import logging # Add parent directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +# Configure file logger for this test: write DEBUG logs to '.log' in cwd +try: + log_name = os.path.splitext(os.path.basename(__file__))[0] + '.log' + fh = logging.FileHandler(log_name, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')) + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + existing = False + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) and os.path.abspath(getattr(h, 'baseFilename', '')) == os.path.abspath(log_name): + existing = True + break + except Exception: + pass + if not existing: + root_logger.addHandler(fh) + print(f"Logging test output to: {os.path.abspath(log_name)}") +except Exception as _e: + print(f"Failed to configure file logger: {_e}") + + def test_Alignment(backend_name=None): if backend_name: print(f"Setting backend to: {backend_name}") From 08ea4473f0af8f7675fb0577f11b6d219d49a32d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:16:30 +0100 Subject: [PATCH 517/523] changed layout --- test/test_datetime_fields.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/test_datetime_fields.py b/test/test_datetime_fields.py index 2759b9d..cbdfd48 100644 --- a/test/test_datetime_fields.py +++ b/test/test_datetime_fields.py @@ -55,18 +55,24 @@ def test_datefield(backend_name=None): dialog = factory.createMainDialog() vbox = factory.createVBox(dialog) - factory.createHeading(vbox, "Date/TimeField Test") + factory.createHeading(vbox, "Date/Time Field Test") factory.createLabel(vbox, f"Backend: {backend.value}") # Create datefield - df = factory.createDateField(vbox, "Select Date:") + hbox = factory.createHBox(vbox) + df = factory.createDateField(hbox, "Select Date:") + #df.setStretchable(yui.YUIDimension.YD_HORIZ, True) now = datetime.datetime.now() df.setValue(now.strftime("%Y-%m-%d")) + factory.createLabel(hbox, "right widget") # Create timefield try: - tf = factory.createTimeField(vbox, "Select Time:") + hbox = factory.createHBox(vbox) + tf = factory.createTimeField(hbox, "Select Time:") + #tf.setStretchable(yui.YUIDimension.YD_HORIZ, True) tf.setValue(now.strftime("%H:%M:%S")) + factory.createLabel(hbox, "right widget") except Exception as e: tf = None logging.getLogger(__name__).exception("Failed to create TimeField: %s", e) From e60a1ac7e2e1a9ca9a4edc051a0c3ec340dcda3d Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:19:29 +0100 Subject: [PATCH 518/523] changed layout --- test/test_image.py | 245 ++++++++++++++++++++++++++++++++++--- test/test_multi_backend.py | 7 +- test/test_tree_example.py | 2 +- 3 files changed, 235 insertions(+), 19 deletions(-) diff --git a/test/test_image.py b/test/test_image.py index 14c70e9..bd9fe28 100644 --- a/test/test_image.py +++ b/test/test_image.py @@ -49,8 +49,8 @@ def test_image(backend_name=None): ui = YUI_ui() factory = ui.widgetFactory() - dialog = factory.createPopupDialog() - vbox = factory.createVBox(dialog) + dlg = factory.createPopupDialog() + vbox = factory.createVBox(dlg) factory.createHeading(vbox, "Image widget demo") # Use an example image path if available (relative to project), otherwise empty @@ -61,40 +61,255 @@ def test_image(backend_name=None): images = [example, "system-software-install"] - img = factory.createImage(vbox, example) - # allow image to expand horizontally - img.setStretchable(yui.YUIDimension.YD_HORIZ, True) - img.setStretchable(yui.YUIDimension.YD_VERT, True) - img.setAutoScale(True) + # Create a MinSize wrapper so we can enforce a minimum visible size: + # start with a conservative minimum (width x height in chars) + min_w = 40 + min_h = 6 + min_container = factory.createMinSize(vbox, min_w, min_h) + img = factory.createImage(min_container, "/usr/share/isodumper/header.png") + # Keep references for interactive toggles + current_mode = {"auto_scale": True, "vert_stretch": True, "min_h": min_h} - # OK button + # helper to inspect backend widget (best-effort multi-backend) + def inspect_backend_widget(w): + info = {} + try: + info['type'] = type(w).__name__ + except Exception: + info['type'] = str(w) + # try Qt style + try: + if hasattr(w, 'size') and callable(getattr(w, 'size')): + s = w.size() + try: + info['w'] = s.width() + info['h'] = s.height() + except Exception: + try: + info['w'] = s.width + info['h'] = s.height + except Exception: + pass + except Exception: + pass + # try Gtk style + try: + if hasattr(w, 'get_allocated_width'): + info['w'] = w.get_allocated_width() + if hasattr(w, 'get_allocated_height'): + info['h'] = w.get_allocated_height() + except Exception: + pass + # try curses placeholder + try: + if hasattr(w, 'widgetClass') and w.widgetClass() == 'YImageCurses': + info['curses'] = True + except Exception: + pass + return info + + def log_image_state(prefix=""): + try: + root_logger.debug("%s Image api: imageFileName=%r", prefix, (img.imageFileName() if hasattr(img, "imageFileName") else None)) + except Exception: + root_logger.debug("%s Image api: imageFileName=", prefix) + try: + root_logger.debug("%s Image api: autoScale=%r", prefix, (img.autoScale() if hasattr(img, "autoScale") else "")) + except Exception: + root_logger.debug("%s Image api: autoScale=", prefix) + try: + root_logger.debug("%s Image api: stretchable_vert=%r", prefix, bool(img.stretchable(yui.YUIDimension.YD_VERT))) + except Exception: + root_logger.debug("%s Image api: stretchable_vert=", prefix) + try: + # Min container introspection + mw = getattr(min_container, 'minHeight', None) + if callable(mw): + root_logger.debug("%s Min container minHeight()=%r", prefix, mw()) + else: + # sometimes stored as attribute + root_logger.debug("%s Min container min_h attr=%r", prefix, getattr(min_container, '_minHeight', '')) + except Exception: + root_logger.debug("%s Min container: ", prefix) + # backend widget inspection + try: + be = None + try: + be = img.get_backend_widget() + except Exception: + try: + be = img._backend_widget + except Exception: + be = None + if be is not None: + info = inspect_backend_widget(be) + root_logger.debug("%s Backend widget info: %s", prefix, info) + else: + root_logger.debug("%s Backend widget: None", prefix) + except Exception: + root_logger.debug("%s Backend inspection failed", prefix) + + # mode application routine + def apply_mode(name, autoscale, vert_stretch, min_h): + try: + # autoscale + try: + if hasattr(img, "setAutoScale"): + img.setAutoScale(bool(autoscale)) + except Exception: + pass + # vertical stretch + try: + img.setStretchable(yui.YUIDimension.YD_VERT, bool(vert_stretch)) + except Exception: + # fallback: setStretchable with older naming + try: + img.setStretchable(yui.YUIDimension.YD_VERT, vert_stretch) + except Exception: + pass + # set minimum height on the container + try: + if hasattr(min_container, "setMinHeight"): + min_container.setMinHeight(int(min_h)) + else: + # try setMinSize if available + if hasattr(min_container, "setMinSize"): + min_container.setMinSize(getattr(min_container, "minWidth", min_w), int(min_h)) + except Exception: + pass + + current_mode.update({"auto_scale": autoscale, "vert_stretch": vert_stretch, "min_h": min_h}) + log_image_state(prefix=f"APPLY_MODE {name}:") + except Exception as e: + root_logger.exception("Failed applying mode %s: %s", name, e) + + # Prepare three typical modes to reproduce behaviour: + modes = [ + ("autoscale=ON, vert_stretch=ON, min_h=6", True, True, 6), + ("autoscale=ON, vert_stretch=OFF, min_h=6", True, False, 6), + ("autoscale=OFF, vert_stretch=OFF, min_h=6", False, False, 6), + ] + + # Controls: AutoScale + Stretch flags + MinHeight presets + ctrl = factory.createFrame(vbox, "Controls") + ctrl_v = factory.createVBox(ctrl) + desc = factory.createLabel(ctrl_v, "Autoscale keeps aspect. Stretch H/V lets the widget expand in width/height. With AutoScale=ON, expansion preserves ratio.") + toggles = factory.createHBox(ctrl_v) + chk_auto = factory.createCheckBox(toggles, "AutoScale", True) + chk_h = factory.createCheckBox(toggles, "Stretch H", True) + chk_v = factory.createCheckBox(toggles, "Stretch V", True) + mhbox = factory.createHBox(ctrl_v) + btn_min6 = factory.createPushButton(factory.createLeft(mhbox), "MinHeight: 6") + btn_min10 = factory.createPushButton(factory.createLeft(mhbox), "MinHeight: 10") + + status = factory.createLabel(vbox, "Status: size=?, pix=?, mode=?") + + # Apply initial state + try: + img.setAutoScale(True) + except Exception: + pass + try: + img.setStretchable(yui.YUIDimension.YD_HORIZ, True) + img.setStretchable(yui.YUIDimension.YD_VERT, True) + except Exception: + pass + try: + min_container.setMinHeight(min_h) + except Exception: + pass + + def backend_sizes(): + # best-effort introspection + try: + be = img.get_backend_widget() + except Exception: + be = None + w = h = pw = ph = None + if be is not None: + try: + s = be.size() + w = getattr(s, "width")() if callable(getattr(s, "width", None)) else getattr(s, "width", None) + h = getattr(s, "height")() if callable(getattr(s, "height", None)) else getattr(s, "height", None) + except Exception: + pass + # image API might not expose pix size; rely on backend size as proxy + return w, h, pw, ph + + def update_status(prefix=""): + try: + w, h, pw, ph = backend_sizes() + mode = f"auto={getattr(img, 'autoScale', lambda: None)()} H={img.stretchable(yui.YUIDimension.YD_HORIZ)} V={img.stretchable(yui.YUIDimension.YD_VERT)}" + status.setText(f"{prefix} Status: widget=({w}x{h}) pix=({pw}x{ph}) {mode}") + except Exception: + pass + + update_status("Init:") + + # Existing buttons: toggle image and close hbox = factory.createHBox(vbox) - toggle = factory.createPushButton(hbox, "Toggle Image") - close = factory.createPushButton(hbox, "Close") + toggle = factory.createPushButton(factory.createLeft(hbox), "Toggle Image") + close = factory.createPushButton(factory.createRight(hbox), "Close") - dialog.open() + dlg.open() while True: - event = dialog.waitForEvent() + event = dlg.waitForEvent() if not event: continue typ = event.eventType() if typ == yui.YEventType.CancelEvent: - dialog.destroy() + dlg.destroy() break elif typ == yui.YEventType.WidgetEvent: wdg = event.widget() + reason = event.reason() if wdg == close: - dialog.destroy() + dlg.destroy() break elif wdg == toggle: img.setImage(images[1] if img.imageFileName() == images[0] else images[0]) + update_status("Toggle:") + elif wdg == chk_auto and reason == yui.YEventReason.ValueChanged: + try: + img.setAutoScale(chk_auto.value()) + except Exception: + pass + update_status("Auto:") + elif wdg == chk_h and reason == yui.YEventReason.ValueChanged: + try: + img.setStretchable(yui.YUIDimension.YD_HORIZ, chk_h.value()) + except Exception: + pass + update_status("StretchH:") + elif wdg == chk_v and reason == yui.YEventReason.ValueChanged: + try: + img.setStretchable(yui.YUIDimension.YD_VERT, chk_v.value()) + except Exception: + pass + update_status("StretchV:") + elif wdg == btn_min6 and reason == yui.YEventReason.Activated: + try: + min_container.setMinHeight(6) + except Exception: + pass + update_status("MinH=6:") + elif wdg == btn_min10 and reason == yui.YEventReason.Activated: + try: + min_container.setMinHeight(10) + except Exception: + pass + update_status("MinH=10:") + # manual log trigger: if clicking image (some backends may send event) + if wdg == img: + log_image_state(prefix="IMAGE_CLICK:") + root_logger.info("Dialog closed") except Exception as e: print(f"Error testing Image with backend {backend_name}: {e}") import traceback traceback.print_exc() -if __name__ == "__main__": +if __name__ == '__main__': if len(sys.argv) > 1: test_image(sys.argv[1]) else: diff --git a/test/test_multi_backend.py b/test/test_multi_backend.py index a989a31..22dd53b 100644 --- a/test/test_multi_backend.py +++ b/test/test_multi_backend.py @@ -41,8 +41,9 @@ def test_backend(backend_name=None): factory.createLabel(vbox, "ComboBox Test: Use SPACE to expand") factory.createLabel(vbox, "Then use ARROWS and ENTER to select") - # Input fields - input_field = factory.createInputField(vbox, "Username:") + # Input fields: Username and Password (password mode) + input_field = factory.createInputField(vbox, "Username") + password_field = factory.createInputField(vbox, "Password", True) # ComboBox - NEW WIDGET combo = factory.createComboBox(vbox, "Select option:", False) @@ -78,7 +79,7 @@ def test_backend(backend_name=None): elif wdg == combo: selected.setText(f"Selected: '{combo.value()}'") elif wdg == ok_button: - selected.setText(f"OK clicked. - {input_field.value()}") + selected.setText(f"OK clicked. - user='{input_field.value()}' password='{password_field.value()}'") elif wdg == checkbox: selected.setText(f"{checkbox.label()} - {checkbox.value()}") diff --git a/test/test_tree_example.py b/test/test_tree_example.py index 9bac848..756f447 100644 --- a/test/test_tree_example.py +++ b/test/test_tree_example.py @@ -147,7 +147,7 @@ def pick_initial_selections(): # control buttons ctrl_h = factory.createHBox(vbox) swap_btn = factory.createPushButton(ctrl_h, "Swap") - quit_btn = factory.createPushButton(ctrl_h, "Quit") + quit_btn = factory.createPushButton(factory.createRight(ctrl_h), "Quit") # track previous selected labels so we can choose different items after swap prev_left_label = None From 3f3e15112c8d3da77ac9aa4485fd35aabf0c2291 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 14:28:01 +0100 Subject: [PATCH 519/523] checkbox aligned correctly --- manatools/aui/backends/gtk/tablegtk.py | 35 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/manatools/aui/backends/gtk/tablegtk.py b/manatools/aui/backends/gtk/tablegtk.py index 9f8f080..86fa53a 100644 --- a/manatools/aui/backends/gtk/tablegtk.py +++ b/manatools/aui/backends/gtk/tablegtk.py @@ -547,21 +547,36 @@ def _create_checkbox_content(self, cell, align_t, item, col): chk.set_active(cell.checked() if cell is not None else False) except Exception: chk.set_active(False) - - # Apply alignment to checkbox + + # Build an explicit fill wrapper and place checkbox according to + # column alignment. This is more reliable than relying on halign + # on Gtk.CheckButton alone in fixed-width table cells. + wrapper = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + wrapper.set_hexpand(True) + wrapper.set_halign(Gtk.Align.FILL) + + def _hspacer(): + spacer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + spacer.set_hexpand(True) + return spacer + if align_t == YAlignmentType.YAlignCenter: - chk.set_halign(Gtk.Align.CENTER) + wrapper.append(_hspacer()) chk.set_margin_start(0) chk.set_margin_end(0) + wrapper.append(chk) + wrapper.append(_hspacer()) elif align_t == YAlignmentType.YAlignEnd: - chk.set_halign(Gtk.Align.END) + wrapper.append(_hspacer()) chk.set_margin_start(0) - chk.set_margin_end(10) + chk.set_margin_end(6) + wrapper.append(chk) else: - chk.set_halign(Gtk.Align.START) - chk.set_margin_start(10) + chk.set_margin_start(6) chk.set_margin_end(0) - + wrapper.append(chk) + wrapper.append(_hspacer()) + chk.set_valign(Gtk.Align.CENTER) # Connect toggle handler @@ -580,8 +595,8 @@ def _on_toggled(btn, item=item, cindex=col): self._logger.exception("Checkbox toggle failed") chk.connect("toggled", _on_toggled) - - return chk + + return wrapper except Exception: self._logger.exception("Failed to create checkbox content") return Gtk.Label(label="") From 3d57a340a1ab22f21b8cdc3051301c52daa30087 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 16:12:38 +0100 Subject: [PATCH 520/523] updated --- sow/TODO.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/sow/TODO.md b/sow/TODO.md index 887d21b..64dc079 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -37,16 +37,6 @@ Optional/special widgets (from `YOptionalWidgetFactory`): [X] YSlider [X] YDateField [X] YTimeField - [ ] YBarGraph - [ ] YPatternSelector (createPatternSelector) - [ ] YSimplePatchSelector (createSimplePatchSelector) - [ ] YMultiProgressMeter - [ ] YPartitionSplitter - [ ] YDownloadProgress - [ ] YDummySpecialWidget - [ ] YTimezoneSelector - [ ] YGraph - [ ] Context menu support / hasContextMenu To check/review: how to manage YEvents [X] and YItems [X] (verify selection attirbute). @@ -55,15 +45,16 @@ To check/review: [X] askForExistingDirectory [X] askForExistingFile [X] askForSaveFileName - [ ] YAboutDialog (aka YMGAAboutDialog) - [ ] adding factory create alternative methods (e.g. createMultiSelectionBox) + [X] YAboutDialog (aka YMGAAboutDialog) - Implemented in manatools.ui + [X] adding factory create alternative methods (e.g. createMultiSelectionBox) [X] managing shortcuts (only menu and pushbutton) [ ] localization Nice to have: improvements outside YUI API - [ ] window title - [ ] window icons + [X] window title + [X] window icons + [ ] Context menu support [ ] selected YItem(s) in event [ ] Improving YEvents management (adding info on widget event containing data such as item selection/s, checked item, rich text url, etc.) @@ -77,6 +68,16 @@ Skipped widgets: [-] YEmpty (not ported) [-] YSquash / createSquash (not ported) [-] YMenuButton (legacy menus) + [-] YBarGraph + [-] YPatternSelector (createPatternSelector) + [-] YSimplePatchSelector (createSimplePatchSelector) + [-] YMultiProgressMeter + [-] YPartitionSplitter + [-] YDownloadProgress + [-] YDummySpecialWidget + [-] YTimezoneSelector + [-] YGraph + [-] Context menu support / hasContextMenu Documentation gaps and recommendations -------------------------------------- From d12cc5307b0d09ddf2125ab62513f181e318b4a4 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 16:14:00 +0100 Subject: [PATCH 521/523] Updated --- sow/TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sow/TODO.md b/sow/TODO.md index 64dc079..763d144 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -48,6 +48,7 @@ To check/review: [X] YAboutDialog (aka YMGAAboutDialog) - Implemented in manatools.ui [X] adding factory create alternative methods (e.g. createMultiSelectionBox) [X] managing shortcuts (only menu and pushbutton) + [ ] YTable sorting on columns management [ ] localization Nice to have: improvements outside YUI API From 289ac074edb60ee10ee764011256fb41d4971cfa Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 19:19:31 +0100 Subject: [PATCH 522/523] Change windows title after dialog creation to avoid changing application one --- manatools/ui/basedialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manatools/ui/basedialog.py b/manatools/ui/basedialog.py index 2aa5da0..2a247cc 100644 --- a/manatools/ui/basedialog.py +++ b/manatools/ui/basedialog.py @@ -117,13 +117,13 @@ def run(self): ''' run the Dialog ''' + self._setupUI() + self.backupTitle = yui.YUI.app().applicationTitle() yui.YUI.app().setApplicationTitle(self._title) if self._icon: backupIcon = yui.YUI.app().applicationIcon() yui.YUI.app().setApplicationIcon(self._icon) - - self._setupUI() self._running = True self._handleEvents() From e2d50685ed599b0529d0ea0762692a0d375646d2 Mon Sep 17 00:00:00 2001 From: Angelo Naselli Date: Sun, 15 Feb 2026 19:20:46 +0100 Subject: [PATCH 523/523] updated --- sow/TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sow/TODO.md b/sow/TODO.md index 763d144..6ee5cc3 100644 --- a/sow/TODO.md +++ b/sow/TODO.md @@ -55,6 +55,7 @@ Nice to have: improvements outside YUI API [X] window title [X] window icons + [ ] window title at window/dialog level [ ] Context menu support [ ] selected YItem(s) in event [ ] Improving YEvents management (adding info on widget event containing data