Intelligent text widget autoscroll

At work we use a simple chat program to keep in touch since we don't all work in a central office. It's a handy little app that is just a few hundred lines long. The program has one little quirk that I find annoying, which is that when a new message comes in the text automatically scrolls to the bottom so one can see the new text. This is what I want 99% of the time but it's that last 1% that was driving me crazy, and it's that last 1% that can sometimes make the difference between an OK application and one that is first class.

This isn't unique to a chat program; there are many types of applications that may do some sort of automatic scrolling. For example, interactive consoles, viewing the log of a running process, etc. If you have this sort of program and have added some simple autoscrolling you might have this same problem and not realize it if you haven't yet tested the program in a live environment.

The problem is this: once in a while I want to scroll back to see some text that came earlier. When I do, if someone sends a message the window will scroll to the bottom even if I have clicked on the scrollbar. If there's a conversation going on it becomes impossible to see data that has scrolled out of view -- I scroll a little, someone sends a message and BAM. I scroll a little, another message comes in, BAM. And so on.

A simple solution would be to add a checkbutton that lets the user turn the autoscroll on or off. A belt-and-suspenders solution might be to put a checkbutton on the toolbar and add a user preference to a menu:

checkbutton .toolbar.scroll -text "Auto-scroll?" -variable prefs(autoscroll)
...
.menubar.prefsMenu add checkbutton -label "Auto-scroll" -variable prefs(autoscroll)
...
proc new_message {message} {
    global prefs

    # insert the text
    .message insert end $message

    # if the user wants autoscroll, do it
    if {$prefs(autoscroll)} {.t see end}
}

That is effective but not very user friendly. If I want to scroll back through the text I must uncheck the checkbutton, do my scrolling, then remember to re-check the checkbutton to turn the scrolling back on. We can do better.

Thinking about the problem for a couple of minutes it is clear that about the only time I don't want the autoscrolling behavior is if I've manually scrolled the window. The solution, then, must lie in knowing when I've scrolled or not. If the code can determine when I've scrolled or not it can decide whether or not to do the autoscrolling.

How can the code decide if I've scrolled back? As it turns out, the text widget can tell us what portion of the text is visible by using the built-in introspection of the yview subcommand. Without any additional arguments, rather than changing what is in view yview will report the visible portion of the text widget as two fractions which describe what portion of the text is presently visible.

Armed with this knowledge, before we insert any additional text we can determine if the user has scrolled or not. Simply stated, if the bottom of the text was visible before we insert any new text it should be visible after we insert any new text. The solution thus becomes:

proc new_message {message} {
    # get the state of the widget before we add the new message
    set yview [.t yview]
    set y1 [lindex $yview 1]

    # insert the text
    .t insert end $message

    # autoscroll only if the user hasn't manually scrolled
    if {$y1 == 1} {
        .t see end
    }
}

As you can see, it doesn't take much effort to change a good solution (allowing the user to turn autoscroll on or off) into a great solution (intelligently turning it on or off). With tk, either solution is trivial to accomplish.