36
$\begingroup$

What are the best (most robust and most convenient) ways to create palettes that can be installed permanently (using Palettes -> Install Palette...) and are safe to use? I'd be interested in how other people have done this in the past to learn more about idiomatic front end programming.

I put some code illustrating the pattern I am using now at the end of this post. I'd appreciate some comments on it.

Notes and requirements:

  1. The palette should always work, regardless of whether the kernel has been quit or an evaluation is running.

  2. The palette may have more than one button which share some code between them.

  3. It must not pollute the Global` context or change the kernel state in a way that might break something unexpectedly.

  4. I'm looking for an easy way to define palettes (minimal boilerplate code and extra work)

  5. It would be nice (non-essential) if several versions of the palette could coexist independently (my current approach doesn't have this because it uses its own context to hide its function definition, but everything in this context is shared)

  6. It would also be nice (non-essential) to integrate documentation in an easy way (help button bringing it up maybe?)


My current approach is illustrated below. It "localizes" its symbols by putting them in a separate context, and uses DynamicModule to ensure that all the definitions are done before any button code is run.

SetAttributes[paletteButton, HoldAll]
paletteButton[name_, tooltip_, func_, opt : OptionsPattern[]] := 
 Tooltip[Button[name, Unevaluated[func], Appearance -> "Palette", 
   opt], tooltip, TooltipDelay -> Automatic]

Begin["SomePalette`"];

PaletteNotebook[
 DynamicModule[{},
  Column[{
    paletteButton["One", "Button one", function[1]],
    paletteButton["Two", "Button two", function[2]]
    }],

  Initialization :> (
    function[x_] := MessageDialog[x]
    )
  ],
 WindowTitle -> "Some Palette"
 ]

End[];
$\endgroup$
6
  • 3
    $\begingroup$ BTW I always found it odd that you there isn't an 'uninstall palette' menu item. $\endgroup$ Commented Jan 18, 2012 at 15:54
  • 2
    $\begingroup$ @Sjoerd It's more difficult to implement, but I agree, it should be present. Palettes usually go into FileNameJoin[{$UserBaseDirectory, "SystemFiles", "FrontEnd", "Palettes"}] $\endgroup$
    – Szabolcs
    Commented Jan 18, 2012 at 16:02
  • $\begingroup$ Note: this is a question asking about "best-practices" / "what is idiomatic" and what different ways are there to achieve this. I already have a solution which satisfies most requirements I have, but it would be tremendously helpful to at least hear from others who have made palettes in the past. I am not sure if these types of questions will be welcome on the site or not. $\endgroup$
    – Szabolcs
    Commented Jan 18, 2012 at 18:02
  • $\begingroup$ I saw the title of this question and thought: "This sounds like a good question for Szabolcs!" I think I learned about non-trivial palettes from your web site. :-) $\endgroup$
    – Mr.Wizard
    Commented Jan 22, 2012 at 7:01
  • $\begingroup$ @Mr.Wizard Unfortunately not. I only made that simple little TablePaste palette because I was tired of importing data from website table manually. But as you can see the code is even included three times in that one (so ugly!) I didn't know anything about making palettes properly, I'm just learning now $\endgroup$
    – Szabolcs
    Commented Jan 22, 2012 at 8:47

4 Answers 4

29
+100
$\begingroup$

All palette state (i.e., variables which affect the palette and should be remembered between sessions) should be vectored through the palette's TaggingRules option, and its initialization should be done in the palette's NotebookDynamicExpression option. That, plus context isolation of any kernel functions you need to define should solve all of the points you raise, excepting the documentation issue.

An example palette which demonstrates these principles:

CreatePalette[
 Column[{Button["Print opener state", 
    MyPalette`Private`DoSomething[
     "The opener is " <> 
      If[CurrentValue[EvaluationNotebook[], {TaggingRules, "opener"}],
        "open", "closed"]]],
   OpenerView[{"Group of buttons", Column[{Button[1], Button[2]}]}, 
    Dynamic[CurrentValue[
      EvaluationNotebook[], {TaggingRules, "opener"}, False]]]}],
 NotebookDynamicExpression :> 
  Refresh[MyPalette`Private`DoSomething[MyPalette`Private`x_] := 
    Print[MyPalette`Private`x], None]]

