I'm using Traits 4 to build a simple interactive GUI application. This application will display a timestamped log of events in a dedicated part of the GUI. This log is currently stored as a String trait.
The default editor (or View? Not sure on the exact nomenclature) for a String trait is a scrollable multi-line display widget. When the internal string value is changed, the widget updates to display the new value. If the length of the content exceeds the viewable size of the widget, then a scrollbar appears to allow the user to scroll up and down, across the entire value.
It appears that when the widget refreshes and a vertical scrollbar is then visible (content exceeds widget size), the view resets to the start of the value (top), and the scrollbar returns to the top also, obscuring the final part of the value.
In my application I wish the latest event in the log (at the bottom) to always be displayed after a value refresh. But because the view resets to the top of the value, it doesn't follow the latest entry and the user must constantly scroll to the bottom after each refresh. This is unusable in this form.
Is there a simple way to configure this trait's editor/View to scroll from the bottom?
If not, how would one go about writing a custom editor/View for this String trait? Would it be necessary to write a new view from scratch using wx/qt4 primitives, or is there some way to derive a new view from the existing one and override only the parts that are needed to implement the desired functionality?
Here's some example code that demonstrates the problem:
# from https://svn.enthought.com/enthought/ticket/1619 - broken SSL cert
from threading import Thread
from time import sleep
from enthought.traits.api import *
from enthought.traits.ui.api import View, Item, ButtonEditor
class TextDisplay(HasTraits):
string = String()
view= View( Item('string',show_label=False, springy=True, style='custom' ))
class CaptureThread(Thread):
def run(self):
self.display.string = 'Camera started\n' + self.display.string
n_img = 0
while not self.wants_abort:
sleep(.5)
n_img += 1
self.display.string += '%d image captured\n' % n_img
self.display.string += 'Camera stopped\n'
class Camera(HasTraits):
start_stop_capture = Button()
display = Instance(TextDisplay)
capture_thread = Instance(CaptureThread)
view = View( Item('start_stop_capture', show_label=False ))
def _start_stop_capture_fired(self):
if self.capture_thread and self.capture_thread.isAlive():
self.capture_thread.wants_abort = True
else:
self.capture_thread = CaptureThread()
self.capture_thread.wants_abort = False
self.capture_thread.display = self.display
self.capture_thread.start()
class MainWindow(HasTraits):
display = Instance(TextDisplay, ())
camera = Instance(Camera)
def _camera_default(self):
return Camera(display=self.display)
view = View('display', 'camera', style="custom", resizable=True)
if __name__ == '__main__':
MainWindow().configure_traits()
Click the "Start stop capture" button multiple times until the view has filled, and you'll observe that subsequent refreshes reset the scrollbar position to the top of the view.