Completed
Pull Request — master (#18)
by
unknown
01:08
created

neovim_gui.GtkUI._flush()   C

Complexity

Conditions 7

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 7
dl 0
loc 25
rs 5.5
1
"""Neovim Gtk+ UI."""
2
from __future__ import print_function, division
3
import math
4
5
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...
6
7
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...
8
9
from .screen import Screen
10
11
12
__all__ = ('GtkUI',)
13
14
15
SHIFT = Gdk.ModifierType.SHIFT_MASK
16
CTRL = Gdk.ModifierType.CONTROL_MASK
17
ALT = Gdk.ModifierType.MOD1_MASK
18
19
20
# Translation table for the names returned by Gdk.keyval_name that don't match
21
# the corresponding nvim key names.
22
KEY_TABLE = {
23
    'slash': '/',
24
    'backslash': '\\',
25
    'dead_circumflex': '^',
26
    'at': '@',
27
    'numbersign': '#',
28
    'dollar': '$',
29
    'percent': '%',
30
    'ampersand': '&',
31
    'asterisk': '*',
32
    'parenleft': '(',
33
    'parenright': ')',
34
    'underscore': '_',
35
    'plus': '+',
36
    'minus': '-',
37
    'bracketleft': '[',
38
    'bracketright': ']',
39
    'braceleft': '{',
40
    'braceright': '}',
41
    'dead_diaeresis': '"',
42
    'dead_acute': "'",
43
    'less': "<",
44
    'greater': ">",
45
    'comma': ",",
46
    'period': ".",
47
    'BackSpace': 'BS',
48
    'Return': 'CR',
49
    'Escape': 'Esc',
50
    'Delete': 'Del',
51
    'Page_Up': 'PageUp',
52
    'Page_Down': 'PageDown',
53
    'Enter': 'CR',
54
    'ISO_Left_Tab': 'Tab'
55
}
56
57
58
if (GLib.MAJOR_VERSION, GLib.MINOR_VERSION,) <= (2, 32,):
59
    GLib.threads_init()
60
61
62
def Rectangle(x, y, w, h):
63
    r = Gdk.Rectangle()
64
    r.x, r.y, r.width, r.height = x, y, w, h
65
    return r
66
67
68
class GtkUI(object):
69
70
    """Gtk+ UI class."""
71
72
    def __init__(self, font):
73
        """Initialize the UI instance."""
74
        self._redraw_arg = None
75
        self._foreground = -1
76
        self._background = -1
77
        self._font_name = font[0]
78
        self._font_size = font[1]
79
        self._screen = None
80
        self._attrs = None
81
        self._busy = False
82
        self._mouse_enabled = False
83
        self._insert_cursor = False
84
        self._blink = False
85
        self._blink_timer_id = None
86
        self._resize_timer_id = None
87
        self._pressed = None
88
        self._invalid = None
89
        self._pending = [0, 0, 0]
90
        self._reset_cache()
91
92
    def start(self, bridge):
93
        """Start the UI event loop."""
94
        bridge.attach(80, 24, True)
95
        drawing_area = Gtk.DrawingArea()
96
        drawing_area.connect('draw', self._gtk_draw)
97
        window = Gtk.Window()
98
        window.add(drawing_area)
99
        window.set_events(window.get_events() |
100
                          Gdk.EventMask.BUTTON_PRESS_MASK |
101
                          Gdk.EventMask.BUTTON_RELEASE_MASK |
102
                          Gdk.EventMask.POINTER_MOTION_MASK |
103
                          Gdk.EventMask.SCROLL_MASK)
104
        window.connect('configure-event', self._gtk_configure)
105
        window.connect('delete-event', self._gtk_quit)
106
        window.connect('key-press-event', self._gtk_key)
107
        window.connect('key-release-event', self._gtk_key_release)
108
        window.connect('button-press-event', self._gtk_button_press)
109
        window.connect('button-release-event', self._gtk_button_release)
110
        window.connect('motion-notify-event', self._gtk_motion_notify)
111
        window.connect('scroll-event', self._gtk_scroll)
112
        window.connect('focus-in-event', self._gtk_focus_in)
113
        window.connect('focus-out-event', self._gtk_focus_out)
114
        window.show_all()
115
        im_context = Gtk.IMMulticontext()
116
        im_context.set_client_window(drawing_area.get_window())
117
        im_context.set_use_preedit(False)  # TODO: preedit at cursor position
118
        im_context.connect('commit', self._gtk_input)
119
        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...
120
        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...
121
        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...
122
        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...
123
        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...
124
        Gtk.main()
125
126
    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...
127
        """Exit the UI event loop."""
128
        GObject.idle_add(Gtk.main_quit)
129
130
    def schedule_screen_update(self, apply_updates):
131
        """Schedule screen updates to run in the UI event loop."""
132
        def wrapper():
133
            apply_updates()
134
            self._flush()
135
            self._start_blinking()
136
            self._screen_invalid()
137
        GObject.idle_add(wrapper)
138
139
    def _screen_invalid(self):
140
        self._drawing_area.queue_draw()
141
142
    def _nvim_resize(self, columns, rows):
143
        da = self._drawing_area
144
        # create FontDescription object for the selected font/size
145
        font_str = '{0} {1}'.format(self._font_name, self._font_size)
146
        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...
147
        # calculate the letter_spacing required to make bold have the same
148
        # width as normal
149
        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...
150
        cell_pixel_width, cell_pixel_height = pixels
151
        # calculate the total pixel width/height of the drawing area
152
        pixel_width = cell_pixel_width * columns
153
        pixel_height = cell_pixel_height * rows
154
        gdkwin = da.get_window()
155
        content = cairo.CONTENT_COLOR
156
        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...
157
                                                            pixel_width,
158
                                                            pixel_height)
