# External Libraries
from unicodedata import east_asian_width
from wcwidth import wcwidth
import time
from typing import Iterable
from prompt_toolkit import ANSI
from prompt_toolkit.application import get_app
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.formatted_text import to_formatted_text, fragment_list_to_text
from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
from prompt_toolkit.layout.processors import Processor, Transformation
from prompt_toolkit.application.current import get_app
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.data_structures import Point
from prompt_toolkit.layout.controls import UIContent
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
from prompt_toolkit.selection import SelectionType
from prompt_toolkit.buffer import Buffer, ValidationState
from prompt_toolkit.filters import (
FilterOrBool,
)
from prompt_toolkit.formatted_text import (
StyleAndTextTuples,
to_formatted_text,
)
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
from prompt_toolkit.layout.containers import (
Window,
WindowAlign,
)
from prompt_toolkit.layout.controls import (
BufferControl,
FormattedTextControl,
)
from prompt_toolkit.layout.processors import (
Processor,
)
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from prompt_toolkit.utils import get_cwidth
from prompt_toolkit.widgets import Button, MenuContainer, MenuItem
from prompt_toolkit.widgets.base import Border
from prompt_toolkit.layout.screen import _CHAR_CACHE, Screen, WritePosition
from prompt_toolkit.layout.utils import explode_text_fragments
from prompt_toolkit.formatted_text.utils import (
fragment_list_to_text,
fragment_list_width,
)
from .settings import Settings
class MudFormatProcessor(Processor):
"在BufferControl中显示ANSI格式的处理器"
def __init__(self) -> None:
super().__init__()
self.FULL_BLOCKS = set("▂▃▅▆▇▄█")
self.SINGLE_LINES = set("┌└├┬┼┴╭╰─")
self.DOUBLE_LINES = set("╔╚╠╦╪╩═")
def width_correction(self, line: str) -> str:
new_str = []
for ch in line:
new_str.append(ch)
if (east_asian_width(ch) in "FWA") and (wcwidth(ch) == 1):
if ch in self.FULL_BLOCKS:
new_str.append(ch)
elif ch in self.SINGLE_LINES:
new_str.append("─")
elif ch in self.DOUBLE_LINES:
new_str.append("═")
else:
new_str.append(' ')
return "".join(new_str)
def return_correction(self, line: str):
return line.replace("\r", "").replace("\x00", "")
def tab_correction(self, line: str):
return line.replace("\t", " " * Settings.client["tabstop"])
def line_correction(self, line: str):
# 处理\r符号(^M)
line = self.return_correction(line)
# 处理Tab(\r)符号(^I)
line = self.tab_correction(line)
# 美化(解决中文英文在Console中不对齐的问题)
if Settings.client["beautify"]:
line = self.width_correction(line)
return line
def apply_transformation(self, transformation_input):
# 准备(先还原为str)
line = fragment_list_to_text(transformation_input.fragments)
line = self.line_correction(line)
# 处理ANSI标记(生成FormmatedText)
fragments = to_formatted_text(ANSI(line))
return Transformation(fragments)
class SessionBuffer(Buffer):
"继承自Buffer,为Session内容所修改,主要修改为只能在最后新增内容,并且支持分屏显示适配"
def __init__(
self,
):
super().__init__()
# 修改内容
self.__text = ""
self.__split = False
def _set_text(self, value: str) -> bool:
"""set text at current working_index. Return whether it changed."""
original_value = self.__text
self.__text = value
# Return True when this text has been changed.
if len(value) != len(original_value):
return True
elif value != original_value:
return True
return False
@property
def text(self) -> str:
return self.__text
@text.setter
def text(self, value: str) -> None:
# SessionBuffer is only appendable
if self.cursor_position > len(value):
self.cursor_position = len(value)
changed = self._set_text(value)
if changed:
self._text_changed()
self.history_search_text = None
@property
def working_index(self) -> int:
return 0
@working_index.setter
def working_index(self, value: int) -> None:
pass
def _text_changed(self) -> None:
# Remove any validation errors and complete state.
self.validation_error = None
self.validation_state = ValidationState.UNKNOWN
self.complete_state = None
self.yank_nth_arg_state = None
self.document_before_paste = None
# 添加内容时,不取消选择
#self.selection_state = None
self.suggestion = None
self.preferred_column = None
# fire 'on_text_changed' event.
self.on_text_changed.fire()
def set_document(self, value: Document, bypass_readonly: bool = False) -> None:
pass
@property
def split(self) -> bool:
return self.__split
@split.setter
def split(self, value: bool) -> None:
self.__split = value
@property
def is_returnable(self) -> bool:
return False
# End of <getters/setters>
def save_to_undo_stack(self, clear_redo_stack: bool = True) -> None:
pass
def delete(self, count: int = 1) -> str:
pass
def insert_text(
self,
data: str,
overwrite: bool = False,
move_cursor: bool = True,
fire_event: bool = True,
) -> None:
# 始终在最后增加内容
self.text += data
# 分隔情况下,光标保持原位置不变,否则光标始终位于最后
if not self.__split:
# 若存在选择状态,则视情保留选择
if self.selection_state:
start = self.selection_state.original_cursor_position
end = self.cursor_position
row, col = self.document.translate_index_to_position(start)
lastrow, col = self.document.translate_index_to_position(len(self.text))
self.exit_selection()
# 还没翻过半页的话,就重新选择上
if lastrow - row < get_app().output.get_size().rows // 2 - 1:
self.cursor_position = len(self.text)
self.cursor_position = start
self.start_selection
self.cursor_position = end
else:
self.cursor_position = len(self.text)
else:
self.cursor_position = len(self.text)
else:
pass
def clear_half(self):
"将Buffer前半段内容清除,并清除缓存"
remain_lines = len(self.document.lines) // 2
start = self.document.translate_row_col_to_index(remain_lines, 0)
new_text = self.text[start:]
del self.history
self.history = InMemoryHistory()
self.text = ""
self._set_text(new_text)
self._document_cache.clear()
new_doc = Document(text = new_text, cursor_position = len(new_text))
self.reset(new_doc, False)
self.__split = False
return new_doc.line_count
def undo(self) -> None:
pass
def redo(self) -> None:
pass
class SessionBufferControl(BufferControl):
def __init__(self, buffer: SessionBuffer = None, input_processors = None, include_default_input_processors: bool = True, lexer: Lexer = None, preview_search: FilterOrBool = False, focusable: FilterOrBool = True, search_buffer_control = None, menu_position = None, focus_on_click: FilterOrBool = False, key_bindings: KeyBindingsBase = None):
# 将所属Buffer类型更改为SessionBuffer
buffer = buffer or SessionBuffer()
super().__init__(buffer, input_processors, include_default_input_processors, lexer, preview_search, focusable, search_buffer_control, menu_position, focus_on_click, key_bindings)
self.buffer = buffer
# def create_content(
# self, width: int, height: int, preview_search: bool = False
# ) -> UIContent:
# """
# Create a UIContent.
# """
# buffer = self.buffer
# # Trigger history loading of the buffer. We do this during the
# # rendering of the UI here, because it needs to happen when an
# # `Application` with its event loop is running. During the rendering of
# # the buffer control is the earliest place we can achieve this, where
# # we're sure the right event loop is active, and don't require user
# # interaction (like in a key binding).
# buffer.load_history_if_not_yet_loaded()
# # Get the document to be shown. If we are currently searching (the
# # search buffer has focus, and the preview_search filter is enabled),
# # then use the search document, which has possibly a different
# # text/cursor position.)
# search_control = self.search_buffer_control
# preview_now = preview_search or bool(
# # Only if this feature is enabled.
# self.preview_search()
# and
# # And something was typed in the associated search field.
# search_control
# and search_control.buffer.text
# and
# # And we are searching in this control. (Many controls can point to
# # the same search field, like in Pyvim.)
# get_app().layout.search_target_buffer_control == self
# )
# if preview_now and search_control is not None:
# ss = self.search_state
# document = buffer.document_for_search(
# SearchState(
# text=search_control.buffer.text,
# direction=ss.direction,
# ignore_case=ss.ignore_case,
# )
# )
# else:
# document = buffer.document
# get_processed_line = self._create_get_processed_line_func(
# document, width, height
# )
# self._last_get_processed_line = get_processed_line
# def translate_rowcol(row: int, col: int) -> Point:
# "Return the content column for this coordinate."
# return Point(x=get_processed_line(row).source_to_display(col), y=row)
# def get_line(i: int) -> StyleAndTextTuples:
# "Return the fragments for a given line number."
# fragments = get_processed_line(i).fragments
# # Add a space at the end, because that is a possible cursor
# # position. (When inserting after the input.) We should do this on
# # all the lines, not just the line containing the cursor. (Because
# # otherwise, line wrapping/scrolling could change when moving the
# # cursor around.)
# fragments = fragments + [("", " ")]
# return fragments
# content = UIContent(
# get_line=get_line,
# line_count=document.line_count,
# cursor_position=translate_rowcol(
# document.cursor_position_row, document.cursor_position_col
# ),
# )
# # If there is an auto completion going on, use that start point for a
# # pop-up menu position. (But only when this buffer has the focus --
# # there is only one place for a menu, determined by the focused buffer.)
# if get_app().layout.current_control == self:
# menu_position = self.menu_position() if self.menu_position else None
# if menu_position is not None:
# assert isinstance(menu_position, int)
# menu_row, menu_col = buffer.document.translate_index_to_position(
# menu_position
# )
# content.menu_position = translate_rowcol(menu_row, menu_col)
# elif buffer.complete_state:
# # Position for completion menu.
# # Note: We use 'min', because the original cursor position could be
# # behind the input string when the actual completion is for
# # some reason shorter than the text we had before. (A completion
# # can change and shorten the input.)
# menu_row, menu_col = buffer.document.translate_index_to_position(
# min(
# buffer.cursor_position,
# buffer.complete_state.original_document.cursor_position,
# )
# )
# content.menu_position = translate_rowcol(menu_row, menu_col)
# else:
# content.menu_position = None
# return content
def mouse_handler(self, mouse_event: MouseEvent):
"""
鼠标处理,修改内容包括:
1. 在CommandLine获得焦点的时候,鼠标对本Control也可以操作
2. 鼠标双击为选中行
"""
buffer = self.buffer
position = mouse_event.position
# Focus buffer when clicked.
cur_control = get_app().layout.current_control
cur_buffer = get_app().layout.current_buffer
# 这里时修改的内容
if (cur_control == self) or (cur_buffer and cur_buffer.name == "input"):
if self._last_get_processed_line:
processed_line = self._last_get_processed_line(position.y)
# Translate coordinates back to the cursor position of the
# original input.
xpos = processed_line.display_to_source(position.x)
index = buffer.document.translate_row_col_to_index(position.y, xpos)
# Set the cursor position.
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
buffer.exit_selection()
buffer.cursor_position = index
elif (
mouse_event.event_type == MouseEventType.MOUSE_MOVE
and mouse_event.button != MouseButton.NONE
):
# Click and drag to highlight a selection
if (
buffer.selection_state is None
and abs(buffer.cursor_position - index) > 0
):
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
buffer.cursor_position = index
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
# When the cursor was moved to another place, select the text.
# (The >1 is actually a small but acceptable workaround for
# selecting text in Vi navigation mode. In navigation mode,
# the cursor can never be after the text, so the cursor
# will be repositioned automatically.)
if abs(buffer.cursor_position - index) > 1:
if buffer.selection_state is None:
buffer.start_selection(
selection_type=SelectionType.CHARACTERS
)
buffer.cursor_position = index
# Select word around cursor on double click.
# Two MOUSE_UP events in a short timespan are considered a double click.
double_click = (
self._last_click_timestamp
and time.time() - self._last_click_timestamp < 0.3
)
self._last_click_timestamp = time.time()
if double_click:
start = buffer.document.translate_row_col_to_index(position.y, 0)
end = buffer.document.translate_row_col_to_index(position.y, 10000000)
buffer.cursor_position = start
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
buffer.cursor_position = end
else:
# Don't handle scroll events here.
return NotImplemented
# Not focused, but focusing on click events.
else:
if (
self.focus_on_click()
and mouse_event.event_type == MouseEventType.MOUSE_UP
):
# Focus happens on mouseup. (If we did this on mousedown, the
# up event will be received at the point where this widget is
# focused and be handled anyway.)
get_app().layout.current_control = self
else:
return NotImplemented
return None
def move_cursor_down(self) -> None:
b = self.buffer
b.cursor_position += b.document.get_cursor_down_position()
def move_cursor_up(self) -> None:
b = self.buffer
b.cursor_position += b.document.get_cursor_up_position()
def move_cursor_right(self, count = 1) -> None:
b = self.buffer
b.cursor_position += count
def move_cursor_left(self, count = 1) -> None:
b = self.buffer
b.cursor_position -= count
class VSplitWindow(Window):
"修改的分块窗口,向上翻页时,下半部保持最后数据不变"
def _copy_body(
self,
ui_content: UIContent,
new_screen: Screen,
write_position: WritePosition,
move_x: int,
width: int,
vertical_scroll: int = 0,
horizontal_scroll: int = 0,
wrap_lines: bool = False,
highlight_lines: bool = False,
vertical_scroll_2: int = 0,
always_hide_cursor: bool = False,
has_focus: bool = False,
align: WindowAlign = WindowAlign.LEFT,
get_line_prefix = None,
isNotMargin = True,
):
"""
Copy the UIContent into the output screen.
Return (visible_line_to_row_col, rowcol_to_yx) tuple.
:param get_line_prefix: None or a callable that takes a line number
(int) and a wrap_count (int) and returns formatted text.
"""
xpos = write_position.xpos + move_x
ypos = write_position.ypos
line_count = ui_content.line_count
new_buffer = new_screen.data_buffer
empty_char = _CHAR_CACHE["", ""]
# Map visible line number to (row, col) of input.
# 'col' will always be zero if line wrapping is off.
visible_line_to_row_col: dict[int, tuple[int, int]] = {}
# Maps (row, col) from the input to (y, x) screen coordinates.
rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
def copy_line(
line: StyleAndTextTuples,
lineno: int,
x: int,
y: int,
is_input: bool = False,
):
"""
Copy over a single line to the output screen. This can wrap over
multiple lines in the output. It will call the prefix (prompt)
function before every line.
"""
if is_input:
current_rowcol_to_yx = rowcol_to_yx
else:
current_rowcol_to_yx = {} # Throwaway dictionary.
# Draw line prefix.
if is_input and get_line_prefix:
prompt = to_formatted_text(get_line_prefix(lineno, 0))
x, y = copy_line(prompt, lineno, x, y, is_input=False)
# Scroll horizontally.
skipped = 0 # Characters skipped because of horizontal scrolling.
if horizontal_scroll and is_input:
h_scroll = horizontal_scroll
line = explode_text_fragments(line)
while h_scroll > 0 and line:
h_scroll -= get_cwidth(line[0][1])
skipped += 1
del line[:1] # Remove first character.
x -= h_scroll # When scrolling over double width character,
# this can end up being negative.
# Align this line. (Note that this doesn't work well when we use
# get_line_prefix and that function returns variable width prefixes.)
if align == WindowAlign.CENTER:
line_width = fragment_list_width(line)
if line_width < width:
x += (width - line_width) // 2
elif align == WindowAlign.RIGHT:
line_width = fragment_list_width(line)
if line_width < width:
x += width - line_width
col = 0
wrap_count = 0
for style, text, *_ in line:
new_buffer_row = new_buffer[y + ypos]
# Remember raw VT escape sequences. (E.g. FinalTerm's
# escape sequences.)
if "[ZeroWidthEscape]" in style:
new_screen.zero_width_escapes[y + ypos][x + xpos] += text
continue
for c in text:
char = _CHAR_CACHE[c, style]
char_width = char.width
# Wrap when the line width is exceeded.
if wrap_lines and x + char_width > width:
visible_line_to_row_col[y + 1] = (
lineno,
visible_line_to_row_col[y][1] + x,
)
y += 1
wrap_count += 1
x = 0
# Insert line prefix (continuation prompt).
if is_input and get_line_prefix:
prompt = to_formatted_text(
get_line_prefix(lineno, wrap_count)
)
x, y = copy_line(prompt, lineno, x, y, is_input=False)
new_buffer_row = new_buffer[y + ypos]
if y >= write_position.height:
return x, y # Break out of all for loops.
# Set character in screen and shift 'x'.
if x >= 0 and y >= 0 and x < width:
new_buffer_row[x + xpos] = char
# When we print a multi width character, make sure
# to erase the neighbours positions in the screen.
# (The empty string if different from everything,
# so next redraw this cell will repaint anyway.)
if char_width > 1:
for i in range(1, char_width):
new_buffer_row[x + xpos + i] = empty_char
# If this is a zero width characters, then it's
# probably part of a decomposed unicode character.
# See: https://en.wikipedia.org/wiki/Unicode_equivalence
# Merge it in the previous cell.
elif char_width == 0:
# Handle all character widths. If the previous
# character is a multiwidth character, then
# merge it two positions back.
for pw in [2, 1]: # Previous character width.
if (
x - pw >= 0
and new_buffer_row[x + xpos - pw].width == pw
):
prev_char = new_buffer_row[x + xpos - pw]
char2 = _CHAR_CACHE[
prev_char.char + c, prev_char.style
]
new_buffer_row[x + xpos - pw] = char2
# Keep track of write position for each character.
current_rowcol_to_yx[lineno, col + skipped] = (
y + ypos,
x + xpos,
)
col += 1
x += char_width
return x, y
# Copy content.
def copy() -> int:
y = -vertical_scroll_2
lineno = vertical_scroll
total = write_position.height
upper = (total - 1) // 2
below = total - upper - 1
if lineno + total < line_count:
if isinstance(self.content, SessionBufferControl):
b = self.content.buffer
b.split = True
while y < upper and lineno < line_count:
line = ui_content.get_line(lineno)
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
x = 0
x, y = copy_line(line, lineno, x, y, is_input=True)
lineno += 1
y += 1
x = 0
x, y = copy_line([("","-"*width)], lineno, x, y, is_input=False)
y += 1
lineno = line_count - below
while y < total and lineno < line_count:
line = ui_content.get_line(lineno)
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
x = 0
x, y = copy_line(line, lineno, x, y, is_input=True)
lineno += 1
y += 1
return y
else:
if isNotMargin and isinstance(self.content, SessionBufferControl):
b = self.content.buffer
b.split = False
while y < write_position.height and lineno < line_count:
# Take the next line and copy it in the real screen.
line = ui_content.get_line(lineno)
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
# Copy margin and actual line.
x = 0
x, y = copy_line(line, lineno, x, y, is_input=True)
lineno += 1
y += 1
return y
copy()
def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
"Translate row/col from UIContent to real Screen coordinates."
try:
y, x = rowcol_to_yx[row, col]
except KeyError:
# Normally this should never happen. (It is a bug, if it happens.)
# But to be sure, return (0, 0)
return Point(x=0, y=0)
# raise ValueError(
# 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
# 'horizontal_scroll=%r, height=%r' %
# (row, col, vertical_scroll, horizontal_scroll, write_position.height))
else:
return Point(x=x, y=y)
# Set cursor and menu positions.
if ui_content.cursor_position:
screen_cursor_position = cursor_pos_to_screen_pos(
ui_content.cursor_position.y, ui_content.cursor_position.x
)
if has_focus:
new_screen.set_cursor_position(self, screen_cursor_position)
if always_hide_cursor:
new_screen.show_cursor = False
else:
new_screen.show_cursor = ui_content.show_cursor
self._highlight_digraph(new_screen)
if highlight_lines:
self._highlight_cursorlines(
new_screen,
screen_cursor_position,
xpos,
ypos,
width,
write_position.height,
)
# Draw input characters from the input processor queue.
if has_focus and ui_content.cursor_position:
self._show_key_processor_key_buffer(new_screen)
# Set menu position.
if ui_content.menu_position:
new_screen.set_menu_position(
self,
cursor_pos_to_screen_pos(
ui_content.menu_position.y, ui_content.menu_position.x
),
)
# Update output screen height.
new_screen.height = max(new_screen.height, ypos + write_position.height)
return visible_line_to_row_col, rowcol_to_yx
def _copy_margin(
self,
margin_content: UIContent,
new_screen: Screen,
write_position: WritePosition,
move_x: int,
width: int,
) -> None:
"""
Copy characters from the margin screen to the real screen.
"""
xpos = write_position.xpos + move_x
ypos = write_position.ypos
margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
self._copy_body(margin_content, new_screen, margin_write_position, 0, width, isNotMargin=False)
def _scroll_down(self) -> None:
"向下滚屏,处理屏幕分隔"
info = self.render_info
if info is None:
return
if isinstance(self.content, SessionBufferControl):
b = self.content.buffer
d = b.document
b.exit_selection()
cur_line = d.cursor_position_row
# # 向下滚动时,如果存在自动折行情况,要判断本行被折成了几行,在行内时要逐行移动(此处未调试好)
# cur_col = d.cursor_position_col
# line = d.current_line
# line_width = len(line)
# line_start = d.translate_row_col_to_index(cur_line, 0)
# screen_width = info.window_width
# offset_y = cur_col // screen_width
# wraplines = math.ceil(1.0 * line_width / screen_width)
if cur_line < info.content_height:
# if offset_y < wraplines: # add
# self.content.move_cursor_right(screen_width) # add
# else: # add
self.content.move_cursor_down()
self.vertical_scroll = cur_line + 1
firstline = d.line_count - len(info.displayed_lines)
if cur_line >= firstline:
b.cursor_position = len(b.text)
def _scroll_up(self) -> None:
"向上滚屏,处理屏幕分隔"
info = self.render_info
if info is None:
return
#if info.cursor_position.y >= 1:
if isinstance(self.content, SessionBufferControl):
b = self.content.buffer
d = b.document
b.exit_selection()
cur_line = d.cursor_position_row
if cur_line > d.line_count - len(info.displayed_lines):
firstline = d.line_count - len(info.displayed_lines)
newpos = d.translate_row_col_to_index(firstline, 0)
b.cursor_position = newpos
cur_line = d.cursor_position_row
self.vertical_scroll = cur_line
elif cur_line > 0:
self.content.move_cursor_up()
self.vertical_scroll = cur_line - 1
class EasternButton(Button):
"解决增加中文等东亚全宽字符后不对齐问题"
def _get_text_fragments(self) -> StyleAndTextTuples:
# 主要改动在这里
width = self.width - (
get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
) - (get_cwidth(self.text) - len(self.text))
text = (f"{{:^{width}}}").format(self.text)
def handler(mouse_event: MouseEvent) -> None:
if (
self.handler is not None
and mouse_event.event_type == MouseEventType.MOUSE_UP
):
self.handler()
return [
("class:button.arrow", self.left_symbol, handler),
#("[SetCursorPosition]", ""),
("class:button.text", text, handler),
("class:button.arrow", self.right_symbol, handler),
]
class EasternMenuContainer(MenuContainer):
"解决增加中文等东亚全宽字符后不对齐问题"
def _submenu(self, level: int = 0) -> Window:
def get_text_fragments() -> StyleAndTextTuples:
result: StyleAndTextTuples = []
if level < len(self.selected_menu):
menu = self._get_menu(level)
if menu.children:
result.append(("class:menu", Border.TOP_LEFT))
result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
result.append(("class:menu", Border.TOP_RIGHT))
result.append(("", "\n"))
try:
selected_item = self.selected_menu[level + 1]
except IndexError:
selected_item = -1
def one_item(
i: int, item: MenuItem
) -> Iterable[OneStyleAndTextTuple]:
def mouse_handler(mouse_event: MouseEvent) -> None:
if item.disabled:
# The arrow keys can't interact with menu items that are disabled.
# The mouse shouldn't be able to either.
return
hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
if (
mouse_event.event_type == MouseEventType.MOUSE_UP
or hover
):
app = get_app()
if not hover and item.handler:
app.layout.focus_last()
item.handler()
else:
self.selected_menu = self.selected_menu[
: level + 1
] + [i]
if i == selected_item:
yield ("[SetCursorPosition]", "")
style = "class:menu-bar.selected-item"
else:
style = ""
yield ("class:menu", Border.VERTICAL)
if item.text == "-":
yield (
style + "class:menu-border",
f"{Border.HORIZONTAL * (menu.width + 3)}",
mouse_handler,
)
else:
# 主要改动在这里,其他地方都未更改.
adj_width = menu.width + 3 - (get_cwidth(item.text) - len(item.text))
yield (
style,
f" {item.text}".ljust(adj_width),
mouse_handler,
)
if item.children:
yield (style, ">", mouse_handler)
else:
yield (style, " ", mouse_handler)
if i == selected_item:
yield ("[SetMenuPosition]", "")
yield ("class:menu", Border.VERTICAL)
yield ("", "\n")
for i, item in enumerate(menu.children):
result.extend(one_item(i, item))
result.append(("class:menu", Border.BOTTOM_LEFT))
result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
result.append(("class:menu", Border.BOTTOM_RIGHT))
return result
return Window(FormattedTextControl(get_text_fragments), style="class:menu")
class MenuItem:
def __init__(
self,
text: str = "",
handler = None,
children = None,
shortcut = None,
disabled: bool = False,
) -> None:
self.text = text
self.handler = handler
self.children = children or []
self.shortcut = shortcut
self.disabled = disabled
self.selected_item = 0
@property
def width(self) -> int:
if self.children:
return max(get_cwidth(c.text) for c in self.children)
else:
return 0
[文档]
class DotDict(dict):
"""
可以通过点.访问内部key-value对的字典。此类型继承自dict。
- 由于继承关系,此类型可以使用所有dict可以使用的方法
- 额外增加的点.访问方法使用示例如下
示例:
.. code:: Python
mydict = DotDict()
# 以下写内容访问等价
mydict["key1"] = "value1"
mydict.key1 = "value1"
# 以下读访问等价
val = mydict["key1"]
val = mydict.key1
"""
def __getattr__(self, __key):
if (not __key in self.__dict__) and (not __key.startswith("__")):
return self.__getitem__(__key)
def __setattr__(self, __name: str, __value):
if __name in self.__dict__:
object.__setattr__(self, __name, __value)
else:
self.__setitem__(__name, __value)
def __getstate__(self):
return self
def __setstate__(self, state):
self.update(state)