From ceda9894b4624f9fc759efb670cbd4eeed4b51c9 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 27 Apr 2016 14:58:53 +0100 Subject: [PATCH] TFT GUI version 0.1 --- tft_gui/button.py | 110 ++++++++++++++---- tft_gui/buttontest.py | 127 ++++++++++++-------- tft_gui/displays.py | 184 +++++++++++++++++++++++++++++ tft_gui/hst.py | 124 ++++++++++++++++++++ tft_gui/slider.py | 263 ++++++++++++++++++++++++++++-------------- tft_gui/slidetest.py | 125 +++++++++++--------- tft_gui/ui.py | 86 +++++++++++++- 7 files changed, 795 insertions(+), 224 deletions(-) create mode 100644 tft_gui/displays.py create mode 100644 tft_gui/hst.py diff --git a/tft_gui/button.py b/tft_gui/button.py index aee68bb..ecf6b51 100644 --- a/tft_gui/button.py +++ b/tft_gui/button.py @@ -1,25 +1,42 @@ +# button.py Pushbutton classes for Pybboard TFT GUI + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + from delay import Delay -from ui import get_stringsize, print_centered, touchable, CIRCLE, RECTANGLE, CLIPPED_RECT +from ui import get_stringsize, print_centered, Touchable, CIRCLE, RECTANGLE, CLIPPED_RECT + # Button coordinates relate to bounding box (BB). x, y are of BB top left corner. # likewise width and height refer to BB, regardless of button shape +# If font is None button will be rendered without text -class Button(touchable): - def __init__(self, objsched, tft, objtouch, location, *, shape=CIRCLE, height=50, width=50, fill=True, - fgcolor=None, bgcolor=None, fontcolor=None, litcolor=None, text='', font=None, show=True, callback=lambda x : None, +class Button(Touchable): + def __init__(self, objsched, tft, objtouch, location, *, font, shape=CIRCLE, height=50, width=50, fill=True, + fgcolor=None, bgcolor=None, fontcolor=None, litcolor=None, text='', show=True, callback=lambda x, y : None, args=[]): - super().__init__(objsched, objtouch) - self.objsched = objsched - self.tft = tft - self.location = location + super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, None) self.shape = shape - self.height = height - self.width = width self.radius = height // 2 self.fill = fill - self.fgcolor = fgcolor - self.bgcolor = bgcolor - self.font = font if font is not None else tft.text_font - self.fontcolor = fontcolor if fontcolor is not None else tft.getColor() self.litcolor = litcolor self.text = text self.callback = callback @@ -35,19 +52,14 @@ class Button(touchable): def show(self): tft = self.tft - fgcolor = tft.getColor() # save old colors - bgcolor = tft.getBGColor() x = self.location[0] y = self.location[1] if not self.visible: # erase the button - tft.setColor(bgcolor) + self.set_color(self.bgcolor) tft.fillRectangle(x, y, x + self.width, y + self.height) - tft.setColor(fgcolor) + self.restore_color() return - if self.fgcolor is not None: - tft.setColor(self.fgcolor) - if self.bgcolor is not None: - tft.setBGColor(self.bgcolor) + self.set_color() # to foreground if self.shape == CIRCLE: # Button coords are of top left corner of bounding box x += self.radius y += self.radius @@ -74,8 +86,7 @@ class Button(touchable): tft.drawClippedRectangle(x, y, x1, y1) if self.font is not None and len(self.text): print_centered(tft, (x + x1) // 2, (y + y1) // 2, self.text, self.fontcolor, self.font) - tft.setColor(fgcolor) # restore them - tft.setBGColor(bgcolor) + self.restore_color() def shownormal(self): self.fgcolor = self.orig_fgcolor @@ -93,7 +104,7 @@ class Button(touchable): x0 = self.location[0] x1 = self.location[0] + self.width y0 = self.location[1] - y1 = self.location[1] + self.height + y1 = self.location[1] + self.height if x0 <= x <= x1 and y0 <= y <= y1: is_touched = True if is_touched and self.litcolor is not None: @@ -169,3 +180,52 @@ class RadioButtons(Buttons): but.fgcolor = but.orig_fgcolor but.show() self.user_callback(button, args) # user gets button with args they specified + + +class Checkbox(Touchable): + def __init__(self, objsched, tft, objtouch, location, *, height=30, fillcolor=None, + fgcolor=None, bgcolor=None, callback=lambda x, y : None, args=[], value=False, border=None): + super().__init__(objsched, tft, objtouch, location, None, height, height, fgcolor, bgcolor, None, border) + self.callback = callback + self.callback_args = args + self.fillcolor = fillcolor + self.busy = False + self.value = value + self.show() + + def show(self): + tft = self.tft + bw = self.draw_border() # and background if required. Result is width of border + x = self.location[0] + bw + y = self.location[1] + bw + height = self.height - 2 * bw + x1 = x + height + y1 = y + height + if self.fillcolor is None or not self.value: + self.set_color(self.bgcolor) # blank + tft.fillRectangle(x, y, x1, y1) + if self.fillcolor is not None and self.value: + self.set_color(self.fillcolor) + tft.fillRectangle(x, y, x1, y1) + self.set_color() + tft.drawRectangle(x, y, x1, y1) + if self.fillcolor is None and self.value: + tft.drawLine(x, y, x1, y1) + tft.drawLine(x, y1, x1, y) + + def touched(self, x, y): # If touched, process it otherwise do nothing + is_touched = False + x0 = self.location[0] + x1 = self.location[0] + self.width + y0 = self.location[1] + y1 = self.location[1] + self.height + if x0 <= x <= x1 and y0 <= y <= y1: + is_touched = True + if is_touched and not self.busy: # Respond once to a press + self.value = not self.value + self.callback(self, self.callback_args) # Callback not a bound method so pass self + self.busy = True # Ensure no response to continued press + self.show() + + def untouched(self): # User has released touchpad or touched elsewhere + self.busy = False diff --git a/tft_gui/buttontest.py b/tft_gui/buttontest.py index 37c7af2..d83d12f 100644 --- a/tft_gui/buttontest.py +++ b/tft_gui/buttontest.py @@ -1,106 +1,137 @@ +# buttontest.py Test/demo of pushbutton classes for Pybboard TFT GUI + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. -import gc from font14 import font14 from tft import TFT, LANDSCAPE from usched import Sched from touch import TOUCH -from button import Button, Buttonset, RadioButtons -from ui import CIRCLE, RECTANGLE, CLIPPED_RECT -#gc.collect() +from button import Button, Buttonset, RadioButtons, Checkbox +from ui import CIRCLE, RECTANGLE, CLIPPED_RECT, WHITE, BLACK, RED, GREEN, BLUE, YELLOW, GREY +from displays import Label def callback(button, args): arg = args[0] - print('Returned: ', arg) - tft = button.tft - tft.setTextPos(0, 240) - tft.setTextStyle(None, None, 0, font14) - tft.printString('Button argument zero: {} '.format(arg)) + label = args[1] + label.show(arg) if arg == 'Q': button.objsched.stop() -#gc.collect() +def cbcb(checkbox, args): + label = args[0] + if checkbox.value: + label.show('True') + else: + label.show('False') + # These tables contain args that differ between members of a set of related buttons table = [ - {'fgcolor' : (0, 128, 0), 'litcolor' : (0, 255, 0), 'text' : 'Yes', 'args' : ('A'), 'fontcolor' : (0, 0, 0)}, - {'fgcolor' : (255, 0, 0), 'text' : 'No', 'args' : ('B')}, - {'fgcolor' : (0, 0, 255), 'text' : '???', 'args' : ('C'), 'fill': False}, - {'fgcolor' : (128, 128, 128), 'text' : 'Quit', 'args' : ('Q'), 'shape' : CLIPPED_RECT}, + {'fgcolor' : GREEN, 'text' : 'Yes', 'args' : ['A'], 'fontcolor' : (0, 0, 0)}, + {'fgcolor' : RED, 'text' : 'No', 'args' : ['B']}, + {'fgcolor' : BLUE, 'text' : '???', 'args' : ['C'], 'fill': False}, + {'fgcolor' : GREY, 'text' : 'Quit', 'args' : ['Q'], 'shape' : CLIPPED_RECT}, ] # similar buttons: only tabulate data that varies table2 = [ - {'text' : 'P', 'args' : ('p')}, - {'text' : 'Q', 'args' : ('q')}, - {'text' : 'R', 'args' : ('r')}, - {'text' : 'S', 'args' : ('s')}, + {'text' : 'P', 'args' : ['p']}, + {'text' : 'Q', 'args' : ['q']}, + {'text' : 'R', 'args' : ['r']}, + {'text' : 'S', 'args' : ['s']}, ] # A Buttonset with two entries # If buttons to be used in a buttonset, Use list rather than tuple for args because buttonset appends. table3 = [ - {'fgcolor' : (0, 255, 0), 'shape' : CLIPPED_RECT, 'text' : 'Start', 'args' : ['Live']}, - {'fgcolor' : (255, 0, 0), 'shape' : CLIPPED_RECT, 'text' : 'Stop', 'args' : ['Die']}, + {'fgcolor' : GREEN, 'shape' : CLIPPED_RECT, 'text' : 'Start', 'args' : ['Live']}, + {'fgcolor' : RED, 'shape' : CLIPPED_RECT, 'text' : 'Stop', 'args' : ['Die']}, ] table4 = [ - {'text' : '1', 'args' : ('1')}, - {'text' : '2', 'args' : ('2')}, - {'text' : '3', 'args' : ('3')}, - {'text' : '4', 'args' : ('4')}, + {'text' : '1', 'args' : ['1']}, + {'text' : '2', 'args' : ['2']}, + {'text' : '3', 'args' : ['3']}, + {'text' : '4', 'args' : ['4']}, ] -#gc.collect() -# THREADS - -def stop(fTim, objsched): # Stop the scheduler after fTim seconds - yield fTim - objsched.stop() +labels = { 'width' : 70, + 'fontcolor' : WHITE, + 'border' : 2, + 'fgcolor' : RED, + 'bgcolor' : (0, 40, 0), + 'font' : font14, + } # USER TEST FUNCTION -def test(duration = 0): - if duration: - print("Test TFT panel for {:3d} seconds".format(duration)) - else: - print('Testing TFT...') +def test(): + print('Testing TFT...') objsched = Sched() # Instantiate the scheduler mytft = TFT("SSD1963", "LB04301", LANDSCAPE) mytouch = TOUCH("XPT2046", objsched) mytft.backlight(100) # light on + lstlbl = [] + for n in range(3): + lstlbl.append(Label(mytft, (350, 50 * n), **labels)) # Button assortment - x = 50 + x = 0 for t in table: + t['args'].append(lstlbl[2]) Button(objsched, mytft, mytouch, (x, 0), font = font14, callback = callback, **t) x += 70 # Highlighting buttons - x = 50 + x = 0 for t in table2: - Button(objsched, mytft, mytouch, (x, 60), font = font14, fgcolor = (128, 128, 128), - fontcolor = (0, 0, 0), litcolor = (255, 255, 255), callback = callback, **t) + t['args'].append(lstlbl[2]) + Button(objsched, mytft, mytouch, (x, 60), fgcolor = GREY, + fontcolor = BLACK, litcolor = WHITE, font = font14, callback = callback, **t) x += 70 # On/Off toggle - x = 50 + x = 0 bs = Buttonset(callback) for t in table3: # Buttons overlay each other at same location - bs.add_button(objsched, mytft, mytouch, (x, 120), font = font14, fontcolor = (0, 0, 0), **t) + t['args'].append(lstlbl[2]) + bs.add_button(objsched, mytft, mytouch, (x, 120), font = font14, fontcolor = BLACK, **t) bs.run() # Radio buttons - x = 50 - rb = RadioButtons(callback, (0, 0, 255)) # color of selected button + x = 0 + rb = RadioButtons(callback, BLUE) # color of selected button for t in table4: - rb.add_button(objsched, mytft, mytouch, (x, 180), font = font14, fontcolor = (255, 255, 255), + t['args'].append(lstlbl[2]) + rb.add_button(objsched, mytft, mytouch, (x, 180), font = font14, fontcolor = WHITE, fgcolor = (0, 0, 90), height = 30, **t) x += 40 rb.run() -# Start scheduler - if duration: - objsched.add_thread(stop(duration, objsched)) # Commit suicide after specified no. of seconds +# Checkbox + Checkbox(objsched, mytft, mytouch, (300, 0), callback = cbcb, args = [lstlbl[0]]) + Checkbox(objsched, mytft, mytouch, (300, 50), fillcolor = RED, callback = cbcb, args = [lstlbl[1]]) + objsched.run() # Run it! -test() # Forever: we have a Quit button! +test() diff --git a/tft_gui/displays.py b/tft_gui/displays.py new file mode 100644 index 0000000..178c653 --- /dev/null +++ b/tft_gui/displays.py @@ -0,0 +1,184 @@ +# displays.py Non touch sensitive display elements for Pyboard TFT GUI + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from ui import print_centered, NoTouch, BLACK, RED +import math +import TFT_io + +class Label(NoTouch): + def __init__(self, tft, location, *, font, border=None, width, fgcolor=None, bgcolor=None, fontcolor=None, text=''): + super().__init__(tft, location, font, None, width, fgcolor, bgcolor, fontcolor, border) + self.height = self.font.bits_vert + self.height += 2 * self.border # Height determined by font and border + self.draw_border() # Must explicitly draw because ctor did not have height + self.show(text) + + def show(self, text): + tft = self.tft + bw = self.border + if text: + x = self.location[0] + y = self.location[1] + self.set_color(self.bgcolor) + tft.fillRectangle(x + bw, y + bw, x + self.width - bw, y + self.height - bw) + tft.setTextStyle(self.fontcolor, None, 2, self.font) + tft.setTextPos(x + bw, y + bw, clip = self.width - 2 * bw, scroll = False) + tft.printString(text) + self.restore_color() # restore fg color + +# class displays angles +class Dial(NoTouch): + def __init__(self, tft, location, *, height=100, fgcolor=None, bgcolor=None, border=None, pointers=(0.9,), ticks=4): + NoTouch.__init__(self, tft, location, None, height, height, fgcolor, bgcolor, None, border) # __super__ provoked Python bug + border = self.border # border width + radius = height / 2 - border + self.radius = radius + self.xorigin = location[0] + border + radius + self.yorigin = location[1] + border + radius + self.pointers = tuple(z * self.radius for z in pointers) # Pointer lengths + self.angles = [None for _ in pointers] + self.set_color() # set fg color + ticklen = 0.1 * radius + for tick in range(ticks): + theta = 2 * tick * math.pi / ticks + x_start = int(self.xorigin + radius * math.sin(theta)) + y_start = int(self.yorigin - radius * math.cos(theta)) + x_end = int(self.xorigin + (radius - ticklen) * math.sin(theta)) + y_end = int(self.yorigin - (radius - ticklen) * math.cos(theta)) + self.tft.drawLine(x_start, y_start, x_end, y_end) + tft.drawCircle(self.xorigin, self.yorigin, radius) + self.restore_color() + + def show(self, angle, pointer=0): + tft = self.tft + if self.angles[pointer] is not None: + self.set_color(self.bgcolor) + self.drawpointer(self.angles[pointer], pointer) # erase old + self.set_color() + self.drawpointer(angle, pointer) # draw new + self.angles[pointer] = angle # update old + self.restore_color() + + def drawpointer(self, radians, pointer): + length = self.pointers[pointer] + x_end = int(self.xorigin + length * math.sin(radians)) + y_end = int(self.yorigin - length * math.cos(radians)) + self.tft.drawLine(int(self.xorigin), int(self.yorigin), x_end, y_end) + +class LED(NoTouch): + def __init__(self, tft, location, *, border=None, height=30, fgcolor=None, bgcolor=None, color=RED): + super().__init__(tft, location, None, height, height, fgcolor, bgcolor, None, border) + self.radius = (self.height - 2 * self.border) / 2 + self.x = location[0] + self.radius + self.border + self.y = location[1] + self.radius + self.border + self.color = color + self.off() + + def _show(self, color): # Light the LED + self.set_color(color) + self.tft.fillCircle(int(self.x), int(self.y), int(self.radius)) + self.set_color() + self.tft.drawCircle(int(self.x), int(self.y), int(self.radius)) + self.restore_color() + + def on(self, color=None): # Light in current color + if color is not None: + self.color = color + self._show(self.color) + + def off(self): + self._show(BLACK) + +class Meter(NoTouch): + def __init__(self, tft, location, *, font=None, height=200, width=30, + fgcolor=None, bgcolor=None, pointercolor=None, fontcolor=None, + divisions=10, legends=None, value=0): + border = 5 if font is None else 1 + font.bits_vert / 2 + NoTouch.__init__(self, tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border) # __super__ provoked Python bug + border = self.border # border width + self.ptrbytes = 3 * (self.width + 1) # 3 bytes per pixel + self.ptrbuf = bytearray(self.ptrbytes) #??? + self.x0 = self.location[0] + self.x1 = self.location[0] + self.width + self.y0 = self.location[1] + border + 2 + self.y1 = self.location[1] + self.height - border + self.divisions = divisions + self.legends = legends + self.pointercolor = pointercolor if pointercolor is not None else fgcolor + self._value = value + self._old_value = -1 # invalidate + self.ptr_y = -1 # Invalidate old position + self.show() + + def show(self): + tft = self.tft + bw = self.draw_border() # and background if required. Result is width of border + width = self.width + dx = 5 + self.set_color() + x0 = self.x0 + x1 = self.x1 + y0 = self.y0 + y1 = self.y1 + height = y1 - y0 + if self.divisions > 0: + dy = height / (self.divisions) # Tick marks + for tick in range(self.divisions + 1): + ypos = int(y0 + dy * tick) + tft.drawHLine(x0, ypos, dx) + tft.drawHLine(x1 - dx, ypos, dx) + + if self.legends is not None and self.font is not None: # Legends + tft.setTextStyle(self.fontcolor, None, 2, self.font) + if len(self.legends) <= 1: + dy = 0 + else: + dy = height / (len(self.legends) -1) + yl = self.y1 # Start at bottom + for legend in self.legends: + print_centered(tft, int(self.x0 + self.width /2), int(yl), legend, self.fontcolor, self.font) + yl -= dy + + y0 = self.ptr_y + y1 = y0 + if self.ptr_y >= 0: # Restore background + tft.setXY(x0, y0, x1, y1) + TFT_io.tft_write_data_AS(self.ptrbuf, self.ptrbytes) + ptrpos = int(self.y1 - self._value * height) + y0 = ptrpos + y1 = ptrpos + tft.setXY(x0, y0, x1, y1) # Read background + TFT_io.tft_read_cmd_data_AS(0x2e, self.ptrbuf, self.ptrbytes) + self.ptr_y = y0 + self.set_color(self.pointercolor) + tft.drawHLine(x0, y0, width) # Draw pointer + self.restore_color() + + def value(self, val=None): + if val is None: + return self._value + self._value = min(max(val, 0.0), 1.0) + if self._value != self._old_value: + self._old_value = self._value + self.show() diff --git a/tft_gui/hst.py b/tft_gui/hst.py new file mode 100644 index 0000000..07e5cc5 --- /dev/null +++ b/tft_gui/hst.py @@ -0,0 +1,124 @@ +# hst.py Demo/test for Horizontal Slider class for Pyboard TFT GUI + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from font10 import font10 +from tft import TFT, LANDSCAPE +from usched import Sched +from touch import TOUCH +from slider import HorizSlider +from button import Button +from displays import Dial, Label, LED, Meter +from ui import CLIPPED_RECT, GREEN, RED, YELLOW, WHITE, BLUE +import pyb +# CALLBACKS +# cb_end occurs when user stops touching the control +def callback(slider, args): + print('{} returned {}'.format(args[0], slider.value())) + +def to_string(val): + return '{:3.1f} ohms'.format(val * 10) + +def master_moved(slider, args): + val = slider.value() + slave1 = args[0] + slave1.value(val) + slave2 = args[1] + slave2.value(val) + label = args[2] + label.show(to_string(val)) + led = args[3] + if val > 0.8: + led.on() + else: + led.off() + +# Either slave has had its slider moved (by user or by having value altered) +def slave_moved(slider, args): + val = slider.value() + if val > 0.8: + slider.fgcolor = RED + else: + slider.fgcolor = GREEN + label = args[0] + label.show(to_string(val)) + +def doquit(button, args): + button.objsched.stop() + +# USER TEST FUNCTION +# Common args for the labels +labels = { 'width' : 70, + 'fontcolor' : WHITE, + 'border' : 2, + 'fgcolor' : RED, + 'bgcolor' : (0, 40, 0), + } + +# '0', '1','2','3','4','5','6','7','8','9','10' +# Common arguments for all three sliders +table = {'fontcolor' : WHITE, + 'legends' : ('0', '5', '10'), + 'cb_end' : callback, + } +# 'border' : 2, + +def testmeter(meter): + oldvalue = 0 + yield + while True: + val = pyb.rng()/2**30 + steps = 20 + delta = (val - oldvalue) / steps + for _ in range(steps): + oldvalue += delta + meter.value(oldvalue) + yield 0.05 + +def test(duration = 0): + print('Test TFT panel...') + objsched = Sched() # Instantiate the scheduler + mytft = TFT("SSD1963", "LB04301", LANDSCAPE) + mytouch = TOUCH("XPT2046", objsched, confidence = 50) #, calibration = (-3886,-0.1287,-3812,-0.132,-3797,-0.07685,-3798,-0.07681)) + mytft.backlight(100) # light on + led = LED(mytft, (420, 0), border = 2) + meter1 = Meter(mytft, (320, 0), font=font10, legends=('0','5','10'), pointercolor = YELLOW, fgcolor = GREEN) + meter2 = Meter(mytft, (360, 0), font=font10, legends=('0','5','10'), pointercolor = YELLOW) + Button(objsched, mytft, mytouch, (420, 240), font = font10, callback = doquit, fgcolor = RED, + height = 30, text = 'Quit', shape = CLIPPED_RECT) + x = 230 + lstlbl = [] + for n in range(3): + lstlbl.append(Label(mytft, (x, 40 + 60 * n), font = font10, **labels)) + x = 0 + slave1 = HorizSlider(objsched, mytft, mytouch, (x, 100), font10, + fgcolor = GREEN, cbe_args = ('Slave1',), cb_move = slave_moved, cbm_args = (lstlbl[1],), **table) + slave2 = HorizSlider(objsched, mytft, mytouch, (x, 160), font10, + fgcolor = GREEN, cbe_args = ('Slave2',), cb_move = slave_moved, cbm_args = (lstlbl[2],), **table) + master = HorizSlider(objsched, mytft, mytouch, (x, 40), font10, + fgcolor = YELLOW, cbe_args = ('Master',), cb_move = master_moved, slidecolor=RED, cbm_args = (slave1, slave2, lstlbl[0], led), value=0.5, **table) + objsched.add_thread(testmeter(meter1)) + objsched.add_thread(testmeter(meter2)) + objsched.run() # Run it! + +test() diff --git a/tft_gui/slider.py b/tft_gui/slider.py index 4070e6d..455f960 100644 --- a/tft_gui/slider.py +++ b/tft_gui/slider.py @@ -1,146 +1,231 @@ -# slider.py +# slider.py Vertical and horizontal slider control classes for Pyboard TFT GUI + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + # A slider's text items lie outside its bounding box (area sensitive to touch) -from ui import touchable -from TFT_io import TFT_io -class Slider(touchable): +from ui import Touchable, get_stringsize +import TFT_io + +class Slider(Touchable): def __init__(self, objsched, tft, objtouch, location, font, *, height=200, width=30, divisions=10, legends=None, - fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, cb_end=lambda x, y : None, - cbe_args=[], cb_move=lambda x, y : None, cbm_args=[], to_string=lambda x : str(x), value=0.0): - super().__init__(objsched, objtouch) - self.objsched = objsched - self.tft = tft - self.location = location - self.height = height - self.width = width + fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, border=None, + cb_end=lambda x, y : None, cbe_args=[], cb_move=lambda x, y : None, cbm_args=[], value=0.0): + super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border) self.divisions = divisions self.legends = legends - self.fgcolor = fgcolor - self.bgcolor = bgcolor - self.fontcolor = fontcolor if fontcolor is not None else tft.getColor() - self.font = font self.slidecolor = slidecolor self.cb_end = cb_end self.cbe_args = cbe_args self.cb_move = cb_move self.cbm_args = cbm_args - self.to_string = to_string # Applied to display at bottom: user converts 0-1.0 to string with any scaling applied self.was_touched = False - self.old_text_end = False - self.slidewidth = int(width / 1.3) - self.slidewidth += self.slidewidth % 2 # Ensure divisible by 2 + slidewidth = int(width / 1.3) & 0xfe # Ensure divisible by 2 self.slideheight = 6 # must be divisible by 2 # We draw an odd number of pixels: - self.slidebytes = (self.slideheight + 1) * (self.slidewidth + 1) * 3 + self.slidebytes = (self.slideheight + 1) * (slidewidth + 1) * 3 self.slidebuf = bytearray(self.slidebytes) - self.slide_x = -1 + self._old_value = -1 # Invalidate + b = self.border + self.pot_dimension = self.height - 2 * (b + self.slideheight // 2) + width = self.width - 2 * b + xcentre = self.location[0] + b + width // 2 + self.slide_x0 = xcentre - slidewidth // 2 + self.slide_x1 = xcentre + slidewidth // 2 # slide X coordinates self.slide_y = -1 # Invalidate old position - self.border_y = min(self.slideheight // 2, 10) # Allow space above and below slot - - self._value = min(max(value, 0.0), 1.0) # User supplies 0-1.0 - self.show() - objsched.add_thread(self.mainthread()) + self.value(value) def show(self): tft = self.tft - fgcolor = tft.getColor() # save old colors - bgcolor = tft.getBGColor() - mybgcolor = bgcolor - x = self.location[0] - y = self.location[1] + self.border_y - if self.bgcolor is not None: - tft.setColor(self.bgcolor) - else: - tft.setColor(bgcolor) - tft.setTextStyle(self.fontcolor, None, 2, self.font) - if self.fgcolor is not None: - tft.setColor(self.fgcolor) - else: - tft.setColor(fgcolor) - if self.bgcolor is not None: - mybgcolor = self.bgcolor - tft.setBGColor(mybgcolor) - height = self.height - width = self.width - dx = width // 3 - xcentre = x + width // 2 + bw = self.draw_border() # and background if required. Result is width of border + x = self.location[0] + bw + y = self.location[1] + bw + self.slideheight // 2 # Allow space above and below slot + width = self.width - 2 * bw + self.set_color() + height = self.pot_dimension # Height of slot + dx = width / 3 tft.drawRectangle(x + dx, y, x + 2 * dx, y + height) if self.divisions > 0: - dy = height // self.divisions # Tick marks - ytick = y - fhdelta = self.font.bits_vert // 2 + dy = height / (self.divisions) # Tick marks for tick in range(self.divisions + 1): - tft.drawHLine(x, ytick, dx) - tft.drawHLine(x + 2 * dx, ytick, dx) - ytick += dy + ypos = int(y + dy * tick) + tft.drawHLine(x + 1, ypos, dx) + tft.drawHLine(x + 1 + 2 * dx, ypos, dx) if self.legends is not None: # Legends + tft.setTextStyle(self.fontcolor, None, 2, self.font) if len(self.legends) <= 1: dy = 0 else: - dy = height // (len(self.legends) -1) + dy = height / (len(self.legends) -1) yl = y + height # Start at bottom + fhdelta = self.font.bits_vert / 2 for legend in self.legends: - tft.setTextPos(x + width, yl - fhdelta) + tft.setTextPos(x + self.width, int(yl - fhdelta)) tft.printString(legend) yl -= dy - sw = self.slidewidth # Handle slider - sh = self.slideheight - sliderpos = int(y + height - self._value * height) - if self.slidecolor is not None: - tft.setColor(self.slidecolor) - if self.slide_x >= 0: # Restore background - tft.setXY(self.slide_x, self.slide_y, self.slide_x + sw, self.slide_y + sh) + sh = self.slideheight # Handle slider + x0 = self.slide_x0 + y0 = self.slide_y + x1 = self.slide_x1 + y1 = y0 + sh + if self.slide_y >= 0: # Restore background + tft.setXY(x0, y0, x1, y1) TFT_io.tft_write_data_AS(self.slidebuf, self.slidebytes) - x0 = xcentre - sw // 2 + sliderpos = int(y + height - self._value * height) y0 = sliderpos - sh // 2 - x1 = xcentre + sw // 2 y1 = sliderpos + sh // 2 tft.setXY(x0, y0, x1, y1) # Read background TFT_io.tft_read_cmd_data_AS(0x2e, self.slidebuf, self.slidebytes) - self.slide_x = x0 self.slide_y = y0 + if self.slidecolor is not None: + self.set_color(self.slidecolor) tft.fillRectangle(x0, y0, x1, y1) # Draw slider - - textx = x - texty = y + height + 2 * self.border_y - tft.setTextPos(textx, texty) - if self.old_text_end: - tft.setColor(mybgcolor) - tft.fillRectangle(textx, texty, self.old_text_end, texty + self.font.bits_vert) - tft.printString(self.to_string(self._value)) - self.old_text_end = tft.getTextPos()[0] - tft.setColor(fgcolor) # restore them - tft.setBGColor(bgcolor) + self.restore_color() def value(self, val=None): if val is None: return self._value self._value = min(max(val, 0.0), 1.0) - self.show() + if self._value != self._old_value: + self._old_value = self._value + self.cb_move(self, self.cbm_args) # Callback not a bound method so pass self + self.show() def touched(self, x, y): # If touched in bounding box, process it otherwise do nothing x0 = self.location[0] x1 = self.location[0] + self.width y0 = self.location[1] - y1 = self.location[1] + self.height + y1 = self.location[1] + self.height if x0 <= x <= x1 and y0 <= y <= y1: self.was_touched = True - self._value = (y1 - y) / self.height + self.value((y1 - y) / self.pot_dimension) def untouched(self): # User has released touchpad or touched elsewhere if self.was_touched: self.cb_end(self, self.cbe_args) # Callback not a bound method so pass self self.was_touched = False - def mainthread(self): - old_value = self._value - while True: - yield - val = self._value - if val != old_value: - old_value = val - self.cb_move(self, self.cbm_args) # Callback not a bound method so pass self - self.show() +class HorizSlider(Touchable): + def __init__(self, objsched, tft, objtouch, location, font, *, height=30, width=200, divisions=10, legends=None, + fgcolor=None, bgcolor=None, fontcolor=None, slidecolor=None, border=None, + cb_end=lambda x, y : None, cbe_args=[], cb_move=lambda x, y : None, cbm_args=[], value=0.0): + super().__init__(objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border) + self.divisions = divisions + self.legends = legends + self.slidecolor = slidecolor + self.cb_end = cb_end + self.cbe_args = cbe_args + self.cb_move = cb_move + self.cbm_args = cbm_args + self.was_touched = False + slideheight = int(height / 1.3) & 0xfe # Ensure divisible by 2 + self.slidewidth = 6 # must be divisible by 2 + # We draw an odd number of pixels: + self.slidebytes = (slideheight + 1) * (self.slidewidth + 1) * 3 + self.slidebuf = bytearray(self.slidebytes) + self._old_value = -1 # Invalidate + b = self.border + self.pot_dimension = self.width - 2 * (b + self.slidewidth // 2) + height = self.height - 2 * b + ycentre = self.location[1] + b + height // 2 + self.slide_y0 = ycentre - slideheight // 2 + self.slide_y1 = ycentre + slideheight // 2 # slide Y coordinates + self.slide_x = -1 # Invalidate old position + self.value(value) + + def show(self): + tft = self.tft + bw = self.draw_border() # and background if required. Result is width of border + x = self.location[0] + bw + self.slidewidth // 2 # Allow space left and right slot for slider at extremes + y = self.location[1] + bw + height = self.height - 2 * bw + self.set_color() + width = self.pot_dimension # Length of slot + dy = height / 3 + ycentre = y + height // 2 + tft.drawRectangle(x, y + dy, x + width, y + 2 * dy) + + if self.divisions > 0: + dx = width / (self.divisions) # Tick marks + for tick in range(self.divisions + 1): + xpos = int(x + dx * tick) + tft.drawVLine(xpos, y + 1, dy) # TODO Why is +1 fiddle required here? + tft.drawVLine(xpos, y + 1 + 2 * dy, dy) # and here + + if self.legends is not None: # Legends + tft.setTextStyle(self.fontcolor, None, 2, self.font) + if len(self.legends) <= 1: + dx = 0 + else: + dx = width / (len(self.legends) -1) + xl = x + for legend in self.legends: + offset = get_stringsize(legend, self.font)[0] / 2 + tft.setTextPos(int(xl - offset), y - self.font.bits_vert) # Arbitrary left shift should be char width /2 + tft.printString(legend) + xl += dx + + sw = self.slidewidth # Handle slider + x0 = self.slide_x + y0 = self.slide_y0 + x1 = x0 + sw + y1 = self.slide_y1 + if self.slide_x >= 0: # Restore background + tft.setXY(x0, y0, x1, y1) + TFT_io.tft_write_data_AS(self.slidebuf, self.slidebytes) + sliderpos = int(x + self._value * width) + x0 = sliderpos - sw // 2 + x1 = sliderpos + sw // 2 + tft.setXY(x0, y0, x1, y1) # Read background + TFT_io.tft_read_cmd_data_AS(0x2e, self.slidebuf, self.slidebytes) + self.slide_x = x0 + if self.slidecolor is not None: + self.set_color(self.slidecolor) + tft.fillRectangle(x0, y0, x1, y1) # Draw slider + self.restore_color() + + def value(self, val=None): + if val is None: + return self._value + self._value = min(max(val, 0.0), 1.0) + if self._value != self._old_value: + self._old_value = self._value + self.cb_move(self, self.cbm_args) # Callback not a bound method so pass self + self.show() + + def touched(self, x, y): # If touched in bounding box, process it otherwise do nothing + x0 = self.location[0] + x1 = self.location[0] + self.width + y0 = self.location[1] + y1 = self.location[1] + self.height + if x0 <= x <= x1 and y0 <= y <= y1: + self.was_touched = True + self.value((x - x0) / self.pot_dimension) + + def untouched(self): # User has released touchpad or touched elsewhere + if self.was_touched: + self.cb_end(self, self.cbe_args) # Callback not a bound method so pass self + self.was_touched = False diff --git a/tft_gui/slidetest.py b/tft_gui/slidetest.py index 6f3d579..d84ce84 100644 --- a/tft_gui/slidetest.py +++ b/tft_gui/slidetest.py @@ -1,52 +1,36 @@ +# slidetest.py Demo/test program for vertical slider class for Pyboard TFT GUI + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. -import gc from font10 import font10 from tft import TFT, LANDSCAPE from usched import Sched from touch import TOUCH from slider import Slider from button import Button -from ui import CLIPPED_RECT -import math -#gc.collect() - -class Dial(object): - def __init__(self, objsched, tft, x, y, r, l): - self.objsched = objsched - self.tft = tft - self.xorigin = x - self.yorigin = y - self.radius = r - self.pointerlen = l - self.angle = None - self.delta = 1 - tft.drawCircle(x, y, r) - objsched.add_thread(self.mainthread()) - - def update(self, angle): - tft = self.tft - fgcolor = tft.getColor() # save old colors - bgcolor = tft.getBGColor() - if self.angle is not None: - tft.setColor(bgcolor) - self.drawpointer(self.angle) # erase old - tft.setColor(fgcolor) - self.drawpointer(angle) # draw new - self.angle = angle # update old - tft.setColor(fgcolor) # restore them - tft.setBGColor(bgcolor) - - def drawpointer(self, radians): - x_end = int(self.xorigin + self.pointerlen * math.sin(radians)) - y_end = int(self.yorigin - self.pointerlen * math.cos(radians)) - self.tft.drawLine(self.xorigin, self.yorigin, x_end, y_end) - - def mainthread(self): - while True: - yield 0.1 - angle = self.angle if self.angle is not None else 0 - angle += math.pi * 2 * self.delta / 10 - self.update(angle) +from displays import Dial, Label +from ui import CLIPPED_RECT, WHITE, BLACK, RED, GREEN, BLUE, YELLOW, GREY +from math import pi # CALLBACKS # cb_end occurs when user stops touching the control @@ -54,7 +38,7 @@ def callback(slider, args): print('{} returned {}'.format(args[0], slider.value())) def to_string(val): - return '{:4.1f}ohms'.format(val * 10) + return '{:3.1f} ohms'.format(val * 10) def master_moved(slider, args): val = slider.value() @@ -62,41 +46,70 @@ def master_moved(slider, args): slave1.value(val) slave2 = args[1] slave2.value(val) + label = args[2] + label.show(to_string(val)) # Either slave has had its slider moved (by user or by having value altered) def slave_moved(slider, args): + val = slider.value() dial = args[0] - dial.delta = slider.value() + dial.delta = val + label = args[1] + label.show(to_string(val)) def doquit(button, args): button.objsched.stop() -# USER TEST FUNCTION +# THREADS +def mainthread(slider, dial): + angle = 0 + yield + while True: + yield 0.1 + delta = slider.value() + angle += pi * 2 * delta / 10 + dial.show(angle) + dial.show(angle /10, 1) + +# DATA +# Common args for the labels +labels = { 'width' : 70, + 'fontcolor' : WHITE, + 'border' : 2, + 'fgcolor' : RED, + 'bgcolor' : (0, 40, 0), + } + # '0', '1','2','3','4','5','6','7','8','9','10' # Common arguments for all three sliders -table = {'fontcolor' : (255, 255, 255), +table = {'fontcolor' : WHITE, 'legends' : ('0', '5', '10'), - 'to_string' : to_string, 'cb_end' : callback, - 'value' : 0.5} + } +# 'border' : 2, def test(duration = 0): print('Test TFT panel...') objsched = Sched() # Instantiate the scheduler mytft = TFT("SSD1963", "LB04301", LANDSCAPE) - mytouch = TOUCH("XPT2046", objsched) + mytouch = TOUCH("XPT2046", objsched, confidence=50) mytft.backlight(100) # light on - Button(objsched, mytft, mytouch, (400, 240), font = font10, callback = doquit, fgcolor = (255, 0, 0), + Button(objsched, mytft, mytouch, (400, 240), font = font10, callback = doquit, fgcolor = RED, height = 30, text = 'Quit', shape = CLIPPED_RECT) - dial1 = Dial(objsched, mytft, 350, 60, 50, 48) - dial2 = Dial(objsched, mytft, 350, 170, 50, 48) + dial1 = Dial(mytft, (350, 10), fgcolor = YELLOW, border = 2, pointers = (0.9, 0.7)) + dial2 = Dial(mytft, (350, 120), fgcolor = YELLOW, bgcolor = GREY, border = 2, pointers = (0.9, 0.7)) + lstlbl = [] + for n in range(3): + lstlbl.append(Label(mytft, (80 * n, 240), font = font10, **labels)) y = 5 slave1 = Slider(objsched, mytft, mytouch, (80, y), font10, - fgcolor = (0, 255, 0), cbe_args = ('Slave1',), cb_move = slave_moved, cbm_args = (dial1,), **table) + fgcolor = (0, 255, 0), cbe_args = ('Slave1',), cb_move = slave_moved, cbm_args = (dial1, lstlbl[1]), **table) slave2 = Slider(objsched, mytft, mytouch, (160, y), font10, - fgcolor = (0, 255, 0), cbe_args = ('Slave2',), cb_move = slave_moved, cbm_args = (dial2,), **table) - Slider(objsched, mytft, mytouch, (0, y), font10, - fgcolor = (255, 255, 0), cbe_args = ('Master',), cb_move = master_moved, cbm_args = (slave1, slave2), **table) + fgcolor = (0, 255, 0), cbe_args = ('Slave2',), cb_move = slave_moved, cbm_args = (dial2, lstlbl[2]), **table) + master = Slider(objsched, mytft, mytouch, (0, y), font10, + fgcolor = (255, 255, 0), cbe_args = ('Master',), cb_move = master_moved, cbm_args = (slave1, slave2, lstlbl[0]), value=0.5, **table) + objsched.add_thread(mainthread(slave1, dial1)) + objsched.add_thread(mainthread(slave2, dial2)) objsched.run() # Run it! test() diff --git a/tft_gui/ui.py b/tft_gui/ui.py index 6eb310a..3f0a12a 100644 --- a/tft_gui/ui.py +++ b/tft_gui/ui.py @@ -1,8 +1,37 @@ -# ui.py Base classes and utilities for TFT GUI +# ui.py Constants, base classes and utilities for Pybboard TFT GUI + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. CIRCLE = 1 RECTANGLE = 2 CLIPPED_RECT = 3 +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +RED = (255, 0, 0) +GREEN = (0, 255, 0) +BLUE = (0, 0, 255) +YELLOW = (255, 255, 0) +GREY = (100, 100, 100) def get_stringsize(s, font): hor = 0 @@ -17,8 +46,51 @@ def print_centered(tft, x, y, s, color, font): tft.setTextPos(x - length // 2, y - height // 2) tft.printString(s) +# Base class for all displayable objects +class NoTouch(object): + old_color = None + def __init__(self, tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border): + self.tft = tft + self.location = location + self.font = font + self.height = height + self.width = width + self.fill = bgcolor is not None + self.fgcolor = fgcolor if fgcolor is not None else tft.getColor() + self.bgcolor = bgcolor if bgcolor is not None else tft.getBGColor() + self.fontcolor = fontcolor if fontcolor is not None else tft.getColor() + self.hasborder = border is not None + self.border = 0 if border is None else border + if NoTouch.old_color is None: + NoTouch.old_color = tft.getColor() + if height is not None and width is not None: # beware special cases where height and width not yet known + self.draw_border() + + def draw_border(self): # Draw background and bounding box if required + tft = self.tft + fgcolor = tft.getColor() + x = self.location[0] + y = self.location[1] + if self.fill: + tft.setColor(self.bgcolor) + tft.fillRectangle(x, y, x + self.width, y + self.height) + bw = 0 # border width + if self.hasborder: # Draw a bounding box + bw = self.border + tft.setColor(self.fgcolor) + tft.drawRectangle(x, y, x + self.width, y + self.height) + tft.setColor(fgcolor) + return bw # Actual width (may be 0) + + def set_color(self, color=None): + new = self.fgcolor if color is None else color + self.tft.setColor(new) + + def restore_color(self): # Restore to system default + self.tft.setColor(NoTouch.old_color) + # Base class for touch-enabled classes. -class touchable(object): +class Touchable(NoTouch): touchlist = [] objtouch = None @@ -36,9 +108,11 @@ class touchable(object): for obj in cls.touchlist: obj.untouched() - def __init__(self, objsched, objtouch): - touchable.touchlist.append(self) + def __init__(self, objsched, tft, objtouch, location, font, height, width, fgcolor, bgcolor, fontcolor, border): + super().__init__(tft, location, font, height, width, fgcolor, bgcolor, fontcolor, border) + Touchable.touchlist.append(self) self.enabled = True # Available to user/subclass - if touchable.objtouch is None: # Initialising class and thread - touchable.objtouch = objtouch + self.objsched = objsched + if Touchable.objtouch is None: # Initialising class and thread + Touchable.objtouch = objtouch objsched.add_thread(self.touchtest()) # One thread only