13
$\begingroup$

Suppose I have a Pane containing some dynamic text, for example that is being updated by a logging function.

Pane[Dynamic[logText],
  Scrollbars -> {False, True},
  ImageSize -> {Full, 200},
  ScrollPosition -> {0, 10000000}]

I want this pane to remain scrolled to the bottom so that I can always see the last line of text. I tried setting ScrollPosition -> {0, Infinity} (or to {0, Full}, or to {0, All}), which would be a possible interface to achieve this, but neither of these work.

$\endgroup$
2
  • 1
    $\begingroup$ Why do you use the Print function? $\endgroup$ Feb 14, 2012 at 22:42
  • $\begingroup$ Good question! I don't think it's necessary. Possibly this is a holdover from when I was outputting this in the code that was doing the calculation. $\endgroup$
    – Ian Hinder
    Feb 15, 2012 at 10:18

3 Answers 3

8
$\begingroup$

I was busy exploring this problem on my own for some time now, and was not satisfied with any of the answers. They both work for some extent, but, concerning Szabolcs's answer, I would like to avoid image-processing and in case of Mr.Wizard's answer there are slight problems with it during startup (see comments) and it breaks down when the scroll position is defined as a dynamic variable itself. Thus I set out to find a better solution.

The problem

First, I want to clearly state the problem (at least what my problem was originally): given a Pane that listens to a dynamic variable text, I have to update it such that whenever text changes by a controller (e.g. a button), the Pane is automatically scrolled down to the actual end of content. But I also want to keep both the manipulable scrollbar of the Pane and the possibility to manipulate the vertical scroll position pos via another external controler (e.g. a slider). Thus the resulting Pane must be updated differently, depending on whether text or pos triggers an update.

I present two solutions, the second one being the better one in my opinion. For both cases, I exploit the fact that if the initial vertical scroll position value (pos) is set large enough, Mathematica automatically finds and sets the scroll position to the end of content.

1. Injecting code in the external controller

The simpler method is to separate the different updates by assigning them to the external controllers. Here, a button is provided that updates text externally (external to the dynamic Pane object), but it also directly redraws the Pane to find the end-of-content position.

pos = 1000; (* vertical scroll position *) 
c = 5; (* text update counter *)
text = "1\n2\n3333333333333333\n44";

(* note that this button explicitly contains the update[] code *)
Button["Update text", cc = c++; text = text <> "\n" <> 
   StringJoin@Table[ToString@cc, {RandomInteger@{1, 15}}] <> "<"; pane = update[]]
