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