tags:

views:

253

answers:

2

I'm trying to write a simple gui-based application in pygtk which provides 'live' previewing of text-based markup. The markup processing, however, can be quite computationally expensive and slow to run, so updating the preview on every keystroke is not really viable. Instead I'd like to have the update run only when user input lapses. Ideally, I'd have it update a specified interval after the last keystroke in a sequence.

I've looked into using threading.Timer, by cancelling and re-starting a timer to invoke the update function each time the "changed" signal is emitted. I've also tried to use gtk.idle_add(), but I can't seem to get it working.

Below is a very simple example - could someone suggest the best strategy to achieve what I'm after, preferably with an example?

import gtk

class example:
    def __init__(self):
        window = gtk.Window()
        window.set_title("example")
        window.resize(600,400)
        box = gtk.HBox(homogeneous = True, spacing = 2)
        buf = gtk.TextBuffer()
        buf.connect("changed", self.buf_on_change)
        textInput = gtk.TextView(buf)
        box.add(textInput)
        self.lbl = gtk.Label()
        box.add(self.lbl)
        window.add(box)
        window.connect("destroy", gtk.main_quit)
        window.show_all()

    def buf_on_change(self, buf):
        txt = buf.get_text(*buf.get_bounds())
        output = self.renderText(txt)
        self.lbl.set_text(output)

    def renderText(self, txt):
        # perform computation-intensive text-manipulation here
        output = txt
        return output

if __name__ == '__main__':
    example()
    gtk.main()
A: 

So i did not have pygtk, so i mimiced your problem using termios and a home made loop for the text input. But the part about the timer should work in gtk too. Please note that the access to the self.timer is not thread safe if you use an object of class example within more than one thread at the time you must lock self.txt and self.timer.

import termios, fcntl, sys, os, time, threading
fd = sys.stdin.fileno()

#just setting up the terminal input
oldterm = termios.tcgetattr(fd)
newattr = termios.tcgetattr(fd)
newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
termios.tcsetattr(fd, termios.TCSANOW, newattr)
oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)


class example:
  def __init__(self, timeout):
    self.timeout = timeout
    self.timer = threading.Timer(timeout, self.render_text)
    self.txt = ''
    self.timer.start() #at the end of __init__ 

  def buf_on_change(self, buf):
    print "Got buffer:", buf, time.ctime()
    self.txt = self.txt + buf
    self.timer.cancel()   #won't do no harm if timer already elapsed
    self.timer = threading.Timer(self.timeout, self.render_text)
    self.timer.start()

  def render_text(self):
    print "starting intensive computation"
    print "rendering", self.txt
    time.sleep(3)
    print "stopping intensive computation"


# just my poor mans event loop for the text input
obj = example(3)
try:
    while 1:
        time.sleep(0.001) #just to keep processor cool
        try:
            buf = sys.stdin.read(5)
            obj.buf_on_change(buf)
        except IOError: pass
finally:
    termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
    fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
snies
thanks for your answer - this is basically what I tried, but I couldn't get it to work because the gtk.main() loop was blocking the thread. think I found a solution though - see my answer below.
simon
A: 

so I think I found a solution using glib.timeout_add() instead of threading.Timer:

import gtk, glib

class example:
    def __init__(self):
        window = gtk.Window()
        window.set_title("example")
        window.resize(600,400)
        box = gtk.HBox(homogeneous = True, spacing = 2)
        self.buf = gtk.TextBuffer()
        self.buf.connect("changed", self.buf_on_change)
        textInput = gtk.TextView(self.buf)
        box.add(textInput)
        self.lbl = gtk.Label()
        box.add(self.lbl)
        window.add(box)
        window.connect("destroy", gtk.main_quit)
        window.show_all()

        self.timer = glib.timeout_add(1000, self.renderText)

    def buf_on_change(self, buf):
        glib.source_remove(self.timer)
        self.timer = glib.timeout_add(1000, self.renderText)


    def renderText(self):
        txt = self.buf.get_text(*self.buf.get_bounds())
        # perform computation-intensive text-manipulation here
        self.lbl.set_text(txt)
        return False

if __name__ == '__main__':
    example()
    gtk.main()

this seems to work as desired, but since I'm completely new to gtk (and desktop dui programming in general - in case you couldn't tell ;) ) I'd like to leave this question open in the hope that someone more experience might comment on the best way to achieve this kind of effect. I hope that's okay?

simon
Yes, `glib.timeout_add` (use `gobject.timeout_add` if this needs to work in slightly older versions of pygobject) is the right way to do this. Or use `timeout_add_seconds` if slightly less precise timing is ok (see http://library.gnome.org/devel/pygobject/stable/glib-functions.html#function-glib--timeout-add-seconds ).One thing I would improve: I would set `self.timer` to `None` before returning `False` from `renderText`, so you can check if the timer is still running and stop it before doing things like hiding this window.
mzz
thanks mzz, that's exactly the kind of reassurance I was looking for :)
simon