159
        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...
160
        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...
161
        self._pango_layout.set_alignment(Pango.Alignment.LEFT)
162
        self._pango_layout.set_font_description(self._font)
163
        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...
164
        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...
165
        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...
166
        self._screen = Screen(columns, rows)
167
        self._window.resize(pixel_width, pixel_height)
168
169
    def _nvim_clear(self):
170
        self._clear_region(self._screen.top, self._screen.bot + 1,
171
                           self._screen.left, self._screen.right + 1)
172
        self._screen.clear()
173
174
    def _nvim_eol_clear(self):
175
        row, col = self._screen.row, self._screen.col
176
        self._clear_region(row, row + 1, col, self._screen.right + 1)
177
        self._screen.eol_clear()
178
179
    def _nvim_cursor_goto(self, row, col):
180
        self._screen.cursor_goto(row, col)
181
182
    def _nvim_busy_start(self):
183
        self._busy = True
184
185
    def _nvim_busy_stop(self):
186
        self._busy = False
187
188
    def _nvim_mouse_on(self):
189
        self._mouse_enabled = True
190
191
    def _nvim_mouse_off(self):
192
        self._mouse_enabled = False
193
194
    def _nvim_mode_change(self, mode):
195
        self._insert_cursor = mode == 'insert'
196
197
    def _nvim_set_scroll_region(self, top, bot, left, right):
198
        self._screen.set_scroll_region(top, bot, left, right)
199
200
    def _nvim_scroll(self, count):
201
        self._flush()
202
        top, bot = self._screen.top, self._screen.bot + 1
203
        left, right = self._screen.left, self._screen.right + 1
204
        # The diagrams below illustrate what will happen, depending on the
205
        # scroll direction. "=" is used to represent the SR(scroll region)
206
        # boundaries and "-" the moved rectangles. note that dst and src share
207
        # a common region
208
        if count > 0:
209
            # move an rectangle in the SR up, this can happen while scrolling
210
            # down
211
            # +-------------------------+
212
            # | (clipped above SR)      |            ^
213
            # |=========================| dst_top    |
214
            # | dst (still in SR)       |            |
215
            # +-------------------------+ src_top    |
216
            # | src (moved up) and dst  |            |
217
            # |-------------------------| dst_bot    |
218
            # | src (cleared)           |            |
219
            # +=========================+ src_bot
220
            src_top, src_bot = top + count, bot
