4
$\begingroup$

Consider the following toy example:

I have a "dynamical, interactive" graphics primitive like the following:

ClearAll[dynamicPoint];
dynamicPoint[point_List] := DynamicModule[{color = Red},
  EventHandler[
   Dynamic@{[email protected], color, Point@point},
   {"MouseClicked" :> (
      If[color === Red,
       color = Green, color = Red
       ]
      )}
   ]
  ]

If I use the above function instead of the regular primitive Point, I get a point that changes color when I click on it:

This is all well and fine, but now I want to be able to add more dynamical points like this by clicking on the graphics. The naive way would be to wrap the whole Graphics into another EventHandler with an appropriately set event, like in the following:

DynamicModule[{
  allData = <|"NumberOfPoints" -> 1, 
    "PointsCoordinates" -> {{-1, 0}}|>
  },
 EventHandler[
  Graphics[{
    Dynamic@Table[
      dynamicPoint[allData["PointsCoordinates"][[pointIndex]]],
      {pointIndex, Range@allData["NumberOfPoints"]}
      ]
    },
   Frame -> True, PlotRange -> {{-2, 2}, {-2, 2}}
   ],
  {"MouseClicked" :> (
     allData["NumberOfPoints"]++;
     AppendTo[allData["PointsCoordinates"], MousePosition["Graphics"]]
     )},
  PassEventsDown -> True
  ]
 ]

