Completed
Push — master ( 368ba5...6de24b )
by Björn
01:34
created

GtkUI._gtk_focus_in()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 2
Ratio 100 %

Importance

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