@@ 27-266 (lines=240) @@ | ||
24 | range = xrange |
|
25 | ||
26 | ||
27 | class NvimTk(object): |
|
28 | ||
29 | """Wraps all nvim/tk event handling.""" |
|
30 | ||
31 | def __init__(self, nvim): |
|
32 | """Initialize with a Nvim instance.""" |
|
33 | self._nvim = nvim |
|
34 | self._attrs = {} |
|
35 | self._nvim_updates = deque() |
|
36 | self._canvas = None |
|
37 | self._fg = '#000000' |
|
38 | self._bg = '#ffffff' |
|
39 | ||
40 | def run(self): |
|
41 | """Start the UI.""" |
|
42 | self._tk_setup() |
|
43 | t = Thread(target=self._nvim_event_loop) |
|
44 | t.daemon = True |
|
45 | t.start() |
|
46 | self._root.mainloop() |
|
47 | ||
48 | def _tk_setup(self): |
|
49 | self._root = Tk() |
|
50 | self._root.bind('<<nvim_redraw>>', self._tk_nvim_redraw) |
|
51 | self._root.bind('<<nvim_detach>>', self._tk_nvim_detach) |
|
52 | self._root.bind('<Key>', self._tk_key) |
|
53 | ||
54 | def _tk_nvim_redraw(self, *args): |
|
55 | update = self._nvim_updates.popleft() |
|
56 | for update in update: |
|
57 | handler = getattr(self, '_tk_nvim_' + update[0]) |
|
58 | for args in update[1:]: |
|
59 | handler(*args) |
|
60 | ||
61 | def _tk_nvim_detach(self, *args): |
|
62 | self._root.destroy() |
|
63 | ||
64 | def _tk_nvim_resize(self, width, height): |
|
65 | self._tk_redraw_canvas(width, height) |
|
66 | ||
67 | def _tk_nvim_clear(self): |
|
68 | self._tk_clear_region(0, self._height - 1, 0, self._width - 1) |
|
69 | ||
70 | def _tk_nvim_eol_clear(self): |
|
71 | row, col = (self._cursor_row, self._cursor_col,) |
|
72 | self._tk_clear_region(row, row, col, self._scroll_right) |
|
73 | ||
74 | def _tk_nvim_cursor_goto(self, row, col): |
|
75 | self._cursor_row = row |
|
76 | self._cursor_col = col |
|
77 | ||
78 | def _tk_nvim_cursor_on(self): |
|
79 | pass |
|
80 | ||
81 | def _tk_nvim_cursor_off(self): |
|
82 | pass |
|
83 | ||
84 | def _tk_nvim_mouse_on(self): |
|
85 | pass |
|
86 | ||
87 | def _tk_nvim_mouse_off(self): |
|
88 | pass |
|
89 | ||
90 | def _tk_nvim_insert_mode(self): |
|
91 | pass |
|
92 | ||
93 | def _tk_nvim_normal_mode(self): |
|
94 | pass |
|
95 | ||
96 | def _tk_nvim_set_scroll_region(self, top, bot, left, right): |
|
97 | self._scroll_top = top |
|
98 | self._scroll_bot = bot |
|
99 | self._scroll_left = left |
|
100 | self._scroll_right = right |
|
101 | ||
102 | def _tk_nvim_scroll(self, count): |
|
103 | top, bot = (self._scroll_top, self._scroll_bot,) |
|
104 | left, right = (self._scroll_left, self._scroll_right,) |
|
105 | ||
106 | if count > 0: |
|
107 | destroy_top = top |
|
108 | destroy_bot = top + count - 1 |
|
109 | move_top = destroy_bot + 1 |
|
110 | move_bot = bot |
|
111 | fill_top = move_bot + 1 |
|
112 | fill_bot = fill_top + count - 1 |
|
113 | else: |
|
114 | destroy_top = bot + count + 1 |
|
115 | destroy_bot = bot |
|
116 | move_top = top |
|
117 | move_bot = destroy_top - 1 |
|
118 | fill_bot = move_top - 1 |
|
119 | fill_top = fill_bot + count + 1 |
|
120 | ||
121 | # destroy items that would be moved outside the scroll region after |
|
122 | # scrolling |
|
123 | # self._tk_clear_region(destroy_top, destroy_bot, left, right) |
|
124 | # self._tk_clear_region(move_top, move_bot, left, right) |
|
125 | self._tk_destroy_region(destroy_top, destroy_bot, left, right) |
|
126 | self._tk_tag_region('move', move_top, move_bot, left, right) |
|
127 | self._canvas.move('move', 0, -count * self._rowsize) |
|
128 | self._canvas.dtag('move', 'move') |
|
129 | # self._tk_fill_region(fill_top, fill_bot, left, right) |
|
130 | ||
131 | ||
132 | def _tk_nvim_highlight_set(self, attrs): |
|
133 | self._attrs = attrs |
|
134 | ||
135 | def _tk_nvim_put(self, data): |
|
136 | # choose a Font instance |
|
137 | font = self._fnormal |
|
138 | if self._attrs.get('bold', False): |
|
139 | font = self._fbold |
|
140 | if self._attrs.get('italic', False): |
|
141 | font = self._fbolditalic if font == self._fbold else self._fitalic |
|
142 | # colors |
|
143 | fg = "#{0:0{1}x}".format(self._attrs.get('foreground', self._fg), 6) |
|
144 | bg = "#{0:0{1}x}".format(self._attrs.get('background', self._bg), 6) |
|
145 | # get the "text" and "rect" which correspond to the current cell |
|
146 | x, y = self._tk_get_coords(self._cursor_row, self._cursor_col) |
|
147 | items = self._canvas.find_overlapping(x, y, x + 1, y + 1) |
|
148 | if len(items) != 2: |
|
149 | # caught part the double-width character in the cell to the left, |
|
150 | # filter items which dont have the same horizontal coordinate as |
|
151 | # "x" |
|
152 | predicate = lambda item: self._canvas.coords(item)[0] == x |
|
153 | items = filter(predicate, items) |
|
154 | # rect has lower id than text, sort to unpack correctly |
|
155 | rect, text = sorted(items) |
|
156 | self._canvas.itemconfig(text, fill=fg, font=font, text=data or ' ') |
|
157 | self._canvas.itemconfig(rect, fill=bg) |
|
158 | self._tk_nvim_cursor_goto(self._cursor_row, self._cursor_col + 1) |
|
159 | ||
160 | def _tk_nvim_bell(self): |
|
161 | self._root.bell() |
|
162 | ||
163 | def _tk_nvim_update_fg(self, fg): |
|
164 | self._fg = "#{0:0{1}x}".format(fg, 6) |
|
165 | ||
166 | def _tk_nvim_update_bg(self, bg): |
|
167 | self._bg = "#{0:0{1}x}".format(bg, 6) |
|
168 | ||
169 | def _tk_redraw_canvas(self, width, height): |
|
170 | if self._canvas: |
|
171 | self._canvas.destroy() |
|
172 | self._fnormal = Font(family='Monospace', size=13) |
|
173 | self._fbold = Font(family='Monospace', weight='bold', size=13) |
|
174 | self._fitalic = Font(family='Monospace', slant='italic', size=13) |
|
175 | self._fbolditalic = Font(family='Monospace', weight='bold', |
|
176 | slant='italic', size=13) |
|
177 | self._colsize = self._fnormal.measure('A') |
|
178 | self._rowsize = self._fnormal.metrics('linespace') |
|
179 | self._canvas = Canvas(self._root, width=self._colsize * width, |
|
180 | height=self._rowsize * height) |
|
181 | self._tk_fill_region(0, height - 1, 0, width - 1) |
|
182 | self._cursor_row = 0 |
|
183 | self._cursor_col = 0 |
|
184 | self._scroll_top = 0 |
|
185 | self._scroll_bot = height - 1 |
|
186 | self._scroll_left = 0 |
|
187 | self._scroll_right = width - 1 |
|
188 | self._width, self._height = (width, height,) |
|
189 | self._canvas.pack() |
|
190 | ||
191 | def _tk_fill_region(self, top, bot, left, right): |
|
192 | # create columns from right to left so the left columns have a |
|
193 | # higher z-index than the right columns. This is required to |
|
194 | # properly display characters that cross cell boundary |
|
195 | for rownum in range(bot, top - 1, -1): |
|
196 | for colnum in range(right, left - 1, -1): |
|
197 | x1 = colnum * self._colsize |
|
198 | y1 = rownum * self._rowsize |
|
199 | x2 = (colnum + 1) * self._colsize |
|
200 | y2 = (rownum + 1) * self._rowsize |
|
201 | # for each cell, create two items: The rectangle is used for |
|
202 | # filling background and the text is for cell contents. |
|
203 | self._canvas.create_rectangle(x1, y1, x2, y2, |
|
204 | fill=self._bg, width=0) |
|
205 | self._canvas.create_text(x1, y1, anchor='nw', |
|
206 | font=self._fnormal, width=1, |
|
207 | fill=self._fg, text=' ') |
|
208 | ||
209 | def _tk_clear_region(self, top, bot, left, right): |
|
210 | self._tk_tag_region('clear', top, bot, left, right) |
|
211 | self._canvas.itemconfig('clear', fill=self._bg) |
|
212 | self._canvas.dtag('clear', 'clear') |
|
213 | ||
214 | def _tk_destroy_region(self, top, bot, left, right): |
|
215 | self._tk_tag_region('destroy', top, bot, left, right) |
|
216 | self._canvas.delete('destroy') |
|
217 | self._canvas.dtag('destroy', 'destroy') |
|
218 | ||
219 | def _tk_tag_region(self, tag, top, bot, left, right): |
|
220 | x1, y1 = self._tk_get_coords(top, left) |
|
221 | x2, y2 = self._tk_get_coords(bot, right) |
|
222 | self._canvas.addtag_overlapping(tag, x1, y1, x2 + 1, y2 + 1) |
|
223 | ||
224 | def _tk_get_coords(self, row, col): |
|
225 | x = col * self._colsize |
|
226 | y = row * self._rowsize |
|
227 | return x, y |
|
228 | ||
229 | def _tk_key(self, event): |
|
230 | if 0xffe1 <= event.keysym_num <= 0xffee: |
|
231 | # this is a modifier key, ignore. Source: |
|
232 | # https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm |
|
233 | return |
|
234 | # Translate to Nvim representation of keys |
|
235 | send = [] |
|
236 | if event.state & 0x1: |
|
237 | send.append('S') |
|
238 | if event.state & 0x4: |
|
239 | send.append('C') |
|
240 | if event.state & (0x8 | 0x80): |
|
241 | send.append('A') |
|
242 | special = len(send) > 0 |
|
243 | key = event.char |
|
244 | if _is_invalid_key(key): |
|
245 | special = True |
|
246 | key = event.keysym |
|
247 | send.append(SPECIAL_KEYS.get(key, key)) |
|
248 | send = '-'.join(send) |
|
249 | if special: |
|
250 | send = '<' + send + '>' |
|
251 | nvim = self._nvim |
|
252 | nvim.session.threadsafe_call(lambda: nvim.input(send)) |
|
253 | ||
254 | def _nvim_event_loop(self): |
|
255 | self._nvim.session.run(self._nvim_request, |
|
256 | self._nvim_notification, |
|
257 | lambda: self._nvim.attach_ui(80, 24)) |
|
258 | self._root.event_generate('<<nvim_detach>>', when='tail') |
|
259 | ||
260 | def _nvim_request(self, method, args): |
|
261 | raise Exception('This UI does not implement any methods') |
|
262 | ||
263 | def _nvim_notification(self, method, args): |
|
264 | if method == 'redraw': |
|
265 | self._nvim_updates.append(args) |
|
266 | self._root.event_generate('<<nvim_redraw>>', when='tail') |
|
267 | ||
268 | ||
269 | def _is_invalid_key(c): |
@@ 26-264 (lines=239) @@ | ||
23 | range = xrange |
|
24 | ||
25 | ||
26 | class NvimTk(object): |
|
27 | ||
28 | """Wraps all nvim/tk event handling.""" |
|
29 | ||
30 | def __init__(self, nvim): |
|
31 | """Initialize with a Nvim instance.""" |
|
32 | self._nvim = nvim |
|
33 | self._attrs = {} |
|
34 | self._nvim_updates = deque() |
|
35 | self._canvas = None |
|
36 | self._fg = '#000000' |
|
37 | self._bg = '#ffffff' |
|
38 | ||
39 | def run(self): |
|
40 | """Start the UI.""" |
|
41 | self._tk_setup() |
|
42 | t = Thread(target=self._nvim_event_loop) |
|
43 | t.daemon = True |
|
44 | t.start() |
|
45 | self._root.mainloop() |
|
46 | ||
47 | def _tk_setup(self): |
|
48 | self._root = Tk() |
|
49 | self._root.bind('<<nvim_redraw>>', self._tk_nvim_redraw) |
|
50 | self._root.bind('<<nvim_detach>>', self._tk_nvim_detach) |
|
51 | self._root.bind('<Key>', self._tk_key) |
|
52 | ||
53 | def _tk_nvim_redraw(self, *args): |
|
54 | update = self._nvim_updates.popleft() |
|
55 | for update in update: |
|
56 | handler = getattr(self, '_tk_nvim_' + update[0]) |
|
57 | for args in update[1:]: |
|
58 | handler(*args) |
|
59 | ||
60 | def _tk_nvim_detach(self, *args): |
|
61 | self._root.destroy() |
|
62 | ||
63 | def _tk_nvim_resize(self, width, height): |
|
64 | self._tk_redraw_canvas(width, height) |
|
65 | ||
66 | def _tk_nvim_clear(self): |
|
67 | self._tk_clear_region(0, self._height - 1, 0, self._width - 1) |
|
68 | ||
69 | def _tk_nvim_eol_clear(self): |
|
70 | row, col = (self._cursor_row, self._cursor_col,) |
|
71 | self._tk_clear_region(row, row, col, self._scroll_right) |
|
72 | ||
73 | def _tk_nvim_cursor_goto(self, row, col): |
|
74 | self._cursor_row = row |
|
75 | self._cursor_col = col |
|
76 | ||
77 | def _tk_nvim_cursor_on(self): |
|
78 | pass |
|
79 | ||
80 | def _tk_nvim_cursor_off(self): |
|
81 | pass |
|
82 | ||
83 | def _tk_nvim_mouse_on(self): |
|
84 | pass |
|
85 | ||
86 | def _tk_nvim_mouse_off(self): |
|
87 | pass |
|
88 | ||
89 | def _tk_nvim_insert_mode(self): |
|
90 | pass |
|
91 | ||
92 | def _tk_nvim_normal_mode(self): |
|
93 | pass |
|
94 | ||
95 | def _tk_nvim_set_scroll_region(self, top, bot, left, right): |
|
96 | self._scroll_top = top |
|
97 | self._scroll_bot = bot |
|
98 | self._scroll_left = left |
|
99 | self._scroll_right = right |
|
100 | ||
101 | def _tk_nvim_scroll(self, count): |
|
102 | top, bot = (self._scroll_top, self._scroll_bot,) |
|
103 | left, right = (self._scroll_left, self._scroll_right,) |
|
104 | ||
105 | if count > 0: |
|
106 | destroy_top = top |
|
107 | destroy_bot = top + count - 1 |
|
108 | move_top = destroy_bot + 1 |
|
109 | move_bot = bot |
|
110 | fill_top = move_bot + 1 |
|
111 | fill_bot = fill_top + count - 1 |
|
112 | else: |
|
113 | destroy_top = bot + count + 1 |
|
114 | destroy_bot = bot |
|
115 | move_top = top |
|
116 | move_bot = destroy_top - 1 |
|
117 | fill_bot = move_top - 1 |
|
118 | fill_top = fill_bot + count + 1 |
|
119 | ||
120 | # destroy items that would be moved outside the scroll region after |
|
121 | # scrolling |
|
122 | # self._tk_clear_region(destroy_top, destroy_bot, left, right) |
|
123 | # self._tk_clear_region(move_top, move_bot, left, right) |
|
124 | self._tk_destroy_region(destroy_top, destroy_bot, left, right) |
|
125 | self._tk_tag_region('move', move_top, move_bot, left, right) |
|
126 | self._canvas.move('move', 0, -count * self._rowsize) |
|
127 | self._canvas.dtag('move', 'move') |
|
128 | # self._tk_fill_region(fill_top, fill_bot, left, right) |
|
129 | ||
130 | def _tk_nvim_highlight_set(self, attrs): |
|
131 | self._attrs = attrs |
|
132 | ||
133 | def _tk_nvim_put(self, data): |
|
134 | # choose a Font instance |
|
135 | font = self._fnormal |
|
136 | if self._attrs.get('bold', False): |
|
137 | font = self._fbold |
|
138 | if self._attrs.get('italic', False): |
|
139 | font = self._fbolditalic if font == self._fbold else self._fitalic |
|
140 | # colors |
|
141 | fg = "#{0:0{1}x}".format(self._attrs.get('foreground', self._fg), 6) |
|
142 | bg = "#{0:0{1}x}".format(self._attrs.get('background', self._bg), 6) |
|
143 | # get the "text" and "rect" which correspond to the current cell |
|
144 | x, y = self._tk_get_coords(self._cursor_row, self._cursor_col) |
|
145 | items = self._canvas.find_overlapping(x, y, x + 1, y + 1) |
|
146 | if len(items) != 2: |
|
147 | # caught part the double-width character in the cell to the left, |
|
148 | # filter items which dont have the same horizontal coordinate as |
|
149 | # "x" |
|
150 | predicate = lambda item: self._canvas.coords(item)[0] == x |
|
151 | items = filter(predicate, items) |
|
152 | # rect has lower id than text, sort to unpack correctly |
|
153 | rect, text = sorted(items) |
|
154 | self._canvas.itemconfig(text, fill=fg, font=font, text=data or ' ') |
|
155 | self._canvas.itemconfig(rect, fill=bg) |
|
156 | self._tk_nvim_cursor_goto(self._cursor_row, self._cursor_col + 1) |
|
157 | ||
158 | def _tk_nvim_bell(self): |
|
159 | self._root.bell() |
|
160 | ||
161 | def _tk_nvim_update_fg(self, fg): |
|
162 | self._fg = "#{0:0{1}x}".format(fg, 6) |
|
163 | ||
164 | def _tk_nvim_update_bg(self, bg): |
|
165 | self._bg = "#{0:0{1}x}".format(bg, 6) |
|
166 | ||
167 | def _tk_redraw_canvas(self, width, height): |
|
168 | if self._canvas: |
|
169 | self._canvas.destroy() |
|
170 | self._fnormal = Font(family='Monospace', size=13) |
|
171 | self._fbold = Font(family='Monospace', weight='bold', size=13) |
|
172 | self._fitalic = Font(family='Monospace', slant='italic', size=13) |
|
173 | self._fbolditalic = Font(family='Monospace', weight='bold', |
|
174 | slant='italic', size=13) |
|
175 | self._colsize = self._fnormal.measure('A') |
|
176 | self._rowsize = self._fnormal.metrics('linespace') |
|
177 | self._canvas = Canvas(self._root, width=self._colsize * width, |
|
178 | height=self._rowsize * height) |
|
179 | self._tk_fill_region(0, height - 1, 0, width - 1) |
|
180 | self._cursor_row = 0 |
|
181 | self._cursor_col = 0 |
|
182 | self._scroll_top = 0 |
|
183 | self._scroll_bot = height - 1 |
|
184 | self._scroll_left = 0 |
|
185 | self._scroll_right = width - 1 |
|
186 | self._width, self._height = (width, height,) |
|
187 | self._canvas.pack() |
|
188 | ||
189 | def _tk_fill_region(self, top, bot, left, right): |
|
190 | # create columns from right to left so the left columns have a |
|
191 | # higher z-index than the right columns. This is required to |
|
192 | # properly display characters that cross cell boundary |
|
193 | for rownum in range(bot, top - 1, -1): |
|
194 | for colnum in range(right, left - 1, -1): |
|
195 | x1 = colnum * self._colsize |
|
196 | y1 = rownum * self._rowsize |
|
197 | x2 = (colnum + 1) * self._colsize |
|
198 | y2 = (rownum + 1) * self._rowsize |
|
199 | # for each cell, create two items: The rectangle is used for |
|
200 | # filling background and the text is for cell contents. |
|
201 | self._canvas.create_rectangle(x1, y1, x2, y2, |
|
202 | fill=self._background, width=0) |
|
203 | self._canvas.create_text(x1, y1, anchor='nw', |
|
204 | font=self._fnormal, width=1, |
|
205 | fill=self._foreground, text=' ') |
|
206 | ||
207 | def _tk_clear_region(self, top, bot, left, right): |
|
208 | self._tk_tag_region('clear', top, bot, left, right) |
|
209 | self._canvas.itemconfig('clear', fill=self._bg) |
|
210 | self._canvas.dtag('clear', 'clear') |
|
211 | ||
212 | def _tk_destroy_region(self, top, bot, left, right): |
|
213 | self._tk_tag_region('destroy', top, bot, left, right) |
|
214 | self._canvas.delete('destroy') |
|
215 | self._canvas.dtag('destroy', 'destroy') |
|
216 | ||
217 | def _tk_tag_region(self, tag, top, bot, left, right): |
|
218 | x1, y1 = self._tk_get_coords(top, left) |
|
219 | x2, y2 = self._tk_get_coords(bot, right) |
|
220 | self._canvas.addtag_overlapping(tag, x1, y1, x2 + 1, y2 + 1) |
|
221 | ||
222 | def _tk_get_coords(self, row, col): |
|
223 | x = col * self._colsize |
|
224 | y = row * self._rowsize |
|
225 | return x, y |
|
226 | ||
227 | def _tk_key(self, event): |
|
228 | if 0xffe1 <= event.keysym_num <= 0xffee: |
|
229 | # this is a modifier key, ignore. Source: |
|
230 | # https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm |
|
231 | return |
|
232 | # Translate to Nvim representation of keys |
|
233 | send = [] |
|
234 | if event.state & 0x1: |
|
235 | send.append('S') |
|
236 | if event.state & 0x4: |
|
237 | send.append('C') |
|
238 | if event.state & (0x8 | 0x80): |
|
239 | send.append('A') |
|
240 | special = len(send) > 0 |
|
241 | key = event.char |
|
242 | if _is_invalid_key(key): |
|
243 | special = True |
|
244 | key = event.keysym |
|
245 | send.append(SPECIAL_KEYS.get(key, key)) |
|
246 | send = '-'.join(send) |
|
247 | if special: |
|
248 | send = '<' + send + '>' |
|
249 | nvim = self._nvim |
|
250 | nvim.session.threadsafe_call(lambda: nvim.input(send)) |
|
251 | ||
252 | def _nvim_event_loop(self): |
|
253 | self._nvim.session.run(self._nvim_request, |
|
254 | self._nvim_notification, |
|
255 | lambda: self._nvim.attach_ui(80, 24)) |
|
256 | self._root.event_generate('<<nvim_detach>>', when='tail') |
|
257 | ||
258 | def _nvim_request(self, method, args): |
|
259 | raise Exception('This UI does not implement any methods') |
|
260 | ||
261 | def _nvim_notification(self, method, args): |
|
262 | if method == 'redraw': |
|
263 | self._nvim_updates.append(args) |
|
264 | self._root.event_generate('<<nvim_redraw>>', when='tail') |
|
265 | ||
266 | ||
267 | def _is_invalid_key(c): |