Dominik Picheta's Blog

GTK+: A method to guarantee scrolling

written on 23/08/2013 23:38

Ever since I started using GTK+ a couple of years ago with a little Python IRC client project called Nyx I have been faced with scrolling problems in GTK+.

Nyx has been a great introduction to GTK+ programming for me but I have since moved away from that project onto a much bigger and more exciting project called Aporia. Aporia has started as a simple text editor written in the Nimrod programming language but quickly evolved from a MS Notepad competitor to a gedit competitor. Nowadays it is used by me and the Nimrod community including the person that created Nimrod.

The problem

One of the most important features to me in Aporia is the session restore feature, it's great as it allows me to open many files, close Aporia and then easily get these files opened again when Aporia starts up (it's essentially a gift for tab hoarders like me). Aporia also saves the location of your cursor when you exit it, this means that the cursor position can also easily be restored. Once the cursor position is set the GtkSourceView should be made to scroll so that the cursor is centered in plain sight. This proved to be quite tricky to get working reliably, many times the GtkSourceView scrolled but not far enough resulting in some random part of the document being shown, other times it didn't scroll at all and only on rare occassions it scrolled successfully. This was one of the longest living bugs in Aporia, or at least one of the longest living bugs which I cared so much about.

The investigation

At first the function that I was using to scroll the GtkSourceView was gtk_text_view_scroll_to_iter. I immediately read the docs again and again to see if there was something that I was missing and I noticed that this function in fact returned a gboolean which determined whether scrolling occurred, furthermore the documentation suggested that I should use gtk_text_view_scroll_to_mark instead as it was more reliable -- it actually guaranteed scrolling. So I listened to the advice and did just that. Sadly, there was no improvement. In desperation I even attempted to change the parameters that I passed to the function which again resulted in no improvements.

I won't bore you with the details of my whole debugging process, it was very long, frustrating and not very interesting.

After a while, I figured that there must be something going on behind the scenes in GTK that I simply was not aware of. This of course was the case.

The code that was used to restore the files from the last session looked something like this:

for file in session.files:
  var sourceView = SourceViewNew() # Create a new source view.
  # Read file into the GtkSourceBuffer ... etc. (ommited for brevity)
  # Set cursor position
  var curMark = sourceView.buffer.getInsert() # Gets the mark at the cursor.
  sourceView.scrollToMark(curMark, ...) # Scrolls the source view to that mark.

The conclusion that I gathered from this piece of code is that the scrollToMark function is called before the sourceView has a chance to initialise fully. Hence the scrolling does not occur because the sourceView has not had a chance to calculate it's height and what not.

I figured that perhaps there is a signal which tells me that the GtkSourceView is ready to be scrolled. I could not find a signal which let me determine this though so I decided to ask people on GNOME's IRC network whether they were aware of such a signal. Even though some signals have been suggested to me I have found that neither of them worked. Thankfully a certain user suggested a different approach which worked perfectly.

The solution

The solution involves creating an idle proc (which is a procedure which GTK will call when it has nothing else to do until told otherwise by the return value of this procedure). In that idle proc the source view is scrolled constantly, until it is determined that the source view has in fact scrolled to the location desired. This was accomplished by using a couple of nice functions:

And here is a code sample for you copy-pasters out there:

proc idleConfirmScroll(sv: PSourceView): gboolean {.cdecl.} =
  result = false
  
  var buff = sv.getBuffer()
  var insertMark = buff.getInsert()
  var insertIter: TTextIter
  buff.getIterAtMark(addr(insertIter), insertMark)
  var insertLoc: TRectangle
  sv.getIterLocation(addr(insertIter), addr(insertLoc))
  
  var rect: TRectangle
  sv.getVisibleRect(addr(rect))
  
  # Now check whether insert iter is inside the visible rect.
  # Width has to be higher than 0 for the 'intersect' proc to work.
  if insertLoc.width <= 0: insertLoc.width = 1
  let inside = intersect(addr(rect), addr(insertLoc), nil)
  if not inside:
    sv.scrollToMark(insertMark, 0.25, False, 0.0, 0.0)
    return true # Tell GTK to keep calling this idle proc.

discard gIdleAdd(idleConfirmScroll, sourceview)

The code should be pretty simple to understand and use. I hope it will help you as much as it helped me.


This article has been tagged as Nimrod , GTK , aporia , issues , programming