1
|
|
|
"""Neovim Gtk+ UI.""" |
2
|
|
|
import math |
3
|
|
|
|
4
|
|
|
import cairo |
|
|
|
|
5
|
|
|
|
6
|
|
|
from gi.repository import GLib, GObject, Gdk, Gtk, Pango, PangoCairo |
|
|
|
|
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() |
|
|
|
|
119
|
|
|
self._drawing_area = drawing_area |
|
|
|
|
120
|
|
|
self._window = window |
|
|
|
|
121
|
|
|
self._im_context = im_context |
|
|
|
|
122
|
|
|
self._bridge = bridge |
|
|
|
|
123
|
|
|
Gtk.main() |
124
|
|
|
|
125
|
|
|
def quit(self): |
|
|
|
|
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) |
|
|
|
|
146
|
|
|
# calculate the letter_spacing required to make bold have the same |
147
|
|
|
# width as normal |
148
|
|
|
self._bold_spacing = normal_width - bold_width |
|
|
|
|
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, |
|
|
|
|
156
|
|
|
pixel_width, |
157
|
|
|
pixel_height) |
158
|
|
|
self._cairo_context = cairo.Context(self._cairo_surface) |
|
|
|
|
159
|
|
|
self._pango_layout = PangoCairo.create_layout(self._cairo_context) |
|
|
|
|
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 |
|
|
|
|
163
|
|
|
self._cell_pixel_width = cell_pixel_width |
|
|
|
|
164
|
|
|
self._cell_pixel_height = cell_pixel_height |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
316
|
|
|
def resize(*args): |
|
|
|
|
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): |
|
|
|
|
335
|
|
|
self._bridge.exit() |
336
|
|
|
|
337
|
|
|
def _gtk_key(self, widget, event, *args): |
|
|
|
|
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): |
|
|
|
|
360
|
|
|
self._im_context.filter_keypress(event) |
361
|
|
|
|
362
|
|
|
def _gtk_button_press(self, widget, event, *args): |
|
|
|
|
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): |
|
|
|
|
378
|
|
|
self._pressed = None |
379
|
|
|
|
380
|
|
|
def _gtk_motion_notify(self, widget, event, *args): |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
405
|
|
|
self._im_context.focus_in() |
406
|
|
|
|
407
|
|
|
def _gtk_focus_out(self, *a): |
|
|
|
|
408
|
|
|
self._im_context.focus_out() |
409
|
|
|
|
410
|
|
|
def _gtk_input(self, widget, input_str, *args): |
|
|
|
|
411
|
|
|
self._bridge.input(input_str.replace('<', '<lt>')) |
412
|
|
|
|
413
|
|
|
def _start_blinking(self): |
414
|
|
|
def blink(*args): |
|
|
|
|
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() |
|
|
|
|
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(): |
|
|
|
|
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() |
|
|
|
|
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
|
|
|
|
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.
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.