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