15
$\begingroup$

I would like to create a notebook element that can replace itself.

Here's an example to illustrate:

Panel[Column[
  {Style["This is a panel", Bold],
   Button["Press me!",
    SelectionMove[EvaluationNotebook[], All, EvaluationCell]; 
    NotebookWrite[EvaluationNotebook[], 
     "\"Now I'm just a piece of static text.\""]]}]]

This code will create a little panel:

Mathematica graphics

If we press the button, the panel will get replaced by a piece of static text (i.e. not a dynamic expression that's shown as text, but just plain old non-dynamic text).

Mathematica graphics

Well, in this implementation actually it's not the panel that gets replaced, but the complete cell containing the panel, and that's my problem: I need only the panel to be replaced, so this will work even if the panel is just a subexpression in a larger expression.

How can we create an UI control that is able to replace itself with an arbitrary expression in the notebook?

$\endgroup$
  • $\begingroup$ This is at least the second time I've read a question without noting the author, and thought "This is probably one for Szabolcs to answer." afterward to see you're asking. $\endgroup$ – Mr.Wizard May 30 '12 at 7:44
  • $\begingroup$ @Mr.Wizard I thought the same thing. $\endgroup$ – Heike May 30 '12 at 7:52
  • $\begingroup$ @Heike :-) -- can you think of any native object that has the behavior Szabolcs describes? I'm drawing a blank. $\endgroup$ – Mr.Wizard May 30 '12 at 7:56
  • $\begingroup$ @Mr.Wizard Yes, there is one, and I should have mentioned it! It is the image cropping GUI. I'm not sure if it's available in 7. Paste an image into the notebook, right click, and select Crop Image. You can adjust the crop boundaries, and double-clicking it will replace the control with the cropped image. If I remember right, last time I checked this was implemented in terms of an undocumented native control that does almost everything alone, and not in terms of low-level Dynamic constructs. $\endgroup$ – Szabolcs May 30 '12 at 7:59
  • $\begingroup$ Nope, not in v7. $\endgroup$ – Mr.Wizard May 30 '12 at 8:00
13
$\begingroup$

This solution relies on putting a TagBox with a custom tag around the part to be replaced, reading the cell and replacing the tag, then writing it back. Personally I've always felt that the need to read the entire cell and write it all again seems kind of clunky, but I don't know of a better way to do this.

MakeBoxes[replacementMarker[a_,tag_],StandardForm]^:=TagBox[MakeBoxes[a],tag]

replaceMark[rule_,nb_:EvaluationNotebook[],which_:All,cell_:EvaluationCell]:=(
SelectionMove[nb,which,cell];
NotebookWrite[EvaluationNotebook[],NotebookRead[EvaluationNotebook[]]/.rule])

Row[{"Not replaced",
replacementMarker[
    Panel[Column[{Style["This is a panel",Bold],Button["Press me!",
    replaceMark[TagBox[_,"replacementTag"] :> MakeBoxes["\" Now I'm just text. \""]]
    ]}]]
,"replacementTag"]
,"Not replaced"}]

Important note! The action cannot be undone, so if used for example to allow dynamic reformatting of data or similar, be aware that mistakes can end up deleting the old contents.

$\endgroup$
  • $\begingroup$ Quite clever! +1 $\endgroup$ – Mr.Wizard May 30 '12 at 8:01
  • $\begingroup$ Thanks. I can't say I like it, it just feels dangerous to be re-writing an entire cell when there's no undo, unless of course the contents aren't important or can be regenerated quickly. $\endgroup$ – jVincent May 30 '12 at 8:13
  • $\begingroup$ Just putting it out here that these days you can just do NotebookWrite[EvaluationCell[],NotebookRead[EvaluationCell[]]/.rule, None, AutoScroll->False] without having to move the selection. There should be a way to get the box using FrontEnd`BoxReferenceFind or something but I don't know how. $\endgroup$ – b3m2a1 May 13 '17 at 21:50
  • $\begingroup$ Also there's FrontEnd`ObjectChildren which also lets you find specific boxes of a front-end object, assuming you don't mind searching for the right one. $\endgroup$ – b3m2a1 May 13 '17 at 22:11