update[] := (
   pos = pos + 1000; 
   Framed@Pane[Dynamic@text, ImageSize -> {120, 60}, 
     Scrollbars -> {False, True}, 
     ScrollPosition -> Dynamic[{0, pos}, (pos = Last@#) &]]
  );

pane = update[]; (* initialize Pane *)
{Dynamic@pane, Dynamic@pos}

Note that updating text via the button or via the scrollbar correctly updates pane and pos. The only problem here is that one has to manually put the pane = update[] code into the controller, which is unwanted (at least in my case), as one only wants to change text via the button.

2. Separating updates via selective triggers

The beautiful answer by @Leonid provides a method to selectively separate different update methods of a dynamic expression depending on which "parent" variable triggers an update.

SetAttributes[makeTrigger, HoldAll];
makeTrigger[res_, var_, updateCode_] := Module[{varOld = var, trigger}, 
   trigger /; varOld =!= var := (varOld = var; res = updateCode); 
   trigger];

pos = 1000;
c = 5;
text = "1\n2\n3333333333333333\n44";
size = {120, 60};

Button["Update text", cc = c++; text = text <> "\n" <> 
   StringJoin@Table[ToString@cc, {RandomInteger@{1, 15}}] <> "<";]
Row@{"Update pos: ", Slider[Dynamic@pos, {0, 200}]}
update[] := Framed@Pane[text, ImageSize -> Dynamic@size, 
    Scrollbars -> {False, True}, 
    ScrollPosition -> Dynamic[{0, pos}, (pos = Last@#) &]];

(* triggers ONLY if text is changed! *)
trigger = makeTrigger[pane, text, (pos = pos + 1000; update[])];
pane = update[]; (* initialize pane *)
{With[{tr = trigger}, Dynamic[tr; pane, TrackedSymbols :> {text, pos}]], Dynamic@pos}

enter image description here

(The "<" character indicates the end of newly added content.)

First, a trigger is set up in the final line, that listens to whether text or pos is changed. If only pos is changed, then the resulting expression becomes Dynamic[pane], updating the vertical scroll position as required. However, if text is changed, then trigger triggers and update[] kicks in, redrawing the whole Pane object from scratch, finding and setting the correct end-of-content position.

Now it became possibly to have various controllers only updating independent variables (like text, pos, etc.) and a dependent expression that listens to the independent variables and updates according to which of them was triggered.

$\endgroup$
3
  • $\begingroup$ +1, This is really cool! I do need this sort of functionality myself for some things, so chances are that I will be using your findings! $\endgroup$ Mar 19, 2013 at 14:36
  • $\begingroup$ What did you use for the screen recording? $\endgroup$
    – Szabolcs
    Mar 19, 2013 at 20:25
  • 1
    $\begingroup$ @Szabolcs I use Corel Capture to capture windows (I cannot capture parts of windows as video) to raw avi and then VirtualDub to export to animated gif. $\endgroup$ Mar 19, 2013 at 21:30
6
$\begingroup$

This is a somewhat hackish, but working possibility:

x = 2000!;

size = {200, 200};
Dynamic@Pane[x, ImageSize -> Dynamic[size], 
  Scrollbars -> {False, True}, 
  ScrollPosition -> {0, 
    Dynamic[Last@
       Rasterize[
        Pane[x, ImageSize -> {size[[1]], Full}, 
         Scrollbars -> {False, True}], "RasterSize"] - size[[2]], 
     None]}]

Now try changing x:

x = 10!;
x = 100!;
x = 1000!;

Mathematica graphics Mathematica graphics

This solution will work even if the pane is resized using the resize grip (but that can be removed using AppearanceElements if needed).

The None in Dynamic was necessary to avoid the system trying to assign a value to Rasterize.

On Windows this scrolls 2 pixels below the lowest position, but this shouldn't cause any problems in practice. (On Windows Pane seems to have some rounding issues.)

$\endgroup$
3
$\begingroup$

There is one obvious error in your code: ScrollBars should be Scrollbars.


You need to put Dynamic outside of Pane so that it is recalculated, and use a sufficiently large value to get to the bottom. 1*^15 should do.

Print @ Dynamic @
  Pane[logText,
       Scrollbars -> {False, True},
       ImageSize -> {Full, 200},
       ScrollPosition -> {0, 1*^15}
  ];
$\endgroup$
7
  • $\begingroup$ @Szabolcs perhaps, but I did intend to complete it, which I have now done. Besides, sometimes a tiny typo fix is the answer. $\endgroup$
    – Mr.Wizard
    Feb 14, 2012 at 15:50
  • 1
    $\begingroup$ This does not work for me when all of logText fits in the Pane. Can you fix that? $\endgroup$
    – Szabolcs
    Feb 14, 2012 at 16:01
  • $\begingroup$ @Szabolcs can you give me an example? I am using logText = StringJoin @@ Table[ToString[i] <> " this here is some text!\n", {i, 5}]; and it appears to work without problem. $\endgroup$
    – Mr.Wizard
    Feb 14, 2012 at 16:04
  • $\begingroup$ I was using logText = 10! (tweak the number to get different lengths). Admittedly, it is not a string ... I can reproduce it with logText = "boo" as well, another version difference? $\endgroup$
    – Szabolcs
    Feb 14, 2012 at 16:06
  • $\begingroup$ @Szabolcs his code specifies ImageSize -> {Full,200} therefore wouldn't that be a single line? $\endgroup$
    – Mr.Wizard
    Feb 14, 2012 at 16:09

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.