euporie 2.3.2__py3-none-any.whl → 2.4.1__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/__main__.py +3 -1
- euporie/console/app.py +6 -4
- euporie/console/tabs/console.py +34 -9
- euporie/core/__init__.py +6 -1
- euporie/core/__main__.py +1 -1
- euporie/core/app.py +79 -109
- euporie/core/border.py +44 -14
- euporie/core/comm/base.py +5 -4
- euporie/core/comm/ipywidgets.py +11 -11
- euporie/core/comm/registry.py +12 -6
- euporie/core/commands.py +30 -23
- euporie/core/completion.py +1 -4
- euporie/core/config.py +15 -5
- euporie/core/convert/{base.py → core.py} +117 -53
- euporie/core/convert/formats/ansi.py +46 -25
- euporie/core/convert/formats/base64.py +3 -3
- euporie/core/convert/formats/common.py +38 -13
- euporie/core/convert/formats/formatted_text.py +54 -12
- euporie/core/convert/formats/html.py +5 -5
- euporie/core/convert/formats/jpeg.py +1 -1
- euporie/core/convert/formats/markdown.py +4 -4
- euporie/core/convert/formats/pdf.py +1 -1
- euporie/core/convert/formats/pil.py +5 -3
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/rich.py +4 -3
- euporie/core/convert/formats/sixel.py +5 -5
- euporie/core/convert/utils.py +1 -1
- euporie/core/current.py +11 -5
- euporie/core/formatted_text/ansi.py +4 -8
- euporie/core/formatted_text/html.py +1630 -856
- euporie/core/formatted_text/markdown.py +177 -166
- euporie/core/formatted_text/table.py +20 -14
- euporie/core/formatted_text/utils.py +21 -10
- euporie/core/io.py +14 -14
- euporie/core/kernel.py +48 -37
- euporie/core/key_binding/bindings/micro.py +5 -1
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/keys.py +3 -0
- euporie/core/launch.py +5 -2
- euporie/core/lexers.py +13 -2
- euporie/core/log.py +135 -139
- euporie/core/margins.py +32 -14
- euporie/core/path.py +273 -0
- euporie/core/processors.py +35 -0
- euporie/core/renderer.py +21 -5
- euporie/core/style.py +34 -19
- euporie/core/tabs/base.py +101 -17
- euporie/core/tabs/notebook.py +72 -30
- euporie/core/terminal.py +56 -48
- euporie/core/utils.py +12 -16
- euporie/core/widgets/cell.py +6 -5
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +74 -82
- euporie/core/widgets/dialog.py +132 -28
- euporie/core/widgets/display.py +76 -24
- euporie/core/widgets/file_browser.py +87 -31
- euporie/core/widgets/formatted_text_area.py +1 -3
- euporie/core/widgets/forms.py +79 -40
- euporie/core/widgets/inputs.py +23 -13
- euporie/core/widgets/layout.py +4 -3
- euporie/core/widgets/menu.py +368 -216
- euporie/core/widgets/page.py +99 -58
- euporie/core/widgets/pager.py +1 -1
- euporie/core/widgets/palette.py +30 -27
- euporie/core/widgets/search_bar.py +38 -25
- euporie/core/widgets/status_bar.py +103 -5
- euporie/data/desktop/euporie-console.desktop +7 -0
- euporie/data/desktop/euporie-notebook.desktop +7 -0
- euporie/hub/__main__.py +3 -1
- euporie/hub/app.py +9 -7
- euporie/notebook/__main__.py +3 -1
- euporie/notebook/app.py +7 -30
- euporie/notebook/tabs/__init__.py +7 -3
- euporie/notebook/tabs/display.py +18 -9
- euporie/notebook/tabs/edit.py +106 -23
- euporie/notebook/tabs/json.py +73 -0
- euporie/notebook/tabs/log.py +18 -8
- euporie/notebook/tabs/notebook.py +60 -41
- euporie/preview/__main__.py +3 -1
- euporie/preview/app.py +2 -1
- euporie/preview/tabs/notebook.py +23 -10
- euporie/web/tabs/web.py +149 -0
- euporie/web/widgets/webview.py +563 -0
- euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
- euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
- euporie-2.4.1.dist-info/RECORD +129 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
- euporie/core/url.py +0 -64
- euporie-2.3.2.dist-info/RECORD +0 -122
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
euporie/core/widgets/menu.py
CHANGED
@@ -6,7 +6,8 @@ import logging
|
|
6
6
|
from functools import partial
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
|
-
from prompt_toolkit.
|
9
|
+
from prompt_toolkit.data_structures import Point
|
10
|
+
from prompt_toolkit.filters import Condition, has_completions, is_done, to_filter
|
10
11
|
from prompt_toolkit.formatted_text.base import to_formatted_text
|
11
12
|
from prompt_toolkit.formatted_text.utils import (
|
12
13
|
fragment_list_to_text,
|
@@ -18,9 +19,16 @@ from prompt_toolkit.layout.containers import (
|
|
18
19
|
ConditionalContainer,
|
19
20
|
Container,
|
20
21
|
Float,
|
22
|
+
HSplit,
|
23
|
+
ScrollOffsets,
|
24
|
+
VSplit,
|
21
25
|
Window,
|
22
26
|
)
|
23
|
-
from prompt_toolkit.layout.controls import FormattedTextControl
|
27
|
+
from prompt_toolkit.layout.controls import FormattedTextControl, UIContent
|
28
|
+
from prompt_toolkit.layout.dimension import Dimension
|
29
|
+
from prompt_toolkit.layout.menus import (
|
30
|
+
CompletionsMenuControl as PtkCompletionsMenuControl,
|
31
|
+
)
|
24
32
|
from prompt_toolkit.layout.utils import explode_text_fragments
|
25
33
|
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
26
34
|
from prompt_toolkit.utils import get_cwidth
|
@@ -38,11 +46,13 @@ if TYPE_CHECKING:
|
|
38
46
|
OneStyleAndTextTuple,
|
39
47
|
StyleAndTextTuples,
|
40
48
|
)
|
49
|
+
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
41
50
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
42
51
|
|
43
52
|
from euporie.core.app import BaseApp
|
44
53
|
from euporie.core.border import GridStyle
|
45
54
|
from euporie.core.commands import Command
|
55
|
+
from euporie.core.widgets.status_bar import StatusBarFields
|
46
56
|
|
47
57
|
|
48
58
|
log = logging.getLogger(__name__)
|
@@ -254,203 +264,6 @@ class MenuItem:
|
|
254
264
|
class MenuBar:
|
255
265
|
"""A container to hold the menubar and main application body."""
|
256
266
|
|
257
|
-
def _get_menu(self, level: int) -> MenuItem:
|
258
|
-
menu = self.menu_items[self.selected_menu[0]]
|
259
|
-
|
260
|
-
for i, index in enumerate(self.selected_menu[1:]):
|
261
|
-
if i < level:
|
262
|
-
try:
|
263
|
-
menu = menu.children[index]
|
264
|
-
except IndexError:
|
265
|
-
return MenuItem("debug")
|
266
|
-
|
267
|
-
return menu
|
268
|
-
|
269
|
-
def _get_menu_fragments(self) -> StyleAndTextTuples:
|
270
|
-
focused = get_app().layout.has_focus(self.window)
|
271
|
-
|
272
|
-
# This is called during the rendering. When we discover that this
|
273
|
-
# widget doesn't have the focus anymore. Reset menu state.
|
274
|
-
if not focused:
|
275
|
-
self.selected_menu = [0]
|
276
|
-
|
277
|
-
def mouse_handler(index: int, mouse_event: MouseEvent) -> None:
|
278
|
-
hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
279
|
-
if mouse_event.event_type == MouseEventType.MOUSE_DOWN or hover and focused:
|
280
|
-
# Toggle focus.
|
281
|
-
app = get_app()
|
282
|
-
if not hover:
|
283
|
-
if app.layout.has_focus(self.window):
|
284
|
-
if self.selected_menu == [index]:
|
285
|
-
app.layout.focus_last()
|
286
|
-
else:
|
287
|
-
app.layout.focus(self.window)
|
288
|
-
self.selected_menu = [index]
|
289
|
-
|
290
|
-
results: StyleAndTextTuples = []
|
291
|
-
used_keys = set()
|
292
|
-
|
293
|
-
for i, item in enumerate(self.menu_items):
|
294
|
-
# Add shortcut key hints
|
295
|
-
key = to_plain_text(item.formatted_text)[0].lower()
|
296
|
-
ft: StyleAndTextTuples
|
297
|
-
if key not in used_keys:
|
298
|
-
ft = explode_text_fragments(item.formatted_text)
|
299
|
-
ft = [(f"underline {ft[0][0]}", ft[0][1]), *ft[1:]]
|
300
|
-
used_keys |= {key}
|
301
|
-
else:
|
302
|
-
ft = item.formatted_text
|
303
|
-
|
304
|
-
mh = partial(mouse_handler, i)
|
305
|
-
selected = i == self.selected_menu[0] and focused
|
306
|
-
style = "class:selection" if selected else ""
|
307
|
-
first_style = f"{style} [SetMenuPosition]" if selected else style
|
308
|
-
|
309
|
-
results.extend(
|
310
|
-
[
|
311
|
-
(first_style, " ", mh),
|
312
|
-
*[(f"{style} {style_}", text, mh) for style_, text, *_ in ft],
|
313
|
-
(style, " ", mh),
|
314
|
-
]
|
315
|
-
)
|
316
|
-
|
317
|
-
return results
|
318
|
-
|
319
|
-
def _submenu(self, level: int = 0) -> Window:
|
320
|
-
def get_text_fragments() -> StyleAndTextTuples:
|
321
|
-
result: StyleAndTextTuples = []
|
322
|
-
if level < len(self.selected_menu):
|
323
|
-
menu = self._get_menu(level)
|
324
|
-
|
325
|
-
if menu.children:
|
326
|
-
result.extend(
|
327
|
-
[
|
328
|
-
("class:menu,border", self.grid.TOP_LEFT),
|
329
|
-
("class:menu,border", self.grid.TOP_MID * menu.width),
|
330
|
-
("class:menu,border", self.grid.TOP_RIGHT),
|
331
|
-
("", "\n"),
|
332
|
-
]
|
333
|
-
)
|
334
|
-
|
335
|
-
try:
|
336
|
-
selected_item = self.selected_menu[level + 1]
|
337
|
-
except IndexError:
|
338
|
-
selected_item = -1
|
339
|
-
|
340
|
-
def one_item(
|
341
|
-
i: int, item: MenuItem
|
342
|
-
) -> Iterable[OneStyleAndTextTuple]:
|
343
|
-
assert isinstance(item, MenuItem)
|
344
|
-
assert isinstance(menu, MenuItem)
|
345
|
-
|
346
|
-
def mouse_handler(mouse_event: MouseEvent) -> None:
|
347
|
-
if item.disabled:
|
348
|
-
# The arrow keys can't interact with menu items that
|
349
|
-
# are disabled. The mouse shouldn't be able to either.
|
350
|
-
return
|
351
|
-
hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
352
|
-
if (
|
353
|
-
mouse_event.event_type == MouseEventType.MOUSE_UP
|
354
|
-
or hover
|
355
|
-
):
|
356
|
-
app = get_app()
|
357
|
-
if not hover and item.handler:
|
358
|
-
app.layout.focus_last()
|
359
|
-
item.handler()
|
360
|
-
else:
|
361
|
-
self.selected_menu = self.selected_menu[
|
362
|
-
: level + 1
|
363
|
-
] + [i]
|
364
|
-
|
365
|
-
if item.separator:
|
366
|
-
# Show a connected line with no mouse handler
|
367
|
-
yield (
|
368
|
-
"class:menu,border",
|
369
|
-
self.grid.SPLIT_LEFT
|
370
|
-
+ (self.grid.SPLIT_MID * menu.width)
|
371
|
-
+ self.grid.SPLIT_RIGHT,
|
372
|
-
)
|
373
|
-
|
374
|
-
else:
|
375
|
-
# Show the right edge
|
376
|
-
style = ""
|
377
|
-
# Set the style if disabled
|
378
|
-
if item.disabled:
|
379
|
-
style += "class:menu,disabled"
|
380
|
-
# Set the style and cursor if selected
|
381
|
-
if i == selected_item:
|
382
|
-
style += "class:menu,selection"
|
383
|
-
yield (f"{style} class:menu,border", self.grid.MID_LEFT)
|
384
|
-
if i == selected_item:
|
385
|
-
yield ("[SetCursorPosition]", "")
|
386
|
-
# Construct the menu item contents
|
387
|
-
prefix_padding = " " * (
|
388
|
-
0
|
389
|
-
if menu.collapse_prefix
|
390
|
-
else menu.prefix_width
|
391
|
-
- fragment_list_width(item.prefix)
|
392
|
-
)
|
393
|
-
suffix_padding = " " * (
|
394
|
-
menu.width
|
395
|
-
- fragment_list_width(item.prefix)
|
396
|
-
- len(prefix_padding)
|
397
|
-
- fragment_list_width(item.formatted_text)
|
398
|
-
- (
|
399
|
-
fragment_list_width(item.suffix)
|
400
|
-
if menu.collapse_suffix
|
401
|
-
else menu.suffix_width
|
402
|
-
)
|
403
|
-
)
|
404
|
-
text_padding = " " * (
|
405
|
-
menu.width
|
406
|
-
- fragment_list_width(item.prefix)
|
407
|
-
- len(prefix_padding)
|
408
|
-
- fragment_list_width(item.formatted_text)
|
409
|
-
- fragment_list_width(item.suffix)
|
410
|
-
- len(suffix_padding)
|
411
|
-
)
|
412
|
-
menu_formatted_text: StyleAndTextTuples = to_formatted_text(
|
413
|
-
[
|
414
|
-
*item.prefix,
|
415
|
-
("", prefix_padding),
|
416
|
-
*item.formatted_text,
|
417
|
-
("", text_padding),
|
418
|
-
("", suffix_padding),
|
419
|
-
*item.suffix,
|
420
|
-
],
|
421
|
-
style=style,
|
422
|
-
)
|
423
|
-
# Apply mouse handler to all fragments
|
424
|
-
menu_formatted_text = [
|
425
|
-
(fragment[0], fragment[1], mouse_handler)
|
426
|
-
for fragment in menu_formatted_text
|
427
|
-
]
|
428
|
-
# Show the menu item contents
|
429
|
-
yield from menu_formatted_text
|
430
|
-
# Position the sub-menu
|
431
|
-
if i == selected_item:
|
432
|
-
yield ("[SetMenuPosition]", "")
|
433
|
-
# Show the right edge
|
434
|
-
yield (f"{style} class:menu,border", self.grid.MID_RIGHT)
|
435
|
-
|
436
|
-
yield ("", "\n")
|
437
|
-
|
438
|
-
for i, item in enumerate(menu.children):
|
439
|
-
if not item.hidden():
|
440
|
-
result.extend(one_item(i, item))
|
441
|
-
|
442
|
-
result.extend(
|
443
|
-
[
|
444
|
-
("class:menu,border", self.grid.BOTTOM_LEFT),
|
445
|
-
("class:menu,border", self.grid.BOTTOM_MID * menu.width),
|
446
|
-
("class:menu,border", self.grid.BOTTOM_RIGHT),
|
447
|
-
]
|
448
|
-
)
|
449
|
-
|
450
|
-
return result
|
451
|
-
|
452
|
-
return Window(FormattedTextControl(get_text_fragments), style="class:menu")
|
453
|
-
|
454
267
|
def __init__(
|
455
268
|
self,
|
456
269
|
app: BaseApp,
|
@@ -495,7 +308,10 @@ class MenuBar:
|
|
495
308
|
|
496
309
|
@kb.add("down", filter=in_main_menu)
|
497
310
|
def _down(event: KeyPressEvent) -> None:
|
498
|
-
self.selected_menu
|
311
|
+
menu = self._get_menu(len(self.selected_menu) - 2)
|
312
|
+
indices = [i for i, item in enumerate(menu.children) if not item.disabled]
|
313
|
+
if indices:
|
314
|
+
self.selected_menu.append(indices[0])
|
499
315
|
|
500
316
|
@kb.add("c-c", filter=in_main_menu)
|
501
317
|
@kb.add("c-g", filter=in_main_menu)
|
@@ -540,7 +356,7 @@ class MenuBar:
|
|
540
356
|
previous_indexes = [
|
541
357
|
i
|
542
358
|
for i, item in enumerate(menu.children)
|
543
|
-
if i < index and not item.disabled
|
359
|
+
if i < index and not item.disabled and not item.hidden()
|
544
360
|
]
|
545
361
|
|
546
362
|
if previous_indexes:
|
@@ -558,7 +374,7 @@ class MenuBar:
|
|
558
374
|
next_indexes = [
|
559
375
|
i
|
560
376
|
for i, item in enumerate(menu.children)
|
561
|
-
if i > index and not item.disabled
|
377
|
+
if i > index and not item.disabled and not item.hidden()
|
562
378
|
]
|
563
379
|
|
564
380
|
if next_indexes:
|
@@ -607,9 +423,7 @@ class MenuBar:
|
|
607
423
|
show_cursor=False,
|
608
424
|
)
|
609
425
|
self.window: Window = Window(
|
610
|
-
height=1,
|
611
|
-
content=self.control,
|
612
|
-
style="class:menu,bar",
|
426
|
+
height=1, content=self.control, style="class:menu,bar"
|
613
427
|
)
|
614
428
|
|
615
429
|
submenu = self._submenu(0)
|
@@ -628,31 +442,242 @@ class MenuBar:
|
|
628
442
|
),
|
629
443
|
)
|
630
444
|
self.app.menus["menu-2"] = Float(
|
631
|
-
attach_to_window=submenu,
|
445
|
+
attach_to_window=submenu.get_children()[1],
|
632
446
|
xcursor=True,
|
633
447
|
ycursor=True,
|
634
448
|
allow_cover_cursor=True,
|
635
449
|
content=ConditionalContainer(
|
636
450
|
content=Shadow(body=submenu2),
|
637
|
-
filter=has_focus
|
451
|
+
filter=has_focus
|
452
|
+
& Condition(lambda: len(self.selected_menu) > 1)
|
453
|
+
& Condition(lambda: bool(self._get_menu(1).children)),
|
638
454
|
),
|
639
455
|
)
|
640
456
|
self.app.menus["menu-3"] = Float(
|
641
|
-
attach_to_window=submenu2,
|
457
|
+
attach_to_window=submenu2.get_children()[1],
|
642
458
|
xcursor=True,
|
643
459
|
ycursor=True,
|
644
460
|
allow_cover_cursor=True,
|
645
461
|
content=ConditionalContainer(
|
646
462
|
content=Shadow(body=submenu3),
|
647
|
-
filter=has_focus
|
463
|
+
filter=has_focus
|
464
|
+
& Condition(lambda: len(self.selected_menu) > 2)
|
465
|
+
& Condition(lambda: bool(self._get_menu(2).children)),
|
648
466
|
),
|
649
467
|
)
|
650
468
|
|
651
|
-
|
469
|
+
def _get_menu(self, level: int) -> MenuItem:
|
470
|
+
menu = self.menu_items[self.selected_menu[0]]
|
471
|
+
for i, index in enumerate(self.selected_menu[1:]):
|
472
|
+
if i < level:
|
473
|
+
try:
|
474
|
+
menu = menu.children[index]
|
475
|
+
except IndexError:
|
476
|
+
return MenuItem("debug")
|
477
|
+
return menu
|
478
|
+
|
479
|
+
def _get_menu_fragments(self) -> StyleAndTextTuples:
|
480
|
+
focused = get_app().layout.has_focus(self.window)
|
652
481
|
|
653
|
-
|
654
|
-
|
655
|
-
|
482
|
+
# This is called during the rendering. When we discover that this
|
483
|
+
# widget doesn't have the focus anymore. Reset menu state.
|
484
|
+
if not focused:
|
485
|
+
self.selected_menu = [0]
|
486
|
+
|
487
|
+
def mouse_handler(index: int, mouse_event: MouseEvent) -> NotImplementedOrNone:
|
488
|
+
hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
489
|
+
if mouse_event.event_type == MouseEventType.MOUSE_DOWN or hover and focused:
|
490
|
+
# Toggle focus.
|
491
|
+
app = get_app()
|
492
|
+
if not hover:
|
493
|
+
if app.layout.has_focus(self.window):
|
494
|
+
if self.selected_menu == [index]:
|
495
|
+
app.layout.focus_last()
|
496
|
+
else:
|
497
|
+
app.layout.focus(self.window)
|
498
|
+
self.selected_menu = [index]
|
499
|
+
return None
|
500
|
+
return NotImplemented
|
501
|
+
|
502
|
+
results: StyleAndTextTuples = []
|
503
|
+
used_keys = set()
|
504
|
+
|
505
|
+
for i, item in enumerate(self.menu_items):
|
506
|
+
# Add shortcut key hints
|
507
|
+
key = to_plain_text(item.formatted_text)[0].lower()
|
508
|
+
ft: StyleAndTextTuples
|
509
|
+
if key not in used_keys:
|
510
|
+
ft = explode_text_fragments(item.formatted_text)
|
511
|
+
ft = [(f"underline {ft[0][0]}", ft[0][1]), *ft[1:]]
|
512
|
+
used_keys |= {key}
|
513
|
+
else:
|
514
|
+
ft = item.formatted_text
|
515
|
+
|
516
|
+
mh = partial(mouse_handler, i)
|
517
|
+
selected = i == self.selected_menu[0] and focused
|
518
|
+
style = "class:selection" if selected else ""
|
519
|
+
first_style = f"{style} [SetMenuPosition]" if selected else style
|
520
|
+
|
521
|
+
results.extend(
|
522
|
+
[
|
523
|
+
(first_style, " ", mh),
|
524
|
+
*[(f"{style} {style_}", text, mh) for style_, text, *_ in ft],
|
525
|
+
(style, " ", mh),
|
526
|
+
]
|
527
|
+
)
|
528
|
+
|
529
|
+
return results
|
530
|
+
|
531
|
+
def _submenu(self, level: int = 0) -> Container:
|
532
|
+
grid = self.grid
|
533
|
+
|
534
|
+
def get_text_fragments() -> StyleAndTextTuples:
|
535
|
+
result: StyleAndTextTuples = []
|
536
|
+
if level < len(self.selected_menu):
|
537
|
+
menu = self._get_menu(level)
|
538
|
+
|
539
|
+
if menu.children:
|
540
|
+
try:
|
541
|
+
selected_item = self.selected_menu[level + 1]
|
542
|
+
except IndexError:
|
543
|
+
selected_item = -1
|
544
|
+
|
545
|
+
def one_item(
|
546
|
+
i: int, item: MenuItem
|
547
|
+
) -> Iterable[OneStyleAndTextTuple]:
|
548
|
+
assert isinstance(item, MenuItem)
|
549
|
+
assert isinstance(menu, MenuItem)
|
550
|
+
|
551
|
+
def mouse_handler(mouse_event: MouseEvent) -> None:
|
552
|
+
if item.disabled:
|
553
|
+
# The arrow keys can't interact with menu items that
|
554
|
+
# are disabled. The mouse shouldn't be able to either.
|
555
|
+
return
|
556
|
+
hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
557
|
+
if (
|
558
|
+
mouse_event.event_type == MouseEventType.MOUSE_UP
|
559
|
+
or hover
|
560
|
+
):
|
561
|
+
app = get_app()
|
562
|
+
if not hover and item.handler:
|
563
|
+
app.layout.focus_last()
|
564
|
+
item.handler()
|
565
|
+
else:
|
566
|
+
self.selected_menu = self.selected_menu[
|
567
|
+
: level + 1
|
568
|
+
] + [i]
|
569
|
+
|
570
|
+
if item.separator:
|
571
|
+
# Show a connected line with no mouse handler
|
572
|
+
yield (
|
573
|
+
"class:menu,border",
|
574
|
+
grid.SPLIT_LEFT
|
575
|
+
+ (grid.SPLIT_MID * menu.width)
|
576
|
+
+ grid.SPLIT_RIGHT,
|
577
|
+
)
|
578
|
+
|
579
|
+
else:
|
580
|
+
# Show the right edge
|
581
|
+
style = ""
|
582
|
+
# Set the style if disabled
|
583
|
+
if item.disabled:
|
584
|
+
style += "class:menu,disabled"
|
585
|
+
# Set the style and cursor if selected
|
586
|
+
if i == selected_item:
|
587
|
+
style += "class:menu,selection"
|
588
|
+
yield (f"{style} class:menu,border", grid.MID_LEFT)
|
589
|
+
if i == selected_item:
|
590
|
+
yield ("[SetCursorPosition]", "")
|
591
|
+
# Construct the menu item contents
|
592
|
+
prefix_padding = " " * (
|
593
|
+
0
|
594
|
+
if menu.collapse_prefix
|
595
|
+
else menu.prefix_width
|
596
|
+
- fragment_list_width(item.prefix)
|
597
|
+
)
|
598
|
+
suffix_padding = " " * (
|
599
|
+
menu.width
|
600
|
+
- fragment_list_width(item.prefix)
|
601
|
+
- len(prefix_padding)
|
602
|
+
- fragment_list_width(item.formatted_text)
|
603
|
+
- (
|
604
|
+
fragment_list_width(item.suffix)
|
605
|
+
if menu.collapse_suffix
|
606
|
+
else menu.suffix_width
|
607
|
+
)
|
608
|
+
)
|
609
|
+
text_padding = " " * (
|
610
|
+
menu.width
|
611
|
+
- fragment_list_width(item.prefix)
|
612
|
+
- len(prefix_padding)
|
613
|
+
- fragment_list_width(item.formatted_text)
|
614
|
+
- fragment_list_width(item.suffix)
|
615
|
+
- len(suffix_padding)
|
616
|
+
)
|
617
|
+
menu_formatted_text: StyleAndTextTuples = to_formatted_text(
|
618
|
+
[
|
619
|
+
*item.prefix,
|
620
|
+
("", prefix_padding),
|
621
|
+
*item.formatted_text,
|
622
|
+
("", text_padding),
|
623
|
+
("", suffix_padding),
|
624
|
+
*item.suffix,
|
625
|
+
],
|
626
|
+
style=style,
|
627
|
+
)
|
628
|
+
# Apply mouse handler to all fragments
|
629
|
+
menu_formatted_text = [
|
630
|
+
(fragment[0], fragment[1], mouse_handler)
|
631
|
+
for fragment in menu_formatted_text
|
632
|
+
]
|
633
|
+
# Show the menu item contents
|
634
|
+
yield from menu_formatted_text
|
635
|
+
# Position the sub-menu
|
636
|
+
if i == selected_item:
|
637
|
+
yield ("[SetMenuPosition]", "")
|
638
|
+
# Show the right edge
|
639
|
+
yield (f"{style} class:menu,border", grid.MID_RIGHT)
|
640
|
+
|
641
|
+
for i, item in enumerate(menu.children):
|
642
|
+
if not item.hidden():
|
643
|
+
result.extend(one_item(i, item))
|
644
|
+
if i < len(menu.children) - 1:
|
645
|
+
result.append(("", "\n"))
|
646
|
+
|
647
|
+
return result
|
648
|
+
|
649
|
+
return HSplit(
|
650
|
+
[
|
651
|
+
VSplit(
|
652
|
+
[
|
653
|
+
Window(char=grid.TOP_LEFT, width=1, height=1),
|
654
|
+
Window(char=grid.TOP_MID, height=1),
|
655
|
+
Window(char=grid.TOP_RIGHT, width=1, height=1),
|
656
|
+
],
|
657
|
+
style="class:border",
|
658
|
+
),
|
659
|
+
Window(
|
660
|
+
FormattedTextControl(get_text_fragments),
|
661
|
+
scroll_offsets=ScrollOffsets(top=1, bottom=1),
|
662
|
+
),
|
663
|
+
VSplit(
|
664
|
+
[
|
665
|
+
Window(char=grid.BOTTOM_LEFT, width=1, height=1),
|
666
|
+
Window(char=grid.BOTTOM_MID, height=1),
|
667
|
+
# Window(char="🭑🭆", height=1),
|
668
|
+
Window(char=grid.BOTTOM_RIGHT, width=1, height=1),
|
669
|
+
],
|
670
|
+
style="class:border",
|
671
|
+
),
|
672
|
+
],
|
673
|
+
style="class:menu",
|
674
|
+
)
|
675
|
+
|
676
|
+
def __pt_container__(self) -> Container:
|
677
|
+
"""Return the menu bar container's content."""
|
678
|
+
return self.window
|
679
|
+
|
680
|
+
def __pt_status__(self) -> StatusBarFields:
|
656
681
|
"""Return the description of the currently selected menu item."""
|
657
682
|
selected_item = self._get_menu(len(self.selected_menu) - 1)
|
658
683
|
if isinstance(selected_item, MenuItem):
|
@@ -660,6 +685,133 @@ class MenuBar:
|
|
660
685
|
else:
|
661
686
|
return (["", ""], [])
|
662
687
|
|
663
|
-
|
664
|
-
|
665
|
-
|
688
|
+
|
689
|
+
class CompletionsMenuControl(PtkCompletionsMenuControl):
|
690
|
+
"""A custom completions menu control."""
|
691
|
+
|
692
|
+
def create_content(self, width: int, height: int) -> UIContent:
|
693
|
+
"""Create a UIContent object for this control."""
|
694
|
+
complete_state = get_app().current_buffer.complete_state
|
695
|
+
if complete_state:
|
696
|
+
completions = complete_state.completions
|
697
|
+
index = complete_state.complete_index # Can be None!
|
698
|
+
|
699
|
+
# Calculate width of completions menu.
|
700
|
+
menu_width = self._get_menu_width(width, complete_state)
|
701
|
+
menu_meta_width = self._get_menu_meta_width(
|
702
|
+
width - menu_width, complete_state
|
703
|
+
)
|
704
|
+
total_width = menu_width + menu_meta_width
|
705
|
+
|
706
|
+
grid = OuterHalfGrid
|
707
|
+
|
708
|
+
def get_line(i: int) -> StyleAndTextTuples:
|
709
|
+
c = completions[i]
|
710
|
+
selected_item = i == index
|
711
|
+
output: StyleAndTextTuples = []
|
712
|
+
|
713
|
+
style = "class:menu"
|
714
|
+
if selected_item:
|
715
|
+
style += ",selection"
|
716
|
+
|
717
|
+
output.append((f"{style},border", grid.MID_LEFT))
|
718
|
+
if selected_item:
|
719
|
+
output.append(("[SetCursorPosition]", ""))
|
720
|
+
# Construct the menu item contents
|
721
|
+
padding = " " * (
|
722
|
+
total_width
|
723
|
+
- fragment_list_width(c.display)
|
724
|
+
- fragment_list_width(c.display_meta)
|
725
|
+
- 2
|
726
|
+
)
|
727
|
+
output.extend(
|
728
|
+
to_formatted_text(
|
729
|
+
[
|
730
|
+
*c.display,
|
731
|
+
("", padding),
|
732
|
+
*to_formatted_text(
|
733
|
+
c.display_meta, style=f"{style} {c.style}"
|
734
|
+
),
|
735
|
+
],
|
736
|
+
style=style,
|
737
|
+
)
|
738
|
+
)
|
739
|
+
output.append((f"{style},border", grid.MID_RIGHT))
|
740
|
+
|
741
|
+
# Apply mouse handler
|
742
|
+
output = [
|
743
|
+
(fragment[0], fragment[1], self.mouse_handler)
|
744
|
+
for fragment in output
|
745
|
+
]
|
746
|
+
|
747
|
+
return output
|
748
|
+
|
749
|
+
return UIContent(
|
750
|
+
get_line=get_line,
|
751
|
+
cursor_position=Point(x=0, y=index or 0),
|
752
|
+
line_count=len(completions),
|
753
|
+
)
|
754
|
+
|
755
|
+
return UIContent()
|
756
|
+
|
757
|
+
def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
|
758
|
+
"""Handle mouse events: clicking and scrolling."""
|
759
|
+
if mouse_event.event_type == MouseEventType.MOUSE_MOVE:
|
760
|
+
# Set completion
|
761
|
+
complete_state = get_app().current_buffer.complete_state
|
762
|
+
if complete_state:
|
763
|
+
complete_state.complete_index = mouse_event.position.y
|
764
|
+
return None
|
765
|
+
|
766
|
+
return super().mouse_handler(mouse_event)
|
767
|
+
|
768
|
+
|
769
|
+
class CompletionsMenu(ConditionalContainer):
|
770
|
+
"""A custom completions menu."""
|
771
|
+
|
772
|
+
def __init__(
|
773
|
+
self,
|
774
|
+
max_height: int | None = 16,
|
775
|
+
scroll_offset: int | Callable[[], int] = 1,
|
776
|
+
extra_filter: FilterOrBool = True,
|
777
|
+
z_index: int = 10**8,
|
778
|
+
) -> None:
|
779
|
+
"""Create a completions menu with borders."""
|
780
|
+
extra_filter = to_filter(extra_filter)
|
781
|
+
grid = OuterHalfGrid
|
782
|
+
super().__init__(
|
783
|
+
content=HSplit(
|
784
|
+
[
|
785
|
+
VSplit(
|
786
|
+
[
|
787
|
+
Window(char=grid.TOP_LEFT, width=1, height=1),
|
788
|
+
Window(char=grid.TOP_MID, height=1),
|
789
|
+
Window(char=grid.TOP_RIGHT, width=1, height=1),
|
790
|
+
],
|
791
|
+
style="class:border",
|
792
|
+
),
|
793
|
+
Window(
|
794
|
+
content=CompletionsMenuControl(),
|
795
|
+
width=Dimension(min=8),
|
796
|
+
height=Dimension(min=1, max=max_height),
|
797
|
+
scroll_offsets=ScrollOffsets(
|
798
|
+
top=scroll_offset, bottom=scroll_offset
|
799
|
+
),
|
800
|
+
dont_extend_width=True,
|
801
|
+
z_index=z_index,
|
802
|
+
),
|
803
|
+
VSplit(
|
804
|
+
[
|
805
|
+
Window(char=grid.BOTTOM_LEFT, width=1, height=1),
|
806
|
+
Window(char=grid.BOTTOM_MID, height=1),
|
807
|
+
Window(char=grid.BOTTOM_RIGHT, width=1, height=1),
|
808
|
+
],
|
809
|
+
style="class:border",
|
810
|
+
),
|
811
|
+
],
|
812
|
+
style="class:menu",
|
813
|
+
),
|
814
|
+
# Show when there are completions but not at the point we are
|
815
|
+
# returning the input.
|
816
|
+
filter=has_completions & ~is_done & extra_filter,
|
817
|
+
)
|