From d34e7693000e379ef0a94f7677f7fdaea7c17fad Mon Sep 17 00:00:00 2001 From: Chris Johns Date: Wed, 15 Nov 2023 08:48:27 +0000 Subject: [PATCH] Message improvements (#35) Allow messages to be sent with a reply callback. Demo application. --- demo/!MsgDemo/!Run,feb | 5 ++ demo/!MsgDemo/!RunImage,a73 | 103 +++++++++++++++++++++ demo/!MsgDemo/Messages | 2 + demo/!MsgDemo/Res,fae | Bin 0 -> 1800 bytes riscos_toolbox/__init__.py | 40 ++++----- riscos_toolbox/_consts.py | 12 +++ riscos_toolbox/events.py | 174 +++++++++++++++++++++++++++++++++--- toolbox_types.py | 0 8 files changed, 306 insertions(+), 30 deletions(-) create mode 100755 demo/!MsgDemo/!Run,feb create mode 100755 demo/!MsgDemo/!RunImage,a73 create mode 100755 demo/!MsgDemo/Messages create mode 100755 demo/!MsgDemo/Res,fae mode change 100755 => 100644 toolbox_types.py diff --git a/demo/!MsgDemo/!Run,feb b/demo/!MsgDemo/!Run,feb new file mode 100755 index 0000000..3b45007 --- /dev/null +++ b/demo/!MsgDemo/!Run,feb @@ -0,0 +1,5 @@ +If "" = "" Then Error "!Python3 has not been seen." +Set MsgDemo$Dir +WimpSlot -min 3M +Run .bin.python38 .!RunImage +| > RAM:Log 2>&1 diff --git a/demo/!MsgDemo/!RunImage,a73 b/demo/!MsgDemo/!RunImage,a73 new file mode 100755 index 0000000..523ed31 --- /dev/null +++ b/demo/!MsgDemo/!RunImage,a73 @@ -0,0 +1,103 @@ +import swi +import sys +import os +import ctypes + +import riscos_toolbox as toolbox +from riscos_toolbox import Point, BBox, Wimp + +from riscos_toolbox.objects.iconbar import Iconbar, IconbarClickedEvent +from riscos_toolbox.events import toolbox_handler, message_handler, reply_handler, UserMessage +from riscos_toolbox.application import Application + +Message_Hello = 0xabcd +Message_AddRequest = 0xadd0 +Message_AddResult = 0xadd1 +Message_NoMessage = 0xdead + +class HelloMessage(UserMessage): + event_id = Message_Hello + _fields_ = [ + ("name", ctypes.c_char*10), + ] + +class AddRequestMessage(UserMessage): + event_id = Message_AddRequest + _fields_ = [ + ("a", ctypes.c_uint32), + ("b", ctypes.c_uint32), + ] + +class AddResultMessage(UserMessage): + event_id = Message_AddResult + _fields_ = [ + ("q", ctypes.c_uint32), + ] + +class NoMessage(UserMessage): + event_id = Message_NoMessage + +class MsgDemo(Application): + def __init__(self): + super().__init__('') + + @message_handler(HelloMessage) + def reset_request(self, code, id_block, message): + if code is not None: + if code.recorded: + message.acknowledge() + + @message_handler(AddRequestMessage) + def add_request(self, code, id_block, message): + arr = AddResultMessage() + arr.q = message.a + message.b + message.reply(arr) + + @toolbox_handler(0xd1e) + def quit(self, event, id_block, poll_block): + toolbox.quit() + + @toolbox_handler(IconbarClickedEvent) + def iconbar_clicked(self, event, id_block, poll_block): + arq = AddRequestMessage() + arq.a = 1 + arq.b = 2 + arq.broadcast(recorded=False, + reply_callback=lambda m:self._add_reply(m, arq.a, arq.b)) + + none = NoMessage() + none.broadcast(recorded=False, + reply_callback=lambda m:self._no_reply(m, False)) + + none = NoMessage() + none.broadcast(recorded=True, + reply_callback=lambda m:self._no_reply(m, True)) + + hello = HelloMessage() + hello.name = b"World" + hello.broadcast(recorded=True, + reply_callback=lambda m:self._hello_reply(m)) + + @reply_handler(AddResultMessage) + def _add_reply(self, code, message, a, b): + if message: + if message.q != a + b: + swi.swi("Wimp_ReportError","sI","FFFFWrong reply.", 1) + else: + swi.swi("Wimp_ReportError","sI","FFFFDidn't get a reply?", 1) + + @reply_handler(NoMessage) + def _no_reply(self, code, message, recorded): + if recorded and message is None: + swi.swi("Wimp_ReportError","sI","FFFFNo bounce.", 1) + if not recorded and 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) + +if __name__ == "__main__": + app = MsgDemo() + app.run() diff --git a/demo/!MsgDemo/Messages b/demo/!MsgDemo/Messages new file mode 100755 index 0000000..212fb96 --- /dev/null +++ b/demo/!MsgDemo/Messages @@ -0,0 +1,2 @@ +# +_TaskName:Message Demo diff --git a/demo/!MsgDemo/Res,fae b/demo/!MsgDemo/Res,fae new file mode 100755 index 0000000000000000000000000000000000000000..07b200665adf07813a3e431c98e762a8be463e5c GIT binary patch literal 1800 zcmZvczfapx5XWB#EwuCptvVD5RUS|k)PM|4B?c-58c>2nMMVeOw3gKh$8Mpzb9?-@b$(-ZF7)y*XD-1oiZpAKYqV}@w zx71C#*4zU3;ho$du1c-LlbW3KS=eux6EP~8%PC3YeQ6}u zZ@{}NUEPzr|8th3jgtK#P;Xjptag6wu=jpAd*U;YIU##x&4RE9CcVGI_!GSNjCWLu z=djWYHY)ylz{276jh=V9U^bzH*vwWGn@aA1QOv zoHBCHrzVyxe`4AkXN~LnA4Z&gJ{BftVaS~K%;N?525+bKe%W&Ux31p^HkCTW8vDOQ zA>M;IK#v^L(sAzJk;E9k&i>$VPT(Q7?O=cvx<>6olpE+9$Ye9Dg#Gz@b=BsBz((Yd zscYyryL)dOt+sw#+c+tn#&>8gpg#kd#vF>M9ZAiZ)5+x39vDMYgZL|$oH4@cDZG2a z7-Vy*2JAu;gI_?_0D5Pu+k~!x0d;#Ib;&ykxi;$BnSy?jeyke^m|%LZDV&5S+my~Z z#ojRI6688kdp*3syw@{cQI*N*Z|E4>0v>J+TCOKKFEM8AjxrVZ-~zf~=*A$pmC{u( z=A1SMZ)Zz-KRoEm$U6jYBc9Y<#kdaVe$C?bQbY|8>pA3JfNS6o$aO#)Yofe1 zl4~@c!B7!3`U?78H<62}apRnXcp=WKY#is4o9aRl2-;Eqq@jtfQ$kji~g{{!8 z$96-_tvB%l_45m@W*FF(ud17t=LMTaZ9BA8)%GCk71ZA#`$dbre{AwS&mzWWDhf-& zvhYC2{}euF^%>4OV{q2Y3p*g=KZK_s>z>Q}QfMGZ9nYE!h1^HZkA%mudo0B literal 0 HcmV?d00001 diff --git a/riscos_toolbox/__init__.py b/riscos_toolbox/__init__.py index a316950..7d9ecbb 100644 --- a/riscos_toolbox/__init__.py +++ b/riscos_toolbox/__init__.py @@ -4,22 +4,11 @@ import ctypes import traceback import struct -import sys from ._types import * +from ._consts import * from .base import Object, _objects, get_object, create_object, find_objects, _application from .events import * -from ._consts import Wimp - - -class Toolbox: - Error = 0x44ec0 - ObjectAutoCreated = 0x44ec1 - ObjectDeleted = 0x44ec2 - - -class Messages: - Quit = 0 _quit = False @@ -84,10 +73,10 @@ def _handler_block(handlers, add=[]): block[index] = id return block - wimp_messages = _handler_block(events._message_handlers) - toolbox_events = _handler_block( - events._toolbox_handlers, - [Toolbox.ObjectAutoCreated, Toolbox.ObjectDeleted]) + wimp_messages = _handler_block(events._message_handlers, + list(events._reply_messages)) + toolbox_events = _handler_block(events._toolbox_handlers, + [Toolbox.ObjectAutoCreated, Toolbox.ObjectDeleted]) wimp_ver, task_handle, sprite_area = \ swi.swi('Toolbox_Initialise', '0IbbsbI;III', @@ -108,9 +97,11 @@ def run(application): global _quit while not _quit: + flags = application.poll_flags + if events.null_polls(): + flags = flags & ~Wimp.Poll.NullMask reason, sender = swi.swi( - 'Wimp_Poll', 'II;I.I', - application.poll_flags, + 'Wimp_Poll', 'II;I.I', flags, ctypes.addressof(poll_buffer)) try: @@ -134,13 +125,22 @@ def run(application): toolbox_dispatch(event_code, application, _id_block, poll_block) - elif reason in (Wimp.UserMessage, Wimp.UserMessageRecorded): - message = struct.unpack("I", poll_block[16:20])[0] + elif reason in [ + Wimp.UserMessage, + Wimp.UserMessageRecorded, + Wimp.UserMessageAcknowledge + ]: + message = MessageInfo.create( + reason, *struct.unpack("IIIII", poll_block[0:20])) + if message == Messages.Quit: _quit = True continue + message_dispatch(message, application, _id_block, poll_block) else: + if reason == Wimp.Null: + events.null_poll() wimp_dispatch(reason, application, _id_block, poll_block) except Exception as e: diff --git a/riscos_toolbox/_consts.py b/riscos_toolbox/_consts.py index 2b8d314..7cf959f 100644 --- a/riscos_toolbox/_consts.py +++ b/riscos_toolbox/_consts.py @@ -1,3 +1,5 @@ +"""RISC OS Toolbox library: constants""" + class Wimp: Null = 0 @@ -35,3 +37,13 @@ class Poll: PollWord = (1 << 22) PollWordHighPriority = (1 << 23) SaveFPRegs = (1 << 24) + + +class Toolbox: + Error = 0x44ec0 + ObjectAutoCreated = 0x44ec1 + ObjectDeleted = 0x44ec2 + + +class Messages: + Quit = 0 diff --git a/riscos_toolbox/events.py b/riscos_toolbox/events.py index 81de41a..262746a 100644 --- a/riscos_toolbox/events.py +++ b/riscos_toolbox/events.py @@ -1,10 +1,12 @@ """RISC OS Toolbox library: events""" from collections.abc import Iterable +from functools import wraps import ctypes import inspect +import swi -from . import BBox, Point +from . import Wimp, BBox, Point # Handlers # -------- @@ -37,7 +39,7 @@ # handler is not found, or returns False, the next one will be tried. Not # returning anything from the handler will therefore cause further handlers not # to be tried. - +# # handlers # { event : # { class-name : @@ -46,6 +48,17 @@ # } # } # } +# +# Wimp message reply handlers +# --------------------------- +# Wimp messages have an extra 'send' function which can be used to send them +# to another task. This optionally has a callback function to call when a reply +# is recieved (a message where the 'your ref' of the a message to matches the +# 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 recieved the callback will be called +# with None. In either case, the callback will be removed from the list of callbacks. class Event(object): @@ -144,6 +157,82 @@ class UserMessage(Event, ctypes.Structure): ("code", ctypes.c_uint32), ] + # Message sending functions. + # if reply_callback is not None, it will be called with a reply + # 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): + """Sends the message as a broadcast.""" + self.your_ref = 0 + self._send(Wimp.UserMessageRecorded if recorded else Wimp.UserMessage, + None, None, size, reply_callback) + + def send(self, task=None, window=None, iconbar=None, + recorded=False, size=None, + reply_callback=None): + """Sendds the message to a task, window or iconbar icon.""" + self.your_ref = 0 + if task: + handle, icon = task, 0 + elif window: + handle, icon = window, 0 + elif iconbar: + handle, icon = -2, iconbar + else: + handle, icon = 0, 0 # Broadcast + + self._send(Wimp.UserMessageRecorded if recorded else Wimp.UserMessage, + handle, icon, 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 + reply._send(Wimp.UserMessageRecorded if recorded else Wimp.UserMessage, + self.sender, None, size, reply_callback) + + def acknowledge(self): + self.your_ref = self.my_ref + return swi.swi('Wimp_SendMessage', 'IIII;..i', + Wimp.UserMessageAcknowledge, + ctypes.addressof(self), + self.sender, 0) + + def _send(self, reason, target, icon, size, reply_callback): + self.size = size or ctypes.sizeof(self) + self.code = self.__class__.event_id + handle = swi.swi('Wimp_SendMessage', 'IIII;..i', + reason, ctypes.addressof(self), + target or 0, icon or 0) + if reply_callback: + _reply_callbacks[self.my_ref] = reply_callback + + return handle if target != 0 else None + + +# Contains info about a message - the data from the header, plus the wimp +# reason it was delivered with. If used like an 'int' will give the message +# code. +class MessageInfo(int): + def create(reason, size, sender, my_ref, your_ref, code): + mc = MessageInfo(code) + mc.reason = reason + mc.size = size + mc.sender = sender + mc.my_ref = my_ref + mc.your_ref = your_ref + mc.code = code + return mc + + @property + def recorded(self): + return self.reason == Wimp.UserMessageRecorded + + @property + def bounce(self): + return self.reason == Wimp.UserMessageAcknowledge + class EventHandler(object): """Base class for things that can handle events.""" @@ -201,18 +290,21 @@ def toolbox_dispatch(self, event, id_block, poll_block): return self._dispatch(self.toolbox_handlers, event, id_block, poll_block) - def wimp_dispatch(self, event, id_block, poll_block): + def wimp_dispatch(self, reason, id_block, poll_block): return self._dispatch(self.wimp_handlers, - event, id_block, poll_block) + reason, id_block, poll_block) - def message_dispatch(self, event, id_block, poll_block): + def message_dispatch(self, code, id_block, poll_block): return self._dispatch(self.message_handlers, - event, id_block, poll_block) + code, id_block, poll_block) +# Handlers _toolbox_handlers = {} _wimp_handlers = {} _message_handlers = {} +_reply_messages = set() # @reply_handler messages +_reply_callbacks = {} # {ref: MessageReplyCallback} def _set_handler(code, component, handler, handlers): @@ -264,10 +356,50 @@ def decorator(handler): return _set_handler(reason, component, handler, _wimp_handlers) return decorator -# List of self, parent, ancestor and application objects (if they exist) -# This is the list of objects to try to handle the event, in order. +def reply_handler(message_s): + _message_map = {} # message number -> class or None + + def _map_data(code_or_class): + if isinstance(code_or_class, int): + return code_or_class, None + elif issubclass(code_or_class, UserMessage): + return code_or_class.event_id, code_or_class + else: + raise RuntimeError("Must be int or UserMessage") + if isinstance(message_s, Iterable): + for m in message_s: + code, klass = _map_data(m) + _message_map[code] = klass + else: + code, klass = _map_data(message_s) + _message_map[code] = klass + + _reply_messages.update(set(_message_map.keys())) + + def decorator(handler): + @wraps((handler, _message_map)) + def wrapper(self, data, *args): + message = None + code = None + if data is not None: + message = ctypes.cast( + data, ctypes.POINTER(UserMessage) + ).contents + + code = message.code + if code in _message_map: + message = ctypes.cast( + data, ctypes.POINTER(_message_map[code]) + ).contents + return handler(self, code, message, *args) + return wrapper + return decorator + + +# List of self, parent, ancestor and application objects (if they exist) +# This is the list of objects to try to handle the event, in order. def _get_spaa(application, id_block): from .base import get_object return list( @@ -289,9 +421,21 @@ def toolbox_dispatch(event_code, application, id_block, poll_block): break -def message_dispatch(message, 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) + 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) + del _reply_callbacks[code.my_ref] + if r is not False: + return + for obj in _get_spaa(application, id_block): - if obj.message_dispatch(message, id_block, poll_block): + if obj.message_dispatch(code, id_block, poll_block): break @@ -301,5 +445,15 @@ def wimp_dispatch(reason, application, id_block, poll_block): break +def null_polls(): + return len(_reply_callbacks) > 0 + + +def null_poll(): + for ref in list(_reply_callbacks.keys()): + _reply_callbacks[ref](None) + del _reply_callbacks[ref] + + def registered_wimp_events(): return _wimp_handlers.keys() diff --git a/toolbox_types.py b/toolbox_types.py old mode 100755 new mode 100644