5
$\begingroup$

Here's a version of the previous answer that doesn't move the selection and only works at the Box level:

MakeBoxes[replacementMarker[a_, tag_], StandardForm] ^:=

 TagBox[MakeBoxes[a],
  tag,
  BoxID -> tag]

replaceMark[obj_, function_, tag_] :=
 With[{ref =
    FE`BoxReference[
     FE`Evaluate@obj,
     {{tag}}
     ]
   },
  FrontEndExecute@
   FrontEnd`BoxReferenceReplace[ref,
    FrontEndExecute@
      FrontEnd`BoxReferenceRead[ref] // function
    ]
  ]

Row[{"Not replaced",
  replacementMarker[
   Panel[
    Column[{Style["This is a panel", Bold],
      Button["Press me!",
       replaceMark[
        EvaluationNotebook[],
        MakeBoxes[" Now I'm just text."] &,
        "replacementTag"
        ]
       ]
      }]],
   "replacementTag"],
  "Not replaced"}
 ]

It works in exactly the same way, but uses the FrontEnd`BoxReference* functions as "documented" here

The nice thing about this is it makes preserving the TagBox pretty easy. We just need to use a BoxOffset to replace the inner contents:

replaceMark[obj_, function_, tag_] :=
 With[{ref =
    FE`BoxReference[
     FE`Evaluate@obj,
     {{tag}},
     FE`BoxOffset -> {FE`BoxChild[1]}
     ]
   },
  FrontEndExecute@
   FrontEnd`BoxReferenceReplace[ref, 
    FrontEndExecute@FrontEnd`BoxReferenceRead[ref] // function]
  ]

One thing to note when playing with this is that it replaces the next instance of the ID from the current selection, so if you don't delete any replaced cells after your cursor it will seem as if it isn't working. Alternatively provide a UUID to your box:

With[{uuid = CreateUUID[]},
 Row[{"Not replaced",
   replacementMarker[
    Panel[
     Column[{
       Style["This is a panel", Bold],
       Button["Press me!",
        replaceMark[EvaluationNotebook[],
         MakeBoxes[uuid] &,
         uuid]
        ]
       }]
     ],
    uuid],
   "Not replaced"}]
 ]

We can even stick this in the MakeBoxes call:

MakeBoxes[replacementMarker[a_], StandardForm] ^:=

  With[{uuid = CreateUUID[]},
   TagBox[MakeBoxes[a] /. FE`$BoxUUID -> uuid,
    uuid,
    BoxID -> uuid
    ]
   ];
With[{uuid = CreateUUID[]},
 Row[{"Not replaced",
   replacementMarker[
    Panel[
     Column[{
       Style["This is a panel", Bold],
       Button["Press me!",
        replaceMark[
         EvaluationNotebook[],
         MakeBoxes[FE`$BoxUUID] &,
         FE`$BoxUUID
         ]
        ]
       }]
     ]],
   "Not replaced"}]
 ]

Now any raw replacementMarker will make itself unique with the ID being propagated down via FE`$BoxUUID with the UUID being preserved after replacement.

$\endgroup$
3
$\begingroup$

If you want to click on a structure somewhere and have it get replaced, you could use EvaluationBox:

{
a,
Button[
    Panel@Column[{Style["This is a panel",Bold],"Press me!"}],
    NotebookWrite[EvaluationBox[], "\"Now I'm just a piece of static text.\""],
    Appearance->None
],
b
}

enter image description here

After clicking it looks like:

{a, "Now I'm just a piece of static text.", b}

If you want to click on a button, and have some parent structure get replaced, you could use ParentBox + EvaluationBox:

{
a,
Panel@Column[{
    Style["This is a panel",Bold],
    Button[
        "Press me!",
        NotebookWrite[
            ParentBox@ParentBox@ParentBox@EvaluationBox[],
            "\"Now I'm just a piece of static text.\""
        ]
    ]
}],
b
}

enter image description here

After clicking:

{a, "Now I'm just a piece of static text.", b}

$\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.