From b3f06710791d606bb212104c23bc6f1500628342 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 10 Feb 2026 22:58:07 +0000 Subject: [PATCH 1/5] Pass the MessageInfo object through to reply callbacks, not just the numeric code --- riscos_toolbox/events.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/riscos_toolbox/events.py b/riscos_toolbox/events.py index 8c779d2..0419d6b 100644 --- a/riscos_toolbox/events.py +++ b/riscos_toolbox/events.py @@ -388,18 +388,19 @@ def decorator(handler): @wraps((handler, _message_map)) def wrapper(self, data, *args): message = None - code = None + info = None if data is not None: + info = data[0] message = ctypes.cast( - data, ctypes.POINTER(UserMessage) + data[1], ctypes.POINTER(UserMessage) ).contents code = message.code if code in _message_map: message = ctypes.cast( - data, ctypes.POINTER(_message_map[code]) + data[1], ctypes.POINTER(_message_map[code]) ).contents - return handler(self, code, message, *args) + return handler(self, info, message, *args) return wrapper return decorator @@ -429,13 +430,13 @@ def toolbox_dispatch(event_code, application, id_block, poll_block): def message_dispatch(code, application, id_block, poll_block): if code.your_ref in _reply_callbacks: - r = _reply_callbacks[code.your_ref](poll_block) + r = _reply_callbacks[code.your_ref]((code, poll_block)) del _reply_callbacks[code.your_ref] if r is not False: return if code.reason == Wimp.UserMessageAcknowledge and code.my_ref in _reply_callbacks: - r = _reply_callbacks[code.my_ref](poll_block) + r = _reply_callbacks[code.my_ref]((code, poll_block)) del _reply_callbacks[code.my_ref] if r is not False: return From c5867a83186061147200bbc37107dcd7d96bd33e Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 4 Mar 2026 22:49:21 +0000 Subject: [PATCH 2/5] Remove repetition in calls to _send --- riscos_toolbox/events.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/riscos_toolbox/events.py b/riscos_toolbox/events.py index 0419d6b..7531eea 100644 --- a/riscos_toolbox/events.py +++ b/riscos_toolbox/events.py @@ -171,8 +171,7 @@ def broadcast(self, recorded=False, size=None, reply_callback=None): """Sends the message as a broadcast.""" self.your_ref = 0 - self._send(Wimp.UserMessageRecorded if recorded else Wimp.UserMessage, - None, None, size, reply_callback) + self._send(recorded, None, None, size, reply_callback) def send(self, task=None, window=None, iconbar=None, recorded=False, size=None, @@ -188,15 +187,13 @@ def send(self, task=None, window=None, iconbar=None, else: handle, icon = 0, 0 # Broadcast - return self._send(Wimp.UserMessageRecorded if recorded else Wimp.UserMessage, - handle, icon, size, reply_callback) + return self._send(recorded, handle, icon, size, reply_callback) def reply(self, reply, recorded=False, size=None, reply_callback=None, reply_messages=None): """Reply to this message with the one given in reply""" reply.your_ref = self.my_ref - return reply._send(Wimp.UserMessageRecorded if recorded else Wimp.UserMessage, - self.sender, None, size, reply_callback) + return reply._send(recorded, self.sender, None, size, reply_callback) def acknowledge(self): self.your_ref = self.my_ref @@ -205,7 +202,8 @@ def acknowledge(self): ctypes.addressof(self), self.sender, 0) - def _send(self, reason, target, icon, size, reply_callback): + def _send(self, recorded, target, icon, size, reply_callback): + reason = Wimp.UserMessageRecorded if recorded else Wimp.UserMessage self.size = size or self.size or ctypes.sizeof(self) self.code = self.__class__.event_id handle = swi.swi('Wimp_SendMessage', 'IIii;..i', From 6d796bed8113919756e7337486a1e43e2c770c18 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 4 Mar 2026 23:40:04 +0000 Subject: [PATCH 3/5] Add timeout option to reply callbacks, defaulting to previous behaviour. --- riscos_toolbox/events.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/riscos_toolbox/events.py b/riscos_toolbox/events.py index 7531eea..aca6585 100644 --- a/riscos_toolbox/events.py +++ b/riscos_toolbox/events.py @@ -5,6 +5,7 @@ import ctypes import inspect import swi +import time from ._consts import Wimp from ._types import BBox, Point @@ -59,8 +60,9 @@ # one sent). On a reply the callback function will be called with the message. # If the callback function doesn't return False, then no further processing will # take place on the message. If it DOES return False, it will be offered to the -# handlers in the usual way. If no reply is received the callback will be called -# with None. In either case, the callback will be removed from the list of callbacks. +# handlers in the usual way. If no reply is received within an optional timeout +# period the callback will be called with None. In either case, the callback will +# be removed from the list of callbacks. class Event(object): @@ -168,14 +170,14 @@ class UserMessage(Event, ctypes.Structure): # or None if no reply is recieved. The reply callback takes two parameters: # the message info and the message data. See the @reply_handler decorator. def broadcast(self, recorded=False, size=None, - reply_callback=None): + reply_callback=None, reply_timeout=0): """Sends the message as a broadcast.""" self.your_ref = 0 - self._send(recorded, None, None, size, reply_callback) + self._send(recorded, None, None, size, reply_callback, reply_timeout) def send(self, task=None, window=None, iconbar=None, recorded=False, size=None, - reply_callback=None): + reply_callback=None, reply_timeout=0): """Sends the message to a task, window or iconbar icon.""" self.your_ref = 0 if task: @@ -187,13 +189,15 @@ def send(self, task=None, window=None, iconbar=None, else: handle, icon = 0, 0 # Broadcast - return self._send(recorded, handle, icon, size, reply_callback) + return self._send(recorded, handle, icon, size, reply_callback, + reply_timeout) def reply(self, reply, recorded=False, size=None, - reply_callback=None, reply_messages=None): + reply_callback=None, reply_timeout=0): """Reply to this message with the one given in reply""" reply.your_ref = self.my_ref - return reply._send(recorded, self.sender, None, size, reply_callback) + return reply._send(recorded, self.sender, None, size, reply_callback, + reply_timeout) def acknowledge(self): self.your_ref = self.my_ref @@ -202,7 +206,7 @@ def acknowledge(self): ctypes.addressof(self), self.sender, 0) - def _send(self, recorded, target, icon, size, reply_callback): + def _send(self, recorded, target, icon, size, reply_callback, reply_timeout): reason = Wimp.UserMessageRecorded if recorded else Wimp.UserMessage self.size = size or self.size or ctypes.sizeof(self) self.code = self.__class__.event_id @@ -210,7 +214,12 @@ def _send(self, recorded, target, icon, size, reply_callback): reason, ctypes.addressof(self), target or 0, icon or 0) if reply_callback: - _reply_callbacks[self.my_ref] = reply_callback + if reply_timeout is not None: + reply_timeout += time.monotonic() + _reply_callbacks[self.my_ref] = { + 'timeout': reply_timeout, + 'function': reply_callback + } return handle if target != 0 else None @@ -428,13 +437,13 @@ def toolbox_dispatch(event_code, application, id_block, poll_block): def message_dispatch(code, application, id_block, poll_block): if code.your_ref in _reply_callbacks: - r = _reply_callbacks[code.your_ref]((code, poll_block)) + r = _reply_callbacks[code.your_ref]['function']((code, poll_block)) del _reply_callbacks[code.your_ref] if r is not False: return if code.reason == Wimp.UserMessageAcknowledge and code.my_ref in _reply_callbacks: - r = _reply_callbacks[code.my_ref]((code, poll_block)) + r = _reply_callbacks[code.my_ref]['function']((code, poll_block)) del _reply_callbacks[code.my_ref] if r is not False: return @@ -456,8 +465,10 @@ def null_polls(): def null_poll(): for ref in list(_reply_callbacks.keys()): - _reply_callbacks[ref](None) - del _reply_callbacks[ref] + if _reply_callbacks[ref]['timeout']: + if _reply_callbacks[ref]['timeout'] < time.monotonic(): + _reply_callbacks[ref]['function'](None) + del _reply_callbacks[ref] def registered_wimp_events(): From 48f253ab70f14525f493782773d3019eeda5fce0 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sun, 8 Mar 2026 21:49:50 +0000 Subject: [PATCH 4/5] Added timeout examples to demo app. --- demo/!MsgDemo/!RunImage,a73 | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/demo/!MsgDemo/!RunImage,a73 b/demo/!MsgDemo/!RunImage,a73 index 523ed31..468a712 100755 --- a/demo/!MsgDemo/!RunImage,a73 +++ b/demo/!MsgDemo/!RunImage,a73 @@ -2,6 +2,7 @@ import swi import sys import os import ctypes +import time import riscos_toolbox as toolbox from riscos_toolbox import Point, BBox, Wimp @@ -40,6 +41,7 @@ class NoMessage(UserMessage): class MsgDemo(Application): def __init__(self): super().__init__('') + self.timeout = None @message_handler(HelloMessage) def reset_request(self, code, id_block, message): @@ -73,11 +75,21 @@ class MsgDemo(Application): none.broadcast(recorded=True, reply_callback=lambda m:self._no_reply(m, True)) + none = NoMessage() + none.broadcast(recorded=False, reply_timeout = None, + reply_callback=lambda m:self._no_reply_ever(m)) + hello = HelloMessage() hello.name = b"World" hello.broadcast(recorded=True, reply_callback=lambda m:self._hello_reply(m)) + hello = HelloMessage() + hello.name = b"World" + hello.broadcast(recorded=True, reply_timeout = 5, + reply_callback=lambda m:self._hello_reply_timed(m)) + self.timeout = time.monotonic() + 4 + @reply_handler(AddResultMessage) def _add_reply(self, code, message, a, b): if message: @@ -93,11 +105,25 @@ class MsgDemo(Application): if not recorded and message is not None: swi.swi("Wimp_ReportError","sI","FFFFGot a reply?", 1) + @reply_handler(NoMessage) + def _no_reply_ever(self, code, message): + if message is None: + swi.swi("Wimp_ReportError","sI","FFFFTimed out!", 1) + if message is not None: + swi.swi("Wimp_ReportError","sI","FFFFGot a reply?", 1) + @reply_handler(HelloMessage) def _hello_reply(self, code, message): if code is not None: swi.swi("Wimp_ReportError","sI","FFFFMessage bounced", 1) + @reply_handler(HelloMessage) + def _hello_reply_timed(self, code, message): + if code is None and time.monotonic() < self.timeout: + swi.swi("Wimp_ReportError","sI","FFFFAlerted too soon", 1) + if code is not None: + swi.swi("Wimp_ReportError","sI","FFFFMessage bounced", 1) + if __name__ == "__main__": app = MsgDemo() app.run() From 2e6a261cdac09039d65c60ab3bd8c8ddc3128356 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 11 Mar 2026 19:43:13 +0000 Subject: [PATCH 5/5] Automatic support for application-level calling of handlers, e.g. for user messages. --- riscos_toolbox/events.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/riscos_toolbox/events.py b/riscos_toolbox/events.py index aca6585..6af031d 100644 --- a/riscos_toolbox/events.py +++ b/riscos_toolbox/events.py @@ -256,16 +256,40 @@ def __init__(self): self.wimp_handlers = {} self.message_handlers = {} + def _toolbox_subclasses(cls=EventHandler, add=False): + # Returns all subclasses of Application and Object which we can + # expect to have handlers associated with toolbox objects. + include = ['Application', 'Object'] + subclasses = [] + for subclass in cls.__subclasses__(): + name = subclass.__name__ + wanted = name in include or add + if wanted: + subclasses.append(name) + subclasses.extend(_toolbox_subclasses(subclass, wanted)) + return subclasses + + def _gather_event_handlers(event, handlers, items): + for component, handler in items: + if event not in handlers: + handlers[event] = {component: [handler]} + elif component not in handlers[event]: + handlers[event][component] = [handler] + else: + handlers[event][component].append(handler) + def _build_handlers(registry, handlers, classname): for event, handler_map in registry.items(): if classname in handler_map: - for component, handler in handler_map[classname].items(): - if event not in handlers: - handlers[event] = {component: [handler]} - elif component not in handlers[event]: - handlers[event][component] = [handler] - else: - handlers[event][component].append(handler) + _gather_event_handlers(event, handlers, + handler_map[classname].items()) + + if classname == 'Application': + objects = _toolbox_subclasses() + for cname in handler_map: + if cname not in objects: + _gather_event_handlers(event, handlers, + handler_map[cname].items()) for klass in inspect.getmro(self.__class__): classname = klass.__qualname__