Completed
Pull Request — master (#7)
by Björn
01:10
created

neovim_gui.GtkUI._gtk_focus_in()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 2
rs 10
1
"""Neovim Gtk+ UI."""
2
import math
3
4
import cairo
0 ignored issues
show
Configuration introduced by
The import cairo could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
5
6
from gi.repository import GLib, GObject, Gdk, Gtk, Pango, PangoCairo
0 ignored issues
show
Configuration introduced by
The import gi.repository could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
7
8
from .screen import Screen
9
10
11
__all__ = ('GtkUI',)
12
13
14
SHIFT = Gdk.ModifierType.SHIFT_MASK
15
CTRL = Gdk.ModifierType.CONTROL_MASK
16
ALT = Gdk.ModifierType.MOD1_MASK
17
18
19
# Translation table for the names returned by Gdk.keyval_name that don't match
20
# the corresponding nvim key names.
21
KEY_TABLE = {
22
    'slash': '/',
23
    'backslash': '\\',
24
    'dead_circumflex': '^',
25
    'at': '@',
26
    'numbersign': '#',
27
    'dollar': '$',
28
    'percent': '%',
29
    'ampersand': '&',
30
    'asterisk': '*',
31
    'parenleft': '(',
32
    'parenright': ')',
33
    'underscore': '_',
34
    'plus': '+',
35
    'minus': '-',
36
    'bracketleft': '[',
37
    'bracketright': ']',
38
    'braceleft': '{',
39
    'braceright': '}',
40
    'dead_diaeresis': '"',
41
    'dead_acute': "'",
42
    'less': "<",
43
    'greater': ">",
44
    'comma': ",",
45
    'period': ".",
46
    'BackSpace': 'BS',
47
    'Return': 'CR',
48
    'Escape': 'Esc',
49
    'Delete': 'Del',
50
    'Page_Up': 'PageUp',
51
    'Page_Down': 'PageDown',
52
    'Enter': 'CR',
53
    'ISO_Left_Tab': 'Tab'
54
}
55
56
57
if (GLib.MAJOR_VERSION, GLib.MINOR_VERSION,) <= (2, 32,):
58
    GLib.threads_init()
59
60
def Rectangle(x,y,w,h):
61
    r = Gdk.Rectangle()
62
    r.x, r.y, r.width, r.height = x,y,w,h
63
    return r
64
65
66
class GtkUI(object):
67
68
    """Gtk+ UI class."""
69
70
    def __init__(self):
71
        """Initialize the UI instance."""
72
        self._redraw_arg = None
73
        self._foreground = -1
74
        self._background = -1
75
        self._font_size = 13
76
        self._font_name = 'Monospace'
77
        self._screen = None
78
        self._attrs = None
79
        self._busy = False
80
        self._mouse_enabled = False
81
        self._insert_cursor = False
82
        self._blink = False
83
        self._blink_timer_id = None
84
        self._resize_timer_id = None
85
        self._pressed = None
86
        self._invalid = None
87
        self._pending = [0, 0, 0]
88
        self._reset_cache()
89
90
    def start(self, bridge):
91
        """Start the UI event loop."""
92
        bridge.attach(80, 24, True)
93
        drawing_area = Gtk.DrawingArea()
94
        drawing_area.connect('draw', self._gtk_draw)
95
        window = Gtk.Window()
96
        window.add(drawing_area)
97
        window.set_events(window.get_events() |
98
                          Gdk.EventMask.BUTTON_PRESS_MASK |
99
                          Gdk.EventMask.BUTTON_RELEASE_MASK |
100
                          Gdk.EventMask.POINTER_MOTION_MASK |
101
                          Gdk.EventMask.SCROLL_MASK)
102
        window.connect('configure-event', self._gtk_configure)
103
        window.connect('delete-event', self._gtk_quit)
104
        window.connect('key-press-event', self._gtk_key)
105
        window.connect('key-release-event', self._gtk_key_release)
106
        window.connect('button-press-event', self._gtk_button_press)
107
        window.connect('button-release-event', self._gtk_button_release)
108
        window.connect('motion-notify-event', self._gtk_motion_notify)
109
        window.connect('scroll-event', self._gtk_scroll)
110
        window.connect('focus-in-event', self._gtk_focus_in)
111
        window.connect('focus-out-event', self._gtk_focus_out)
112
        window.show_all()
113
        im_context = Gtk.IMMulticontext()
114
        im_context.set_client_window(drawing_area.get_window())
115
        im_context.set_use_preedit(False) #TODO: preedit at cursor position
116
        im_context.connect('commit', self._gtk_input)
117
        self._pango_context = drawing_area.create_pango_context()
0 ignored issues
show
Coding Style introduced by
The attribute _pango_context was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
118
        self._drawing_area = drawing_area
0 ignored issues
show
Coding Style introduced by
The attribute _drawing_area was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
119
        self._window = window
0 ignored issues
show
Coding Style introduced by
The attribute _window was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
120
        self._im_context = im_context
0 ignored issues
show
Coding Style introduced by
The attribute _im_context was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
121
        self._bridge = bridge
0 ignored issues
show
Coding Style introduced by
The attribute _bridge was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
122
        Gtk.main()
123
124
    def quit(self):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
125
        """Exit the UI event loop."""
126
        GObject.idle_add(Gtk.main_quit)
127
128
    def schedule_screen_update(self, apply_updates):
129
        """Schedule screen updates to run in the UI event loop."""
130
        def wrapper():
131
            apply_updates()
132
            self._flush()
133
            self._start_blinking()
134
            self._screen_invalid()
135
        GObject.idle_add(wrapper)
136
137
    def _screen_invalid(self):
138
        self._drawing_area.queue_draw()
139
140
    def _nvim_resize(self, columns, rows):
141
        da = self._drawing_area
142
        # create FontDescription object for the selected font/size
143
        font_str = '{0} {1}'.format(self._font_name, self._font_size)
144
        self._font, pixels, normal_width, bold_width = _parse_font(font_str)
0 ignored issues
show
Coding Style introduced by
The attribute _font was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
145
        # calculate the letter_spacing required to make bold have the same
146
        # width as normal
147
        self._bold_spacing = normal_width - bold_width
0 ignored issues
show
Coding Style introduced by
The attribute _bold_spacing was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
148
        cell_pixel_width, cell_pixel_height = pixels
149
        # calculate the total pixel width/height of the drawing area
150
        pixel_width = cell_pixel_width * columns
151
        pixel_height = cell_pixel_height * rows
152
        gdkwin = da.get_window()
153
        content = cairo.CONTENT_COLOR
154
        self._cairo_surface = gdkwin.create_similar_surface(content,
0 ignored issues
show
Coding Style introduced by
The attribute _cairo_surface was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
155
                                                            pixel_width,
156
                                                            pixel_height)
157
        self._cairo_context = cairo.Context(self._cairo_surface)
0 ignored issues
show
Coding Style introduced by
The attribute _cairo_context was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
158
        self._pango_layout = PangoCairo.create_layout(self._cairo_context)
0 ignored issues
show
Coding Style introduced by
The attribute _pango_layout was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
159
        self._pango_layout.set_alignment(Pango.Alignment.LEFT)
160
        self._pango_layout.set_font_description(self._font)
161
        self._pixel_width, self._pixel_height = pixel_width, pixel_height
0 ignored issues
show
Coding Style introduced by
The attribute _pixel_width was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
Coding Style introduced by
The attribute _pixel_height was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
162
        self._cell_pixel_width = cell_pixel_width
0 ignored issues
show
Coding Style introduced by
The attribute _cell_pixel_width was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
163
        self._cell_pixel_height = cell_pixel_height
0 ignored issues
show
Coding Style introduced by
The attribute _cell_pixel_height was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
164
        self._screen = Screen(columns, rows)
165
        self._window.resize(pixel_width, pixel_height)
166
167
    def _nvim_clear(self):
168
        self._clear_region(self._screen.top, self._screen.bot + 1,
169
                           self._screen.left, self._screen.right + 1)
170
        self._screen.clear()
171
172
    def _nvim_eol_clear(self):
173
        row, col = self._screen.row, self._screen.col
174
        self._clear_region(row, row + 1, col, self._screen.right + 1)
175
        self._screen.eol_clear()
176
177
    def _nvim_cursor_goto(self, row, col):
178
        self._screen.cursor_goto(row, col)
179
180
    def _nvim_busy_start(self):
181
        self._busy = True
182
183
    def _nvim_busy_stop(self):
184
        self._busy = False
185
186
    def _nvim_mouse_on(self):
187
        self._mouse_enabled = True
188
189
    def _nvim_mouse_off(self):
190
        self._mouse_enabled = False
191
192
    def _nvim_mode_change(self, mode):
193
        self._insert_cursor = mode == 'insert'
194
195
    def _nvim_set_scroll_region(self, top, bot, left, right):
196
        self._screen.set_scroll_region(top, bot, left, right)
197
198
    def _nvim_scroll(self, count):
199
        self._flush()
200
        top, bot = self._screen.top, self._screen.bot + 1
201
        left, right = self._screen.left, self._screen.right + 1
202
        # The diagrams below illustrate what will happen, depending on the
203
        # scroll direction. "=" is used to represent the SR(scroll region)
204
        # boundaries and "-" the moved rectangles. note that dst and src share
205
        # a common region
206
        if count > 0:
207
            # move an rectangle in the SR up, this can happen while scrolling
208
            # down
209
            # +-------------------------+
210
            # | (clipped above SR)      |            ^
211
            # |=========================| dst_top    |
212
            # | dst (still in SR)       |            |
213
            # +-------------------------+ src_top    |
214
            # | src (moved up) and dst  |            |
215
            # |-------------------------| dst_bot    |
216
            # | src (cleared)           |            |
217
            # +=========================+ src_bot
218
            src_top, src_bot = top + count, bot
219
            dst_top, dst_bot = top, bot - count
220
            clr_top, clr_bot = dst_bot, src_bot
221
        else:
222
            # move a rectangle in the SR down, this can happen while scrolling
223
            # up
224
            # +=========================+ src_top
225
            # | src (cleared)           |            |
226
            # |------------------------ | dst_top    |
227
            # | src (moved down) and dst|            |
228
            # +-------------------------+ src_bot    |
229
            # | dst (still in SR)       |            |
230
            # |=========================| dst_bot    |
231
            # | (clipped below SR)      |            v
232
            # +-------------------------+
233
            src_top, src_bot = top, bot + count
234
            dst_top, dst_bot = top - count, bot
235
            clr_top, clr_bot = src_top, dst_top
236
        self._cairo_surface.flush()
237
        self._cairo_context.save()
238
        # The move is performed by setting the source surface to itself, but
239
        # with a coordinate transformation.
240
        _, y = self._get_coords(dst_top - src_top, 0)
241
        self._cairo_context.set_source_surface(self._cairo_surface, 0, y)
242
        # Clip to ensure only dst is affected by the change
243
        self._mask_region(dst_top, dst_bot, left, right)
244
        # Do the move
245
        self._cairo_context.paint()
246
        self._cairo_context.restore()
247
        # Clear the emptied region
248
        self._clear_region(clr_top, clr_bot, left, right)
249
        self._screen.scroll(count)
250
251
    def _nvim_highlight_set(self, attrs):
252
        self._attrs = self._get_pango_attrs(attrs)
253
254
    def _nvim_put(self, text):
255
        if self._screen.row != self._pending[0]:
256
            # flush pending text if jumped to a different row
257
            self._flush()
258
        # work around some redraw glitches that can happen
259
        self._redraw_glitch_fix()
260
        # Update internal screen
261
        self._screen.put(self._get_pango_text(text), self._attrs)
262
        self._pending[1] = min(self._screen.col - 1, self._pending[1])
263
        self._pending[2] = max(self._screen.col, self._pending[2])
264
265
    def _nvim_bell(self):
266
        self._window.get_window().beep()
267
268
    def _nvim_visual_bell(self):
269
        pass
270
271
    def _nvim_update_fg(self, fg):
272
        self._foreground = fg
273
        self._reset_cache()
274
275
    def _nvim_update_bg(self, bg):
276
        self._background = bg
277
        self._reset_cache()
278
279
    def _nvim_suspend(self):
280
        self._window.iconify()
281
282
    def _nvim_set_title(self, title):
283
        self._window.set_title(title)
284
285
    def _nvim_set_icon(self, icon):
286
        self._window.set_icon_name(icon)
287
288
    def _gtk_draw(self, wid, cr):
0 ignored issues
show
Unused Code introduced by
The argument wid seems to be unused.
Loading history...
289
        if not self._screen:
290
            return
291
        # from random import random
292
        # cr.rectangle(0, 0, self._pixel_width, self._pixel_height)
293
        # cr.set_source_rgb(random(), random(), random())
294
        # cr.fill()
295
        self._cairo_surface.flush()
296
        cr.save()
297
        cr.rectangle(0, 0, self._pixel_width, self._pixel_height)
298
        cr.clip()
299
        cr.set_source_surface(self._cairo_surface, 0, 0)
300
        cr.paint()
301
        cr.restore()
302
        if not self._busy and self._blink:
303
            # Cursor is drawn separately in the window. This approach is
304
            # simpler because it doesn't taint the internal cairo surface,
305
            # which is used for scrolling
306
            row, col = self._screen.row, self._screen.col
307
            text, attrs = self._screen.get_cursor()
308
            self._pango_draw(row, col, [(text, attrs,)], cr=cr, cursor=True)
309
            x, y = self._get_coords(row, col)
310
            self._im_context.set_cursor_location(Rectangle(x, y, self._cell_pixel_width, self._cell_pixel_height))
311
312
    def _gtk_configure(self, widget, event):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
313
        def resize(*args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
314
            self._resize_timer_id = None
315
            width, height = self._window.get_size()
316
            columns = width / self._cell_pixel_width
317
            rows = height / self._cell_pixel_height
318
            if self._screen.columns == columns and self._screen.rows == rows:
319
                return
320
            self._bridge.resize(columns, rows)
321
322
        if not self._screen:
323
            return
324
        if event.width == self._pixel_width and \
325
           event.height == self._pixel_height:
326
            return
327
        if self._resize_timer_id is not None:
328
            GLib.source_remove(self._resize_timer_id)
329
        self._resize_timer_id = GLib.timeout_add(250, resize)
330
331
    def _gtk_quit(self, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
332
        self._bridge.exit()
333
334
    def _gtk_key(self, widget, event, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
335
        # This function was adapted from pangoterm source code
336
        keyval = event.keyval
337
        state = event.state
338
        # GtkIMContext will eat a Shift-Space and not tell us about shift.
339
        # Also don't let IME eat any GDK_KEY_KP_ events
340
        done = (False if state & SHIFT and keyval == ord(' ') else
341
                False if Gdk.KEY_KP_Space <= keyval <= Gdk.KEY_KP_Divide else
342
                self._im_context.filter_keypress(event))
343
        if done:
344
            # input method handled keypress
345
            return True
346
        if event.is_modifier:
347
            # We don't need to track the state of modifier bits
348
            return
349
        # translate keyval to nvim key
350
        key_name = Gdk.keyval_name(keyval)
351
        if key_name.startswith('KP_'):
352
            key_name = key_name[3:]
353
        input_str = _stringify_key(KEY_TABLE.get(key_name, key_name), state)
354
        self._bridge.input(input_str)
355
356
    def _gtk_key_release(self, widget, event, *args):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
357
        self._im_context.filter_keypress(event)
358
359
    def _gtk_button_press(self, widget, event, *args):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
360
        if not self._mouse_enabled or event.type != Gdk.EventType.BUTTON_PRESS:
361
            return
362
        button = 'Left'
363
        if event.button == 2:
364
            button = 'Middle'
365
        elif event.button == 3:
366
            button = 'Right'
367
        col = int(math.floor(event.x / self._cell_pixel_width))
368
        row = int(math.floor(event.y / self._cell_pixel_height))
369
        input_str = _stringify_key(button + 'Mouse', event.state)
370
        input_str += '<{0},{1}>'.format(col, row)
371
        self._bridge.input(input_str)
372
        self._pressed = button
373
374
    def _gtk_button_release(self, widget, event, *args):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
Unused Code introduced by
The argument event seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
375
        self._pressed = None
376
377
    def _gtk_motion_notify(self, widget, event, *args):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
378
        if not self._mouse_enabled or not self._pressed:
379
            return
380
        col = int(math.floor(event.x / self._cell_pixel_width))
381
        row = int(math.floor(event.y / self._cell_pixel_height))
382
        input_str = _stringify_key(self._pressed + 'Drag', event.state)
383
        input_str += '<{0},{1}>'.format(col, row)
384
        self._bridge.input(input_str)
385
386
    def _gtk_scroll(self, widget, event, *args):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
387
        if not self._mouse_enabled:
388
            return
389
        col = int(math.floor(event.x / self._cell_pixel_width))
390
        row = int(math.floor(event.y / self._cell_pixel_height))
391
        if event.direction == Gdk.ScrollDirection.UP:
392
            key = 'ScrollWheelUp'
393
        elif event.direction == Gdk.ScrollDirection.DOWN:
394
            key = 'ScrollWheelDown'
395
        else:
396
            return
397
        input_str = _stringify_key(key, event.state)
398
        input_str += '<{0},{1}>'.format(col, row)
399
        self._bridge.input(input_str)
400
401
    def _gtk_focus_in(self, *a):
0 ignored issues
show
Unused Code introduced by
The argument a seems to be unused.
Loading history...
402
        self._im_context.focus_in()
403
404
    def _gtk_focus_out(self, *a):
0 ignored issues
show
Unused Code introduced by
The argument a seems to be unused.
Loading history...
405
        self._im_context.focus_out()
406
407
    def _gtk_input(self, widget, input_str, *args):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
Unused Code introduced by
The argument args seems to be unused.
Loading history...
408
        self._bridge.input(input_str.replace('<', '<lt>'))
409
410
    def _start_blinking(self):
411
        def blink(*args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
412
            self._blink = not self._blink
413
            self._screen_invalid()
414
            self._blink_timer_id = GLib.timeout_add(500, blink)
415
        if self._blink_timer_id:
416
            GLib.source_remove(self._blink_timer_id)
417
        self._blink = False
418
        blink()
419
420
    def _clear_region(self, top, bot, left, right):
421
        self._flush()
422
        self._cairo_context.save()
423
        self._mask_region(top, bot, left, right)
424
        r, g, b = _split_color(self._background)
425
        r, g, b = r / 255.0, g / 255.0, b / 255.0
426
        self._cairo_context.set_source_rgb(r, g, b)
427
        self._cairo_context.paint()
428
        self._cairo_context.restore()
429
430
    def _mask_region(self, top, bot, left, right, cr=None):
431
        if not cr:
432
            cr = self._cairo_context
433
        x1, y1, x2, y2 = self._get_rect(top, bot, left, right)
434
        cr.rectangle(x1, y1, x2 - x1, y2 - y1)
435
        cr.clip()
436
437
    def _get_rect(self, top, bot, left, right):
438
        x1, y1 = self._get_coords(top, left)
439
        x2, y2 = self._get_coords(bot, right)
440
        return x1, y1, x2, y2
441
442
    def _get_coords(self, row, col):
443
        x = col * self._cell_pixel_width
444
        y = row * self._cell_pixel_height
445
        return x, y
446
447
    def _flush(self):
448
        row, startcol, endcol = self._pending
449
        self._pending[0] = self._screen.row
450
        self._pending[1] = self._screen.col
451
        self._pending[2] = self._screen.col
452
        if startcol == endcol:
453
            return
454
        self._cairo_context.save()
455
        ccol = startcol
456
        buf = []
457
        bold = False
458
        for _, col, text, attrs in self._screen.iter(row, row, startcol,
459
                                                     endcol - 1):
460
            newbold = attrs and 'bold' in attrs[0]
461
            if newbold != bold or not text:
462
                if buf:
463
                    self._pango_draw(row, ccol, buf)
464
                bold = newbold
465
                buf = [(text, attrs,)]
466
                ccol = col
467
            else:
468
                buf.append((text, attrs,))
469
        if buf:
470
            self._pango_draw(row, ccol, buf)
471
        self._cairo_context.restore()
472
473
    def _pango_draw(self, row, col, data, cr=None, cursor=False):
474
        markup = []
475
        for text, attrs in data:
476
            if not attrs:
477
                attrs = self._get_pango_attrs(None)
478
            attrs = attrs[1] if cursor else attrs[0]
479
            markup.append('<span {0}>{1}</span>'.format(attrs, text))
480
        markup = ''.join(markup)
481
        self._pango_layout.set_markup(markup, -1)
482
        # Draw the text
483
        if not cr:
484
            cr = self._cairo_context
485
        x, y = self._get_coords(row, col)
486
        if cursor and self._insert_cursor:
487
            cr.rectangle(x, y, self._cell_pixel_width / 4,
488
                         self._cell_pixel_height)
489
            cr.clip()
490
        cr.move_to(x, y)
491
        PangoCairo.update_layout(cr, self._pango_layout)
492
        PangoCairo.show_layout(cr, self._pango_layout)
493
        _, r = self._pango_layout.get_pixel_extents()
0 ignored issues
show
Unused Code introduced by
The variable r seems to be unused.
Loading history...
494
495
    def _get_pango_text(self, text):
496
        rv = self._pango_text_cache.get(text, None)
497
        if rv is None:
498
            rv = GLib.markup_escape_text(text or '')
499
            self._pango_text_cache[text] = rv
500
        return rv
501
502
    def _get_pango_attrs(self, attrs):
503
        key = tuple(sorted((k, v,) for k, v in (attrs or {}).items()))
504
        rv = self._pango_attrs_cache.get(key, None)
505
        if rv is None:
506
            fg = self._foreground if self._foreground != -1 else 0
507
            bg = self._background if self._background != -1 else 0xffffff
508
            n = {
509
                'foreground': _split_color(fg),
510
                'background': _split_color(bg),
511
            }
512
            if attrs:
513
                # make sure that foreground and background are assigned first
514
                for k in ['foreground', 'background']:
515
                    if k in attrs:
516
                        n[k] = _split_color(attrs[k])
517
                for k, v in attrs.items():
0 ignored issues
show
Unused Code introduced by
The variable v seems to be unused.
Loading history...
518
                    if k == 'reverse':
519
                        n['foreground'], n['background'] = \
520
                            n['background'], n['foreground']
521
                    elif k == 'italic':
522
                        n['font_style'] = 'italic'
523
                    elif k == 'bold':
524
                        n['font_weight'] = 'bold'
525
                        if self._bold_spacing:
526
                            n['letter_spacing'] = str(self._bold_spacing)
527
                    elif k == 'underline':
528
                        n['underline'] = 'single'
529
            c = dict(n)
530
            c['foreground'] = _invert_color(*_split_color(fg))
531
            c['background'] = _invert_color(*_split_color(bg))
532
            c['foreground'] = _stringify_color(*c['foreground'])
533
            c['background'] = _stringify_color(*c['background'])
534
            n['foreground'] = _stringify_color(*n['foreground'])
535
            n['background'] = _stringify_color(*n['background'])
536
            n = ' '.join(['{0}="{1}"'.format(k, v) for k, v in n.items()])
537
            c = ' '.join(['{0}="{1}"'.format(k, v) for k, v in c.items()])
538
            rv = (n, c,)
539
            self._pango_attrs_cache[key] = rv
540
        return rv
541
542
    def _reset_cache(self):
543
        self._pango_text_cache = {}
544
        self._pango_attrs_cache = {}
545
546
    def _redraw_glitch_fix(self):
547
        row, col = self._screen.row, self._screen.col
548
        text, attrs = self._screen.get_cursor()
0 ignored issues
show
Unused Code introduced by
The variable attrs seems to be unused.
Loading history...
549
        # when updating cells in italic or bold words, the result can become
550
        # messy(characters can be clipped or leave remains when removed). To
551
        # prevent that, always update non empty sequences of cells and the
552
        # surrounding space.
553
        # find the start of the sequence
554
        lcol = col - 1
555
        while lcol >= 0:
556
            text, _ = self._screen.get_cell(row, lcol)
557
            lcol -= 1
558
            if text == ' ':
559
                break
560
        self._pending[1] = min(lcol + 1, self._pending[1])
561
        # find the end of the sequence
562
        rcol = col + 1
563
        while rcol < self._screen.columns:
564
            text, _ = self._screen.get_cell(row, rcol)
565
            rcol += 1
566
            if text == ' ':
567
                break
568
        self._pending[2] = max(rcol, self._pending[2])
569
570
571
def _split_color(n):
572
    return ((n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff,)
573
574
575
def _invert_color(r, g, b):
576
    return (255 - r, 255 - g, 255 - b,)
577
578
579
def _stringify_color(r, g, b):
580
    return '#{0:0{1}x}'.format((r << 16) + (g << 8) + b, 6)
581
582
583
def _stringify_key(key, state):
584
    send = []
585
    if state & SHIFT:
586
        send.append('S')
587
    if state & CTRL:
588
        send.append('C')
589
    if state & ALT:
590
        send.append('A')
591
    send.append(key)
592
    return '<' + '-'.join(send) + '>'
593
594
595
def _parse_font(font, cr=None):
596
    if not cr:
597
        ims = cairo.ImageSurface(cairo.FORMAT_RGB24, 300, 300)
598
        cr = cairo.Context(ims)
599
    fd = Pango.font_description_from_string(font)
600
    layout = PangoCairo.create_layout(cr)
601
    layout.set_font_description(fd)
602
    layout.set_alignment(Pango.Alignment.LEFT)
603
    layout.set_markup('<span font_weight="bold">A</span>')
604
    bold_width, _ = layout.get_size()
605
    layout.set_markup('<span>A</span>')
606
    pixels = layout.get_pixel_size()
607
    normal_width, _ = layout.get_size()
608
    return fd, pixels, normal_width, bold_width
609