I tried to do this, and ended up with some of the functionality you want. The idea I pursued was to create a copy (from a non-maximized window because I don't know how to programmatically resize a maximized one):
copy[original_] := Module[{copy = NotebookPut[NotebookGet[original]],
size = WindowSize /. AbsoluteOptions[original, WindowSize],
margins = WindowMargins /. AbsoluteOptions[original, WindowMargins]},
size[[2]] /= 2;
SetOptions[original, WindowSize -> size, WindowMargins -> margins];
margins[[2, 2]] += size[[2]] + 50; (* fails for maximized window *)
SetOptions[copy, WindowSize -> size, WindowMargins -> margins];
copy]
Then refresh the copy whenever changes to the original are made, and vice versa:
refresh[original_, copy_, options_] := (NotebookPut[NotebookGet[original], copy];
SetOptions[copy, options];)
Initially, I tried to do the latter by just using NotebookEventActions
to handle KeyDown
events:
sync[original_, copy_] := SetOptions[original, NotebookEventActions ->
{"KeyDown" :> refresh[original, copy, AbsoluteOptions[copy]], PassEventsDown -> True}]
This function is called twice, once with 1st and 2nd arguments being the original and copy notebook respectively, and once with the arguments swapped, e.g.:
split[original_] := Module[{copy = copy[original]},
{sync[original, copy], sync[copy, original]}]
Apart from being prohibitively slow, this method has several limitations:
- Synchronization only occurs when typing in a notebook, and is not triggered by other ways notebooks can be changed.
- Keyboard shortcuts, e.g. Shift+Enter or Ctrl+V, aren't handled.
- Refreshed scroll position is always reset to the end of the notebook.
- Synchronization occurs before key is input.
Limitation 1 may be overcome by calling refresh
on other user inputs in NotebookEventActions
, and in CellEpilog
. For limitation 2, I'm not sure - maybe messing with the keyboard translations file would work. In any case, pressing an additional arrow key after using the shortcut would force synchronization.
Limitation 3 can be somewhat alleviated by using SelectionMove
in refresh
to control the scroll position, which cannot otherwise be controlled programmatically:
refresh[original_, copy_, options_] :=
(If[0 == Length[SelectedCells[copy]], SelectionMove[copy, Next, Cell];
If[0 == Length[SelectedCells[copy]], SelectionMove[copy, Previous, Cell];]];
Module[{position = FirstPosition[Cells[copy], SelectedCells[copy][[1]]][[1]]},
NotebookPut[NotebookGet[original], copy];
SetOptions[copy, options];
SelectionMove[Cells[copy][[position]], All, Cell];])
This function can be refined to have finer control over the scroll position if need be.
As with limitation 2, an easy fix for limitation 4 is to press an arrow key to force synchronization whenever necessary. Otherwise, because KeyDown
events reach the notebook event handlers before input, we need to somehow delay handling the events until after input. The only way I could do so was to queue the event handling actions, instead of letting them run preemptively. Because there isn't a direct way to do this via NotebookEventActions
, I had to resort to Dynamic
without SynchronousUpdating
:
sync[original_, copy_] := DynamicModule[{key, options = AbsoluteOptions[copy]}, Dynamic[{
SetOptions[original, NotebookEventActions ->
{"KeyDown" :> (key = CurrentValue["EventKey"]), PassEventsDown -> True}];
Dynamic[options = AbsoluteOptions[copy];],
Dynamic[refresh[original, copy, options];
key, TrackedSymbols :> {key}, SynchronousUpdating -> False]}]]
Similar to the original non-dynamic sync
, this function is called twice. However, because it creates dynamic output, which must be maintained for dynamic synchronization, the function must be called in a separate notebook, lest the dynamic in one notebook refresh the dynamic in the other in an infinite loop. CreateDialog[split[EvaluationNotebook[]]]
works but prints a "NotebookPredictions" packet error I don't understand. If desired, the error can be avoided by calling split
from another notebook.
Speed is still an issue with this method. Also, beware of race conditions resulting from switching off SynchronousUpdating
. E.g. any input entered in a notebook before a queued refresh will be cleared. If immediate synchronization isn't required, sync
can be modified to only synchronize when switching input notebooks, and queueing refresh
is no longer needed:
sync[original_, copy_] := DynamicModule[{options = AbsoluteOptions[copy]}, Dynamic[{
Dynamic[options = AbsoluteOptions[copy];],
Dynamic[If[InputNotebook[][[2]] == copy[[2]], refresh[original, copy, options]],
TrackedSymbols :> {InputNotebook}]}]]
Alternatively, replace "KeyDown"
in the first sync
definition above to synchronize only on "MouseClicked"
, "LeftArrowKeyDown"
etc.