euporie 2.8.6__py3-none-any.whl → 2.8.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euporie/console/app.py +2 -0
- euporie/console/tabs/console.py +27 -17
- euporie/core/__init__.py +2 -2
- euporie/core/app/_commands.py +4 -21
- euporie/core/app/app.py +13 -7
- euporie/core/bars/command.py +9 -6
- euporie/core/bars/search.py +43 -2
- euporie/core/border.py +7 -2
- euporie/core/comm/base.py +2 -2
- euporie/core/comm/ipywidgets.py +3 -3
- euporie/core/commands.py +44 -8
- euporie/core/completion.py +14 -6
- euporie/core/convert/datum.py +7 -7
- euporie/core/data_structures.py +20 -1
- euporie/core/filters.py +8 -0
- euporie/core/ft/html.py +47 -40
- euporie/core/graphics.py +11 -3
- euporie/core/history.py +15 -5
- euporie/core/inspection.py +16 -9
- euporie/core/kernel/__init__.py +53 -1
- euporie/core/kernel/base.py +571 -0
- euporie/core/kernel/{client.py → jupyter.py} +173 -430
- euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
- euporie/core/kernel/local.py +694 -0
- euporie/core/key_binding/bindings/basic.py +6 -3
- euporie/core/keys.py +26 -25
- euporie/core/layout/cache.py +31 -7
- euporie/core/layout/containers.py +88 -13
- euporie/core/layout/scroll.py +45 -148
- euporie/core/log.py +1 -1
- euporie/core/style.py +2 -1
- euporie/core/suggest.py +155 -74
- euporie/core/tabs/__init__.py +10 -0
- euporie/core/tabs/_commands.py +76 -0
- euporie/core/tabs/_settings.py +16 -0
- euporie/core/tabs/base.py +22 -8
- euporie/core/tabs/kernel.py +81 -35
- euporie/core/tabs/notebook.py +14 -22
- euporie/core/utils.py +1 -1
- euporie/core/validation.py +8 -8
- euporie/core/widgets/_settings.py +19 -2
- euporie/core/widgets/cell.py +31 -31
- euporie/core/widgets/cell_outputs.py +10 -1
- euporie/core/widgets/dialog.py +30 -75
- euporie/core/widgets/forms.py +71 -59
- euporie/core/widgets/inputs.py +7 -4
- euporie/core/widgets/layout.py +281 -93
- euporie/core/widgets/menu.py +55 -15
- euporie/core/widgets/palette.py +3 -1
- euporie/core/widgets/tree.py +86 -76
- euporie/notebook/app.py +35 -16
- euporie/notebook/tabs/edit.py +4 -4
- euporie/notebook/tabs/json.py +6 -2
- euporie/notebook/tabs/notebook.py +26 -8
- euporie/preview/tabs/notebook.py +17 -13
- euporie/web/tabs/web.py +22 -3
- euporie/web/widgets/webview.py +3 -0
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/METADATA +1 -1
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/RECORD +64 -61
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
- {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
euporie/core/widgets/layout.py
CHANGED
@@ -4,14 +4,15 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
from abc import ABCMeta, abstractmethod
|
7
|
-
from functools import partial
|
8
|
-
from typing import TYPE_CHECKING, NamedTuple, cast
|
7
|
+
from functools import lru_cache, partial
|
8
|
+
from typing import TYPE_CHECKING, ClassVar, NamedTuple, cast
|
9
9
|
|
10
10
|
from prompt_toolkit.application.current import get_app
|
11
11
|
from prompt_toolkit.cache import SimpleCache
|
12
12
|
from prompt_toolkit.filters import Condition, to_filter
|
13
13
|
from prompt_toolkit.formatted_text.base import to_formatted_text
|
14
14
|
from prompt_toolkit.formatted_text.utils import fragment_list_width
|
15
|
+
from prompt_toolkit.key_binding.key_bindings import KeyBindings
|
15
16
|
from prompt_toolkit.layout.containers import (
|
16
17
|
ConditionalContainer,
|
17
18
|
DynamicContainer,
|
@@ -25,6 +26,7 @@ from prompt_toolkit.layout.controls import (
|
|
25
26
|
)
|
26
27
|
from prompt_toolkit.layout.dimension import Dimension as D
|
27
28
|
from prompt_toolkit.layout.dimension import to_dimension
|
29
|
+
from prompt_toolkit.layout.utils import explode_text_fragments
|
28
30
|
from prompt_toolkit.mouse_events import MouseButton, MouseEventType
|
29
31
|
from prompt_toolkit.utils import Event
|
30
32
|
|
@@ -39,11 +41,16 @@ if TYPE_CHECKING:
|
|
39
41
|
from typing import Any, Callable
|
40
42
|
|
41
43
|
from prompt_toolkit.filters import FilterOrBool
|
42
|
-
from prompt_toolkit.formatted_text.base import
|
44
|
+
from prompt_toolkit.formatted_text.base import (
|
45
|
+
AnyFormattedText,
|
46
|
+
OneStyleAndTextTuple,
|
47
|
+
StyleAndTextTuples,
|
48
|
+
)
|
43
49
|
from prompt_toolkit.key_binding.key_bindings import (
|
44
50
|
KeyBindingsBase,
|
45
51
|
NotImplementedOrNone,
|
46
52
|
)
|
53
|
+
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
47
54
|
from prompt_toolkit.layout.containers import AnyContainer, Container, _Split
|
48
55
|
from prompt_toolkit.layout.dimension import AnyDimension
|
49
56
|
from prompt_toolkit.mouse_events import MouseEvent
|
@@ -199,30 +206,35 @@ class ReferencedSplit:
|
|
199
206
|
|
200
207
|
|
201
208
|
class TabBarTab(NamedTuple):
|
202
|
-
"""A
|
209
|
+
"""A class representing a tab and its callbacks."""
|
203
210
|
|
204
211
|
title: AnyFormattedText
|
205
|
-
on_activate: Callable
|
206
|
-
on_deactivate: Callable | None = None
|
207
|
-
on_close: Callable | None = None
|
212
|
+
on_activate: Callable[[], NotImplementedOrNone]
|
213
|
+
on_deactivate: Callable[[], NotImplementedOrNone] | None = None
|
214
|
+
on_close: Callable[[], NotImplementedOrNone] | None = None
|
215
|
+
closeable: bool = False
|
216
|
+
|
217
|
+
def __hash__(self) -> int:
|
218
|
+
"""Hash the Tab based on current title value."""
|
219
|
+
return hash(tuple(to_formatted_text(self.title))) * hash(
|
220
|
+
(self.on_activate, self.on_deactivate, self.on_close, self.closeable)
|
221
|
+
)
|
208
222
|
|
209
223
|
|
210
224
|
class TabBarControl(UIControl):
|
211
225
|
"""A control which shows a tab bar."""
|
212
226
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
char_top = "▁"
|
217
|
-
char_close = "✖"
|
227
|
+
char_scroll_left = "◀"
|
228
|
+
char_scroll_right = "▶"
|
229
|
+
char_close: ClassVar[str] = "✖"
|
218
230
|
|
219
231
|
def __init__(
|
220
232
|
self,
|
221
233
|
tabs: Sequence[TabBarTab] | Callable[[], Sequence[TabBarTab]],
|
222
234
|
active: int | Callable[[], int],
|
223
235
|
spacing: int = 1,
|
224
|
-
closeable: bool = False,
|
225
236
|
max_title_width: int = 30,
|
237
|
+
grid: GridStyle = OutsetGrid,
|
226
238
|
) -> None:
|
227
239
|
"""Create a new tab bar instance.
|
228
240
|
|
@@ -231,19 +243,22 @@ class TabBarControl(UIControl):
|
|
231
243
|
when the tab is activated.
|
232
244
|
active: The index of the currently active tab
|
233
245
|
spacing: The number of characters between the tabs
|
234
|
-
closeable: Whether to show close buttons the the tabs
|
235
246
|
max_title_width: The maximum width of the title to display
|
247
|
+
grid: The grid style to use for drawing borders
|
236
248
|
|
237
249
|
"""
|
238
250
|
self._tabs = tabs
|
239
251
|
self.spacing = spacing
|
240
|
-
self.closeable = closeable
|
241
252
|
self.max_title_width = max_title_width
|
242
253
|
self._active = active
|
243
|
-
|
244
|
-
self.
|
245
|
-
|
246
|
-
|
254
|
+
self._last_active: int | None = None
|
255
|
+
self.scroll = -1
|
256
|
+
self.grid = grid
|
257
|
+
|
258
|
+
self.mouse_handlers: dict[int, Callable[[], NotImplementedOrNone] | None] = {}
|
259
|
+
self.tab_widths: list[int] = []
|
260
|
+
# Caches
|
261
|
+
self.render_tab = lru_cache(self._render_tab)
|
247
262
|
self._content_cache: SimpleCache = SimpleCache(maxsize=50)
|
248
263
|
|
249
264
|
@property
|
@@ -262,16 +277,43 @@ class TabBarControl(UIControl):
|
|
262
277
|
@property
|
263
278
|
def active(self) -> int:
|
264
279
|
"""Return the index of the active tab."""
|
265
|
-
if callable(self._active)
|
266
|
-
|
267
|
-
|
268
|
-
|
280
|
+
current_active = self._active() if callable(self._active) else self._active
|
281
|
+
|
282
|
+
# Check if active tab has changed
|
283
|
+
if self._last_active != current_active:
|
284
|
+
# Handle tab switching
|
285
|
+
if self._last_active is not None and 0 <= self._last_active < len(
|
286
|
+
self.tabs
|
287
|
+
):
|
288
|
+
old_tab = self.tabs[self._last_active]
|
289
|
+
if callable(on_deactivate := old_tab.on_deactivate):
|
290
|
+
on_deactivate()
|
291
|
+
|
292
|
+
# Call on_activate for new tab
|
293
|
+
if current_active is not None and 0 <= current_active < len(self.tabs):
|
294
|
+
new_tab = self.tabs[current_active]
|
295
|
+
if callable(on_activate := new_tab.on_activate):
|
296
|
+
on_activate()
|
297
|
+
|
298
|
+
# Ensure active tab is visible
|
299
|
+
self.scroll_to(current_active)
|
300
|
+
|
301
|
+
# Update last known active value
|
302
|
+
self._last_active = current_active
|
303
|
+
|
304
|
+
return current_active
|
269
305
|
|
270
306
|
@active.setter
|
271
307
|
def active(self, active: int | Callable[[], int]) -> None:
|
272
308
|
"""Set the currently active tab."""
|
309
|
+
# Store new active value
|
273
310
|
self._active = active
|
274
311
|
|
312
|
+
# If it's a direct value (not callable), handle tab switching immediately
|
313
|
+
if not callable(active):
|
314
|
+
# Force property getter to handle the change
|
315
|
+
_ = self.active
|
316
|
+
|
275
317
|
def preferred_width(self, max_available_width: int) -> int | None:
|
276
318
|
"""Return the preferred width of the tab-bar control, the maximum available."""
|
277
319
|
return max_available_width
|
@@ -292,87 +334,221 @@ class TabBarControl(UIControl):
|
|
292
334
|
|
293
335
|
def create_content(self, width: int, height: int) -> UIContent:
|
294
336
|
"""Generate the formatted text fragments which make the controls output."""
|
337
|
+
self.available_width = width
|
295
338
|
|
296
|
-
def get_content() ->
|
297
|
-
|
339
|
+
def get_content() -> tuple[
|
340
|
+
UIContent, dict[int, Callable[[], NotImplementedOrNone] | None]
|
341
|
+
]:
|
342
|
+
*fragment_lines, mouse_handlers = self.render(width)
|
298
343
|
|
299
344
|
return UIContent(
|
300
345
|
get_line=lambda i: fragment_lines[i],
|
301
346
|
line_count=len(fragment_lines),
|
302
347
|
show_cursor=False,
|
303
|
-
)
|
348
|
+
), mouse_handlers
|
349
|
+
|
350
|
+
key = (hash(tuple(self.tabs)), width, self.active, self.scroll)
|
351
|
+
ui_content, self.mouse_handlers = self._content_cache.get(key, get_content)
|
352
|
+
return ui_content
|
353
|
+
|
354
|
+
def scroll_to(self, active: int) -> None:
|
355
|
+
"""Adjust scroll position to ensure the active tab is visible."""
|
356
|
+
# Calculate position of active tab
|
357
|
+
pos = self.spacing # Initial spacing
|
358
|
+
for i in range(len(self.tabs)):
|
359
|
+
if i == active:
|
360
|
+
# Found active tab - check if it's visible
|
361
|
+
tab_width = self.tab_widths[i] if i < len(self.tab_widths) else 0
|
362
|
+
# Scroll left if tab start is before visible area
|
363
|
+
if pos < self.scroll:
|
364
|
+
self.scroll = pos - self.spacing - 1
|
365
|
+
# Scroll right if tab end is after visible area
|
366
|
+
elif pos + tab_width > self.scroll + self.available_width:
|
367
|
+
self.scroll = pos + tab_width - self.available_width + 2
|
368
|
+
break
|
369
|
+
|
370
|
+
# Add tab width and spacing
|
371
|
+
pos += (
|
372
|
+
self.tab_widths[i] if i < len(self.tab_widths) else 0
|
373
|
+
) + self.spacing
|
374
|
+
|
375
|
+
def _render_tab(
|
376
|
+
self,
|
377
|
+
title: tuple[OneStyleAndTextTuple, ...],
|
378
|
+
on_activate: Callable[[], NotImplementedOrNone],
|
379
|
+
on_deactivate: Callable[[], NotImplementedOrNone] | None,
|
380
|
+
on_close: Callable[[], NotImplementedOrNone] | None,
|
381
|
+
closeable: bool,
|
382
|
+
active: bool,
|
383
|
+
max_title_width: int,
|
384
|
+
grid: GridStyle,
|
385
|
+
) -> tuple[
|
386
|
+
StyleAndTextTuples,
|
387
|
+
StyleAndTextTuples,
|
388
|
+
list[Callable[[], NotImplementedOrNone] | None],
|
389
|
+
]:
|
390
|
+
"""Render the tab as formatted text.
|
304
391
|
|
305
|
-
|
306
|
-
|
392
|
+
Args:
|
393
|
+
title: The formatted text fragments making up the tab's title
|
394
|
+
on_activate: Callback function to run when the tab is activated
|
395
|
+
on_deactivate: Optional callback function to run when the tab is deactivated
|
396
|
+
on_close: Optional callback function to run when the tab is closed
|
397
|
+
closeable: Whether the tab can be closed
|
398
|
+
active: Whether this tab is currently active
|
399
|
+
max_title_width: Maximum width to display the tab title
|
400
|
+
grid: The grid style to use for drawing borders
|
307
401
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
402
|
+
Returns:
|
403
|
+
Tuple containing:
|
404
|
+
- Top line formatted text fragments
|
405
|
+
- Bottom line formatted text fragments
|
406
|
+
- List of mouse handler callbacks for each character position
|
407
|
+
"""
|
408
|
+
title_ft = truncate(list(title), max_title_width)
|
409
|
+
title_width = fragment_list_width(title_ft)
|
410
|
+
style = "class:active" if active else "class:inactive"
|
313
411
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
title_width = fragment_list_width(title_ft)
|
323
|
-
style = "class:active" if self.active == j else "class:inactive"
|
324
|
-
|
325
|
-
# Add top edge over title
|
326
|
-
top_line += [
|
327
|
-
(f"{style} class:tab,border,top", self.char_top * (title_width + 2))
|
328
|
-
]
|
412
|
+
top_line: StyleAndTextTuples = explode_text_fragments([])
|
413
|
+
tab_line: StyleAndTextTuples = explode_text_fragments([])
|
414
|
+
mouse_handlers: list[Callable[[], NotImplementedOrNone] | None] = []
|
415
|
+
|
416
|
+
# Add top edge over title
|
417
|
+
top_line.append(
|
418
|
+
(f"{style} class:tab,border,top", grid.TOP_MID * (title_width + 2))
|
419
|
+
)
|
329
420
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
i += 1
|
421
|
+
# Left edge
|
422
|
+
tab_line.append((f"{style} class:tab,border,left", grid.MID_LEFT))
|
423
|
+
mouse_handlers.append(on_activate)
|
334
424
|
|
335
|
-
|
336
|
-
|
425
|
+
# Title
|
426
|
+
tab_line.extend(
|
427
|
+
[
|
337
428
|
(f"{style} class:tab,title {frag_style}", text)
|
338
429
|
for frag_style, text, *_ in title_ft
|
339
430
|
]
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
431
|
+
)
|
432
|
+
for _ in range(title_width):
|
433
|
+
mouse_handlers.append(on_activate)
|
434
|
+
|
435
|
+
# Close button
|
436
|
+
if closeable:
|
437
|
+
top_line.append((f"{style} class:tab,border,top", grid.TOP_MID * 2))
|
438
|
+
mouse_handlers.append(on_activate)
|
439
|
+
tab_line.extend(
|
440
|
+
[
|
350
441
|
(f"{style} class:tab", " "),
|
351
442
|
(f"{style} class:tab,close", self.char_close),
|
352
443
|
]
|
353
|
-
self.mouse_handlers[i] = tab.on_close
|
354
|
-
i += 1
|
355
|
-
|
356
|
-
# Right edge
|
357
|
-
tab_line += [(f"{style} class:tab,border,right", self.char_right)]
|
358
|
-
self.mouse_handlers[i] = tab.on_activate
|
359
|
-
i += 1
|
360
|
-
|
361
|
-
# Spacing
|
362
|
-
for _ in range(self.spacing):
|
363
|
-
top_line += [("", " ")]
|
364
|
-
tab_line += [("class:border,bottom", self.char_bottom)]
|
365
|
-
i += 1
|
366
|
-
|
367
|
-
# Add border to fill width
|
368
|
-
tab_line += [
|
369
|
-
(
|
370
|
-
"class:border,bottom",
|
371
|
-
self.char_bottom * (width - fragment_list_width(tab_line)),
|
372
444
|
)
|
445
|
+
mouse_handlers.append(on_close)
|
446
|
+
|
447
|
+
# Right edge
|
448
|
+
tab_line.append((f"{style} class:tab,border,right", grid.MID_RIGHT))
|
449
|
+
mouse_handlers.append(on_activate)
|
450
|
+
|
451
|
+
return (top_line, tab_line, mouse_handlers)
|
452
|
+
|
453
|
+
def render(
|
454
|
+
self, width: int
|
455
|
+
) -> tuple[
|
456
|
+
StyleAndTextTuples,
|
457
|
+
StyleAndTextTuples,
|
458
|
+
dict[int, Callable[[], NotImplementedOrNone] | None],
|
459
|
+
]:
|
460
|
+
"""Render the tab-bar as lines of formatted text."""
|
461
|
+
top_line: StyleAndTextTuples = []
|
462
|
+
tab_line: StyleAndTextTuples = []
|
463
|
+
mouse_handlers: dict[int, Callable[[], NotImplementedOrNone] | None] = {}
|
464
|
+
pos = 0
|
465
|
+
full = 0
|
466
|
+
|
467
|
+
renderings = [
|
468
|
+
self.render_tab(
|
469
|
+
title=tuple(to_formatted_text(tab.title)),
|
470
|
+
max_title_width=self.max_title_width,
|
471
|
+
on_activate=tab.on_activate,
|
472
|
+
on_deactivate=tab.on_deactivate,
|
473
|
+
on_close=tab.on_close,
|
474
|
+
closeable=tab.closeable,
|
475
|
+
active=(self.active == j),
|
476
|
+
grid=self.grid,
|
477
|
+
)
|
478
|
+
for j, tab in enumerate(self.tabs)
|
373
479
|
]
|
480
|
+
self.tab_widths = [len(x[0]) for x in renderings]
|
481
|
+
|
482
|
+
# Do initial scroll if first render
|
483
|
+
if self.scroll == -1:
|
484
|
+
self.scroll_to(self._active() if callable(self._active) else self._active)
|
485
|
+
|
486
|
+
# Apply scroll limits
|
487
|
+
self.scroll = max(
|
488
|
+
0,
|
489
|
+
min(
|
490
|
+
self.scroll,
|
491
|
+
self.spacing * (len(self.tabs) + 1)
|
492
|
+
+ sum(len(x[1]) for x in renderings)
|
493
|
+
- width,
|
494
|
+
),
|
495
|
+
)
|
496
|
+
scroll = self.scroll
|
374
497
|
|
375
|
-
|
498
|
+
# Initial spacing
|
499
|
+
for _ in range(self.spacing):
|
500
|
+
if full >= scroll:
|
501
|
+
top_line += [("", " ")]
|
502
|
+
tab_line += [("class:border,bottom", self.grid.TOP_MID)]
|
503
|
+
pos += 1
|
504
|
+
full += 1
|
505
|
+
|
506
|
+
for rendering in renderings:
|
507
|
+
# Add the rendered tab content
|
508
|
+
for tab_top, tab_bottom, handler in zip(*rendering):
|
509
|
+
if full >= scroll:
|
510
|
+
top_line.append(tab_top)
|
511
|
+
tab_line.append(tab_bottom)
|
512
|
+
mouse_handlers[pos] = handler
|
513
|
+
pos += 1
|
514
|
+
full += 1
|
515
|
+
if pos == width:
|
516
|
+
break
|
517
|
+
|
518
|
+
if pos == width:
|
519
|
+
break
|
520
|
+
|
521
|
+
# Inter-tab spacing
|
522
|
+
if rendering is not renderings[-1]:
|
523
|
+
if full >= scroll:
|
524
|
+
for _ in range(self.spacing):
|
525
|
+
top_line += [("", " ")]
|
526
|
+
tab_line += [("class:border,bottom", self.grid.TOP_MID)]
|
527
|
+
if pos == width:
|
528
|
+
break
|
529
|
+
pos += 1
|
530
|
+
full += 1
|
531
|
+
|
532
|
+
if pos == width:
|
533
|
+
break
|
534
|
+
|
535
|
+
# Add scroll indicators
|
536
|
+
if scroll > 0:
|
537
|
+
top_line[0] = ("", " ")
|
538
|
+
tab_line[0] = ("class:overflow", self.char_scroll_left)
|
539
|
+
if pos >= width:
|
540
|
+
top_line[-1] = ("", " ")
|
541
|
+
tab_line[-1] = ("class:overflow", self.char_scroll_right)
|
542
|
+
else:
|
543
|
+
# Otherwise add border to fill width
|
544
|
+
tab_line += [
|
545
|
+
(
|
546
|
+
"class:border,bottom",
|
547
|
+
self.grid.TOP_MID * (width - pos + 1),
|
548
|
+
)
|
549
|
+
]
|
550
|
+
|
551
|
+
return top_line, tab_line, mouse_handlers
|
376
552
|
|
377
553
|
def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
|
378
554
|
"""Handle mouse events."""
|
@@ -386,31 +562,27 @@ class TabBarControl(UIControl):
|
|
386
562
|
# Activate the tab
|
387
563
|
handler()
|
388
564
|
return None
|
389
|
-
elif mouse_event.button == MouseButton.MIDDLE
|
565
|
+
elif mouse_event.button == MouseButton.MIDDLE:
|
390
566
|
if callable(handler := self.mouse_handlers.get(col)):
|
391
567
|
# Activate tab
|
392
568
|
handler()
|
393
569
|
# Close the now active tab
|
394
570
|
tabs = self.tabs
|
395
|
-
|
396
|
-
if callable(on_close):
|
397
|
-
on_close()
|
571
|
+
tab = tabs[self.active]
|
572
|
+
if tab.closeable and callable(tab.on_close):
|
573
|
+
tab.on_close()
|
398
574
|
return None
|
399
575
|
|
400
576
|
tabs = self.tabs
|
401
577
|
if mouse_event.event_type == MouseEventType.SCROLL_UP:
|
402
578
|
index = max(self.active - 1, 0)
|
403
579
|
if index != self.active:
|
404
|
-
if callable(deactivate := tabs[self.active].on_deactivate):
|
405
|
-
deactivate()
|
406
580
|
if callable(activate := tabs[index].on_activate):
|
407
581
|
activate()
|
408
582
|
return None
|
409
583
|
elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
|
410
584
|
index = min(self.active + 1, len(tabs) - 1)
|
411
585
|
if index != self.active:
|
412
|
-
if callable(deactivate := tabs[self.active].on_deactivate):
|
413
|
-
deactivate()
|
414
586
|
if callable(activate := tabs[index].on_activate):
|
415
587
|
activate()
|
416
588
|
return None
|
@@ -474,7 +646,7 @@ class StackedSplit(metaclass=ABCMeta):
|
|
474
646
|
value: The index of the tab to make active
|
475
647
|
"""
|
476
648
|
if value is not None:
|
477
|
-
value = max(0, min(value, len(self.children)))
|
649
|
+
value = max(0, min(value, len(self.children) - 1))
|
478
650
|
if value != self._active:
|
479
651
|
self._active = value
|
480
652
|
self.refresh()
|
@@ -537,6 +709,21 @@ class TabbedSplit(StackedSplit):
|
|
537
709
|
"""Initialize a new tabbed container."""
|
538
710
|
self.border = border
|
539
711
|
self.show_borders = show_borders or DiBool(False, True, True, True)
|
712
|
+
|
713
|
+
kb = KeyBindings()
|
714
|
+
|
715
|
+
@kb.add("left")
|
716
|
+
def _prev(event: KeyPressEvent) -> None:
|
717
|
+
"""Previous tab."""
|
718
|
+
self.active = (self.active or 0) - 1
|
719
|
+
|
720
|
+
@kb.add("right")
|
721
|
+
def _next(event: KeyPressEvent) -> None:
|
722
|
+
"""Next tab."""
|
723
|
+
self.active = (self.active or 0) + 1
|
724
|
+
|
725
|
+
self.key_bindings = kb
|
726
|
+
|
540
727
|
super().__init__(
|
541
728
|
children=children,
|
542
729
|
titles=titles,
|
@@ -579,6 +766,7 @@ class TabbedSplit(StackedSplit):
|
|
579
766
|
style="class:tabbed-split",
|
580
767
|
width=self.width,
|
581
768
|
height=self.height,
|
769
|
+
key_bindings=self.key_bindings,
|
582
770
|
)
|
583
771
|
|
584
772
|
def refresh(self) -> None:
|
euporie/core/widgets/menu.py
CHANGED
@@ -365,14 +365,16 @@ class MenuBar:
|
|
365
365
|
menu = self._get_menu(len(self.selected_menu) - 2)
|
366
366
|
index = self.selected_menu[-1]
|
367
367
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
368
|
+
previous_index = next(
|
369
|
+
(
|
370
|
+
i
|
371
|
+
for i, item in reversed(list(enumerate(menu.children)))
|
372
|
+
if i < index and not item.disabled and not item.hidden()
|
373
|
+
),
|
374
|
+
None,
|
375
|
+
)
|
376
|
+
if previous_index is not None:
|
377
|
+
self.selected_menu[-1] = previous_index
|
376
378
|
elif len(self.selected_menu) == 2:
|
377
379
|
# Return to main menu.
|
378
380
|
self.selected_menu.pop()
|
@@ -384,14 +386,17 @@ class MenuBar:
|
|
384
386
|
menu = self._get_menu(len(self.selected_menu) - 2)
|
385
387
|
index = self.selected_menu[-1]
|
386
388
|
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
389
|
+
next_index = next(
|
390
|
+
(
|
391
|
+
i
|
392
|
+
for i, item in enumerate(menu.children)
|
393
|
+
if i > index and not item.disabled and not item.hidden()
|
394
|
+
),
|
395
|
+
None,
|
396
|
+
)
|
392
397
|
|
393
|
-
if
|
394
|
-
self.selected_menu[-1] =
|
398
|
+
if next_index is not None:
|
399
|
+
self.selected_menu[-1] = next_index
|
395
400
|
self.refocus()
|
396
401
|
|
397
402
|
@kb.add("enter")
|
@@ -621,6 +626,41 @@ class MenuBar:
|
|
621
626
|
len(self.selected_menu) - 1
|
622
627
|
]
|
623
628
|
)
|
629
|
+
elif mouse_event.event_type == MouseEventType.SCROLL_UP:
|
630
|
+
menu = self._get_menu(len(self.selected_menu) - 2)
|
631
|
+
index = self.selected_menu[-1]
|
632
|
+
|
633
|
+
previous_index = next(
|
634
|
+
(
|
635
|
+
i
|
636
|
+
for i, item in reversed(
|
637
|
+
list(enumerate(menu.children))
|
638
|
+
)
|
639
|
+
if i < index
|
640
|
+
and not item.disabled
|
641
|
+
and not item.hidden()
|
642
|
+
),
|
643
|
+
None,
|
644
|
+
)
|
645
|
+
|
646
|
+
if previous_index is not None:
|
647
|
+
self.selected_menu[-1] = previous_index
|
648
|
+
elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
|
649
|
+
menu = self._get_menu(len(self.selected_menu) - 2)
|
650
|
+
index = self.selected_menu[-1]
|
651
|
+
|
652
|
+
next_index = next(
|
653
|
+
(
|
654
|
+
i
|
655
|
+
for i, item in enumerate(menu.children)
|
656
|
+
if i > index
|
657
|
+
and not item.disabled
|
658
|
+
and not item.hidden()
|
659
|
+
),
|
660
|
+
None,
|
661
|
+
)
|
662
|
+
if next_index is not None:
|
663
|
+
self.selected_menu[-1] = next_index
|
624
664
|
|
625
665
|
if item.separator:
|
626
666
|
# Show a connected line with no mouse handler
|
euporie/core/widgets/palette.py
CHANGED
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, NamedTuple
|
|
10
10
|
from prompt_toolkit.data_structures import Point
|
11
11
|
from prompt_toolkit.filters import Condition
|
12
12
|
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
|
13
|
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
13
14
|
from prompt_toolkit.layout.containers import ScrollOffsets
|
14
15
|
from prompt_toolkit.layout.controls import UIContent, UIControl
|
15
16
|
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
@@ -167,7 +168,6 @@ class CommandPalette(Dialog):
|
|
167
168
|
# self.kb = KeyBindings()
|
168
169
|
self.kb.add("s-tab")(focus_previous)
|
169
170
|
self.kb.add("tab")(focus_next)
|
170
|
-
self.kb.add("escape")(self.hide)
|
171
171
|
self.kb.add("up", filter=Condition(lambda: bool(self.matches)))(
|
172
172
|
partial(self.select, -1)
|
173
173
|
)
|
@@ -222,6 +222,8 @@ class CommandPalette(Dialog):
|
|
222
222
|
"""Reset the dialog ready for display."""
|
223
223
|
self.text_area.buffer.text = ""
|
224
224
|
self.to_focus = self.text_area
|
225
|
+
app = get_app()
|
226
|
+
app.vi_state.input_mode = InputMode.INSERT
|
225
227
|
|
226
228
|
def select(self, n: int, event: KeyPressEvent | None = None) -> None:
|
227
229
|
"""Change the index of the selected command.
|