20
$\begingroup$

OpenerView can be nested to create a hierarchical structure such as this:

Example with OpenerView

One great aspect is that it remembers which lists are open and which are closed even after the parent has been closed and then opened again.

The two primary problems with OpenerView are:

  • Cannot be customized stylistically
  • To build the nested hierarchy all content has to be evaluated upfront, it cannot do lazy evaluation. i.e. it gets all regions at once, instead of getting lists of regions as they are requested.

I built a version of OpenerView to address this:

Example with CustomOpenerView

The animation shows that it does not remember which lists have been opened and which have not. If I close the parent list and then open it again, all children lists will be closed. However, with it I fully control how it looks (in this case I added ornamental typography to the end of each list), and it does lazy evaluation, i.e. does not request all region names at once. For example, if I open Asia, only then does it get the list of countries in Asia.

Question: How can I get the advantages of custom styling and lazy evaluation, while also having the property that it remembers which lists are open and which are not?

(In reality I am working with much deeper recursive structures than this, that's why I cannot load all lists at once.)

Sample data and OpenerView version

continents = {
   EntityClass["Country", "Asia"],
   EntityClass["Country", "Africa"],
   EntityClass["Country", "Europe"],
   EntityClass["Country", "SouthAmerica"],
   EntityClass["Country", "NorthAmerica"],
   EntityClass["Country", "Oceania"]
   };

children[reg_EntityClass] := EntityList[reg]
children[reg_Entity] := reg["RegionNames"]
children[reg_String] := {}

heading[reg : (_EntityClass | _Entity)] := CommonName[reg]
heading[reg_String] := reg

display[data_] := OpenerView[{
   heading[data],
   Column[display /@ children[data]]
   }]
display[data_String] := data

OpenerView[{"Continents", Column[display /@ continents]}]

Mathematica graphics

CustomOpenerView version

SetAttributes[CustomOpenerView, HoldAll]
CustomOpenerView[{heading_, content_, prolog_, epilog_}] := 
 DynamicModule[{x = False},
  Dynamic@CustomPaneSelector[x,
    Grid[{
      {Opener[Dynamic[x]], Item[prolog, Alignment -> Left]},
      {"", Item[content, Alignment -> Left]},
      {"", Item[epilog, Alignment -> Left]}
      }],
    Grid[{
      {Opener[Dynamic[x]], Item[heading, Alignment -> Left]}
      }]
    ]
  ]

SetAttributes[CustomPaneSelector, HoldRest]
CustomPaneSelector[x_, true_, false_] := If[x, true, false]

display[data_] := CustomOpenerView[{
   heading[data],
   Column[display /@ children[data]],
   heading[data],
   "~*~*~*~*~*~*~*~"
   }]
display[data_String] := data

CustomOpenerView[{
  "Continents",
  Column[display /@ continents],
  "Continents",
  "~*~*~*~*~*~*~*~"
  }]

Mathematica graphics

$\endgroup$
  • $\begingroup$ What kind of stylization do you have in mind? $\endgroup$ – Kuba Oct 21 '17 at 17:48
  • $\begingroup$ @Kuba The project this question pertains to is now published here. You can see the tree at the bottom of the readme in the section called "DOM Tree interface". Performance is very much an issue, so a solution would be appreciated! $\endgroup$ – C. E. Oct 21 '17 at 18:33
12
$\begingroup$

This is my solution to the problem. I keep an object at the root level which keeps track of which lists are open and which are closed. That way when a list is opened I can open it in the right state.

For me, this is not a satisfactory solution because it's much slower than using OpenerView. OpenerView doesn't recompute its values when a branch is closed and then opened again, it appears to be able to efficiently "hide" parts of the view while keeping the data structure. I have not been able to figure out how it does that.

SetAttributes[CustomPaneSelector, HoldRest]
CustomPaneSelector[x_, true_, false_] := If[x, true, false]

SetAttributes[CustomOpenerView, HoldFirst]
CustomOpenerView[{heading_, content_, prolog_, epilog_}, parent_, ID_,
   state_] := DynamicModule[{x = state},
  Dynamic@CustomPaneSelector[x,
    Grid[{
      {Opener[Dynamic[x, (parent[ID] = x = #) &]], Item[prolog, Alignment -> Left]},
      {"", Item[content, Alignment -> Left]},
      {"", Item[epilog, Alignment -> Left]}
      }],
    Grid[{
      {Opener[Dynamic[x, (parent[ID] = x = #) &]], 
       Item[heading, Alignment -> Left]}
      }]
    ]
  ]

display[data_, parent_, ID_, state_] := CustomOpenerView[{
   heading[data],
   Column@
    MapIndexed[
     display[#, parent, Join[ID, #2], parent@Join[ID, #2]] &, 
     children[data]],
   heading[data],
   "~*~*~*~*~*~*~*~"
   }, parent, ID, state]

display[data_String, parent_, ID_, state_] := data

DynamicModule[{parent},
 parent[_] = False;
 CustomOpenerView[{
   "Continents",
   Column@MapIndexed[display[#, parent, #2, parent[#2]] &, continents],
   "Continents",
   "~*~*~*~*~*~*~*~"
   }, parent, {}, False]
 ]
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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