Completed
Pull Request — master (#21)
by
unknown
01:17
created

neovim_gui.GtkUI._gtk_configure()   C

Complexity

Conditions 8

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 8
dl 0
loc 18
rs 6.6666

1 Method

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