As you can see, I can now add points, but I lost all the ability to change their colors. As I understand it, this happens because when I click on an existing point the corresponding change-color event is triggered (it's easy to check), but immediately after the EventHandler associated with the whole Graphics is also triggered, causing a redraw of all the points, and therefore of all the DynamicModules associated with them, in which the local variables are initialized again.

I think the most obvious way to avoid this problem is to prevent the outer EventHandler to be triggered when I click on a point, but how can I accomplish that?

I can put a check inside the outer event handler to make it redraw only when the mouse click is not on an already occupied part of the graphics, but I don't want to do it in this way because that would require me to change the code in this check every time I change the actual things produced by dynamicPoint. In my actual project I want these objects to be also dynamically resizable, which would make this solution hardly feasible.

Another method I thought was to add an element to allData marking whether the mouse is on a point or not. I could easily set this flag to True with a MouseMoved event in the EventHandler of every point, and then checks whether the flag is true when the outer EventHandler is triggered. The problem with this is that I cannot think of an easy way to set this flag back to False when the mouse exits the point, as there seems to be no MouseExited event.

EDIT:

As of Kuba's suggestion, to have the two EventHandler events not interfere with each other it is enough to add the option PassEventUp -> False to the inner EventHandlers. Using this we get the following:

ClearAll[dynamicPoint];
dynamicPoint[point_List] := DynamicModule[{color = Red},
  EventHandler[
   Dynamic@{[email protected], color, Point@point},
   {"MouseClicked" :> (
      If[color === Red,
       color = Green, color = Red
       ]
      )},
   PassEventsUp -> False
   ]
  ]

DynamicModule[{
  allData = <|"NumberOfPoints" -> 1,
    "PointsCoordinates" -> {{-1, 0}}|>
  },
 EventHandler[
  Graphics[{
    Dynamic@Table[
      dynamicPoint[allData["PointsCoordinates"][[pointIndex]]],
      {pointIndex, Range@allData["NumberOfPoints"]}
      ]
    },
   Frame -> True, PlotRange -> {{-2, 2}, {-2, 2}}
   ],
  {"MouseClicked" :> (
     allData["NumberOfPoints"]++;
     AppendTo[allData["PointsCoordinates"], MousePosition["Graphics"]]
     )},
  PassEventsDown -> True
  ]
 ]

As you can see, we still have the problem that redrawing all the points when a new one is added, all the DynamicModules reinitialize their variables and so all the objects' states reinitialize.

How can we solve this?

$\endgroup$
7
  • $\begingroup$ Actually I just realized that PassEventsUp -> False put into the inner EventHandlers solved the problem, great! Did you mean this with your comment? Because with PassEventDown -> False put into the outer event handler it seems I just prevent the inner ones (of the points) to be triggered, and if I put PassEventDown -> False into the inner event handlers it doesn't do anything $\endgroup$
    – glS
    Feb 21, 2017 at 20:25
  • $\begingroup$ @Kuba there is still a problem though: when the points are redrawn (clicking somewhere else in the graphics), their internal states are initialized again. Meaning with this that when I add a new point all the previous points go back to their initial states. How do you solve that? $\endgroup$
    – glS
    Feb 21, 2017 at 20:30
  • $\begingroup$ Yes, I'm aware of the problem. Not sure how to solve it without having external (graphics level) list of coordinates and colors which would govern than. $\endgroup$
    – Kuba
    Feb 21, 2017 at 21:01
  • $\begingroup$ Would you kindly include complete code for the last almost-working example? $\endgroup$
    – Mr.Wizard
    Feb 22, 2017 at 4:41
  • 1
    $\begingroup$ I'd say closely related: Is there a better way to repeatedly add elements to a Graphics(Box)?. Shortly, there isn't any general way to add objects to the scene without preparing the scene (Graphics) for this during creation. $\endgroup$
    – Kuba
    Feb 22, 2017 at 11:19

2 Answers 2

3
$\begingroup$

Perhaps I am missing some subtlety in your problem description, but it seems to me you are trying make things more complicated than necessary. I don't see why you don't alter the state of your interactive graphics objects directly using just one EventHandler For your example, such an approach takes the following form.

With[{size = .03},
  DynamicModule[{pts = {}},
    EventHandler[
      Dynamic[
        Graphics[If[Length[pts] < 1, {}, pts],
          PlotRange -> 10,
          Frame -> True]],
      {"MouseClicked" :>
         Module[
             {xy = MousePosition["Graphics"],
              cntrs = pts[[All, 3, 1]],
              pxy, pclicked},
           If[Length[pts] < 1 || Length[pxy = Nearest[cntrs, xy, {1, .4}]] == 0, 
             AppendTo[pts, {Red, PointSize[size], Point[xy]}],
             {pindex} = Pick[Range @ Length @ pts, cntrs, pxy[[1]]];
             pts[[pindex, 1]] = If[pts[[pindex, 1]] === Red, Green, Red]]]}]]]

panel

This produces a graphics panel where clicking at a position that is not too close to an already existing point produces a new red point. Clicking on an existing point flips its color.

$\endgroup$
1
  • $\begingroup$ The reason is modularity. If I do it this way every other interactive element of the point I want to implement will have to be added to the checks in the outer event handler. Also the size itself of the points may not be fixed (and they may not be points). I want to be able to add features to the points without having to modify the code with which their are included into the graphics $\endgroup$
    – glS
    Feb 22, 2017 at 8:40
2
$\begingroup$

This might work for you. Store the clicked color values by coordinate outside of dynamicPoint.

ClearAll[dynamicPoint];

color[_] := Red;

dynamicPoint[point_List] := With[{col := color[point]},
  EventHandler[
   Dynamic@{[email protected], col, 
     Point@point}, {"MouseClicked" :> (If[col === Red, col = Green, col = Red])}, 
   PassEventsUp -> False]]

DynamicModule[{allData = <|"NumberOfPoints" -> 1, 
    "PointsCoordinates" -> {{-1, 0}}|>}, 
 EventHandler[
  Graphics[{Dynamic@
     Table[dynamicPoint[allData["PointsCoordinates"][[pointIndex]]], {pointIndex, 
       Range@allData["NumberOfPoints"]}]}, Frame -> True, 
   PlotRange -> {{-2, 2}, {-2, 2}}], {"MouseClicked" :> (allData["NumberOfPoints"]++;
     AppendTo[allData["PointsCoordinates"], MousePosition["Graphics"]])}, 
  PassEventsDown -> True]]

Of course that could be written other ways as well e.g. making color local to the main DynamicModule expression.

ClearAll[dynamicPoint];

dynamicPoint[color_][point_List] := With[{col := color[point]},
  EventHandler[
   Dynamic@{[email protected], col, 
     Point@point}, {"MouseClicked" :> (If[col === Red, col = Green, col = Red])}, 
   PassEventsUp -> False]]

DynamicModule[{allData = <|"NumberOfPoints" -> 1, "PointsCoordinates" -> {{-1, 0}}|>,
   color}, color[_] := Red;
 EventHandler[
  Graphics[{Dynamic@
     Table[dynamicPoint[color][
       allData["PointsCoordinates"][[pointIndex]]], {pointIndex, 
       Range@allData["NumberOfPoints"]}]}, Frame -> True, 
   PlotRange -> {{-2, 2}, {-2, 2}}], {"MouseClicked" :> (allData["NumberOfPoints"]++;
     AppendTo[allData["PointsCoordinates"], MousePosition["Graphics"]])}, 
  PassEventsDown -> True]]

(Sorry for the hideous code formatting but I don't want to bother to manually format it unless I know this is going in the right direction.)

$\endgroup$
3
  • $\begingroup$ I like the approach, makes it very easy to handle the expressions. I found another solution myself storing instead the colors as a list of held variables stored into allData, which work but makes it tricky to handle them afterwards (hence my other question on setting held variables). To you think there is any inherent advantage in using one approach instead of the other (except for the semantic used to handle the variables)? $\endgroup$
    – glS
    Feb 22, 2017 at 16:53
  • $\begingroup$ well I'm sure you can see the link between this question and my other one, which raises the following issue: with your present solution all points will be redrawn when changing the color of a single one. To prevent this we should use method of held variables, which however incurs in the problem I raised in my other question: how to make sure the variables are still defined in the FE. How do we come out of this? :) $\endgroup$
    – glS
    Feb 23, 2017 at 12:01
  • $\begingroup$ @glS If we get the other question answered to your satisfaction I shall try to apply it here. $\endgroup$
    – Mr.Wizard
    Feb 23, 2017 at 12:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.