221
            dst_top, dst_bot = top, bot - count
222
            clr_top, clr_bot = dst_bot, src_bot
223
        else:
224
            # move a rectangle in the SR down, this can happen while scrolling
225
            # up
226
            # +=========================+ src_top
227
            # | src (cleared)           |            |
228
            # |------------------------ | dst_top    |
229
            # | src (moved down) and dst|            |
230
            # +-------------------------+ src_bot    |
231
            # | dst (still in SR)       |            |
232
            # |=========================| dst_bot    |
233
            # | (clipped below SR)      |            v
234
            # +-------------------------+
235
            src_top, src_bot = top, bot + count
236
            dst_top, dst_bot = top - count, bot
237
            clr_top, clr_bot = src_top, dst_top
238
        self._cairo_surface.flush()
239
        self._cairo_context.save()
240
        # The move is performed by setting the source surface to itself, but
241
        # with a coordinate transformation.
242
        _, y = self._get_coords(dst_top - src_top, 0)
243
        self._cairo_context.set_source_surface(self._cairo_surface, 0, y)
244
        # Clip to ensure only dst is affected by the change
245
        self._mask_region(dst_top, dst_bot, left, right)
246
        # Do the move
247
        self._cairo_context.paint()
248
        self._cairo_context.restore()
249
        # Clear the emptied region
250
        self._clear_region(clr_top, clr_bot, left, right)
251
        self._screen.scroll(count)
252
253
    def _nvim_highlight_set(self, attrs):
254
        self._attrs = self._get_pango_attrs(attrs)
255
256
    def _nvim_put(self, text):
257
        if self._screen.row != self._pending[0]:
258
            # flush pending text if jumped to a different row
259
            self._flush()
260
        # work around some redraw glitches that can happen
261
        self._redraw_glitch_fix()
262
        # Update internal screen
263
        self._screen.put(self._get_pango_text(text), self._attrs)
264
        self._pending[1] = min(self._screen.col - 1, self._pending[1])
265
        self._pending[2] = max(self._screen.col, self._pending[2])
266
267
    def _nvim_bell(self):
268
        self._window.get_window().beep()
269
270
    def _nvim_visual_bell(self):
271
        pass
272
273
    def _nvim_update_fg(self, fg):
274
        self._foreground = fg
275
        self._reset_cache()
276
277
    def _nvim_update_bg(self, bg):
278
        self._background = bg
279
        self._reset_cache()
280
281
    def _nvim_suspend(self):
282
        self._window.iconify()
283
284
    def _nvim_set_title(self, title):
285
        self._window.set_title(title)
286
287
    def _nvim_set_icon(self, icon):
288
        self._window.set_icon_name(icon)
289
290
    def _gtk_draw(self, wid, cr):
0 ignored issues
show
Unused Code introduced by
The argument wid seems to be unused.
Loading history...
291
        if not self._screen:
292
            return
293
        # from random import random
294
        # cr.rectangle(0, 0, self._pixel_width, self._pixel_height)
295
        # cr.set_source_rgb(random(), random(), random())
296
        # cr.fill()
297
        self._cairo_surface.flush()
298
        cr.save()
299
        cr.rectangle(0, 0, self._pixel_width, self._pixel_height)
300
        cr.clip()
301
        cr.set_source_surface(self._cairo_surface, 0, 0)
302
        cr.paint()
303
        cr.restore()
304
        if not self._busy and self._blink:
305
            # Cursor is drawn separately in the window. This approach is
306
            # simpler because it doesn't taint the internal cairo surface,
307
            # which is used for scrolling
308
            row, col = self._screen.row, self._screen.col
309
            text, attrs = self._screen.get_cursor()
310
            self._pango_draw(row, col, [(text, attrs,)], cr=cr, cursor=True)
311
            x, y = self._get_coords(row, col)
312
            currect = Rectangle(x, y, self._cell_pixel_width,
313
                                self._cell_pixel_height)
314
            self._im_context.set_cursor_location(currect)
315
316
    def _gtk_configure(self, widget, event):
