Completed
Push — master ( d43816...59890b )
by Björn
01:03
created

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