kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
First pass at touch support.
rodzic
17554ccf7c
commit
16b73b7502
178
gui/core/ugui.py
178
gui/core/ugui.py
|
@ -15,6 +15,7 @@ from gui.core.colors import *
|
||||||
from hardware_setup import ssd
|
from hardware_setup import ssd
|
||||||
|
|
||||||
from gui.primitives import Pushbutton
|
from gui.primitives import Pushbutton
|
||||||
|
|
||||||
# Globally available singleton objects
|
# Globally available singleton objects
|
||||||
display = None # Singleton instance
|
display = None # Singleton instance
|
||||||
ssd = None
|
ssd = None
|
||||||
|
@ -26,8 +27,11 @@ __version__ = (0, 1, 6)
|
||||||
# Null function
|
# Null function
|
||||||
dolittle = lambda *_: None
|
dolittle = lambda *_: None
|
||||||
|
|
||||||
|
|
||||||
async def _g():
|
async def _g():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
type_coro = type(_g())
|
type_coro = type(_g())
|
||||||
|
|
||||||
# Navigation destinations
|
# Navigation destinations
|
||||||
|
@ -36,23 +40,25 @@ _NEXT = const(1)
|
||||||
_PREV = const(2)
|
_PREV = const(2)
|
||||||
_LAST = const(3)
|
_LAST = const(3)
|
||||||
|
|
||||||
|
|
||||||
def quiet():
|
def quiet():
|
||||||
global _vb
|
global _vb
|
||||||
_vb = False
|
_vb = False
|
||||||
|
|
||||||
|
|
||||||
# Input abstracts input from 2-5 pushbuttons or 3 buttons + encoder. Handles
|
# Input abstracts input from 2-5 pushbuttons or 3 buttons + encoder. Handles
|
||||||
# transitions between modes (normal, precision, adjustment)
|
# transitions between modes (normal, precision, adjustment)
|
||||||
|
# BTN class instantiates a push button (may be other than a switch).
|
||||||
class Input:
|
class Input:
|
||||||
|
def __init__(self, nxt, sel, prev, incr, decr, encoder, BTN):
|
||||||
def __init__(self, nxt, sel, prev, incr, decr, encoder):
|
|
||||||
self._encoder = encoder # Encoder in use
|
self._encoder = encoder # Encoder in use
|
||||||
self._precision = False # Precision mode
|
self._precision = False # Precision mode
|
||||||
self._adj = False # Adjustment mode
|
self._adj = False # Adjustment mode
|
||||||
# Count buttons
|
# Count buttons
|
||||||
self._nb = sum(1 for x in (nxt, sel, prev, incr, decr) if x is not None)
|
self._nb = sum(1 for x in (nxt, sel, prev, incr, decr) if x is not None)
|
||||||
# Mandatory buttons
|
# Mandatory buttons
|
||||||
self._next = Pushbutton(nxt)
|
self._next = BTN(nxt)
|
||||||
self._sel = Pushbutton(sel, suppress=True)
|
self._sel = BTN(sel, suppress=True)
|
||||||
# Call current screen bound method
|
# Call current screen bound method
|
||||||
self._next.press_func(Screen.ctrl_move, (_NEXT,))
|
self._next.press_func(Screen.ctrl_move, (_NEXT,))
|
||||||
self._sel.release_func(Screen.sel_ctrl)
|
self._sel.release_func(Screen.sel_ctrl)
|
||||||
|
@ -62,22 +68,23 @@ class Input:
|
||||||
self._sel.double_func(self.adj_mode) # Double click toggles adjust
|
self._sel.double_func(self.adj_mode) # Double click toggles adjust
|
||||||
# Optional buttons
|
# Optional buttons
|
||||||
if prev is not None:
|
if prev is not None:
|
||||||
self._prev = Pushbutton(prev)
|
self._prev = BTN(prev)
|
||||||
self._prev.press_func(Screen.ctrl_move, (_PREV,))
|
self._prev.press_func(Screen.ctrl_move, (_PREV,))
|
||||||
if encoder:
|
if encoder:
|
||||||
_vb and print('Using encoder.')
|
_vb and print("Using encoder.")
|
||||||
if incr is None or decr is None:
|
if incr is None or decr is None:
|
||||||
raise ValueError('Must specify pins for encoder.')
|
raise ValueError("Must specify pins for encoder.")
|
||||||
from gui.primitives.encoder import Encoder
|
from gui.primitives.encoder import Encoder
|
||||||
|
|
||||||
self._enc = Encoder(incr, decr, div=encoder, callback=Screen.adjust)
|
self._enc = Encoder(incr, decr, div=encoder, callback=Screen.adjust)
|
||||||
else:
|
else:
|
||||||
_vb and print('Using {:d} switches.'.format(self._nb))
|
_vb and print("Using {:d} switches.".format(self._nb))
|
||||||
# incr and decr methods get the button as an arg.
|
# incr and decr methods get the button as an arg.
|
||||||
if incr is not None:
|
if incr is not None:
|
||||||
sup = Pushbutton(incr)
|
sup = BTN(incr)
|
||||||
sup.press_func(Screen.adjust, (sup, 1))
|
sup.press_func(Screen.adjust, (sup, 1))
|
||||||
if decr is not None:
|
if decr is not None:
|
||||||
sdn = Pushbutton(decr)
|
sdn = BTN(decr)
|
||||||
sdn.press_func(Screen.adjust, (sdn, -1))
|
sdn.press_func(Screen.adjust, (sdn, -1))
|
||||||
|
|
||||||
def precision(self, val): # Also called by Screen.ctrl_move to cancel mode
|
def precision(self, val): # Also called by Screen.ctrl_move to cancel mode
|
||||||
|
@ -111,14 +118,21 @@ class Input:
|
||||||
def is_adjust(self):
|
def is_adjust(self):
|
||||||
return self._adj
|
return self._adj
|
||||||
|
|
||||||
|
|
||||||
# Normal way to populate the global display instance
|
# Normal way to populate the global display instance
|
||||||
def Display(objssd, nxt, sel, prev=None, incr=None, decr=None, encoder=False):
|
def Display(objssd, nxt, sel, prev=None, incr=None, decr=None,
|
||||||
ipdev = Input(nxt, sel, prev, incr, decr, encoder)
|
encoder=False, touch=False):
|
||||||
|
if touch:
|
||||||
|
from gui.primitives import Touchbutton
|
||||||
|
|
||||||
|
ipdev = Input(nxt, sel, prev, incr, decr, encoder, Touchbutton)
|
||||||
|
else:
|
||||||
|
ipdev = Input(nxt, sel, prev, incr, decr, encoder, Pushbutton)
|
||||||
return DisplayIP(objssd, ipdev)
|
return DisplayIP(objssd, ipdev)
|
||||||
|
|
||||||
|
|
||||||
# Wrapper for ssd poviding framebuf compatible methods with abstract input device
|
# Wrapper for ssd poviding framebuf compatible methods with abstract input device
|
||||||
class DisplayIP:
|
class DisplayIP:
|
||||||
|
|
||||||
def __init__(self, objssd, ipdev):
|
def __init__(self, objssd, ipdev):
|
||||||
global display, ssd
|
global display, ssd
|
||||||
self.ipdev = ipdev
|
self.ipdev = ipdev
|
||||||
|
@ -148,8 +162,11 @@ class DisplayIP:
|
||||||
# Greying out has only one option given limitation of 4-bit display driver
|
# Greying out has only one option given limitation of 4-bit display driver
|
||||||
# It would be possible to do better with RGB565 but would need inverse transformation
|
# It would be possible to do better with RGB565 but would need inverse transformation
|
||||||
# to (r, g, b), scale and re-convert to integer.
|
# to (r, g, b), scale and re-convert to integer.
|
||||||
def _getcolor(self, color): # Takes in an integer color, bit size dependent on driver
|
def _getcolor(self, color):
|
||||||
return color_map[GREY_OUT] if self._is_grey and color != color_map[BG] else color
|
# Takes in an integer color, bit size dependent on driver
|
||||||
|
return (
|
||||||
|
color_map[GREY_OUT] if self._is_grey and color != color_map[BG] else color
|
||||||
|
)
|
||||||
|
|
||||||
def usegrey(self, val): # display.usegrey(True) sets greyed-out
|
def usegrey(self, val): # display.usegrey(True) sets greyed-out
|
||||||
self._is_grey = val
|
self._is_grey = val
|
||||||
|
@ -188,12 +205,12 @@ class DisplayIP:
|
||||||
ssd.pixel(x0 + x, y0 - y, color)
|
ssd.pixel(x0 + x, y0 - y, color)
|
||||||
ssd.pixel(x0 - x, y0 - y, color)
|
ssd.pixel(x0 - x, y0 - y, color)
|
||||||
e2 = err
|
e2 = err
|
||||||
if (e2 <= y):
|
if e2 <= y:
|
||||||
y += 1
|
y += 1
|
||||||
err += y * 2 + 1
|
err += y * 2 + 1
|
||||||
if (-x == y and e2 <= x):
|
if -x == y and e2 <= x:
|
||||||
e2 = 0
|
e2 = 0
|
||||||
if (e2 > x):
|
if e2 > x:
|
||||||
x += 1
|
x += 1
|
||||||
err += x * 2 + 1
|
err += x * 2 + 1
|
||||||
|
|
||||||
|
@ -213,12 +230,12 @@ class DisplayIP:
|
||||||
ssd.line(x0 - x, y0 - y, x0 - x, y0 + y, color)
|
ssd.line(x0 - x, y0 - y, x0 - x, y0 + y, color)
|
||||||
ssd.line(x0 + x, y0 - y, x0 + x, y0 + y, color)
|
ssd.line(x0 + x, y0 - y, x0 + x, y0 + y, color)
|
||||||
e2 = err
|
e2 = err
|
||||||
if (e2 <= y):
|
if e2 <= y:
|
||||||
y += 1
|
y += 1
|
||||||
err += y * 2 + 1
|
err += y * 2 + 1
|
||||||
if (-x == y and e2 <= x):
|
if -x == y and e2 <= x:
|
||||||
e2 = 0
|
e2 = 0
|
||||||
if (e2 > x):
|
if e2 > x:
|
||||||
x += 1
|
x += 1
|
||||||
err += x * 2 + 1
|
err += x * 2 + 1
|
||||||
|
|
||||||
|
@ -306,12 +323,12 @@ class Screen:
|
||||||
if forward:
|
if forward:
|
||||||
if isinstance(cls_new_screen, type):
|
if isinstance(cls_new_screen, type):
|
||||||
if isinstance(cs_old, Window):
|
if isinstance(cs_old, Window):
|
||||||
raise ValueError('Windows are modal.')
|
raise ValueError("Windows are modal.")
|
||||||
new_screen = cls_new_screen(*args, **kwargs)
|
new_screen = cls_new_screen(*args, **kwargs)
|
||||||
if not len(new_screen.lstactive):
|
if not len(new_screen.lstactive):
|
||||||
raise ValueError("Screen has no active widgets.")
|
raise ValueError("Screen has no active widgets.")
|
||||||
else:
|
else:
|
||||||
raise ValueError('Must pass Screen class or subclass (not instance)')
|
raise ValueError("Must pass Screen class or subclass (not instance)")
|
||||||
new_screen.parent = cs_old
|
new_screen.parent = cs_old
|
||||||
cs_new = new_screen
|
cs_new = new_screen
|
||||||
else:
|
else:
|
||||||
|
@ -346,7 +363,7 @@ class Screen:
|
||||||
# no factor, do_refresh confers no benefit, so use synchronous code.
|
# no factor, do_refresh confers no benefit, so use synchronous code.
|
||||||
@classmethod
|
@classmethod
|
||||||
async def auto_refresh(cls):
|
async def auto_refresh(cls):
|
||||||
arfsh = hasattr(ssd, 'do_refresh') # Refresh can be asynchronous.
|
arfsh = hasattr(ssd, "do_refresh") # Refresh can be asynchronous.
|
||||||
# By default rfsh_start is permanently set. User code can clear this.
|
# By default rfsh_start is permanently set. User code can clear this.
|
||||||
cls.rfsh_start.set()
|
cls.rfsh_start.set()
|
||||||
if arfsh:
|
if arfsh:
|
||||||
|
@ -378,7 +395,7 @@ class Screen:
|
||||||
def addobject(cls, obj):
|
def addobject(cls, obj):
|
||||||
cs = cls.current_screen
|
cs = cls.current_screen
|
||||||
if cs is None:
|
if cs is None:
|
||||||
raise OSError('You must create a Screen instance')
|
raise OSError("You must create a Screen instance")
|
||||||
# Populate list of active widgets (i.e. ones that can acquire focus).
|
# Populate list of active widgets (i.e. ones that can acquire focus).
|
||||||
if obj.active:
|
if obj.active:
|
||||||
# Append to active list regrdless of disabled state which may
|
# Append to active list regrdless of disabled state which may
|
||||||
|
@ -403,7 +420,9 @@ class Screen:
|
||||||
self.width = ssd.width
|
self.width = ssd.width
|
||||||
self.row = 0
|
self.row = 0
|
||||||
self.col = 0
|
self.col = 0
|
||||||
if Screen.current_screen is None and Screen.do_gc: # Initialising class and task
|
if (
|
||||||
|
Screen.current_screen is None and Screen.do_gc
|
||||||
|
): # Initialising class and task
|
||||||
# Here we create singleton tasks
|
# Here we create singleton tasks
|
||||||
asyncio.create_task(self._garbage_collect())
|
asyncio.create_task(self._garbage_collect())
|
||||||
Screen.current_screen = self
|
Screen.current_screen = self
|
||||||
|
@ -484,7 +503,7 @@ class Screen:
|
||||||
|
|
||||||
def do_adj(self, button, val):
|
def do_adj(self, button, val):
|
||||||
co = self.get_obj()
|
co = self.get_obj()
|
||||||
if co is not None and hasattr(co, 'do_adj'):
|
if co is not None and hasattr(co, "do_adj"):
|
||||||
co.do_adj(button, val) # Widget can handle up/down
|
co.do_adj(button, val) # Widget can handle up/down
|
||||||
else:
|
else:
|
||||||
Screen.current_screen.move(_FIRST if val < 0 else _LAST)
|
Screen.current_screen.move(_FIRST if val < 0 else _LAST)
|
||||||
|
@ -515,9 +534,10 @@ class Screen:
|
||||||
gc.collect()
|
gc.collect()
|
||||||
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
|
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
|
||||||
n += 1
|
n += 1
|
||||||
n &= 0x1f
|
n &= 0x1F
|
||||||
_vb and (not n) and print("Free RAM", gc.mem_free())
|
_vb and (not n) and print("Free RAM", gc.mem_free())
|
||||||
|
|
||||||
|
|
||||||
# Very basic window class. Cuts a rectangular hole in a screen on which
|
# Very basic window class. Cuts a rectangular hole in a screen on which
|
||||||
# content may be drawn.
|
# content may be drawn.
|
||||||
class Window(Screen):
|
class Window(Screen):
|
||||||
|
@ -535,8 +555,18 @@ class Window(Screen):
|
||||||
def close(): # More intuitive name for popup window
|
def close(): # More intuitive name for popup window
|
||||||
Screen.back()
|
Screen.back()
|
||||||
|
|
||||||
def __init__(self, row, col, height, width, *, draw_border=True,
|
def __init__(
|
||||||
bgcolor=None, fgcolor=None, writer=None):
|
self,
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
*,
|
||||||
|
draw_border=True,
|
||||||
|
bgcolor=None,
|
||||||
|
fgcolor=None,
|
||||||
|
writer=None,
|
||||||
|
):
|
||||||
Screen.__init__(self)
|
Screen.__init__(self)
|
||||||
self.row = row
|
self.row = row
|
||||||
self.col = col
|
self.col = col
|
||||||
|
@ -563,12 +593,22 @@ class Window(Screen):
|
||||||
y = self.row
|
y = self.row
|
||||||
return x, y, x + w, y + h, w, h
|
return x, y, x + w, y + h, w, h
|
||||||
|
|
||||||
|
|
||||||
# Base class for all displayable objects
|
# Base class for all displayable objects
|
||||||
class Widget:
|
class Widget:
|
||||||
|
def __init__(
|
||||||
def __init__(self, writer, row, col, height, width,
|
self,
|
||||||
fgcolor, bgcolor, bdcolor,
|
writer,
|
||||||
value=None, active=False):
|
row,
|
||||||
|
col,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
fgcolor,
|
||||||
|
bgcolor,
|
||||||
|
bdcolor,
|
||||||
|
value=None,
|
||||||
|
active=False,
|
||||||
|
):
|
||||||
self.active = active
|
self.active = active
|
||||||
# By default widgets cannot be adjusted: no green border in adjust mode
|
# By default widgets cannot be adjusted: no green border in adjust mode
|
||||||
self.adjustable = False
|
self.adjustable = False
|
||||||
|
@ -623,7 +663,11 @@ class Widget:
|
||||||
self.args = []
|
self.args = []
|
||||||
|
|
||||||
def warning(self):
|
def warning(self):
|
||||||
print('Warning: attempt to create {} outside screen dimensions.'.format(self.__class__.__name__))
|
print(
|
||||||
|
"Warning: attempt to create {} outside screen dimensions.".format(
|
||||||
|
self.__class__.__name__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def value(self, val=None): # User method to get or set value
|
def value(self, val=None): # User method to get or set value
|
||||||
if val is not None:
|
if val is not None:
|
||||||
|
@ -640,10 +684,12 @@ class Widget:
|
||||||
|
|
||||||
# Some widgets (e.g. Dial) have an associated Label
|
# Some widgets (e.g. Dial) have an associated Label
|
||||||
def text(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None):
|
def text(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None):
|
||||||
if hasattr(self, 'label'):
|
if hasattr(self, "label"):
|
||||||
self.label.value(text, invert, fgcolor, bgcolor, bdcolor)
|
self.label.value(text, invert, fgcolor, bgcolor, bdcolor)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Method {}.text does not exist.'.format(self.__class__.__name__))
|
raise ValueError(
|
||||||
|
"Method {}.text does not exist.".format(self.__class__.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
# Called from subclass prior to populating framebuf with control
|
# Called from subclass prior to populating framebuf with control
|
||||||
def show(self, black=True):
|
def show(self, black=True):
|
||||||
|
@ -657,7 +703,9 @@ class Widget:
|
||||||
dev = display.usegrey(self._greyed_out)
|
dev = display.usegrey(self._greyed_out)
|
||||||
x = self.col
|
x = self.col
|
||||||
y = self.row
|
y = self.row
|
||||||
dev.fill_rect(x, y, self.width, self.height, color_map[BG] if black else self.bgcolor)
|
dev.fill_rect(
|
||||||
|
x, y, self.width, self.height, color_map[BG] if black else self.bgcolor
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Called by Screen.show(). Draw background and bounding box if required.
|
# Called by Screen.show(). Draw background and bounding box if required.
|
||||||
|
@ -672,7 +720,11 @@ class Widget:
|
||||||
# print('border', self, display.ipdev.is_adjust())
|
# print('border', self, display.ipdev.is_adjust())
|
||||||
if self.has_focus() and not isinstance(self, DummyWidget):
|
if self.has_focus() and not isinstance(self, DummyWidget):
|
||||||
color = color_map[FOCUS]
|
color = color_map[FOCUS]
|
||||||
precision = hasattr(self, 'do_precision') and self.do_precision and display.ipdev.is_precision()
|
precision = (
|
||||||
|
hasattr(self, "do_precision")
|
||||||
|
and self.do_precision
|
||||||
|
and display.ipdev.is_precision()
|
||||||
|
)
|
||||||
if precision:
|
if precision:
|
||||||
color = self.prcolor
|
color = self.prcolor
|
||||||
elif display.ipdev.is_adjust() and self.adjustable:
|
elif display.ipdev.is_adjust() and self.adjustable:
|
||||||
|
@ -733,20 +785,32 @@ class Widget:
|
||||||
# def do_up(self, button)
|
# def do_up(self, button)
|
||||||
# def do_down(self, button)
|
# def do_down(self, button)
|
||||||
|
|
||||||
|
|
||||||
# A LinearIO widget uses the up and down buttons to vary a float. Such widgets
|
# A LinearIO widget uses the up and down buttons to vary a float. Such widgets
|
||||||
# have do_up and do_down methods which adjust the control's value in a
|
# have do_up and do_down methods which adjust the control's value in a
|
||||||
# time-dependent manner.
|
# time-dependent manner.
|
||||||
class LinearIO(Widget):
|
class LinearIO(Widget):
|
||||||
|
def __init__(
|
||||||
def __init__(self, writer, row, col, height, width,
|
self,
|
||||||
fgcolor, bgcolor, bdcolor,
|
writer,
|
||||||
value=None, active=True, prcolor=False,
|
row,
|
||||||
min_delta=0.01, max_delta=0.1):
|
col,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
fgcolor,
|
||||||
|
bgcolor,
|
||||||
|
bdcolor,
|
||||||
|
value=None,
|
||||||
|
active=True,
|
||||||
|
prcolor=False,
|
||||||
|
min_delta=0.01,
|
||||||
|
max_delta=0.1,
|
||||||
|
):
|
||||||
self.min_delta = min_delta
|
self.min_delta = min_delta
|
||||||
self.max_delta = max_delta
|
self.max_delta = max_delta
|
||||||
super().__init__(writer, row, col, height, width,
|
super().__init__(
|
||||||
fgcolor, bgcolor, bdcolor,
|
writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active
|
||||||
value, active)
|
)
|
||||||
self.adjustable = True # Can show adjustable border
|
self.adjustable = True # Can show adjustable border
|
||||||
self.do_precision = prcolor is not False
|
self.do_precision = prcolor is not False
|
||||||
if self.do_precision:
|
if self.do_precision:
|
||||||
|
@ -761,7 +825,9 @@ class LinearIO(Widget):
|
||||||
|
|
||||||
# Handle increase and decrease buttons. Redefined by textbox.py, scale_log.py
|
# Handle increase and decrease buttons. Redefined by textbox.py, scale_log.py
|
||||||
async def btnhan(self, button, up, d):
|
async def btnhan(self, button, up, d):
|
||||||
maxd = self.max_delta if self.precision() else d * 4 # Why move fast in precision mode?
|
maxd = (
|
||||||
|
self.max_delta if self.precision() else d * 4
|
||||||
|
) # Why move fast in precision mode?
|
||||||
t = ticks_ms()
|
t = ticks_ms()
|
||||||
while button():
|
while button():
|
||||||
await asyncio.sleep_ms(0) # Quit fast on button release
|
await asyncio.sleep_ms(0) # Quit fast on button release
|
||||||
|
@ -774,11 +840,21 @@ class LinearIO(Widget):
|
||||||
def precision(self):
|
def precision(self):
|
||||||
return self.do_precision and display.ipdev.is_precision()
|
return self.do_precision and display.ipdev.is_precision()
|
||||||
|
|
||||||
|
|
||||||
# The dummy enables popup windows by satisfying the need for at least one active
|
# The dummy enables popup windows by satisfying the need for at least one active
|
||||||
# widget on a screen. It is invisible and is drawn by Window constructor before
|
# widget on a screen. It is invisible and is drawn by Window constructor before
|
||||||
# any user labels..
|
# any user labels..
|
||||||
class DummyWidget(Widget):
|
class DummyWidget(Widget):
|
||||||
|
|
||||||
def __init__(self, writer, window):
|
def __init__(self, writer, window):
|
||||||
super().__init__(writer, window.row + 1, window.col + 1, 4, 4,
|
super().__init__(
|
||||||
window.fgcolor, window.bgcolor, False, None, True)
|
writer,
|
||||||
|
window.row + 1,
|
||||||
|
window.col + 1,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
window.fgcolor,
|
||||||
|
window.bgcolor,
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
|
@ -6,6 +6,7 @@ _attrs = {
|
||||||
"Delay_ms": "delay_ms",
|
"Delay_ms": "delay_ms",
|
||||||
"Switch": "switch",
|
"Switch": "switch",
|
||||||
"Pushbutton": "pushbutton",
|
"Pushbutton": "pushbutton",
|
||||||
|
"Touchbutton": "touchbutton",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Lazy loader, effectively does:
|
# Lazy loader, effectively does:
|
||||||
|
|
Ładowanie…
Reference in New Issue