File: utilities.py

package info (click to toggle)
python-asciimatics 1.15.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,488 kB
  • sloc: python: 15,713; sh: 8; makefile: 2
file content (252 lines) | stat: -rw-r--r-- 11,444 bytes parent folder | download
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)