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