262 lines
12 KiB
Python
262 lines
12 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
"""This module defines commonly used pieces for widgets"""
|
||
|
from __future__ import division
|
||
|
from __future__ import absolute_import
|
||
|
from __future__ import print_function
|
||
|
from __future__ import unicode_literals
|
||
|
from logging import getLogger
|
||
|
from math import sqrt
|
||
|
from builtins import str
|
||
|
from collections import defaultdict
|
||
|
from wcwidth import wcswidth, wcwidth
|
||
|
try:
|
||
|
from functools import lru_cache
|
||
|
except ImportError:
|
||
|
from backports.functools_lru_cache import lru_cache
|
||
|
from asciimatics.screen import Screen
|
||
|
|
||
|
# Logging
|
||
|
logger = getLogger(__name__)
|
||
|
|
||
|
#: Standard palettes for use with :py:meth:`~Frame.set_theme`.
|
||
|
#: Each entry in THEMES contains a colour palette for use by the widgets within a Frame.
|
||
|
#: Each colour palette is a dictionary mapping a colour key to a 3-tuple of
|
||
|
#: (foreground colour, attribute, background colour).
|
||
|
#: The "default" theme defines all the required keys for a palette.
|
||
|
THEMES = {
|
||
|
"default": {
|
||
|
"background": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"shadow": (Screen.COLOUR_BLACK, None, Screen.COLOUR_BLACK),
|
||
|
"disabled": (Screen.COLOUR_BLACK, Screen.A_BOLD, Screen.COLOUR_BLUE),
|
||
|
"invalid": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_RED),
|
||
|
"label": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLUE),
|
||
|
"borders": (Screen.COLOUR_BLACK, Screen.A_BOLD, Screen.COLOUR_BLUE),
|
||
|
"scroll": (Screen.COLOUR_CYAN, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"title": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLUE),
|
||
|
"edit_text": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"focus_edit_text": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_CYAN),
|
||
|
"readonly": (Screen.COLOUR_BLACK, Screen.A_BOLD, Screen.COLOUR_BLUE),
|
||
|
"focus_readonly": (Screen.COLOUR_BLACK, Screen.A_BOLD, Screen.COLOUR_CYAN),
|
||
|
"button": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"focus_button": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_CYAN),
|
||
|
"control": (Screen.COLOUR_YELLOW, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"selected_control": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLUE),
|
||
|
"focus_control": (Screen.COLOUR_YELLOW, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"selected_focus_control": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_CYAN),
|
||
|
"field": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"selected_field": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLUE),
|
||
|
"focus_field": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLUE),
|
||
|
"selected_focus_field": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_CYAN),
|
||
|
},
|
||
|
"monochrome": defaultdict(
|
||
|
lambda: (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK),
|
||
|
{
|
||
|
"invalid": (Screen.COLOUR_BLACK, Screen.A_NORMAL, Screen.COLOUR_RED),
|
||
|
"label": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"title": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"selected_focus_field": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"focus_edit_text": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"focus_button": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"selected_focus_control": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"disabled": (Screen.COLOUR_BLACK, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
}
|
||
|
),
|
||
|
"green": defaultdict(
|
||
|
lambda: (Screen.COLOUR_GREEN, Screen.A_NORMAL, Screen.COLOUR_BLACK),
|
||
|
{
|
||
|
"invalid": (Screen.COLOUR_BLACK, Screen.A_NORMAL, Screen.COLOUR_RED),
|
||
|
"label": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"title": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"selected_focus_field": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"focus_edit_text": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"focus_button": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"selected_focus_control": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"disabled": (Screen.COLOUR_BLACK, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
}
|
||
|
),
|
||
|
"bright": defaultdict(
|
||
|
lambda: (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
{
|
||
|
"invalid": (Screen.COLOUR_BLACK, Screen.A_NORMAL, Screen.COLOUR_RED),
|
||
|
"label": (Screen.COLOUR_GREEN, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"control": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"focus_control": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"selected_focus_control": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"selected_focus_field": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"focus_button": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"focus_edit_text": (Screen.COLOUR_YELLOW, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
"disabled": (Screen.COLOUR_BLACK, Screen.A_BOLD, Screen.COLOUR_BLACK),
|
||
|
}
|
||
|
),
|
||
|
"tlj256": defaultdict(
|
||
|
lambda: (16, 0, 15),
|
||
|
{
|
||
|
"invalid": (0, 0, 196),
|
||
|
"label": (88, 0, 15),
|
||
|
"title": (88, 0, 15),
|
||
|
"selected_focus_field": (15, 0, 88),
|
||
|
"focus_edit_text": (15, 0, 88),
|
||
|
"focus_button": (15, 0, 88),
|
||
|
"selected_focus_control": (15, 0, 88),
|
||
|
"disabled": (8, 0, 15),
|
||
|
}
|
||
|
),
|
||
|
"warning": defaultdict(
|
||
|
lambda: (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_RED),
|
||
|
{
|
||
|
"label": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_RED),
|
||
|
"title": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_RED),
|
||
|
"focus_edit_text": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_RED),
|
||
|
"focus_field": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_RED),
|
||
|
"focus_button": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_YELLOW),
|
||
|
"focus_control": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_RED),
|
||
|
"disabled": (Screen.COLOUR_WHITE, Screen.A_BOLD, Screen.COLOUR_RED),
|
||
|
"shadow": (Screen.COLOUR_BLACK, None, Screen.COLOUR_BLACK),
|
||
|
}
|
||
|
),
|
||
|
}
|
||
|
|
||
|
|
||
|
def _enforce_width(text, width, unicode_aware=True):
|
||
|
"""
|
||
|
Enforce a displayed piece of text to be a certain number of cells wide. This takes into
|
||
|
account double-width characters used in CJK languages.
|
||
|
|
||
|
:param text: The text to be truncated
|
||
|
:param width: The screen cell width to enforce
|
||
|
:return: The resulting truncated text
|
||
|
"""
|
||
|
# Double-width strings cannot be more than twice the string length, so no need to try
|
||
|
# expensive truncation if this upper bound isn't an issue.
|
||
|
if (2 * len(text) < width) or (len(text) < width and not unicode_aware):
|
||
|
return text
|
||
|
|
||
|
# Can still optimize performance if we are not handling unicode characters.
|
||
|
if unicode_aware:
|
||
|
size = 0
|
||
|
for i, char in enumerate(str(text)):
|
||
|
c_width = wcwidth(char) if ord(char) >= 256 else 1
|
||
|
if size + c_width > width:
|
||
|
return text[0:i]
|
||
|
size += c_width
|
||
|
elif len(text) + 1 > width:
|
||
|
return text[0:width]
|
||
|
return text
|
||
|
|
||
|
|
||
|
def _find_min_start(text, max_width, unicode_aware=True, at_end=False):
|
||
|
"""
|
||
|
Find the starting point in the string that will reduce it to be less than or equal to the
|
||
|
specified width when displayed on screen.
|
||
|
|
||
|
:param text: The text to analyze.
|
||
|
:param max_width: The required maximum width
|
||
|
:param at_end: At the end of the editable line, so allow spaced for cursor.
|
||
|
|
||
|
:return: The offset within `text` to start at to reduce it to the required length.
|
||
|
"""
|
||
|
# Is the solution trivial? Worth optimizing for text heavy UIs...
|
||
|
if 2 * len(text) < max_width:
|
||
|
return 0
|
||
|
|
||
|
# OK - do it the hard way...
|
||
|
result = 0
|
||
|
string_len = wcswidth if unicode_aware else len
|
||
|
char_len = wcwidth if unicode_aware else lambda x: 1
|
||
|
display_end = string_len(text)
|
||
|
while display_end > max_width:
|
||
|
result += 1
|
||
|
display_end -= char_len(text[0])
|
||
|
text = text[1:]
|
||
|
if at_end and display_end == max_width:
|
||
|
result += 1
|
||
|
return result
|
||
|
|
||
|
|
||
|
def _get_offset(text, visible_width, unicode_aware=True):
|
||
|
"""
|
||
|
Find the character offset within some text for a given visible offset (taking into account the
|
||
|
fact that some character glyphs are double width).
|
||
|
|
||
|
:param text: The text to analyze
|
||
|
:param visible_width: The required location within that text (as seen on screen).
|
||
|
:return: The offset within text (as a character offset within the string).
|
||
|
"""
|
||
|
result = 0
|
||
|
width = 0
|
||
|
if unicode_aware:
|
||
|
for char in text:
|
||
|
if visible_width - width <= 0:
|
||
|
break
|
||
|
result += 1
|
||
|
width += wcwidth(char)
|
||
|
if visible_width - width < 0:
|
||
|
result -= 1
|
||
|
else:
|
||
|
result = min(len(text), visible_width)
|
||
|
return result
|
||
|
|
||
|
|
||
|
@lru_cache(256)
|
||
|
def _split_text(text, width, height, unicode_aware=True):
|
||
|
"""
|
||
|
Split text to required dimensions.
|
||
|
|
||
|
This will first try to split the text into multiple lines, then put a "..." on the last
|
||
|
3 characters of the last line if this still doesn't fit.
|
||
|
|
||
|
:param text: The text to split.
|
||
|
:param width: The maximum width for any line.
|
||
|
:param height: The maximum height for the resulting text.
|
||
|
:return: A list of strings of the broken up text.
|
||
|
"""
|
||
|
# At a high level, just try to split on whitespace for the best results.
|
||
|
tokens = text.split(" ")
|
||
|
result = []
|
||
|
current_line = ""
|
||
|
string_len = wcswidth if unicode_aware else len
|
||
|
for token in tokens:
|
||
|
for i, line_token in enumerate(token.split("\n")):
|
||
|
if string_len(current_line + line_token) > width or i > 0:
|
||
|
# Don't bother inserting completely blank lines
|
||
|
# which should only happen on the very first
|
||
|
# line (as the rest will inject whitespace/newlines)
|
||
|
if len(current_line) > 0:
|
||
|
result.append(current_line.rstrip())
|
||
|
current_line = line_token + " "
|
||
|
else:
|
||
|
current_line += line_token + " "
|
||
|
|
||
|
# At this point we've either split nicely or have a hugely long unbroken string
|
||
|
# (e.g. because the language doesn't use whitespace.
|
||
|
# Either way, break this last line up as best we can.
|
||
|
current_line = current_line.rstrip()
|
||
|
while string_len(current_line) > 0:
|
||
|
new_line = _enforce_width(current_line, width, unicode_aware)
|
||
|
result.append(new_line)
|
||
|
current_line = current_line[len(new_line):]
|
||
|
|
||
|
# Check for a height overrun and truncate.
|
||
|
if len(result) > height:
|
||
|
result = result[:height]
|
||
|
result[height - 1] = result[height - 1][:width - 3] + "..."
|
||
|
|
||
|
# Very small columns could be shorter than individual words - truncate
|
||
|
# each line if necessary.
|
||
|
for i, line in enumerate(result):
|
||
|
if len(line) > width:
|
||
|
result[i] = line[:width - 3] + "..."
|
||
|
return result
|
||
|
|
||
|
|
||
|
def _euclidian_distance(widget1, widget2):
|
||
|
"""
|
||
|
Find the Euclidian distance between 2 widgets.
|
||
|
|
||
|
:param widget1: first widget
|
||
|
:param widget2: second widget
|
||
|
"""
|
||
|
point1 = widget1.get_location()
|
||
|
point2 = widget2.get_location()
|
||
|
return sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2)
|