Mathematica graphics

Let's hit the items raised in this code one by one...

  • The palette uses a kernel-defined function which is in NotebookDynamicExpression. The code is wrapped in Refresh[_,None] to ensure that it evaluates once only when the notebook is opened. The code is context isolated by hand. Note that Begin and End won't work here, although they would work inside of a package, or if you wrapped the code in ToExpression (e.g., Begin["foo`"];ToExpression["code"];End[]).
  • A palette-wide state variable is stored in the palette's TaggingRules, which can be accessed by using CurrentValue[EvaluationNotebook[],{TaggingRules,"opener"}]. Because "opener" is a string, no symbols are introduced into any context.
  • State variables will typically need to be initialized. I could do that in various standard ways, but I used the undocumented third argument to CurrentValue which sets it to False if it doesn't already have a value.
  • Once the palette is installed, the TaggingRules setting will persist between instances of the palette, even if you quit Mathematica. Mathematica automatically serializes an installed palette's TaggingRules settings when you close it by storing the value into the global option PalettesMenuSettings.
  • If you have multiple versions of the palette open, they'll each operate using independent state variables because the state variable is attached to the palette notebook. If multiple versions of the palette are installed under different names then the PalettesMenuSettings trick will store the TaggingRules separately.
$\endgroup$
9
  • 2
    $\begingroup$ Again, the highlight is TaggingRules. I have one concern about using NotebookDynamicExpression with Refresh[..., None] instead of the Initialization option of a DynamicModule wrapping everything: the definitions won't survive a kernel restart if we use Refresh[..., None]. Is there any reason not to use a DynamicModule[..., Initialization :> ...] here? Regarding Begin and End: I think if we evaluate Begin["..."] before evaluating the CreatePalette[...], then evaluate End[] after, everything new symbol in the palette will be correctly localized into the proper... $\endgroup$
    – Szabolcs
    Commented Feb 1, 2012 at 16:33
  • $\begingroup$ ... context. Is this correct? (We just need to take care that Begin, CreatePalette and End are sent to the kernel as separate expressions to ensure that Begin will affect the parsing of CreatePalette). $\endgroup$
    – Szabolcs
    Commented Feb 1, 2012 at 16:34
  • $\begingroup$ @Szabolcs The Initialization option of DynamicModule is fine if all of the code you're initializing for is inside that DynamicModule. What you cannot do is to make assumptions about the order in which subsequent DynamicModule or Dynamic expressions will fire, or even if they will fire (if they're not in the same position). If NotebookDynamicExpression doesn't fire in new kernel sessions, it certainly should. I admit to not having tried this recently. $\endgroup$
    – John Fultz
    Commented Feb 6, 2012 at 15:12
  • 2
    $\begingroup$ I hope the bug of NotebookDynamicExpression not firing in new kernel sessions gets fixed soon $\endgroup$
    – Rojo
    Commented Jan 27, 2014 at 5:24
  • 1
    $\begingroup$ @Rojolalalalalalalalalalalalala 2 years and counting so I suppose this answer is obsolete since one have to manually close and open a palette to triger definitions load. Not to mention that Dynamic[DateString[], UpdateInterval -> 1] put on top of the column here will freeze forever after kernel quit. $\endgroup$
    – Kuba
    Commented Mar 15, 2016 at 9:14
7
$\begingroup$

You could generate the palette from code in a separate notebook, and have the generated palette use a unique context by setting CellContext -> Notebook when creating the palette notebook.

I think this should help with items 2, 3, and 5.


Example (there may be better ways..)

CreateDocument[
   {Cell[BoxData[MakeBoxes[x = 2]], "Input"]}, 
   CellContext -> Notebook]

