Current section
Files
Jump to
Current section
Files
src/user-interface/src/graphical/gui_event.erl
% Copyright (C) 2010-2019 Olivier Boudeville
%
% This file is part of the Ceylan-Myriad library.
%
% This library is free software: you can redistribute it and/or modify
% it under the terms of the GNU Lesser General Public License or
% the GNU General Public License, as they are published by the Free Software
% Foundation, either version 3 of these Licenses, or (at your option)
% any later version.
% You can also redistribute it and/or modify it under the terms of the
% Mozilla Public License, version 1.1 or later.
%
% This library is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU Lesser General Public License and the GNU General Public License
% for more details.
%
% You should have received a copy of the GNU Lesser General Public
% License, of the GNU General Public License and of the Mozilla Public License
% along with this library.
% If not, see <http://www.gnu.org/licenses/> and
% <http://www.mozilla.org/MPL/>.
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Creation date: Monday, February 15, 2010.
% Gathers all elements relative to the MyriadGUI events, including the event
% loop.
%
-module(gui_event).
% Implementation notes.
% Events can be managed as messages or callbacks. We generally prefer the former
% (as messages can be selectively received, any context can be kept in the
% receive loop, no temporary process is created, no wx include is needed hence
% the backend can be well encapsulated, etc.).
%
% Whether an event shall be also dispatched to subsequent handlers may be
% decided by using propagate_event/1.
%
% Event messages are internally converted, in order to hide the wx backend,
% augment it with other primitives (ex: canvas widget) and make them compliant
% with the MyriadGUI conventions, as seen by the user code (hint: these
% conventions comply with the WOOPER ones, should the GUI be used in an OOP
% context).
%
% Regarding events, see also:
% https://wiki.wxwidgets.org/Events#Event.Skip_and_Event.Veto
% Please refer to wx.pdf (available on http://www.erlang.org/doc/apps/wx/wx.pdf)
% for more architecture/implementation details about wx.
% Function export section.
% Main event primitives:
-export([ start_main_event_loop/2, propagate_event/1, set_instance_state/3 ]).
% Stringification:
-export([ event_table_to_string/1 ]).
% To silence unused warnings:
-export([ reassign_table_to_string/1, get_instance_state/2,
type_table_to_string/1, instance_referential_to_string/1 ]).
% Type section.
% Event messages.
%
% Lower-level, backend-specific events are translated into MyriadGUI event
% messages, to be received by their respective event subscribers.
% An event message is a pair whose first element is the event type, as an atom
% (ex: 'onWindowClosed'), and whose second element is a list, whose first
% element is the GUI object that generated that event (the closed window, here),
% and whose last element is the event context (intermediary elements carrying
% event-specific information):
%
% { event_type(), [ gui_object(), ..., event_context() ] }
%
% Ex: { onWindowClosed, [ Window, CloseContext ] }.
%
% Note: these messages respect the WOOPER conventions, and this is done on
% purpose, to facilitate any integration with upper layers.
%
-type event_message() :: { event_type(), [ any() ] }.
% A count of instances of a given object type:
-type instance_count() :: basic_utils:count().
% Event management.
%
% In general we promote managing events thanks to messages rather than thanks to
% callbacks, as the former can access easily to the full context of the program
% whereas the latter is only executed into a transient process (less convenient,
% probably less efficient).
-type event_source() :: wx_event_handler() | myriad_event_handler().
-type wx_event_handler() :: wxEvtHandler:wxEvtHandler().
% Only one currently:
-type myriad_event_handler() :: gui_canvas:canvas().
% Using the wx-event type, leaked by wx.hrl:
%
% (enrich this union whenever needed)
%
-type wx_event_type() :: wx_close_event_type(). % | ...
% Associated to wxCloseEvent:
-type wx_close_event_type() :: 'close_window'
| 'end_session'
| 'query_end_session'.
% Our own event types, independent from any backend:
-type event_type() :: 'onWindowClosed'.
% The PID of a user calling process:
-type user_pid() :: pid().
% The PID of an event subscriber:
-type event_subscriber_pid() :: pid().
% So that user process(es) can subscribe to GUI events:
-type event_subscription() ::
{ list_utils:maybe_list( event_type() ),
list_utils:maybe_list( gui_object() ),
list_utils:maybe_list( event_subscriber_pid() ) }.
% Specifies, for an event subscriber (by default: the calling process), any
% combination of type of events and GUI objects that shall be listened to.
%
-type event_subscription_spec() ::
list_utils:maybe_list( event_subscription() ).
-export_type([ event_message/0, event_source/0,
wx_event_handler/0, myriad_event_handler/0,
wx_event_type/0, wx_close_event_type/0,
event_type/0, user_pid/0,
event_subscriber_pid/0, event_subscription/0,
event_subscription_spec/0 ]).
% An indirection table dispatching events according to subscription
% specifications.
%
% For an incoming event, we see this type (virtually, logically) as:
% table( { gui_object(), event_type() },
% set_utils:set( event_subscriber_pid() ) ):
%
% - the first key is the GUI object (e.g. widget) from which the event emanates
% (ex: a frame)
%
% - the second key is its corresponding (internal) event type (ex:
% 'onWindowClosed')
%
% - the associated value is a list/set of the PID of the subscribers regarding
% this (object,event) combination
%
% Note: two nested tables (one table(), one list_table()) are used also in
% order to ensure that there is up to one entry per GUI object and per event
% type stored.
%
-type event_table() :: table:table( gui_object(), event_dispatch_table() ).
% Tells, for a given event type (e.g. in the context of a specific GUI object),
% to which event subscribers the corresponding GUI messages shall be sent.
%
-type event_dispatch_table() :: list_table:table( event_type(),
[ event_subscriber_pid() ] ).
% To replace source events objects (ex: a panel) by others (ex: its associated
% canvas, if any):
%
-type reassign_table() :: table:table( gui_object(), gui_object() ).
% To store the MyriadGUI instances (sorted by types) and manage them like wx
% native objects.
%
% Keys are like 'canvas'.
%
-type myriad_type_table() :: table:table( gui:myriad_object_type(),
instance_referential() ).
% To store, for a given MyriadGUI type (ex: 'canvas'), all information about all
% instances.
%
% - a total count of the instances already created for that type
%
% - a table whose keys are the identifiers of the objects of that type, and
% whose values are the actual state of these instances.
%
% Note: the total count is not the same as the size of the table, as instances
% may be deleted.
%
-record( instance_referential, {
% Total count of the instances already created for that type:
instance_count :: instance_count(),
instance_table :: table:table( gui:myriad_instance_pid(),
gui:myriad_object_state() ) }).
-type instance_referential() :: #instance_referential{}.
% Stores the current MyriadGUI state, as managed by its main event loop.
-record( loop_state, {
% Identifier of the current top-level wx server:
wx_server :: gui:wx_server(),
% To dispatch appropriately the backend-originating events:
event_table :: event_table(),
% Allows to replace an event source by another.
%
% For example useful when having defined a canvas (which thus embeds
% a wx panel): when the internal event loop receives a 'paint' wx
% event for that wx panel, the actual object referred to by the GUI
% message that we will send to the user code shall not be that panel,
% but the canvas that owns it (for example so that other elements of
% that canvas can then be used when the user code processes this
% event - like the bitmap or the back-buffer of this canvas).
%
reassign_table :: reassign_table(),
% Stores, by types, the current widget instances that have been
% introduced by MyriadGUI to complement the backend (ex: canvas
% instances).
%
type_table :: myriad_type_table(),
% List of the MyriadGUI objects that shall be adjusted after a show:
%
objects_to_adjust=[] :: [ myriad_object_ref() ] }).
-type loop_state() :: #loop_state{}.
% A #wx event record comprises:
%
% - (the 'wx' record tag, if the record instance is seen as a tuple)
%
% - id :: wx_id() the (integer) identifier of the object (e.g. widget) that
% received the event (event source)
%
% - obj :: wx:wx_object() is the reference of the wx object that was specified
% in the connect/n call, i.e. on which connect/n was called (ex:
% {wx_ref,35,wxFrame,[]})
%
% - userData :: user_data() is the user-specified data that was specified in the
% connect/n call (typically [], as not very useful)
%
% - event :: wx_event_info() is the description of the event itself
%
% As always, same as: -record( wx,...
%
-type wx_event() :: { 'wx', wx_id(), wx:wx_object(), gui:user_data(),
wx_event_info() }.
% A wx-defined record describing an actual event.
%
% WxFoobar record whose first field is 'type', and which may have other
% fields.
%
% Examples of descriptions, as tuples: {wxClose,close_window}, or
% {wxCommand,command_button_clicked,CmdString,CmdInt,...}
%
-type wx_event_info() :: tuple().
-export_type([ wx_event/0, wx_event_info/0 ]).
% Shorthands:
-type gui_object() :: gui:gui_object().
-type wx_id() :: gui_wx_backend:wx_id().
% For gui_event_context():
-include("gui.hrl").
% For canvas_state():
-include("gui_canvas.hrl").
% For wx headers:
-include("gui_internal_defines.hrl").
% Implementation section.
% Starts the internal, main event loop of MyriadGUI.
%
% The backend events received will result in callbacks to be triggered on their
% respective subscribers.
%
% The goal is to devise a generic event loop, while still being able to be
% notified of all relevant information (and only them).
%
-spec start_main_event_loop( gui:wx_server(), wx:wx_env() ) -> no_return().
start_main_event_loop( WxServer, WxEnv ) ->
% To be done first, so that we are able to use wx from that process from now
% on:
%
wx:set_env( WxEnv ),
EmptyTable = table:new(),
InitialLoopState = #loop_state{ wx_server=WxServer,
event_table=EmptyTable,
reassign_table=EmptyTable,
type_table=EmptyTable },
%trace_utils:debug_fmt( "Starting main MyriadGUI loop." ] ),
% Enter the infinite event loop:
process_event_messages( InitialLoopState ).
% Receives and process all messages (actual main event loop), coming:
%
% - either from controlling processes (typically from application processes
% subscribing to some events)
%
% - or from the backend, that notifies this loop of the actual, lower-level
% events
%
-spec process_event_messages( loop_state() ) -> no_return().
process_event_messages( LoopState ) ->
%trace_utils:trace( "Waiting for event messages..." ),
NewLoopState = receive
% So that no large series of repaint requests for the same object pile
% up:
FirstWxRepaintEvent=#wx{ obj=SourceObject, event={wxPaint,paint} } ->
trace_utils:debug_fmt( "Received first repaint event: ~p.",
[ FirstWxRepaintEvent ] ),
process_only_latest_repaint_event( FirstWxRepaintEvent,
SourceObject, LoopState );
OtherEvent ->
trace_utils:debug_fmt( "Received other event: ~p.",
[ OtherEvent ] ),
process_event_message( OtherEvent, LoopState )
end,
process_event_messages( NewLoopState ).
% Event types roughly sorted in clauses by decreasing frequency of appearance:
%
% (defined in lib/wx/include/wx.hrl)
%
% A wx event has been received here:
%
% Structure: { wx, EventSourceId, Obj, UserData, EventInfo }, with EventInfo:
% { WxEventName, EventType, ...}
%
% Ex: { wx, -2006, {wx_ref,35,wxFrame,[]}, [], {wxClose,close_window} }.
%
process_event_message( WxEvent=#wx{ id=EventSourceId, obj=GUIObject,
userData=UserData, event=WxEventInfo },
LoopState ) ->
process_wx_event( EventSourceId, GUIObject, UserData, WxEventInfo,
WxEvent, LoopState );
process_event_message( { setCanvasDrawColor, [ CanvasId, Color ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:set_draw_color( CanvasState, Color ),
LoopState;
process_event_message( { setCanvasFillColor, [ CanvasId, Color ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:set_fill_color( CanvasState, Color ),
LoopState;
process_event_message( { setCanvasBackgroundColor, [ CanvasId, Color ] },
LoopState ) ->
%trace_utils:debug_fmt( "Canvas: ~p", [ Canvas ] ),
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
%trace_utils:debug_fmt( "CanvasState: ~p", [ CanvasState ] ),
gui_canvas:set_background_color( CanvasState, Color ),
LoopState;
process_event_message( { getCanvasRGB, [ CanvasId, Point ], CallerPid },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
Color = gui_canvas:get_rgb( CanvasState, Point ),
CallerPid ! { notifyCanvasRGB, Color },
LoopState;
process_event_message( { setCanvasRGB, [ CanvasId, Point ] }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:set_rgb( CanvasState, Point ),
LoopState;
process_event_message( { drawCanvasLine, [ CanvasId, P1, P2 ] }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_line( CanvasState, P1, P2 ),
LoopState;
process_event_message( { drawCanvasLine, [ CanvasId, P1, P2, Color ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_line( CanvasState, P1, P2, Color ),
LoopState;
process_event_message( { drawCanvasLines, [ CanvasId, Points ] }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_lines( CanvasState, Points ),
LoopState;
process_event_message( { drawCanvasLines, [ CanvasId, Points, Color ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_lines( CanvasState, Points, Color ),
LoopState;
process_event_message( { drawCanvasSegment, [ CanvasId, Points ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_segment( CanvasState, Points ),
LoopState;
process_event_message( { drawCanvasPolygon, [ CanvasId, Points ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_polygon( CanvasState, Points ),
LoopState;
process_event_message( { drawCanvasLabel, [ CanvasId, Point, Label ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_label( CanvasState, Point, Label ),
LoopState;
process_event_message( { drawCanvasCross, [ CanvasId, Location ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_cross( CanvasState, Location ),
LoopState;
process_event_message( { drawCanvasCross, [ CanvasId, Location, EdgeLength ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_cross( CanvasState, Location, EdgeLength ),
LoopState;
process_event_message( { drawCanvasCross,
[ CanvasId, Location, EdgeLength, Color ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_cross( CanvasState, Location, EdgeLength, Color ),
LoopState;
process_event_message( { drawCanvasLabelledCross,
[ CanvasId, Location, EdgeLength, LabelText ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_labelled_cross( CanvasState, Location, EdgeLength,
LabelText ),
LoopState;
process_event_message( { drawCanvasLabelledCross,
[ CanvasId, Location, EdgeLength, Color,
LabelText ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_labelled_cross( CanvasState, Location, EdgeLength,
Color, LabelText ),
LoopState;
process_event_message( { drawCanvasCircle, [ CanvasId, Center, Radius ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_circle( CanvasState, Center, Radius ),
LoopState;
process_event_message( { drawCanvasCircle,
[ CanvasId, Center, Radius, Color ] }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_circle( CanvasState, Center, Radius, Color ),
LoopState;
process_event_message( { drawCanvasNumberedPoints, [ CanvasId, Points ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:draw_numbered_points( CanvasState, Points ),
LoopState;
process_event_message( { loadCanvasImage, [ CanvasId, Filename ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:load_image( CanvasState, Filename ),
LoopState;
process_event_message( { loadCanvasImage, [ CanvasId, Position, Filename ] },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:load_image( CanvasState, Position, Filename ),
LoopState;
process_event_message( { blitCanvas, CanvasId }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:blit( CanvasState ),
LoopState;
process_event_message( { clearCanvas, CanvasId }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui_canvas:clear( CanvasState ),
LoopState;
process_event_message( { setTooltip, [ CanvasId, Label ] }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
gui:set_tooltip( CanvasState#canvas_state.panel, Label ),
LoopState;
process_event_message( { getPanelForCanvas, CanvasId, CallerPid },
LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
CallerPid ! { notifyCanvasPanel, CanvasState#canvas_state.panel },
LoopState;
process_event_message( { getCanvasSize, CanvasId, CallerPid }, LoopState ) ->
CanvasState = get_canvas_instance_state( CanvasId,
LoopState#loop_state.type_table ),
Size = gui_canvas:get_size( CanvasState ),
CallerPid ! { notifyCanvasSize, Size },
LoopState;
% MyriadGUI user request (ex: emanating from gui:create_canvas/1):
process_event_message( { createInstance, [ ObjectType, ConstructionParams ],
CallerPid }, LoopState ) ->
process_myriad_creation( ObjectType, ConstructionParams,
CallerPid, LoopState );
process_event_message( { subscribeToEvents,
[ SubscribedEvents, SubscriberPid ] }, LoopState ) ->
trace_utils:debug_fmt( "Subscribing process ~w to events ~p.",
[ SubscriberPid, SubscribedEvents ] ),
update_event_loop_tables( SubscribedEvents, SubscriberPid,
LoopState );
% To account for example for a silently resized inner panel of a canvas:
%
% (done only once, initially):
%
process_event_message( { adjustObject, ObjectRef }, LoopState ) ->
trace_utils:debug_fmt( "Recording object to adjust: ~w.",
[ ObjectRef ] ),
NewAdjustList =
[ ObjectRef | LoopState#loop_state.objects_to_adjust ],
LoopState#loop_state{ objects_to_adjust=NewAdjustList };
% Currently we update widgets regardless of whether one of their parent windows
% is reported here as shown:
%
process_event_message( { onShow, [ _Windows ] }, LoopState ) ->
ObjectsToAdjust = LoopState#loop_state.objects_to_adjust,
trace_utils:debug_fmt( "Adjusting after show: ~p.",
[ ObjectsToAdjust ] ),
EventTable = LoopState#loop_state.event_table,
NewTypeTable = adjust_objects( ObjectsToAdjust, EventTable,
LoopState#loop_state.type_table ),
LoopState#loop_state{ type_table=NewTypeTable,
objects_to_adjust=[] };
process_event_message( UnmatchedEvent, LoopState ) ->
trace_utils:warning_fmt( "Ignored following unmatched event "
"message:~n~p", [ UnmatchedEvent ] ),
LoopState.
% Drops all intermediate repaint events, and processes the last one, and the
% next non-repaint event.
%
-spec process_only_latest_repaint_event( wx_event(), wx:wx_object(),
loop_state() ) -> loop_state().
process_only_latest_repaint_event( CurrentWxRepaintEvent, SourceObject,
LoopState ) ->
receive
% Ignores all repaints applying to specified object of a series, except
% the last:
%
NewWxRepaintEvent=#wx{ obj=SourceObject, event={wxPaint,paint} } ->
trace_utils:debug_fmt( "Received next repaint event: ~p.",
[ NewWxRepaintEvent ] ),
process_only_latest_repaint_event( NewWxRepaintEvent, SourceObject,
LoopState );
OtherEvent ->
% End of a series of repaints; thus process the last one we got:
#wx{ id=EventSourceId, obj=GUIObject, userData=UserData,
event=WxEventInfo } = CurrentWxRepaintEvent,
% By design this is a wx (repaint) event:
PostRepaintLoopState = process_wx_event( EventSourceId, GUIObject,
UserData, WxEventInfo, CurrentWxRepaintEvent, LoopState ),
trace_utils:debug_fmt( "Received post-repaint event: ~p.",
[ OtherEvent ] ),
% And then process the first non-repaint event that was just
% received:
%
process_event_message( OtherEvent, PostRepaintLoopState )
% We should not delay arbitrarily the processing of a unique repaint event:
after 5 ->
#wx{ id=EventSourceId, obj=GUIObject, userData=UserData,
event=WxEventInfo } = CurrentWxRepaintEvent,
% By design this is a wx (repaint) event:
process_wx_event( EventSourceId, GUIObject, UserData, WxEventInfo,
CurrentWxRepaintEvent, LoopState )
end.
% Processes specified wx event message.
-spec process_wx_event( wx_id(), wx:wx_object(),
gui:user_data(), wx_event_info(), wx_event(),
loop_state() ) -> loop_state().
process_wx_event( EventSourceId, GUIObject, UserData, WxEventInfo, WxEvent,
LoopState=#loop_state{ event_table=EventTable,
reassign_table=ReassignTable } ) ->
ActualGUIObject = case table:lookup_entry( GUIObject, ReassignTable ) of
key_not_found ->
trace_utils:trace_fmt( "Wx event received about '~s':~n~p.",
[ gui:object_to_string( GUIObject ), WxEventInfo ] ),
GUIObject;
{ value, TargetGUIObject } ->
trace_utils:trace_fmt( "Wx event received about '~s', "
"reassigned to '~s':~n~p.",
[ gui:object_to_string( GUIObject ),
gui:object_to_string( TargetGUIObject ),
WxEventInfo ] ),
TargetGUIObject
end,
% Here some events shall be intercepted and specific processing actions
% shall be triggered: if a canvas-owned panel is resized, then that canvas
% shall itself by resized, see gui_canvas:update/2.
% Then notify the subscribers (could use get_subscribers_for/3):
case table:lookup_entry( ActualGUIObject, EventTable ) of
key_not_found ->
% At least one subscriber would be expected:
trace_utils:warning_fmt( "No event subscription for GUI "
"object '~s', hence ignoring event.",
[ gui:object_to_string( ActualGUIObject ) ] ),
LoopState;
{ value, DispatchTable } ->
% Example: WxEventType = close_window (first element is the record
% name, such as wxClose).
%
WxEventType = element( 2, WxEventInfo ),
EventType = gui_wx_backend:from_wx_event_type( WxEventType ),
case list_table:lookup_entry( EventType, DispatchTable ) of
{ value, _Subscribers=[] } ->
trace_utils:error_fmt( "For GUI object '~s', event type "
"'~s' not registered whereas notified (abnormal).",
[ gui:object_to_string( ActualGUIObject ), EventType ] );
{ value, Subscribers } ->
trace_utils:debug_fmt( "Sending ~p event to subscribers "
"~w.", [ EventType, Subscribers ] ),
send_event( Subscribers, EventType, EventSourceId,
ActualGUIObject, UserData, WxEvent )
end,
LoopState
end.
% Returns the subscribers (if any) to the specified GUI object, for the
% specified event type.
%
-spec get_subscribers_for( gui_object(), event_type(), event_table() ) ->
[ event_subscriber_pid() ].
get_subscribers_for( GUIObject, EventType, EventTable ) ->
case table:lookup_entry( GUIObject, EventTable ) of
key_not_found ->
[];
{ value, DispatchTable } ->
case list_table:lookup_entry( EventType, DispatchTable ) of
key_not_found ->
[];
{ value, Subscribers } ->
Subscribers
end
end.
% Creates specified MyriadGUI object.
-spec process_myriad_creation( gui:myriad_object_type(),
gui:construction_parameters(), user_pid(),
loop_state() ) -> loop_state().
process_myriad_creation( ObjectType, ConstructionParams, CallerPid,
LoopState=#loop_state{ reassign_table=ReassignTable,
type_table=TypeTable } ) ->
trace_utils:debug_fmt( "Myriad instance creation request received from ~w, "
"for type '~s', with construction parameters ~w.",
[ CallerPid, ObjectType, ConstructionParams ] ),
case ObjectType of
canvas ->
{ CanvasInitialState, PanelRef } = gui_canvas:create_instance(
ConstructionParams ),
{ CanvasRef, NewTypeTable } = register_instance( ObjectType,
CanvasInitialState, TypeTable ),
CallerPid ! { instance_created, ObjectType, CanvasRef },
% An event about its internal panel will be reassigned to its canvas
% owner:
%
NewReassignTable = table:add_new_entry( PanelRef, CanvasRef,
ReassignTable ),
% After this canvas is created, its panel, when finally shown, will
% be sized, with no specific notification; to adapt that canvas
% (otherwise it will be never be set at the right initial size), we
% post ourselves an event, the first one to be handled by our event
% loop:
%
self() ! { adjustObject, CanvasRef },
LoopState#loop_state{ reassign_table=NewReassignTable,
type_table=NewTypeTable };
UnexpectedType ->
trace_utils:error_fmt( "'~s' is not a known MyriadGUI type.",
[ UnexpectedType ] ),
throw( { unexpected_myriad_type, UnexpectedType } )
end.
% Registers the creation of a MyriadGUI instance of specified type and initial
% state, in specified instance table.
%
-spec register_instance( gui:myriad_object_type(), gui:myriad_object_state(),
myriad_type_table() ) -> { myriad_object_ref(), myriad_type_table() }.
register_instance( ObjectType, ObjectInitialState, TypeTable ) ->
trace_utils:trace_fmt( "Registering a MyriadGUI instance of type '~s', "
"of following state:~n~p.",
[ ObjectType, ObjectInitialState ] ),
{ NewInstanceId, NewInstanceReferential } = case table:lookup_entry(
ObjectType, TypeTable ) of
key_not_found ->
% First instance of its type:
FirstInstanceId = 1,
FirstInstanceTable = table:new(
[ { FirstInstanceId, ObjectInitialState } ] ),
FirstInstanceReferential = #instance_referential{
instance_count=1,
instance_table=FirstInstanceTable },
{ FirstInstanceId, FirstInstanceReferential };
{ value, InstanceReferential=#instance_referential{
instance_count=InstanceCount,
instance_table=InstanceTable } } ->
NextInstanceId = InstanceCount + 1,
NextInstanceTable = table:add_entry( NextInstanceId,
ObjectInitialState, InstanceTable ),
NextInstanceReferential = InstanceReferential#instance_referential{
instance_count=NextInstanceId,
instance_table=NextInstanceTable },
{ NextInstanceId, NextInstanceReferential }
end,
% Prepares the reference onto this new instance:
MyriadRef = #myriad_object_ref{ object_type=ObjectType,
myriad_instance_pid=NewInstanceId },
NewTypeTable = table:add_entry( ObjectType, NewInstanceReferential,
TypeTable ),
{ MyriadRef, NewTypeTable }.
% Sends the specified MyriadGUI event to the relevant subscribers.
%
% (helper)
%
-spec send_event( [ event_subscriber_pid() ], event_type(), gui:id(),
gui_object(), gui:user_data(), gui:backend_event() ) -> void().
send_event( _Subscribers=[], _EventType, _EventSourceId, _GUIObject, _UserData,
_Event ) ->
ok;
send_event( Subscribers, _EventType=onResized, EventSourceId, GUIObject,
UserData, Event ) ->
Context = #gui_event_context{ id=EventSourceId, user_data=UserData,
backend_event=Event },
WxEventInfo = Event#wx.event,
% Defined in wx.hrl:
NewSize = WxEventInfo#wxSize.size,
%trace_utils:debug_fmt( "onResized event: new size is ~p.", [ NewSize ] ),
Msg = { onResized, [ GUIObject, NewSize, Context ] },
trace_utils:debug_fmt( "Sending event '~p' to ~w.",
[ Msg, Subscribers ] ),
[ SubPid ! Msg || SubPid <- Subscribers ];
send_event( Subscribers, EventType, Id, GUIObject, UserData, Event ) ->
Context = #gui_event_context{ id=Id, user_data=UserData,
backend_event=Event },
Msg = { EventType, [ GUIObject, Context ] },
trace_utils:debug_fmt( "Sending event '~p' to ~w.",
[ Msg, Subscribers ] ),
[ SubPid ! Msg || SubPid <- Subscribers ].
% Enriches the specified event table with specified subscription information.
%
% (helper)
%
-spec update_event_loop_tables( event_subscription_spec(),
event_subscriber_pid(), loop_state() ) -> loop_state().
update_event_loop_tables( _SubscribedEvents=[], _DefaultSubscriberPid,
LoopState ) ->
LoopState;
update_event_loop_tables( _SubscribedEvents=[
{ EventTypeMaybeList, GUIObjectMaybeList, SubscriberMaybeList } | T ],
DefaultSubscriberPid, LoopState ) ->
EventTypeList = list_utils:ensure_list_of_atoms( EventTypeMaybeList ),
GUIObjectList = list_utils:ensure_list_of_tuples( GUIObjectMaybeList ),
SubscriberList= list_utils:ensure_list_of_pids( SubscriberMaybeList ),
NewLoopState = lists:foldl( fun( Obj, AccState ) ->
register_event_types_for( Obj, EventTypeList,
SubscriberList, AccState )
end,
_Acc0=LoopState,
_List=GUIObjectList ),
update_event_loop_tables( T, DefaultSubscriberPid, NewLoopState );
update_event_loop_tables( _SubscribedEvents=[
{ EventTypeMaybeList, GUIObjectMaybeList } | T ],
DefaultSubscriberPid, LoopState ) ->
update_event_loop_tables( [ { EventTypeMaybeList, GUIObjectMaybeList,
[ DefaultSubscriberPid ] } | T ],
DefaultSubscriberPid, LoopState ).
% (helper)
-spec register_event_types_for( gui_object(), [ event_type() ],
[ event_subscriber_pid() ], loop_state() ) -> loop_state().
register_event_types_for( Canvas={ myriad_object_ref, canvas, CanvasId },
EventTypes, Subscribers, LoopState=#loop_state{
event_table=EventTable,
type_table=TypeTable } ) ->
%trace_utils:debug_fmt( "Registering subscribers ~w for event types ~p "
% "regarding canvas '~s'.", [ Subscribers, EventTypes,
% gui:object_to_string( Canvas ) ] ),
NewEventTable = record_subscriptions( Canvas, EventTypes, Subscribers,
EventTable ),
% A canvas is registered in wx as a panel (as wx will send events about it)
% that will be reassigned as a canvas:
CanvasState = get_instance_state( canvas, CanvasId, TypeTable ),
Panel = CanvasState#canvas_state.panel,
% Will defer all events (paint, size) of the underlying panel to the canvas:
[ gui_wx_backend:connect( Panel, EvType ) || EvType <- EventTypes ],
LoopState#loop_state{ event_table=NewEventTable };
register_event_types_for( GUIObject, EventTypes, Subscribers,
LoopState=#loop_state{ event_table=EventTable } ) ->
trace_utils:debug_fmt( "Registering subscribers ~w for event types ~p "
"regarding object '~s'.", [ Subscribers, EventTypes,
gui:object_to_string( GUIObject ) ] ),
% Auto-connection to the current PID (i.e. the one of the internal, main
% event loop), so that it receives these events for their upcoming
% dispatching to the actual subscribers:
%
[ gui_wx_backend:connect( GUIObject, EvType ) || EvType <- EventTypes ],
% Now prepare the upcoming routing to the right subscriber:
NewEventTable = record_subscriptions( GUIObject, EventTypes, Subscribers,
EventTable ),
LoopState#loop_state{ event_table=NewEventTable }.
% Records the specified subscribers for each of the specified event types for
% the specified GUI object.
%
% (helper)
%
-spec record_subscriptions( gui_object(), [ event_type() ],
[ event_subscriber_pid() ], event_table() ) -> event_table().
record_subscriptions( GUIObject, EventTypes, Subscribers, EventTable ) ->
NewDispatchTable= case table:lookup_entry( GUIObject, EventTable ) of
key_not_found ->
UniqueSubscribers = list_utils:uniquify( Subscribers ),
Entries = [ { EvType, UniqueSubscribers } || EvType <- EventTypes ],
list_table:new( Entries );
{ value, DispatchTable } ->
update_event_table( EventTypes, Subscribers, DispatchTable )
end,
table:add_entry( GUIObject, NewDispatchTable, EventTable ).
% Returns an event dispatch table recording specified event type / subscriber
% associations.
%
-spec update_event_table( [ event_type() ], [ event_subscriber_pid() ],
event_dispatch_table() ) -> event_dispatch_table().
update_event_table( _EventTypes=[], _Subscribers, DispatchTable ) ->
DispatchTable;
update_event_table( _EventTypes=[ EventType | T ], Subscribers,
DispatchTable ) ->
NewSubscribers = case list_table:lookup_entry( EventType,
DispatchTable ) of
key_not_found ->
list_utils:uniquify( Subscribers );
{ value, CurrentSubscribers } ->
list_utils:union( CurrentSubscribers, Subscribers )
end,
NewDispatchTable = list_table:add_entry( EventType, NewSubscribers,
DispatchTable ),
update_event_table( T, Subscribers, NewDispatchTable ).
% Adjusts the specified MyriadGUI instances.
-spec adjust_objects( [ myriad_object_ref() ], event_table(),
myriad_type_table() ) -> myriad_type_table().
adjust_objects( _ObjectsToAdjust=[], _EventTable, TypeTable ) ->
TypeTable;
adjust_objects( _ObjectsToAdjust=[ CanvasRef=#myriad_object_ref{
object_type=canvas,
myriad_instance_pid=CanvasId } | T ],
EventTable, TypeTable ) ->
CanvasState = get_canvas_instance_state( CanvasId, TypeTable ),
NewCanvasState = case gui_canvas:adjust_size( CanvasState ) of
{ _NeedsRepaint=true, AdjCanvasState } ->
EventType = onRepaintNeeded,
Subscribers = get_subscribers_for( CanvasRef, EventType,
EventTable ),
send_event( Subscribers, EventType, _EventSourceId=undefined,
CanvasRef, _UserData=[], _Event=undefined ),
AdjCanvasState;
{ _NeedsRepaint=false, AdjCanvasState } ->
AdjCanvasState
end,
NewTypeTable = set_canvas_instance_state( CanvasId, NewCanvasState,
TypeTable ),
adjust_objects( T, EventTable, NewTypeTable ).
% Returns the internal state of the specified canvas instance.
-spec get_canvas_instance_state( gui:myriad_instance_pid(),
myriad_type_table() ) -> gui:myriad_object_state().
get_canvas_instance_state( CanvasId, TypeTable ) ->
get_instance_state( _MyriadObjectType=canvas, CanvasId, TypeTable ).
% Returns the internal state of the specified MyriadGUI instance.
-spec get_instance_state( myriad_object_ref(), myriad_type_table() ) ->
gui:myriad_object_state().
get_instance_state( { myriad_object_ref, MyriadObjectType, InstanceId },
TypeTable ) ->
get_instance_state( MyriadObjectType, InstanceId, TypeTable ).
% Returns the internal state of the specified MyriadGUI instance.
-spec get_instance_state( myriad_object_ref(), myriad_type_table(),
myriad_type_table() ) -> gui:myriad_object_state().
get_instance_state( MyriadObjectType, InstanceId, TypeTable ) ->
%trace_utils:debug_fmt( "~s", [ type_table_to_string( TypeTable ) ] ),
case table:lookup_entry( MyriadObjectType, TypeTable ) of
{ value, #instance_referential{ instance_table=InstanceTable } } ->
case table:lookup_entry( InstanceId, InstanceTable ) of
{ value, InstanceState } ->
InstanceState;
key_not_found ->
trace_utils:error_fmt( "Non-existing instance ~w "
"of type ~s; ~s",
[ InstanceId, MyriadObjectType,
type_table_to_string( TypeTable ) ] ),
throw( { non_existing_instance, InstanceId,
MyriadObjectType } )
end;
key_not_found ->
throw( { invalid_myriad_object_type, MyriadObjectType } )
end.
% Sets the internal state of the specified canvas instance.
-spec set_canvas_instance_state( gui:myriad_instance_pid(),
gui:myriad_object_state(), myriad_type_table() ) -> myriad_type_table().
set_canvas_instance_state( CanvasId, CanvasState, TypeTable ) ->
set_instance_state( _MyriadObjectType=canvas, CanvasId, CanvasState,
TypeTable ).
% Returns the internal state of the specified MyriadGUI instance.
-spec set_instance_state( myriad_object_ref(), gui:myriad_object_state(),
myriad_type_table() ) -> myriad_type_table().
set_instance_state( { myriad_object_ref, MyriadObjectType, InstanceId },
InstanceState, TypeTable ) ->
set_instance_state( MyriadObjectType, InstanceId, InstanceState,
TypeTable ).
% Returns the internal state of the specified, already-existing MyriadGUI
% instance.
%
-spec set_instance_state( gui:myriad_object_type(), gui:myriad_instance_pid(),
gui:myriad_object_state(), myriad_type_table() ) ->
myriad_type_table().
set_instance_state( MyriadObjectType, InstanceId, InstanceState, TypeTable ) ->
%trace_utils:debug_fmt( "~s", [ type_table_to_string( TypeTable ) ] ),
case table:lookup_entry( MyriadObjectType, TypeTable ) of
{ value, Referential=#instance_referential{
instance_table=InstanceTable } } ->
% Already existing, hence no change in instance count:
NewInstanceTable = table:update_entry( InstanceId, InstanceState,
InstanceTable ),
NewReferential = Referential#instance_referential{
instance_table=NewInstanceTable },
% An update actually:
table:add_entry( MyriadObjectType, NewReferential, TypeTable );
key_not_found ->
throw( { invalid_myriad_object_type, MyriadObjectType } )
end.
% Propagates the event designated by the specified context upward in the widget
% hierarchy (instead of the default, which is considering that it has been
% processed once for all, and thus shall not be propagated further).
%
% Events are handled in order, from bottom to top in the widgets hierarchy, by
% the last subscribed handler first. Most of the events have default event
% handler(s) set.
%
% As a result, calling this function results in having the corresponding event
% handled by the other handler(s) afterwards.
%
% In general, it is recommended to propagate all non-command events to allow the
% default handling to take place. The command events are, however, normally not
% propagated as usually a single command such as a button click or menu item
% selection must only be processed by one handler.
%
% Note: to be called from an event handler, i.e. at least from a process which
% set the wx environment.
%
-spec propagate_event( gui_event_context() ) -> void().
propagate_event( #gui_event_context{ backend_event=WxEvent } ) ->
% Honestly the skip semantics looks a bit unclear.
% 'skip' is here a synonymous of 'propagate'.
% Default is having skip=true, so same as:
% wxEvent:skip( WxEvent, _Opts=[ { skip, true } ] ):
wxEvent:skip( WxEvent ).
% Helper section.
% Stringification subsection.
% Returns a textual representation of specified event table.
-spec event_table_to_string( event_table() ) -> string().
event_table_to_string( EventTable ) ->
case table:enumerate( EventTable ) of
[] ->
"empty event table";
DispatchPairs ->
DispatchStrings = [ dispatch_table_to_string( Object, Table )
|| { Object, Table } <- DispatchPairs ],
DispatchString = text_utils:strings_to_string( DispatchStrings ),
text_utils:format( "event table with ~B GUI objects registered: ~s",
[ length( DispatchPairs ), DispatchString ] )
end.
% (helper)
-spec dispatch_table_to_string( gui_object(), event_dispatch_table() ) ->
string().
dispatch_table_to_string( GUIObject, DispatchTable ) ->
EventPairs = list_table:enumerate( DispatchTable ),
EventStrings = [ text_utils:format( "subscribers for event '~s': ~w",
[ EvType, EvSubscribers ] )
|| { EvType, EvSubscribers } <- EventPairs ],
EventString = text_utils:strings_to_string( EventStrings,
_IndentationLevel=1 ),
text_utils:format( "for GUI object '~s': ~s",
[ gui:object_to_string( GUIObject ), EventString ] ).
% Returns a textual representation of the specified reassign table.
-spec reassign_table_to_string( reassign_table() ) -> string().
reassign_table_to_string( ReassignTable ) ->
case table:enumerate( ReassignTable ) of
[] ->
"no GUI object reassignment defined";
ObjectPairs ->
Strings = [ text_utils:format( "events sent to '~s' will be "
"reassigned to '~s'",
[ gui:object_to_string( From ),
gui:object_to_string( To ) ] )
|| { From, To } <- ObjectPairs ],
text_utils:format( "~B GUI object reassignments defined: ~s",
[ length( Strings),
text_utils:strings_to_string( Strings ) ] )
end.
% Returns a textual representation of the specified type table.
-spec type_table_to_string( myriad_type_table() ) -> string().
type_table_to_string( Table ) ->
case table:enumerate( Table ) of
[] ->
"empty type table";
Pairs ->
Strings = [ text_utils:format( "for type '~s', ~s", [ Type,
instance_referential_to_string( Referential ) ] )
|| { Type, Referential } <- Pairs ],
text_utils:format( "Type table with ~B object types registered: ~s",
[ length( Strings ),
text_utils:strings_to_string( Strings ) ] )
end.
% Returns a textual representation of the specified type table.
-spec instance_referential_to_string( instance_referential() ) -> string().
instance_referential_to_string( #instance_referential{
instance_count=Count,
instance_table=InstanceTable } ) ->
case table:enumerate( InstanceTable ) of
[] ->
Count = 0,
"no instance recorded";
Pairs ->
Count = length( Pairs),
Strings = [ text_utils:format(
"ID #~B for instance whose state is: ~w",
[ Id, State ] ) || { Id, State } <- Pairs ],
text_utils:strings_to_string( Strings, _IndentLevel=1 )
end.