1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
|
"""This module defines commonly used pieces for widgets"""
from logging import getLogger
from math import sqrt
from collections import defaultdict
from functools import lru_cache
from wcwidth import wcswidth, wcwidth
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)
|