17
$\begingroup$

There is no dynamic controller that can represent a hierarchical menu structure: ActionMenu can only handle flat action lists. Thanks to Kuba, we know a lot about FrontEnd`AttachCell (also here), which seems to be exactly the tool for this. However, I have problems making it work correctly in my rudimentary approach:

ClearAll[subMenu];
subMenu[lbl_, top_: False, menu : {___RuleDelayed}] := 
  DynamicModule[{parent},
   EventHandler[If[TrueQ@top, Panel, Identity]@Grid[{{lbl, "\[FilledRightTriangle]"}}], {"MouseEntered" :> (MathLink`CallFrontEnd[FrontEnd`AttachCell[
         parent,
         ToBoxes@ExpressionCell[

           Panel[Column[
             DynamicModule[{over = False}, 
                EventHandler[
                 Style[First@#, 
                  Background -> 
                   Dynamic@If[over, Hue[.66, .7, .9, .5]]], {
                  "MouseEntered" :> (over = True),
                  "MouseExited" :> (over = False;),

                  "MouseClicked" :> (NotebookDelete@
                    ParentCell@EvaluationBox[]; Last@#;)
                  }]] & /@ menu, Alignment -> Left], 
            FrameMargins -> 2],
           StripOnInput -> True],
         {Automatic, {Right, Top}},
         {Left, Top},
         "ClosingActions" -> If[TrueQ@top,
           {"SelectionDeparture"(*,"MouseExited","MouseClicked",
            "ParentChanged","EvaluatorQuit","OutsideMouseClick"*)},
           {"ParentChanged"}]
         ]])}],
   Initialization :> (parent = EvaluationBox[];)];
subMenu[lbl_, lvl_: False, menu_List] := subMenu[lbl, lvl, 
   If[MatchQ[#, _RuleDelayed | _Rule], #, # :> {}] & /@ menu];
subMenu[lbl_, lvl_: False, menu_] := subMenu[lbl, lvl, {menu}];

And my example structure is:

subMenu["Menu", Top, {
  Style["choices", Gray],
  subMenu["sub 1",
    {"subsub 1.1" :> Print@11, "subsub 1.2" :> Print@12}],
  "sub 2" :> Print@2,
  subMenu["sub 3",
    {"subsub 3.1" :> Print@31, "subsub 3.2" :> Print@32}]
  }]

enter image description here

Somehow I couldn't get a grip on how the children should be destroyed. Also, highlighting is only working partime.

$\endgroup$
1
  • $\begingroup$ Nice. This is the sort of functionality I am searching for as well. I spent a few minutes yesterday after discovering Kuba's post but wasn't able to make the two popups stick. $\endgroup$ Commented Dec 21, 2015 at 22:47

1 Answer 1

11
$\begingroup$

Here's beta version, basic functionality is delivered. I have to polish it but probably I won't have time for that this year. It has to be packed into self contained module and styling options have to be enabled.

I will update a nice description of an approach too, but meanwhile, if anything is not clear, feel free to ask.

ActionNestedMenu[
 "Test menu" -> {
   "lbl 11" :> Print[1],
   "lbl 12" -> {
     "lbl 21" :> Print[21],
     "lbl 22" :> Print[22]
     },
   "lbl 13" :> Print[3],
   "lbl 14" -> {
     "lbl 41" :> Print[41],
     "lbl 42" :> Print[42]
     },
   "lbl 15" -> {
     "lbl 51" -> {
       "lbl 511" :> Print[531],
       "lbl 512" :> Print[532]
       },
     "lbl 52" :> Print[41],
     "lbl 53" :> Print[42],
     "lbl 54" -> {
       "lbl 541" :> Print[531],
       "lbl 542" :> Print[532]
       }
     }
   }
 ]

enter image description here

Code

BeginPackage["MoreUI`"];

    ActionNestedMenu;


Begin["`Private`"];

  ActionNestedMenu[menuLabel_ -> spec_] := DynamicModule[{},

    subMenuWrapper[
      menuStates,
      0,
      {
        subMenuGate[
          menuStates,
          <|
            "label"             -> menuLabel,
            "subMenuAlignment"  -> {Left, Bottom},
            "subMenu"           -> parseSpec[menuStates, spec, 1]
          |>,
          0
        ]
      }
    ]
  ];


  SetAttributes[parseSpec, HoldFirst];

  parseSpec[menuStates_, spec_List, n_]:= subMenuWrapper[
    menuStates,
    n,
    parseSpec[menuStates, #, n]& /@ spec
  ];

  parseSpec[menuStates_, lbl_ -> subMenu_, n_]:= subMenuGate[
    menuStates,
    <|
        "label"             -> lbl,
        "subMenu"           -> parseSpec[menuStates, subMenu, n+1],
        "subMenuAlignment"  -> {Right, Top}
        |>,
    n
  ];

    (************* action label wrapper **************)
  parseSpec[menuStates_, RuleDelayed[lbl_ , action_], n_]:= EventHandler[
    Button[lbl, action; (*optional*) dropSubMenu[menuStates, 1],
      Appearance    -> "Frameless",
      FrameMargins  -> 5,
      ImageMargins  -> 0,
      Alignment     -> Left,
      ImageSize     -> {{120, Full}, {Automatic, Full}},
      Background    -> Dynamic[If[CurrentValue@"MouseOver", [email protected], None]]
    ]
    ,
    {
      "MouseEntered" :> (
        dropSubMenu[menuStates, n + 1]
      )
    },
    PassEventsDown -> True
  ];


  SetAttributes[subMenuWrapper, HoldFirst];

  subMenuWrapper[menuStates_, level_, content_]:= EventHandler[
    Framed[
      Column[content],
      FrameStyle->None,
      FrameMargins->0,
      ImageMargins ->0,
      ImageSize->{{120, Full}, {Automatic, Full}}
    ],
    {
      "MouseEntered" :> (
        forgetAboutClosing @ menuStates[-1];

      ),
      "MouseExited" :> (
        menuStates[-1] = scheduleMenuClosing[menuStates, level];
      )
    }
    ,
    PassEventsDown -> True
    ];

  (************* gate item label wrapper **************)
  SetAttributes[subMenuGate, HoldFirst];

  subMenuGate[menuStates_, label_String, level_Integer]:= subMenuGate[
    menuStates,
    <|"label"->label|>,
    level
  ];

  subMenuGate[menuStates_, spec_Association, level_Integer]:= DynamicModule[{thisBox, sumbMenuBox},

    EventHandler[
      Framed[
        Grid[{{ Pane[spec["label"], 100], ">"}}],
        BaseStyle -> "Panel",
        FrameStyle-> If[level == 0, 2, None],
        FrameMargins->5,
        ImageMargins ->0,
        ImageSize->{{120, Full}, {Automatic, Full}},
        Background -> Dynamic[If[CurrentValue@"MouseOver", [email protected], None]]
      ]
      ,
      {
        "MouseEntered" :> (

          dropSubMenu[menuStates, level + 1];
          NotebookDelete @ sumbMenuBox;

          menuStates[level + 1] = sumbMenuBox = attachTo[
            thisBox, spec["subMenu"], Lookup[spec, "subMenuAlignment", {Right, Top}]];

        )
      },
      PassEventsDown -> True

    ]
    ,
    Initialization:>(
      thisBox = EvaluationBox[]
    )
  ];



  attachTo[parentbox_, what_, alignment_] := MathLink`CallFrontEnd[
        FrontEnd`AttachCell[
          parentbox,
          ToBoxes[ExpressionCell[
            what,
            StripOnInput      -> True,
            Background        -> CurrentValue@"PanelBackground",
            CellFrame         -> 1,
            CellFrameMargins -> 0
            ]
          ],
          {Automatic, alignment},
          {Left, Top},
          "ClosingActions" -> { "ParentChanged", "EvaluatorQuit"}
        ]
  ];

  SetAttributes[dropSubMenu, HoldFirst];

  dropSubMenu[menuStates_, n_]:= (

    If[
      ValueQ @ menuStates[n]
      ,
      NotebookDelete @ menuStates[n];
      menuStates[n]=.;
    ]
  );

  SetAttributes[forgetAboutClosing, HoldFirst];
  forgetAboutClosing[task_]:=(

    RunScheduledTask[ Quiet @ RemoveScheduledTask @ task, {.1}]

  );


  SetAttributes[scheduleMenuClosing, HoldFirst];

  scheduleMenuClosing[menuStates_, level_]:=RunScheduledTask[
    If[

      True
      ,

      dropSubMenu[menuStates, 1];(*this is enough since ParentChanged is included*)
      menuStates[-1]=.;

    ];
    RemoveScheduledTask[$ScheduledTask];

    ,
    {.5}
  ];

End[];
EndPackage[];
$\endgroup$
6
  • $\begingroup$ Nice, thank you! I was wondering beforehand what kind of dedicated wrapper (like an inert Subitem) to use to wrap submenus in, but it seems that RuleDelayed is inert enough for the general nested case. $\endgroup$ Commented Dec 22, 2015 at 11:46
  • $\begingroup$ @IstvánZachar Subitem is nice too when it comes to providing options for specific items. My ide was just to replace string with associations in such case. But that's not done yet. p.s. I'm using this strange ScheduledTask setup because when going from submenu to subsubmenu the latter Entered event is almost always triggered before Exited of the one we are exiting :) $\endgroup$
    – Kuba
    Commented Dec 22, 2015 at 11:49
  • 1
    $\begingroup$ This is a very interesting code. Would it be possible to have the top element of the menu react just when pressing it down? It reacts just with a MouseOver. This is an "intuitive" behaviour for submenus, but "intuitively" I would expect the top element to react with a MouseDown. $\endgroup$
    – faysou
    Commented Dec 24, 2015 at 8:37
  • 1
    $\begingroup$ Also in Windows, menus usually disappear when clicking outside of the menu, once a menu has started to be displayed. In your code the menu closes after the mouse gets out of the menu area. These comments are just suggestions, I know it must be hard. Thank you ! $\endgroup$
    – faysou
    Commented Dec 24, 2015 at 8:43
  • $\begingroup$ @ faysou thanks for feedback, will be back with something after New Year $\endgroup$
    – Kuba
    Commented Dec 24, 2015 at 10:42

Your Answer

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

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