0 ignored issues
show
Unused Code introduced by
The argument widget seems to be unused.
Loading history...
317
        def resize(*args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
318
            self._resize_timer_id = None
319
            width, height = self._window.get_size()
320
            columns = width // self._cell_pixel_width
321
            rows = height // self._cell_pixel_height
322
            if self._screen.columns == columns and self._screen.rows == rows:
323
                return
324
            self._bridge.resize(columns, rows)
325
326
        if not self._screen:
327
            return
328
        if event.width == self._pixel_width and \
329
           event.height == self._pixel_height:
330
            return
331
        if self._resize_timer_id is not None:
332
            GLib.source_remove(self._resize_timer_id)
333
        self._resize_timer_id = GLib.timeout_add(250, resize)
334
335
    def _gtk_quit(self, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
336
        self._bridge.exit()
337
338
    def _gtk_key(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...
339
        # This function was adapted from pangoterm source code
340
        keyval = event.keyval
341
        state = event.state
342
        # GtkIMContext will eat a Shift-Space and not tell us about shift.
343
        # Also don't let IME eat any GDK_KEY_KP_ events
344
        done = (False if state & SHIFT and keyval == ord(' ') else
345
                False if Gdk.KEY_KP_Space <= keyval <= Gdk.KEY_KP_Divide else
346
                self._im_context.filter_keypress(event))
347
        if done:
348
            # input method handled keypress
349
            return True
350
        if event.is_modifier:
351
            # We don't need to track the state of modifier bits
352
            return
353
        # translate keyval to nvim key
354
        key_name = Gdk.keyval_name(keyval)
355
        if key_name.startswith('KP_'):
356
            key_name = key_name[3:]
357
        input_str = _stringify_key(KEY_TABLE.get(key_name, key_name), state)
358
        self._bridge.input(input_str)
359
360
    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...
361
        self._im_context.filter_keypress(event)
362
363 View Code Duplication
    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...
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
364
        if not self._mouse_enabled or event.type != Gdk.EventType.BUTTON_PRESS:
365
            return
366
        button = 'Left'
367
        if event.button == 2:
368
            button = 'Middle'
369
        elif event.button == 3:
370
            button = 'Right'
371
        col = int(math.floor(event.x / self._cell_pixel_width))
372
        row = int(math.floor(event.y / self._cell_pixel_height))
373
        input_str = _stringify_key(button + 'Mouse', event.state)
374
        input_str += '<{0},{1}>'.format(col, row)
375
        self._bridge.input(input_str)
376
        self._pressed = button
377
378
    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...
379
        self._pressed = None
380
381
    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...
382
        if not self._mouse_enabled or not self._pressed:
383
            return
384
        col = int(math.floor(event.x / self._cell_pixel_width))
385
        row = int(math.floor(event.y / self._cell_pixel_height))
386
        input_str = _stringify_key(self._pressed + 'Drag', event.state)
387
        input_str += '<{0},{1}>'.format(col, row)
388
        self._bridge.input(input_str)
389
390 View Code Duplication
    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...
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
391
        if not self._mouse_enabled:
392
            return
393
        col = int(math.floor(event.x / self._cell_pixel_width))
394
        row = int(math.floor(event.y / self._cell_pixel_height))
395
        if event.direction == Gdk.ScrollDirection.UP:
396
            key = 'ScrollWheelUp'
397
        elif event.direction == Gdk.ScrollDirection.DOWN:
398
            key = 'ScrollWheelDown'
399
        else:
400
            return
401
        input_str = _stringify_key(key, event.state)
402
        input_str += '<{0},{1}>'.format(col, row)
403
        self._bridge.input(input_str)
404
405
    def _gtk_focus_in(self, *a):
0 ignored issues
show
Unused Code introduced by
The argument a seems to be unused.
Loading history...
406
        self._im_context.focus_in()
407
408
    def _gtk_focus_out(self, *a):
0 ignored issues
show
Unused Code introduced by
The argument a seems to be unused.
Loading history...
409
        self._im_context.focus_out()
410
411
    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...
412
        self._bridge.input(input_str.replace('<', '<lt>'))
413
414
    def _start_blinking(self):
415
        def blink(*args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
416
            self._blink = not self._blink
417
            self._screen_invalid()
418
            self._blink_timer_id = GLib.timeout_add(500, blink)
419
        if self._blink_timer_id:
420
            GLib.source_remove(self._blink_timer_id)
421
        self._blink = False
422
        blink()
423
424
    def _clear_region(self, top, bot, left, right):
425
        self._flush()
426
        self._cairo_context.save()
427
        self._mask_region(top, bot, left, right)
428
        r, g, b = _split_color(self._background)
429
        r, g, b = r / 255.0, g / 255.0, b / 255.0
430
        self._cairo_context.set_source_rgb(r, g, b)
431
        self._cairo_context.paint()
432
        self._cairo_context.restore()
433
434
    def _mask_region(self, top, bot, left, right, cr=None):
435
        if not cr:
436
            cr = self._cairo_context
437
        x1, y1, x2, y2 = self._get_rect(top, bot, left, right)
438
        cr.rectangle(x1, y1, x2 - x1, y2 - y1)
439
        cr.clip()
440
441
    def _get_rect(self, top, bot, left, right):
442
        x1, y1 = self._get_coords(top, left)
443
        x2, y2 = self._get_coords(bot, right)
444
        return x1, y1, x2, y2
445
446
    def _get_coords(self, row, col):
447
        x = col * self._cell_pixel_width
448
        y = row * self._cell_pixel_height
449
        return x, y
450
451
    def _flush(self):
452
        row, startcol, endcol = self._pending
453
        self._pending[0] = self._screen.row
454
        self._pending[1] = self._screen.col
455
        self._pending[2] = self._screen.col
456
        if startcol == endcol:
457
            return
458
        self._cairo_context.save()
459
        ccol = startcol
460
        buf = []
461
        bold = False
462
        for _, col, text, attrs in self._screen.iter(row, row, startcol,
463
                                                     endcol - 1):
464
            newbold = attrs and 'bold' in attrs[0]
465
            if newbold != bold or not text:
466
                if buf:
467
                    self._pango_draw(row, ccol, buf)
468
                bold = newbold
469
                buf = [(text, attrs,)]
470
                ccol = col
471
            else:
472
                buf.append((text, attrs,))
473
        if buf:
474
            self._pango_draw(row, ccol, buf)
475
        self._cairo_context.restore()
476
477
    def _pango_draw(self, row, col, data, cr=None, cursor=False):
478
        markup = []
479
        for text, attrs in data:
480
            if not attrs:
481
                attrs = self._get_pango_attrs(None)
482
            attrs = attrs[1] if cursor else attrs[0]
483
            markup.append('<span {0}>{1}</span>'.format(attrs, text))
484
        markup = ''.join(markup)
485
        self._pango_layout.set_markup(markup, -1)
486
        # Draw the text
487
        if not cr:
488
            cr = self._cairo_context
489
        x, y = self._get_coords(row, col)
490
        if cursor and self._insert_cursor:
491
            cr.rectangle(x, y, self._cell_pixel_width / 4,
492
                         self._cell_pixel_height)
493
            cr.clip()
494
        cr.move_to(x, y)
495
        PangoCairo.update_layout(cr, self._pango_layout)
496
        PangoCairo.show_layout(cr, self._pango_layout)
497
        _, r = self._pango_layout.get_pixel_extents()
0 ignored issues
show
Unused Code introduced by
The variable r seems to be unused.
Loading history...
498
499
    def _get_pango_text(self, text):
500
        rv = self._pango_text_cache.get(text, None)
501
        if rv is None:
502
            rv = GLib.markup_escape_text(text or '')
503
            self._pango_text_cache[text] = rv
504
        return rv
505
506
    def _get_pango_attrs(self, attrs):
507
        key = tuple(sorted((k, v,) for k, v in (attrs or {}).items()))
508
        rv = self._pango_attrs_cache.get(key, None)
509
        if rv is None:
510
            fg = self._foreground if self._foreground != -1 else 0
511
            bg = self._background if self._background != -1 else 0xffffff
512
            n = {
513
                'foreground': _split_color(fg),
514
                'background': _split_color(bg),
515
            }
516
            if attrs:
517
                # make sure that foreground and background are assigned first
518
                for k in ['foreground', 'background']:
519
                    if k in attrs:
520
                        n[k] = _split_color(attrs[k])
521
                for k, v in attrs.items():
0 ignored issues
show
Unused Code introduced by
The variable v seems to be unused.
Loading history...
522
                    if k == 'reverse':
523
                        n['foreground'], n['background'] = \
524
                            n['background'], n['foreground']
525
                    elif k == 'italic':
526
                        n['font_style'] = 'italic'
527
                    elif k == 'bold':
528
                        n['font_weight'] = 'bold'
529
                        if self._bold_spacing:
530
                            n['letter_spacing'] = str(self._bold_spacing)
531
                    elif k == 'underline':
532
                        n['underline'] = 'single'
533
            c = dict(n)
534
            c['foreground'] = _invert_color(*_split_color(fg))
535
            c['background'] = _invert_color(*_split_color(bg))
536
            c['foreground'] = _stringify_color(*c['foreground'])
537
            c['background'] = _stringify_color(*c['background'])
538
            n['foreground'] = _stringify_color(*n['foreground'])
539
            n['background'] = _stringify_color(*n['background'])
540
            n = ' '.join(['{0}="{1}"'.format(k, v) for k, v in n.items()])
541
            c = ' '.join(['{0}="{1}"'.format(k, v) for k, v in c.items()])
542
            rv = (n, c,)
543
            self._pango_attrs_cache[key] = rv
544
        return rv
545
546
    def _reset_cache(self):
547
        self._pango_text_cache = {}
548
        self._pango_attrs_cache = {}
549
550
    def _redraw_glitch_fix(self):
551
        row, col = self._screen.row, self._screen.col
552
        text, attrs = self._screen.get_cursor()
0 ignored issues
show
Unused Code introduced by
The variable attrs seems to be unused.
Loading history...
553
        # when updating cells in italic or bold words, the result can become
554
        # messy(characters can be clipped or leave remains when removed). To
555
        # prevent that, always update non empty sequences of cells and the
556
        # surrounding space.
557
        # find the start of the sequence
558
        lcol = col - 1
559
        while lcol >= 0:
560
            text, _ = self._screen.get_cell(row, lcol)
561
            lcol -= 1
562
            if text == ' ':
563
                break
564
        self._pending[1] = min(lcol + 1, self._pending[1])
565
        # find the end of the sequence
566
        rcol = col + 1
567
        while rcol < self._screen.columns:
568
            text, _ = self._screen.get_cell(row, rcol)
569
            rcol += 1
570
            if text == ' ':
571
                break
572
        self._pending[2] = max(rcol, self._pending[2])
573
574
575
def _split_color(n):
576
    return ((n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff,)
577
578
579
def _invert_color(r, g, b):
580
    return (255 - r, 255 - g, 255 - b,)
581
582
583
def _stringify_color(r, g, b):
584
    return '#{0:0{1}x}'.format((r << 16) + (g << 8) + b, 6)
585
586
587
def _stringify_key(key, state):
588
    send = []
589
    if state & SHIFT:
590
        send.append('S')
591
    if state & CTRL:
592
        send.append('C')
593
    if state & ALT:
594
        send.append('A')
595
    send.append(key)
596
    return '<' + '-'.join(send) + '>'
597
598
599
def _parse_font(font, cr=None):
600
    if not cr:
601
        ims = cairo.ImageSurface(cairo.FORMAT_RGB24, 300, 300)
602
        cr = cairo.Context(ims)
603
    fd = Pango.font_description_from_string(font)
604
    layout = PangoCairo.create_layout(cr)
605
    layout.set_font_description(fd)
606
    layout.set_alignment(Pango.Alignment.LEFT)
607
    layout.set_markup('<span font_weight="bold">A</span>')
608
    bold_width, _ = layout.get_size()
609
    layout.set_markup('<span>A</span>')
610
    pixels = layout.get_pixel_size()
611
    normal_width, _ = layout.get_size()
612
    return fd, pixels, normal_width, bold_width
613