rss_feed

GTK-3 Textview interactions

homeHome
pagesGTK-3
pagespython
pageswidgets

Textview widget

This example shows using the text view widget to get text entered by a use, it makes use of marked text blocks so that only some text can be edited and will dynamically read the current line and run it in python interactively.

You can type app.test() or app.showdrawing(True) as examples of the interaction between the widget and python.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/usr/bin/env python
import sys
from StringIO import StringIO
from gi.repository import Gtk, Gdk
import code
import math


class interactiveGtk:
    
    def __init__(self):
        window = Gtk.Window()
        window.set_default_size(380, 300)
        window.connect("destroy", lambda w: Gtk.main_quit())

        box = Gtk.VBox()

        self.drawingarea = Gtk.DrawingArea()

        #python console using a textview 
        console = interactive_console(Gtk.TextView())
        self.drawingarea.connect("draw", self.area_expose_cb)

        box.add(self.drawingarea)
        box.add(console.textarea)

        window.add(box)
        window.show_all()

        self.drawarea = False

    def show_drawing(self, state):
        """self.show_drawing(True) to enable showing the lines"""
        self.drawarea = state

    def test(self):
        """run app.test() when program is running to print this message"""
        print ('hello world')

    def area_expose_cb(self, widget, context):
        """expose event lets draw, lines will only display if we run self.show_lines first.
        demonstrating running state change of our program"""
        self.style = self.drawingarea.get_style()
        if self.drawarea is True:
            self.drawing(context, 210, 10)

    def drawing(self, cr, x, y):
        """ draw a circle in the drawing area """
        cr.set_line_width(10)
        cr.set_source_rgb(0.5, 0.8, 0.0)

        cr.translate(20 / 2, 20 / 2)
        cr.arc(50, 50, 50, 0, 2 * math.pi)
        cr.stroke_preserve()

        cr.set_source_rgb(0.3, 0.4, 0.4)
        cr.fill()


class interactive_console:
    editor_chars = '>>>'
    editor_chars_other = '...'
    editor_history = []
    editor_history_position = 0

    def __init__(self, textview):
        #the python editor window
        self.textarea = textview
        self.textarea.connect('key-press-event', self.key_pressed)
        self.console_buffer = self.textarea.get_buffer()

        #setup some characters which can not be changed
        self.console_buffer.set_text(self.editor_chars + 'app.show_drawing(True)')
        self.console_buffer.create_tag("uneditable", editable=False, editable_set=True)
        self.console_buffer.create_mark("input_position", self.console_buffer.get_end_iter(), True)
        self.console_buffer.create_mark("end_position", self.console_buffer.get_end_iter(), False)
        self.console_buffer.apply_tag_by_name("uneditable", self.console_buffer.get_start_iter(), self.console_buffer.get_iter_at_offset(len(self.editor_chars)))

        #interactive mode interpreter,
        #pass locals() or globals() so we can access our programs functions and variables
        #using global here so we have access to the app object in the global scope
        self.interpreter = code.InteractiveInterpreter(globals())

    def key_pressed(self, widget, event):
        """
            grab key presses, run code from textview on return
            navigate history if arrow keys are pressed
        """
        if event.keyval == Gdk.keyval_from_name('Return'):
            self.execute_line()
            return True

        if event.keyval == Gdk.keyval_from_name('Up'):
            self.console_history(-1)
            return True

        if event.keyval == Gdk.keyval_from_name('Down'):
            self.console_history(1)
            return True
        return False
    
    def execute_line(self):
        """
            carriage return was captured so lets process the textview contents for code to run
        """

        text = self.console_buffer.get_text(self.console_buffer.get_start_iter(), self.console_buffer.get_end_iter(), False)
        source = ''
        block = False
        indent = 0
        #work out code to run if its not a block or a blank line then run what we have,
        #if its a block of code like a for loop or if condition dont run it yet unless the block has finished.
        last_line = ''
        for line in text.split("\n"):
            line = line[3:].strip('')
            if line.strip() == '':
                block = False
                indent = 0
            else:
                if line.endswith(':'):
                    if block is True:
                        source += line + "\n\t"
                    else:
                        source = line + "\n\t"
                    block = True
                    indent += 1
                else:
                    if line.startswith("\t"):
                        source += line + "\n"
                    else:
                        block = False
                        source = line + "\n"
                        indent = 0
                last_line = line
        if last_line.strip() != '':
            self.append_history(last_line)
        chars = self.editor_chars

        
        # run the code grabbed from the text buffer and execute it if its not a block
        results = ''
        if block is True:
            chars = self.editor_chars_other
        else:
            chars = self.editor_chars
            results = self.execute_code(source)

        #build text for the editor, and workout which part of the text should be locked
        text_output = text + '\n' + results + chars
        text_output_length = len(text_output)
        if block is True:
            text_output += "\t" * indent
        self.update_editor(text_output, text_output_length)

    def execute_code(self, source):
        """
            run any code sent here and capture output and return the results
        """
         # capture output from stdio so we can display the errors in our editor
        result = StringIO()
        sys.stdout = result
        sys.stderr = result
        #run code output will be put into result because we are capturing stdout 
        self.interpreter.runsource(source, "<<console>>")
        # restore stdout so future output is capture to the terminal again
        sys.stdout = sys.__stdout__
        sys.stdout = sys.__stderr__
        return result.getvalue()

    def update_editor(self, text, uneditable_end=0):
        """
            pass in text to put in the editor and portion to be locked from editing
        """
        self.console_buffer.set_text(text)
        self.console_buffer.create_mark("input_position", self.console_buffer.get_end_iter(), True)
        self.console_buffer.create_mark("end_position", self.console_buffer.get_end_iter(), False)
        self.console_buffer.apply_tag_by_name("uneditable", self.console_buffer.get_start_iter(), self.console_buffer.get_iter_at_offset(uneditable_end))

    def append_history(self, line):
        """
        Store command in history drop command if limit has been reached
        """
        if len(self.editor_history) > 10:
            self.editor_history.pop()
        self.editor_history.append(line)
        self.editor_history_position = len(self.editor_history)

    def console_history(self, direction=-1):
        """ 
        drop any text on last line and insert text from history based on position
        """
        # get current text excluding last line as we will update from our history
        text = self.console_buffer.get_text(
            self.console_buffer.get_start_iter(),
            self.console_buffer.get_iter_at_line_index(
                self.console_buffer.get_line_count(), 0),
            False)

        #work out position in history and what should be displayed
        linenumber = self.editor_history_position + direction 
        if linenumber >= 0 and linenumber < len(self.editor_history):
            self.editor_history_position += direction 
            self.console_buffer.set_text(text + self.editor_chars + self.editor_history[self.editor_history_position])


app = interactiveGtk()
Gtk.main()