micropython-micro-gui/gui/widgets/textbox.py

148 wiersze
5.2 KiB
Python

# textbox.py Extension to nanogui providing the Textbox class
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2020 Peter Hinch
# Usage:
# from gui.widgets.textbox import Textbox
from gui.core.ugui import LinearIO
from hardware_setup import ssd # Display driver for Writer
from gui.core.writer import Writer
from time import ticks_diff, ticks_ms
import uasyncio as asyncio
# Reason for no tab support in nano-gui/private/reason_for_no_tabs
class Textbox(LinearIO):
def __init__(self, writer, row, col, width, nlines, *,
bdcolor=None, fgcolor=None,
bgcolor=None, clip=True, active=False):
height = nlines * writer.height
devht = writer.device.height
devwd = writer.device.width
if ((row + height + 2) > devht) or ((col + width + 2) > devwd):
raise ValueError('Textbox extends beyond physical screen.')
super().__init__(writer, row, col, height, width,
fgcolor, bgcolor, bdcolor, 0, active)
self.nlines = nlines
self.clip = clip
self.lines = []
self.start = 0 # Start line for display
def _add_lines(self, s):
width = self.width
font = self.writer.font
n = -1 # Index into string
newline = True
while True:
n += 1
if newline:
newline = False
ls = n # Start of line being processed
col = 0 # Column relative to text area
if n >= len(s): # End of string
if n > ls:
self.lines.append(s[ls :])
return
c = s[n] # Current char
if c == '\n':
self.lines.append(s[ls : n])
newline = True
continue # Line fits window
col += font.get_ch(c)[2] # width of current char
if col > width:
if self.clip:
p = s[ls :].find('\n') # end of 1st line
if p == -1:
self.lines.append(s[ls : n]) # clip, discard all to right
return
self.lines.append(s[ls : n]) # clip, discard to 1st newline
n = p # n will move to 1st char after newline
elif c == ' ': # Easy word wrap
self.lines.append(s[ls : n])
else: # Edge splits a word
p = s.rfind(' ', ls, n + 1)
if p >= 0: # spacechar in line: wrap at space
assert (p > 0), 'space char in position 0'
self.lines.append(s[ls : p])
n = p
else: # No spacechar: wrap at end
self.lines.append(s[ls : n])
n -= 1 # Don't skip current char
newline = True
def _print_lines(self):
if len(self.lines) == 0:
return
wri = self.writer
col = self.col
row = self.row
left = col
ht = wri.height
wri.setcolor(self.fgcolor, self.bgcolor)
# Print the first (or last?) lines that fit widget's height
#for line in self.lines[-self.nlines : ]:
for line in self.lines[self.start : self.start + self.nlines]:
Writer.set_textpos(ssd, row, col)
wri.printstring(line)
row += ht
col = left
wri.setcolor() # Restore defaults
def show(self):
if super().show(False):
self._print_lines()
def append(self, s, ntrim=None, line=None):
self._add_lines(s)
if ntrim is None: # Default to no. of lines that can fit
ntrim = self.nlines
if len(self.lines) > ntrim:
self.lines = self.lines[-ntrim:]
self.goto(line)
def scroll(self, n): # Relative scrolling
value = len(self.lines)
if n == 0 or value <= self.nlines: # Nothing to do
return False
s = self.start
self.start = max(0, min(self.start + n, value - self.nlines))
if s != self.start:
self.draw = True # Cause a refresh
return True
return False
def value(self):
return len(self.lines)
def clear(self):
self.lines = []
self.draw = True # Cause a refresh
def goto(self, line=None): # Absolute scrolling
if line is None:
self.start = max(0, len(self.lines) - self.nlines)
else:
self.start = max(0, min(line, len(self.lines) - self.nlines))
self.draw = True # Cause a refresh
def do_adj(self, button, val):
if isinstance(button, int): # Using an encoder
self.scroll(val)
else:
asyncio.create_task(self.btn_handler(button, val))
async def btn_handler(self, button, up): # Only runs if not using encoder
self.scroll(-up)
t = ticks_ms()
d = 1
while not button():
await asyncio.sleep_ms(0) # Quit fast on button release
if ticks_diff(ticks_ms(), t) > 500: # Button was held down
d = min(16, d * 2)
self.scroll(-up * d)
t = ticks_ms()