If you then look at Context[x] in the created notebook, you get something like Notebook$$21$666892`

$\endgroup$
3
  • 2
    $\begingroup$ I think you should mention that this can be done with setting CellContext->Notebook for the PaletteNotebook. $\endgroup$ Commented Jan 21, 2012 at 21:21
  • $\begingroup$ @AlbertRetey Agreed, it's good to be detailed about that. $\endgroup$ Commented Jan 21, 2012 at 21:29
  • $\begingroup$ Could you give a code example? If I simply add CellContex -> Notebook to PaletteNotebook in my example above, function will still be created in the SomePalette` context (or Global` if I remove Begin/End). This is because contexts are decided at parse time. $\endgroup$
    – Szabolcs
    Commented Jan 27, 2012 at 11:25
5
$\begingroup$

This is a supplementary answer to what John Fultz has provided.

Problem 1:

The problem is that sometimes I can't include all functionality inside the palette nor I can call Needs to load it after kernel restart because the package initialization is a little bit complicated.

What I'm fine with though is to be able to close the Palette/Dialog/GUInotebook as soon as the new session starts.

Solution:

Warning - it is based on not stable behavior that NotebookDynamicExpression is loaded only when the notebook is opened, and not when the kernel restarts. As said in comments in accepted answer, this should not be the case. But it is for more than 3 years.

So I will abuse here the fact that NotebookDynamicExpression only fires when the notebook is opened while Initialization every time the session was terminated. So we will check if Initialization is done in the same session and we can close the notebook otherwise.

This notebook will close itself after you close the kernel (which is restarted by dynamics):

CreateDocument[
 DynamicModule[{},
  Dynamic[{"Date :", DateString[]}, UpdateInterval -> 1],
  Initialization :> (
    If[CurrentValue[
       EvaluationNotebook[], {TaggingRules, "Opened"}] =!= $SessionID,
      NotebookClose[]]
    )
  ],
 TaggingRules -> {ParentList, "Opened" -> $SessionID},
 NotebookDynamicExpression :> Refresh[
   CurrentValue[
     EvaluationNotebook[], {TaggingRules, "Opened"}] = $SessionID
   , None]
 ]

Problem 2

I don't like to write full names like: NotebookDynamicExpression :> Refresh[MyPalette`Private`DoSomething[MyPalette`Private`x_] := Print[MyPalette`Private`x], None]]

Method:

You can inject the package to the palette the same way I've done this for a CDF: How can I include functions from a package into a CDF file?

$\endgroup$
3
$\begingroup$

My go-to currently is to have a template that is laid out like this:

BeginPackage["paletteContext`Private`"]

(* ::Subsubsection:: *)
(* Palette Builder Initialization *)

(* define code to initialize the palette builder *)
paletteBuilderInit[]

(* ::Subsubsection:: *)
(* Palette Options *)

(* define palette options *)

paletteOps = {  };

(* ::Subsubsection:: *)
(* Palette Formatting Objects *)

(* define attractive formatting objects the deploy as Button or other interface elements that don't load from a package *)

defFormattingObject1[]
defFormattingObject2[]

(* ::Subsubsection:: *)
(* Palette Functions *)

(* define helper functions for finding functions by name (not by explicit symbol lookup) and for loading the parent paclet of the palette *)

defLoadUnderlyingPaclet[]
defLoadFunctionByName[]
defOtherFuncs[]

(* ::Subsubsection:: *)
(* Palette Elements *)

(* define a collection of palette elements *)

paletteElement1[]
paletteElement2[]
paletteElement3[]

paletteEls = { paletteElement1, spacer , ...};

(* ::Subsubsection:: *)
(* Palette Expression *)

(* 
  This could be done just as well / better with DynamicModule, 
  but that's just not how I have it currently
 *)
paletteExpr[]=
With[{
 functionName1DVs=DownValues[functionName1],
 functionName2DVs=DownValues[functionName2]
 },
 DynamicWrapper[
  Panel[
   Column[{paletteParts}]
   ],
  DownValues[functionName1]=functionName1DVs;
  DownValues[functionName2]=functionName2DVs
  ]
 ]


(* ::Subsubsection:: *)
(* Generate Palette *)

(* Generate the palette and dump it to a file *)
generatePalette[]:=
 With[
  {
   nb=CreateDocument[paletteExpr[]]
   },
  closeOpenPalettesWithSameFileName[];
  NotebookSave[nb, paletteFileName[]];
  ]

(* ::Subsubsection:: *)
(* Reset Palette Builder State *)

resetState[]

EndPackage[]

Unfortunately, since I always use the same template, all my palettes look like this:

asdasd

But they function well and look decent, so I call it a win.

$\endgroup$

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.