[Pharo-project] An idea about visual feedback in Athens
so, the problem is that all you see on screen consists from shapes, which are drawn by Athens. Now, since drawings can be quite complex and include various transformations, it will be hard to track them manually to use for mouse (or fat finger) pointer hits. A simplest example: i wanna draw a rectangle, rotated by 45 degrees. and i wanna change it's color when mouse is over it. If you look at the situation from global coordinate system perspective, in order to find whether some morph contains given point or not, you have to iterate over all it's parent morphs (because they also can have coordinate system transformations). But in a local coordinate system, things remain extremely simple: to test whether a point is inside of rectangle (most morphs usually is) is a piece of cake, isnt? I think i found an interesting approach for a coordinate system feedback, (so it can be used for 'hit tests' as well as many other things) So, lets consider a simple draw method: Morph>>drawOnAthensCanvas: aCanvas aCanvas setPaint: Color red; drawShape: self bounds now if we add a single line here: self registerShapeForEvents: self bounds canvas: aCanvas. Things getting very interesting: what this method should do, is to simply capture enough state from current canvas (like transformation matrix, clipping and the shape , of course), so later it can be used to test whether some point (like mouse cursor position) is over it or not. Why in drawing method , you might ask? Where else, i would answer.. A parent morphs branch can impose own clipping and transformation, which you can easily observe during drawing, but it becomes tedious once you leave outside of it: to get same amount of information you'll have to manually go over all morph's hierarchy and apply same transformation/clipping calculations once you get to the morph under the question. The drawing method responsible for visual representation of morph on display media, which means that it actually defines the shape(s) with which users will interact. So, by drawing a bunch of morphs on desktop using such approach, we can automatically capture all the geometry (as well as its relative hierarchy), which needs to be tested for mouse hits (or receiving a mouse events in general). And the hit test , which we need to perform every mouse move, boils down to simply iterating over that hierarchical list and testing the current hand position against each element. The morphs, which are not interested in receiving a mouse events (like hidden or parent morphs of various kinds), apparently can simply avoid publishing anything into that list, instead of implementing numerous #handlesMouseDown/#handlesMouseOver etcc... and like that saving cycles for being tested every time you touching the mouse, because some of their submorphs are actually interested in receiving such events. And of course a most interesting aspect of this approach is handling a complex geometry for morphs. Because with rectangles it is fairly simple to imagine a visual feedback system which based solely on morph's bounds (like the one which we currently using), but for morphs with complex geometry, things can become quite tedious. -- Best regards, Igor Stasenko.
how much would you think your idea would cost? Stef On Aug 31, 2012, at 8:19 PM, Igor Stasenko wrote:
so, the problem is that all you see on screen consists from shapes, which are drawn by Athens. Now, since drawings can be quite complex and include various transformations, it will be hard to track them manually to use for mouse (or fat finger) pointer hits.
A simplest example: i wanna draw a rectangle, rotated by 45 degrees. and i wanna change it's color when mouse is over it.
If you look at the situation from global coordinate system perspective, in order to find whether some morph contains given point or not, you have to iterate over all it's parent morphs (because they also can have coordinate system transformations).
But in a local coordinate system, things remain extremely simple: to test whether a point is inside of rectangle (most morphs usually is) is a piece of cake, isnt?
I think i found an interesting approach for a coordinate system feedback, (so it can be used for 'hit tests' as well as many other things)
So, lets consider a simple draw method:
Morph>>drawOnAthensCanvas: aCanvas aCanvas setPaint: Color red; drawShape: self bounds
now if we add a single line here:
self registerShapeForEvents: self bounds canvas: aCanvas.
Things getting very interesting:
what this method should do, is to simply capture enough state from current canvas (like transformation matrix, clipping and the shape , of course), so later it can be used to test whether some point (like mouse cursor position) is over it or not.
Why in drawing method , you might ask? Where else, i would answer.. A parent morphs branch can impose own clipping and transformation, which you can easily observe during drawing, but it becomes tedious once you leave outside of it: to get same amount of information you'll have to manually go over all morph's hierarchy and apply same transformation/clipping calculations once you get to the morph under the question.
The drawing method responsible for visual representation of morph on display media, which means that it actually defines the shape(s) with which users will interact.
So, by drawing a bunch of morphs on desktop using such approach, we can automatically capture all the geometry (as well as its relative hierarchy), which needs to be tested for mouse hits (or receiving a mouse events in general). And the hit test , which we need to perform every mouse move, boils down to simply iterating over that hierarchical list and testing the current hand position against each element.
The morphs, which are not interested in receiving a mouse events (like hidden or parent morphs of various kinds), apparently can simply avoid publishing anything into that list, instead of implementing numerous #handlesMouseDown/#handlesMouseOver etcc... and like that saving cycles for being tested every time you touching the mouse, because some of their submorphs are actually interested in receiving such events.
And of course a most interesting aspect of this approach is handling a complex geometry for morphs. Because with rectangles it is fairly simple to imagine a visual feedback system which based solely on morph's bounds (like the one which we currently using), but for morphs with complex geometry, things can become quite tedious.
-- Best regards, Igor Stasenko.
On 31 August 2012 20:28, Stéphane Ducasse <stephane.ducasse@inria.fr> wrote:
how much would you think your idea would cost?
i don't have an idea how much an idea would cost :) to me the idea and cost are two parallel, non-intersecting dimensions. implementation, yes, it has some cost.
Stef
-- Best regards, Igor Stasenko.
and to give an idea, how current Morphic doing that, a starting point is MorphicEventDispatcher personally, i find this code too complex to understand, given heavy use of case statements and branches. But i figured out, that this method: dispatchEvent: anEvent with: aMorph "Dispatch the given event for a morph that has chosen the receiver to dispatch its events. The method implements a shortcut for repeated dispatches of events using the same dispatcher." anEvent type == lastType ifTrue:[^self perform: lastDispatch with: anEvent with: aMorph]. "Otherwise classify" lastType := anEvent type. anEvent isMouse ifTrue:[ anEvent isMouseDown ifTrue:[ lastDispatch := #dispatchMouseDown:with:. ^self dispatchMouseDown: anEvent with: aMorph]]. anEvent type == #dropEvent ifTrue:[ lastDispatch := #dispatchDropEvent:with:. ^self dispatchDropEvent: anEvent with: aMorph]. anEvent isWindowEvent ifTrue:[ lastDispatch := #dispatchWindowEvent:with:. ^self dispatchWindowEvent: anEvent with: aMorph]. lastDispatch := #dispatchDefault:with:. ^self dispatchDefault: anEvent with: aMorph --- actually can be replaced by one-liner: dispatchEvent: anEvent with: aMorph morph := aMorph. ^ anEvent sentTo: self .. yeah.. my favorite thing: double-dispatch. -- Best regards, Igor Stasenko.
On 31 August 2012 21:21, Igor Stasenko <siguctua@gmail.com> wrote:
and to give an idea, how current Morphic doing that, a starting point is MorphicEventDispatcher
personally, i find this code too complex to understand, given heavy use of case statements and branches.
But i figured out, that this method:
dispatchEvent: anEvent with: aMorph "Dispatch the given event for a morph that has chosen the receiver to dispatch its events. The method implements a shortcut for repeated dispatches of events using the same dispatcher." anEvent type == lastType ifTrue:[^self perform: lastDispatch with: anEvent with: aMorph]. "Otherwise classify" lastType := anEvent type. anEvent isMouse ifTrue:[ anEvent isMouseDown ifTrue:[ lastDispatch := #dispatchMouseDown:with:. ^self dispatchMouseDown: anEvent with: aMorph]]. anEvent type == #dropEvent ifTrue:[ lastDispatch := #dispatchDropEvent:with:. ^self dispatchDropEvent: anEvent with: aMorph]. anEvent isWindowEvent ifTrue:[ lastDispatch := #dispatchWindowEvent:with:. ^self dispatchWindowEvent: anEvent with: aMorph]. lastDispatch := #dispatchDefault:with:. ^self dispatchDefault: anEvent with: aMorph
---
actually can be replaced by one-liner:
dispatchEvent: anEvent with: aMorph morph := aMorph. ^ anEvent sentTo: self
oh.. it is two lines, sorry..
.. yeah.. my favorite thing: double-dispatch.
-- Best regards, Igor Stasenko.
-- Best regards, Igor Stasenko.
i am actually quite intrigued. .. by following a numerous code paths i found this: handlerForMouseDown: anEvent "Return the (prospective) handler for a mouse down event. The handler is temporarily installed and can be used for morphs further down the hierarchy to negotiate whether the inner or the outer morph should finally handle the event." anEvent blueButtonPressed ifTrue: [^ self handlerForBlueButtonDown: anEvent]. anEvent yellowButtonPressed ifTrue: [^ self handlerForYellowButtonDown: anEvent]. anEvent controlKeyPressed ifTrue: [^ self handlerForMetaMenu: anEvent]. (self handlesMouseDown: anEvent) ifFalse: [^ nil]. "not interested" anEvent handler ifNil: [^ self ]. "Same priority but I am innermost" "Nobody else was interested" ^self mouseDownPriority >= anEvent handler mouseDownPriority ifTrue: [ self] ifFalse: [ nil] now, i was wondering, what that #mouseDownPriority means.. and how it influencing the overall logic for determining who will handle the event.. and as it appears, this guy means nothing.. because the only implementation of it in Pharo is: mouseDownPriority "Return the default mouse down priority for the receiver" ^ 0 and 2 methods sending it (the above one and following): handlerForYellowButtonDown: anEvent "Return the (prospective) handler for a mouse down event with the yellow button pressed. The handler is temporarily installed and can be used for morphs further down the hierarchy to negotiate whether the inner or the outer morph should finally handle the event." (self hasYellowButtonMenu or: [ self handlesMouseDown: anEvent ]) ifFalse: [ ^ nil]. "Not interested." anEvent handler ifNil: [^ self]. "Nobody else was interested" "Same priority but I am innermost." ^ self mouseDownPriority >= anEvent handler mouseDownPriority ifFalse: [nil ] ifTrue: [self] Notice that both methods are actually semantically equivalent in their last statement, but at first glance i thought not, because first uses ifTrue: [ self] ifFalse: [ nil] but second uses: ifFalse: [nil ] ifTrue: [self] so, the last statement is always answers self... Ok, after reading it, i think i finally got an idea what it does: it looks like a way to redefine the natural z-order of morphs by overriding this method and answering other value(s). But my mind fails to figure at least one case where it can be useful. What about yours? -- Best regards, Igor Stasenko.
Yes we should really refactor this hierarchy. We should finish first the event model cleaning at VM/Image border then we work on this one. On Aug 31, 2012, at 9:21 PM, Igor Stasenko wrote:
and to give an idea, how current Morphic doing that, a starting point is MorphicEventDispatcher
personally, i find this code too complex to understand, given heavy use of case statements and branches.
But i figured out, that this method:
dispatchEvent: anEvent with: aMorph "Dispatch the given event for a morph that has chosen the receiver to dispatch its events. The method implements a shortcut for repeated dispatches of events using the same dispatcher." anEvent type == lastType ifTrue:[^self perform: lastDispatch with: anEvent with: aMorph]. "Otherwise classify" lastType := anEvent type. anEvent isMouse ifTrue:[ anEvent isMouseDown ifTrue:[ lastDispatch := #dispatchMouseDown:with:. ^self dispatchMouseDown: anEvent with: aMorph]]. anEvent type == #dropEvent ifTrue:[ lastDispatch := #dispatchDropEvent:with:. ^self dispatchDropEvent: anEvent with: aMorph]. anEvent isWindowEvent ifTrue:[ lastDispatch := #dispatchWindowEvent:with:. ^self dispatchWindowEvent: anEvent with: aMorph]. lastDispatch := #dispatchDefault:with:. ^self dispatchDefault: anEvent with: aMorph
---
actually can be replaced by one-liner:
dispatchEvent: anEvent with: aMorph morph := aMorph. ^ anEvent sentTo: self
.. yeah.. my favorite thing: double-dispatch.
-- Best regards, Igor Stasenko.
participants (2)
-
Igor Stasenko -
Stéphane Ducasse