Module Python-Screen-Stack-Manager.pssm
Expand source code
#!/usr/bin/env python
import os
import json
import threading
# Load Pillow
from PIL import Image, ImageDraw, ImageFont, ImageOps
from copy import deepcopy
# Load some useful functions
from tools import returnFalse, coordsInArea, insertStr, getPartialEltImg, \
getRectanglesIntersection, tools_convertXArgsToPX, tools_convertYArgsToPX,\
tools_parseKnownImageFile, tools_parseKnownFonts, get_Color, debug, timer
# ########################## - VARIABLES - ####################################
lastUsedId = 0
# GENERAL
PATH_TO_PSSM = os.path.dirname(os.path.abspath(__file__))
DEFAULT_FONT = "default"
DEFAULT_FONTBOLD = "default-Bold"
DEFAULT_FONT_SIZE = "H*0.036"
CURSOR_CHAR = "|"
DEFAULT_INVERT_DURATION = 0.2
# OSK CONSTANTS
DEFAULT_KEYMAP_PATH_STANDARD = os.path.join(PATH_TO_PSSM,
"config",
"default-keymap-en_us.json")
DEFAULT_KEYMAP_PATH_CAPS = os.path.join(PATH_TO_PSSM,
"config",
"default-keymap-en_us_CAPS.json")
DEFAULT_KEYMAP_PATH_ALT = os.path.join(PATH_TO_PSSM,
"config",
"default-keymap-en_us_ALT.json")
DEFAULT_KEYMAP_PATH = {
'standard': DEFAULT_KEYMAP_PATH_STANDARD,
'caps': DEFAULT_KEYMAP_PATH_CAPS,
'alt': DEFAULT_KEYMAP_PATH_ALT
}
KTstandardChar = 0
KTcarriageReturn = 1
KTbackspace = 2
KTdelete = 3
KTcapsLock = 4
KTcontrol = 5
KTalt = 6
# ########################## - StackManager - ##############################
class PSSMScreen:
"""
This is the class which handles most of the logic.
Args:
deviceName (str): "Kobo" for Kobo ereaders
(and probably all FBInk supported devices)
name (str): The name of the class instance
(deprecated, I will eventually remove it)
stack (list): Do not use it unless you know what you are doing.
The list of all the pssm Elements which are on the screen.
isInverted (bool): ...
Attributes:
device: the device.py module.
You can with it access a few useful functions like
`self.device.readBatteryPercentage()`
colorType: "L" for grayscale devices, "RGBA" for others
width: The screen width
height : The screen height
view_width: The width of the screen portion not hidden behind bezels
view_height: ...
name: ...
isInverted: ...
Example of usage:
screen = pssm.PSSMScreen("Kobo","Main"))
"""
def __init__(self, deviceName, name="screen", stack=[], isInverted=False):
self.name = name
if deviceName == "Kobo":
import devices.kobo.device as pssm_device
else:
import devices.emulator.device as pssm_device
self.device = pssm_device
self.colorType = self.device.colorType
self.width = self.device.screen_width
self.height = self.device.screen_height
self.view_width = self.device.view_width
self.view_height = self.device.view_height
self.w_offset = self.device.w_offset
self.h_offset = self.device.h_offset
self.area = [(0, 0), (self.view_width, self.view_height)]
self.stack = stack
self.isInverted = isInverted
self.isInputThreadStarted = False
self.lastX = -1
self.lastY = -1
self.osk = None
self.numberEltOnTop = 0
self.isOSKShown = False
self.isBatch = False
def findEltWithId(self, myElementId, stack=None):
"""
(Deprecated)
Returns the element which has such an ID.
Avoid using this function as much as possible:
it has terrible performance.
(Recursive search through all the elements of the stack)
And anyway there is no reason why you should use it.
"""
if stack is None:
stack = self.stack
for elt in stack:
if elt.id == myElementId:
return elt
elif elt.isLayout:
layoutEltList = elt.createEltList()
search = self.findEltWithId(myElementId, stack=layoutEltList)
if search is not None:
return search
return None
def printStack(self, area=None, forceLayoutGen=False):
"""
Prints the stack Elements in the stack order
If a area is set, then, we only display
the part of the stack which is in this area
"""
if self.isBatch:
# Do not do anything during batch mode
return None
pil_image = self.capture(area=area, forceLayoutGen=forceLayoutGen)
if area:
[(x, y), (w, h)] = area
else:
[(x, y), (w, h)] = self.area
self.device.print_pil(pil_image, x, y, isInverted=self.isInverted)
def capture(self,area=None, forceLayoutGen=False):
"""
Returns a screen capture of the current stack state.
"""
white = get_Color("white", self.colorType)
dim = (self.width, self.height)
img = Image.new(self.colorType, dim, color=white)
for elt in self.stack:
[(x, y), (w, h)] = elt.area
if elt.isLayout and forceLayoutGen:
elt.generator(area=elt.area, skipNonLayoutGen=True)
if elt.isInverted:
pil_image = ImageOps.invert(elt.imgData)
else:
pil_image = elt.imgData
img.paste(pil_image, (x, y))
if area:
[(x, y), (w, h)] = area
box = (x, y, x+w, y+h)
return img.crop(box=box)
else:
return img
def simplePrintElt(self, myElement, skipGen=False):
"""
Prints the Element without adding it to the stack.
Does not honor isBatch (you can simplePrint even during batch mode)
Args:
myElement (PSSM Element): The element you want to display
skipGen (bool): Do you want to regenerate the image?
"""
if not skipGen:
# First, the element must be generated
myElement.generator()
# Then, we print it
[(x, y), (w, h)] = myElement.area
# What follows is a Workaround :
self.device.print_pil(
myElement.imgData,
x, y,
isInverted=myElement.isInverted
)
def startBatchWriting(self):
"""
Toggle batch writing: nothing will be displayed on the screen until
you use screen.stopBatchWriting()
"""
self.isBatch = True
def stopBatchWriting(self):
"""
Updates the screen after batch writing
"""
self.isBatch = False
self.printStack(area=self.area, forceLayoutGen=True)
def addElt(self, myElement, skipPrint=False, skipRegistration=False):
"""
Adds Element to the stack and prints it
myElement (PSSM Element): The Element you want to add
skipPrint (bool): True if you don't want to update the screen
skipRegistration (bool): True if you don't want to add the Element
to the stack
"""
for i in range(len(self.stack)):
elt = self.stack[i]
if elt.id == myElement.id:
# There is already an Element in the stack with the same ID.
# Let's update the Element in the stack
if not skipPrint:
self.stack[i] = myElement
self.printStack(area=myElement.area)
break # The Element is already in the stack
else:
# the Element is not already in the stack
if not skipRegistration:
# We append the element to the stack
myElement.parentPSSMScreen = self
if self.numberEltOnTop > 0:
# There is something on top, addnig it at position -2
# (before the last one)
pos = - 1 - self.numberEltOnTop
self.stack.insert(pos, myElement)
else:
self.stack.append(myElement)
if not skipPrint :
if self.numberEltOnTop > 0 and not self.forcePrintOnTop:
# TODO : make it faster, we only need to display the
# image behind the keyboard, not reprint everything
myElement.generator()
self.printStack(area=myElement.area)
else:
# No keyboard on the horizon, let's do it
if self.isBatch:
# Then we only generate
myElement.generator()
else:
myElement.generator()
self.simplePrintElt(myElement, skipGen=True)
def removeElt(self, elt=None, eltid=None, skipPrint=False):
"""
Removes the Element from the stack and hides it from the screen
"""
if elt:
self.stack.remove(elt)
if not skipPrint:
self.printStack(area=elt.area)
elif eltid:
elt = self.findEltWithId(eltid)
if elt:
self.stack.remove(elt)
if not skipPrint:
self.printStack(area=elt.area)
else:
print('No element given')
def getStackLevel(self, myElementId):
elt = self.findEltWithId(myElementId)
return self.stack.index(elt)
def setStackLevel(self, elt, stackLevel="last"):
"""
Set the position of said Element
Then prints every Element above it (including itself)
"""
# TODO : Must be able to accept another stackLevel
if stackLevel == "last" or stackLevel == -1:
stackLevel = len(self.stack)
self.removeElt(elt, skipPrint=True)
self.stack.insert(stackLevel, elt)
self.printStack(area=[elt.xy, elt.xy2])
return True
def invertElt(self, elt, invertDuration=-1,
useFastPrint=True, skipPrint=False):
"""
Inverts an Element
Args:
elt (Element): The PSSM Element to invert
invertDuration (int) : -1 or 0 if permanent, else an integer
skipPrint (bool): Save only or save + print?
useFastPrint (bool): Use FBInk's partial refresh with nightmode
(much faster) instead of printing the whole stack.
"""
if elt is None:
print("No element given")
return False
# First, let's get the Element's initial inverted state
Element_initial_state = bool(elt.isInverted)
elt.isInverted = not Element_initial_state
if not skipPrint:
if useFastPrint:
# Run as thread to make things a bit faster
args = [
elt.area,
invertDuration,
not Element_initial_state
]
invertThread = threading.Thread(
target=self._invertArea_helper,
args=args
)
invertThread.start()
# self._invertArea_helper(elt.area, invertDuration, True)
else:
elt.update()
elt.isInverted = Element_initial_state
def _invertArea_helper(self, area, invertDuration, isInverted=False):
"""
Helper function to properly setup the timer.
"""
# TODO: To be tested
initial_mode = isInverted
isTemporaryinvertion = bool(invertDuration > 0)
self.device.do_screen_refresh(
isInverted=isInverted,
area=area,
isInvertionPermanent=False,
isFlashing=False,
useFastInvertion=True
)
if isTemporaryinvertion:
# Now we call this funcion, without starting a timer
# And the screen is now in an opposite state as the initial one
myTimer = threading.Timer(
interval=invertDuration,
function=self._invertArea_helper,
args=[area, -1, not initial_mode]
)
myTimer.start()
return True
def invert(self):
"""
Inverts the whole screen
"""
self.isInverted = not self.isInverted
self.device.do_screen_refresh(self.isInverted)
return True
def refresh(self):
"""
Refreshes the screeen
"""
self.device.do_screen_refresh()
return True
def clear(self):
"""
Clears the screen
"""
self.device.do_screen_clear()
return True
def OSKInit(self, onKeyPress=None, area=None, keymapPath=None):
if not area:
x = 0
y = int(2*self.view_height/3)
w = self.view_width
h = int(self.view_height/3)
area = [(x, y), (w, h)]
self.osk = OSK(onKeyPress=onKeyPress, area=area, keymapPath=keymapPath)
def OSKShow(self, onKeyPress=None):
if not self.osk:
print("OSK not initialized, it can't be shown")
return None
if onKeyPress:
self.osk.onKeyPress = onKeyPress
self.addElt(self.osk) # It has already been generated
self.numberEltOnTop += 1
self.isOSKShown = True
def OSKHide(self):
if self.isOSKShown:
self.removeElt(elt=self.osk)
self.numberEltOnTop -= 1
self.isOSKShown = False
def startListenerThread(self, grabInput=False):
"""
Starts the touch listener as a separate thread
Args:
grabInput (boolean): Do an EVIOCGRAB IOCTL call to prevent
any other software from registering touch events
"""
self.isInputThreadStarted = True
self.device.isInputThreadStarted = True
print("[PSSM - Touch handler] : Input thread started")
args = [self._clickHandler, True, grabInput]
inputThread = threading.Thread(
target=self.device.eventBindings,
args=args
)
inputThread.start()
def _clickHandler(self, x, y):
n = len(self.stack)
for i in range(n):
j = n-1-i # We go through the stack in descending order
elt = self.stack[j]
if elt.area is None:
# An object without area, it should not happen, but if it does,
# it can be skipped
continue
if coordsInArea(x, y, elt.area):
if elt.onclickInside is not None:
self.lastX = x
self.lastY = y
if elt is not None:
self._dispatchClickToElt((x, y), elt)
break
def _dispatchClickToElt(self, coords, elt):
"""
Once given an object on which the user clicked, this function calls the
appropriate function on the object
(ie elt.onclickInside or elt._dispatchClick)
It also handles invertion.
"""
if elt.isLayout:
if elt.onclickInside is not None:
elt.onclickInside(elt, coords)
if elt.invertOnClick:
self.invertElt(elt, elt.invertDuration)
elt._dispatchClick(coords)
else:
if elt.invertOnClick:
self.invertElt(elt, elt.invertDuration)
# Execute PSSM action on click
elt.pssmOnClickInside(coords)
# Execute user action attached to it too
elt.onclickInside(elt, coords)
def stopListenerThread(self):
self.isInputThreadStarted = False
self.device.isInputThreadStarted = False
print("[PSSM - Touch handler] : Input thread stopped")
# ########################## - Core Element - ##############################
class Element:
"""
Everything which is going to be displayed on the screen is an Element.
Args:
isInverted (bool): Is the element inverted
data (dict, or any): A parameter for you to store whatever you want
area (list): a [(x, y), (w, h)] list. If used in a Layout, the layout
will take care of calculating the area.
imgData (PILImage): the PIL image of the object (None by default, the
generator function takes care of generating one)
onclickInside (function): A function to be executed when the user
clicks on the Element
invertOnClick (bool): Invert the element when a click is registered ?
invertDuration (int): Duration in seconds of the element invertion
after a click is registered (use 0 for infinite)
forcePrintOnTop (bool): Force the element to be printed on top of the
stack, even if there is an on-screen keyboard or a popup
"""
def __init__(self, area=None, imgData=None, onclickInside=returnFalse,
isInverted=False, data={}, invertOnClick=False,
invertDuration=DEFAULT_INVERT_DURATION, forcePrintOnTop=False
):
global lastUsedId
self.id = lastUsedId
lastUsedId += 1
self.isLayout = False
self.imgData = imgData
self.area = area
self.onclickInside = onclickInside
self.isInverted = isInverted
self.user_data = data
self.invertOnClick = invertOnClick
self.invertDuration = invertDuration
self.forcePrintOnTop = forcePrintOnTop
self.parentLayouts = []
self.parentPSSMScreen = None
def __hash__(self):
return hash(self.id)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.id == other.id
return NotImplemented
def update(self, newAttributes={}, skipGen=False, skipPrint=False,
reprintOnTop=False):
"""
Pass a dict as argument, and it will update the Element's attributes
accordingly (both its attribute and then the screen).
Note:
Updating an element can be very slow ! It depends on every specific
cases, but know there are a few ways to make it faster:
- Use `screen.startBatchWriting()` and `screen.stopBatchWriting()`
- If you know this specific element is on top of the screen, use:
`elt.update(newAttributes=myDict, reprintOnTop=True)`
Which will do the same, except it won't rebuild the whole stack
image, it will just print this object on top. (On my tests, I
could spare up to 0.5s !)
Args :
newAttributes (dict): The element's new attributes
skipGen (bool): Just update the element's attribute, but do
not do any generation or printing
reprintOnTop (bool): Do not reprint the whole stack, but do print
this element on top of the screen. (much faster when possible)
skipPrint (bool): Do not update the screen, but do regenerate.
"""
# First, we set the attributes
for param in newAttributes:
setattr(self, param, newAttributes[param])
if not skipGen:
# we recreate the pillow image of this particular object
self.generator()
if (not skipPrint) and (not skipGen): # No need to update if no regen
isBatch = self.parentPSSMScreen.isBatch
if reprintOnTop:
self.parentPSSMScreen.simplePrintElt(self)
elif not isBatch:
hasParent = len(self.parentLayouts) > 0
# We don't want unncesseray generation when printing batch
if hasParent:
# We recreate the pillow image of the oldest parent
# And it is not needed to regenerate standard objects, since
oldest_parent = self.parentLayouts[0]
oldest_parent.generator(skipNonLayoutGen=True)
# Then, let's reprint the stack
self.parentPSSMScreen.printStack(area=self.area)
return True
def generator(self):
"""
The generator is the function which is called when the container layout
wants to build an image. It therefore returns a pillow image.
"""
return NotImplemented
def convertDimension(self, dimension):
"""
Converts the user dimension input (like "h*0.1") to to proper integer
amount of pixels.
Basically, you give it a string. And it will change a few characters to
their corresponding value, then return the evaluated string.
Examples:
I HIGHLY recommend doing only simple operation, like "H*0.1", or
"W/10", always starting with the corresponding variable.
But you can if you want do more complicated things:
elt.convertDimension("H+W")
-> screen_height + screen_width
elt.convertDimension("p*300+max(w, h)")
-> 300 + max(element_width, screen_height)
Note:
When using question mark dimension (like "?*2"), the question mark
MUST be at the beginning of the string
"""
if isinstance(dimension, int):
return dimension
elif isinstance(dimension, str):
nd = ""
W = self.parentPSSMScreen.width
H = self.parentPSSMScreen.height
if self.area:
(x, y), (w, h) = self.area
else:
# area not defined. Instead of being stuck, let's assume the
# screen height and width are a decent alternative
w, h = W, H
for c in dimension:
if c == 'p' or c == 'P':
nd += '1'
elif c == 'W': # screen width
nd += str(W)
elif c == 'H': # screen height
nd += str(H)
elif c == 'w': # element width
nd += str(w)
elif c == 'h': # element height
nd += str(h)
else: # A standard character
nd += c
if dimension[0] == '?':
# We return the string, another function will take care of
# evaluating it
return nd
else:
return int(eval(nd)) # Then we can evaluate the input
else:
print("[PSSM] Could not parse the dimension")
return dimension
def pssmOnClickInside(self, coords=None):
"""
Each Element can also have a pssm function implemented on click.
By default, it does nothing.
"""
return None
# ########################## - Layout Elements - ##############################
class Layout(Element):
"""
A layout is a quite general kind of Element :
If must be given the working area, and a layout, and will generate every
element of the layout
Args:
layout (list): The given layout (see example below). It is basically a
list of rows. Each row is a list containing : the height of the row,
then as many tuples as you want, each tuple being a
(pssm.Element, width) instance
background_color
area
... all other arguments from the pssm.Element class
Example of usage:
See [examples](examples/index.html)
"""
def __init__(self, layout, area=None, background_color="white", **kwargs):
super().__init__()
self.area = area
self.layout = layout
self.isValid = self.isLayoutValid()
self.background_color = background_color
self.areaMatrix = None
self.imgMatrix = None
self.borders = None
self.isLayout = True
for param in kwargs:
setattr(self, param, kwargs[param])
def isLayoutValid(self):
# TODO : to be tested
layout = self.layout
if not isinstance(layout, list):
raise Exception("Layout Element is supposed to be a list")
for row in layout:
if not isinstance(row, list):
raise Exception("A layout row is supposed to be a list")
elif len(row) == 0:
raise Exception("A layout row cannot be empty")
elif not isinstance(row[0], str) and not isinstance(row[0], int):
raise Exception(
"The first element of a row (its height) should be a " +
"string or an integer"
)
for j in range(1, len(row)):
eltTuple = row[j]
isTuple = isinstance(eltTuple, tuple)
isList = isinstance(eltTuple, list)
if not (isTuple or isList):
raise Exception(
"A layout row should be a list of Tuple " +
"(except for its first element)"
)
if len(eltTuple) != 2:
raise Exception(
"A layout element should be a Tuple : " +
"(Element, elementWidth)"
)
isStr = isinstance(eltTuple[1], str)
isInt = isinstance(eltTuple[1], int)
if not (isInt or isStr):
raise Exception(
"An element width should be a string or an integer"
)
isElement = isinstance(eltTuple[0], Element)
if not (isElement or eltTuple[0] is None):
raise Exception(
"A layout element should be a Tuple : " +
"(Element, elementWidth), with Element designating " +
" a PSSM Element"
)
return True
def generator(self, area=None, skipNonLayoutGen=False):
"""
Builds one img out of all the Elements it is being given
"""
if area is not None:
self.area = area
self.createAreaMatrix()
self.createImgMatrix(skipNonLayoutGen=skipNonLayoutGen)
[(x, y), (w, h)] = self.area
colorType = self.parentPSSMScreen.colorType
color = get_Color(self.background_color, colorType)
placeholder = Image.new(colorType, (w, h), color=color)
for i in range(len(self.areaMatrix)):
for j in range(len(self.areaMatrix[i])):
[(elt_x, elt_y), (elt_w, elt_h)] = self.areaMatrix[i][j]
relative_x = elt_x - x
relative_y = elt_y - y
elt_img = self.imgMatrix[i][j]
if elt_img is not None:
pos = (relative_x, relative_y)
placeholder.paste(self.imgMatrix[i][j], pos)
self.imgData = placeholder
return self.imgData
def createImgMatrix(self, skipNonLayoutGen=False):
matrix = []
if not self.areaMatrix:
print("[PSSM Layout] Error, areaMatrix has to be defined first")
return None
for i in range(len(self.layout)):
row = []
for j in range(1, len(self.layout[i])):
elt, _ = self.layout[i][j]
if elt is None:
elt_area = self.areaMatrix[i][j-1]
elt_img = None
else:
elt_area = self.areaMatrix[i][j-1]
if not elt.isLayout and skipNonLayoutGen:
elt_img = elt.imgData
else:
elt_img = elt.generator(area=elt_area)
row.append(elt_img)
matrix.append(row)
self.imgMatrix = matrix
def createAreaMatrix(self):
# TODO : must honor min and max
matrix = []
n_rows = len(self.layout)
[(x, y), (w, h)] = self.area[:]
x0, y0 = x, y
for i in range(n_rows): # Lets loop through the rows
row = self.layout[i]
row_cols = [] # All the columns of this particular row
row_height = row[0]
converted_height = self.convertDimension(row_height)
if isinstance(converted_height, int):
true_row_height = converted_height
else:
remaining_height = self.calculate_remainingHeight()
dim = str(remaining_height) + converted_height[1:]
true_row_height = int(eval(dim))
for j in range(1, len(row)):
(element, element_width) = row[j]
converted_width = self.convertDimension(element_width)
if element is not None:
for parent in self.parentLayouts:
self.layout[i][j][0].parentLayouts.append(parent)
self.layout[i][j][0].parentLayouts.append(self)
self.layout[i][j][0].parentPSSMScreen = \
self.parentPSSMScreen
if isinstance(converted_width, int):
true_elt_width = converted_width
else:
remaining_width = self.calculate_remainingWidth(i)
dim = str(remaining_width) + converted_width[1:]
true_elt_width = int(eval(dim))
self.layout[i][j] = (self.layout[i][j][0], true_elt_width)
element_area = [(x0, y0), (true_elt_width, true_row_height)]
x0 += true_elt_width
row_cols.append(element_area)
y0 += true_row_height
x0 = x
matrix.append(row_cols)
self.areaMatrix = matrix
def createEltList(self):
"""
Returns a list of all the elements the Layout Element contains
"""
eltList = []
for row in self.layout:
for i in range(1, len(row)):
elt, _ = row[i]
if elt is not None:
eltList.append(elt)
return eltList
def calculate_remainingHeight(self):
rows = self.extract_rowsHeight()
total_questionMarks_weight = 0
total_height = 0
for dimension in rows:
converted_dimension = self.convertDimension(dimension)
if isinstance(converted_dimension, int):
total_height += converted_dimension
else:
weight = eval("1" + converted_dimension[1:])
total_questionMarks_weight += weight
layout_height = self.area[1][1]
return int((layout_height - total_height)/total_questionMarks_weight)
def calculate_remainingWidth(self, rowIndex):
cols = self.extract_colsWidth(rowIndex)
total_width = 0
total_questionMarks_weight = 0
for dimension in cols:
converted_dimension = self.convertDimension(dimension)
if isinstance(converted_dimension, int):
total_width += converted_dimension
else:
weight = eval("1" + converted_dimension[1:])
total_questionMarks_weight += weight
layout_width = self.area[1][0]
return int((layout_width - total_width)/total_questionMarks_weight)
def extract_rowsHeight(self):
rows = []
for row in self.layout:
rows.append(row[0])
return rows
def extract_colsWidth(self, rowIndex):
cols = []
for col in self.layout[rowIndex]:
if isinstance(col, tuple):
cols.append(col[1])
return cols
def _dispatchClick(self, coords):
"""
Finds the element on which the user clicked
"""
self._dispatchClick_LINEAR(coords)
def _dispatchClick_LINEAR(self, coords):
"""
Linear search throuh both the rows and the columns
"""
click_x, click_y = coords
# Linear search though the rows
for i in range(len(self.areaMatrix)):
if len(self.areaMatrix[i]) == 0:
# That's a fake row (a margin row)
continue
first_row_elt = self.areaMatrix[i][0]
last_row_elt = self.areaMatrix[i][-1]
x = first_row_elt[0][0]
y = first_row_elt[0][1]
w = last_row_elt[0][0] + last_row_elt[1][0] - first_row_elt[0][0]
h = last_row_elt[0][1] + last_row_elt[1][1] - first_row_elt[0][1]
if coordsInArea(click_x, click_y, [(x, y), (w, h)]):
# CLick was in that row
for j in range(len(self.areaMatrix[i])):
# Linear search through the columns
if coordsInArea(click_x, click_y, self.areaMatrix[i][j]):
# Click was on that element
elt, _ = self.layout[i][j+1]
if elt is not None and elt.onclickInside is not None:
self.parentPSSMScreen._dispatchClickToElt(
coords, elt
)
return True
return False
def _dispatchClick_DICHOTOMY_colsOnly(self, coords):
"""
Linear search through the rows, dichotomy for the columns
(Because of the empty rows, a dichotomy for the rows doesn't work)
NEEDS TO BE FIXED TOO (example : two buttons in a row)
"""
click_x, click_y = coords
row_A = -1
for i in range(len(self.areaMatrix)):
# Linear search though the rows
if len(self.areaMatrix[i]) == 0:
# That's a fake row (a margin row)
continue
first_row_elt = self.areaMatrix[i][0]
last_row_elt = self.areaMatrix[i][-1]
x = first_row_elt[0][0]
y = first_row_elt[0][1]
w = last_row_elt[0][0] + last_row_elt[1][0] - first_row_elt[0][0]
h = last_row_elt[0][1] + last_row_elt[1][1] - first_row_elt[0][1]
if coordsInArea(click_x, click_y, [(x, y), (w, h)]):
# CLick was in that row
row_A = i
break
if row_A == -1:
return None
col_A = 0
col_C = max(len(self.areaMatrix[row_A]) - 1, 0)
xA = self.areaMatrix[row_A][col_A][0][0]
xC = self.areaMatrix[row_A][col_C][0][0]
if click_x < xA:
return None
if click_x > xC + self.areaMatrix[row_A][col_C][1][0]:
return None
while col_C > col_A + 1:
col_B = int(0.5*(col_A+col_C)) # The average of the two
xB = self.areaMatrix[row_A][col_B][0][0]
if click_x >= xB or col_B == col_C:
col_A = col_B
xA = xB
else:
col_C = col_B
xC = xB
# Element is at indexes row_A, col_A
elt, _ = self.layout[row_A][col_A+1]
if elt is not None and elt.onclickInside is not None:
self.parentPSSMScreen._dispatchClickToElt(coords, elt)
return True
def _dispatchClick_DICHOTOMY_Full_ToBeFixed(self, coords):
"""
Finds the element on which the user clicked
Implemented with dichotomy search (with the hope of making things
faster, especially the integrated keyboard)
"""
# TODO : To be fixed
# For now it does not work, because there are empty rows which
# break the loop
click_x, click_y = coords
row_A = 0
row_C = max(len(self.areaMatrix) - 1, 0)
print(self.areaMatrix[row_C])
while len(self.areaMatrix[row_A]) == 0:
row_A += 1
while len(self.areaMatrix[row_C]) == 0:
row_C -= 1
# First column THEN first row , [(x, y), (w, h)] THUS first tuple of
# list THEN second coordinate of tuple
yA = self.areaMatrix[row_A][0][0][1]
yC = self.areaMatrix[row_C][0][0][1]
if click_y < yA:
return None
if click_y > yC + self.areaMatrix[row_C][0][1][1]:
return None
while row_C > row_A+1:
row_B = int(0.5*(row_A+row_C)) # The average of the two
while len(self.areaMatrix[row_B]) == 0:
row_B += 1
yB = self.areaMatrix[row_B][0][0][1]
if click_y >= yB or row_B == row_C:
row_A = row_B
yA = yB
else:
row_C = row_B
yC = yB
# User clicked on element ar row of index row_A
# Let's do the same for the column
col_A = 0
col_C = max(len(self.areaMatrix[row_A]) - 1, 0)
xA = self.areaMatrix[row_A][col_A][0][0]
xC = self.areaMatrix[row_A][col_C][0][0]
if click_x < xA:
return None
if click_x > xC + self.areaMatrix[row_A][col_C][1][0]:
return None
while col_C > col_A + 1:
col_B = int(0.5*(col_A+col_C)) # The average of the two
xB = self.areaMatrix[row_A][col_B][0][0]
if click_x >= xB or col_B == col_C:
col_A = col_B
xA = xB
else:
col_C = col_B
xC = xB
# Element is at indexes row_A, col_A
elt, _ = self.layout[row_A-2][col_A+1]
if elt is not None and elt.onclickInside is not None:
self.parentPSSMScreen._dispatchClickToElt(coords, elt)
return True
class ButtonList(Layout):
"""
Generates a Layout with only one item per row, all the same type (buttons)
and same height and width
Args:
button (list): a [{"text":"my text","onclickInside":onclickInside},
someOtherDict, someOtherDict] array. Each dict will contain the
parameters of each button of the button list
borders (list): a [top, bottom,left,right] array
"""
def __init__(self, buttons, margins=[0, 0, 0, 0], spacing=0, **kwargs):
self.buttons = buttons
self.margins = margins
self.spacing = spacing
layout = self.build_layoutFromButtons()
super().__init__(layout)
for param in kwargs:
setattr(self, param, kwargs[param])
def build_layoutFromButtons(self):
# TODO : must honor min_width,max_width etc
[top, bottom, left, right] = self.margins
buttonLayout = [[top-self.spacing]]
for button in self.buttons:
buttonElt = Button(text=button['text'])
for param in button:
setattr(buttonElt, param, button[param])
row_height = "?"
buttonLayout.append([self.spacing])
row = [row_height, (None, left), (buttonElt, "?"), (None, right)]
buttonLayout.append(row)
buttonLayout.append([bottom])
return buttonLayout
class OSK(Layout):
"""
A PSSM Layout element which builds an on-screen keyboard
Args:
keymapPath (str): a path to a PSSMOSK keymap (like the one included)
onKeyPress (function): A callback function. Will be given keyType and
keyChar as argument
"""
def __init__(self, keymapPath=DEFAULT_KEYMAP_PATH, onKeyPress=None,
area=None, **kwargs):
if not keymapPath:
keymapPath = DEFAULT_KEYMAP_PATH
self.keymapPaths = keymapPath
self.keymap = {'standard': None, 'caps': None, 'alt': None}
self.keymap_layouts = {'standard': None, 'caps': None, 'alt': None}
self.keymap_imgs = {'standard': None, 'caps': None, 'alt': None}
with open(self.keymapPaths['standard']) as json_file:
self.keymap['standard'] = json.load(json_file)
with open(self.keymapPaths['caps']) as json_file:
self.keymap['caps'] = json.load(json_file)
with open(self.keymapPaths['alt']) as json_file:
self.keymap['alt'] = json.load(json_file)
self.lang = self.keymap['standard']["lang"]
self.onKeyPress = onKeyPress
for param in kwargs:
setattr(self, param, kwargs[param])
self.view = 'standard'
self.keymap_layouts['standard'] = self.build_layout(
self.keymap['standard'])
self.keymap_layouts['caps'] = self.build_layout(self.keymap['caps'])
self.keymap_layouts['alt'] = self.build_layout(self.keymap['alt'])
# Initialize layout with standard view
self.layout = self.keymap_layouts['standard']
super().__init__(self.layout)
self.area = area
def generator(self, area=None, forceRegenerate=False,
skipNonLayoutGen=False):
"""
This generator is a bit special : we don't want it to regenerate
everything everytime we change view. So we will generate all the views
at once the first time. Then, unless asked to, we will only return the
appropriate image.
"""
isStDefined = self.keymap_imgs['standard']
isCaDefined = self.keymap_imgs['caps']
isAlDefined = self.keymap_imgs['alt']
areAllDefined = isStDefined and isCaDefined and isAlDefined
if forceRegenerate or (not areAllDefined):
print("[PSSM OSK] Regenration started")
# Let's create all the Images
# Standard view is created last, because it is the one which is to
# be displayed
def generateLayout(name):
self.layout = self.keymap_layouts[name]
self.keymap_imgs[name] = super(OSK, self).generator(area=area)
generateLayout("caps")
generateLayout("alt")
generateLayout("standard")
self.imgData = self.keymap_imgs[self.view]
return self.keymap_imgs[self.view]
def build_layout(self, keymap):
oskLayout = []
spacing = keymap["spacing"]
for row in keymap["rows"]:
buttonRow = ["?", (None, spacing)]
for key in row:
label = self.getKeyLabel(key)
color_condition = key["keyType"] != KTstandardChar
background_color = "gray12" if color_condition else "white"
outline_color = "white" if key["isPadding"] else "black"
willChangeLayout = key["keyType"] in [
KTcapsLock, KTalt, KTcarriageReturn
]
invertOnClick = False if willChangeLayout else True
buttonElt = Button(
text=label,
font_size="H*0.02",
background_color=background_color,
outline_color=outline_color,
onclickInside=self.handleKeyPress,
user_data=key,
wrap_textOverflow=False,
invertOnClick=invertOnClick
)
key_width = key["keyWidth"]
buttonRow.append((buttonElt, key_width))
buttonRow.append((None, spacing))
oskLayout.append(buttonRow)
oskLayout.append([spacing])
return oskLayout
def handleKeyPress(self, elt, coords):
keyType = elt.user_data["keyType"]
keyChar = elt.user_data["char"]
if keyType == KTcapsLock:
# In this particular case, we can assume the keyboard will always
# be on top.
# Therefore, no need to print everything
self.view = 'caps' if self.view != 'caps' else 'standard'
self.layout = self.keymap_layouts[self.view]
self.imgData = self.keymap_imgs[self.view]
self.parentPSSMScreen.simplePrintElt(self)
elif keyType == KTalt:
# In this particular case, we can assume the keyboard will always
# be on top
# Therefore, no need to print everything
self.view = 'alt' if self.view != 'alt' else 'standard'
self.layout = self.keymap_layouts[self.view]
self.imgData = self.keymap_imgs[self.view]
self.parentPSSMScreen.simplePrintElt(self)
if self.onKeyPress:
self.onKeyPress(keyType, keyChar)
def getKeyLabel(self, key):
kt = key["keyType"]
if kt == KTstandardChar:
return key["char"]
elif kt == KTalt:
return "ALT"
elif kt == KTbackspace:
return "BACK"
elif kt == KTcapsLock:
return "CAPS"
elif kt == KTcarriageReturn:
return "RET"
elif kt == KTcontrol:
return "CTRL"
elif kt == KTdelete:
return "DEL"
return ""
class Popup(Layout):
"""
A popup to be displayed above everything else, to simple ask a question
Args:
layout (list): The list of PSSMElements to be displayed. cf Layout
width (str): The width of the popup
height (str): The height of the popup
xPos (float): Relative position on the x axis of the center point
yPos (float): Relative position on the y axis of the center point
"""
def __init__(self, layout=[], width="W*0.8", height="H*0.5",
xPos=0.5, yPos=0.3, **kwargs):
super().__init__(layout=layout)
self.width = width
self.height = height
self.xPos = xPos
self.yPos = yPos
for param in kwargs:
setattr(self, param, kwargs[param])
def make_area(self):
w = self.convertDimension(self.width)
h = self.convertDimension(self.height)
x = self.convertDimension("W*" + str(self.xPos)) - int(0.5*w)
y = self.convertDimension("H*" + str(self.yPos)) - int(0.5*h)
self.area = [(x, y), (w, h)]
return self.area
class PoputInput(Popup):
def __init__(self, titleText="", mainText="", confirmText="OK",
titleFont=DEFAULT_FONT, titleFontSize=DEFAULT_FONT_SIZE,
mainFont=DEFAULT_FONT, mainFontSize=DEFAULT_FONT_SIZE,
inputFont=DEFAULT_FONT, inputFontSize=DEFAULT_FONT_SIZE,
confirmFont=DEFAULT_FONT, confirmFontSize=DEFAULT_FONT_SIZE,
titleFontColor="black", mainFontColor="black",
inputFontColor="black", confirmFontColor="black",
mainTextXPos="center", mainTextYPos="center",
isMultiline=False, **kwargs):
super().__init__()
self.titleText = titleText
self.mainText = mainText
self.confirmText = confirmText
self.isMultiline = isMultiline
self.titleFont = titleFont
self.mainFont = mainFont
self.inputFont = inputFont
self.confirmFont = confirmFont
self.titleFontSize = titleFontSize
self.mainFontSize = mainFontSize
self.inputFontSize = inputFontSize
self.confirmFontSize = confirmFontSize
self.titleFontColor = titleFontColor
self.mainFontColor = mainFontColor
self.inputFontColor = inputFontColor
self.confirmFontColor = confirmFontColor
self.mainTextXPos = mainTextXPos
self.mainTextYPos = mainTextYPos
self.userConfirmed = False
self.inputBtn = None
self.okBtn = None
for param in kwargs:
setattr(self, param, kwargs[param])
self.build_layout()
def generator(self,**kwargs):
self.make_area()
super().generator(**kwargs)
def build_layout(self):
titleBtn = Button(
text=self.titleText,
font=self.titleFont,
font_size=self.titleFontSize,
font_color=self.titleFontColor
)
mainBtn = Button(
text=self.mainText,
font=self.mainFont,
font_size=self.mainFontSize,
font_color=self.mainFontColor,
text_xPosition=self.mainTextXPos,
text_yPosition=self.mainTextYPos
)
if self.isMultiline:
onReturn = returnFalse
else:
onReturn = self.toggleConfirmation
inputBtn = Input(
font=self.inputFont,
font_size=self.inputFontSize,
font_color=self.inputFontColor,
isMultiline=self.isMultiline,
onReturn=onReturn
)
okBtn = Button(
text=self.confirmText,
font=self.confirmFont,
font_size=self.confirmFontSize,
font_color=self.confirmFontColor,
onclickInside=self.toggleConfirmation
)
self.inputBtn = inputBtn
lM = (None,1)
layout = [
["?*1.5", (titleBtn, "?"), lM],
["?*3", (mainBtn, "?"), lM],
["?*2", (inputBtn, "?"), lM],
["?*1", (okBtn, "?"), lM]
]
self.layout = layout
return layout
def toggleConfirmation(self, elt=None, coords=None):
print("Toggling confirmation")
self.userConfirmed = True
def waitForResponse(self):
while not self.userConfirmed:
self.parentPSSMScreen.device.wait(0.01)
self.parentPSSMScreen.OSKHide()
input = self.inputBtn.getInput()
self.userConfirmed = False # Reset the state
self.parentPSSMScreen.removeElt(self)
return input
class PopupConfirm(Popup):
def __init__(self, titleText="", mainText="", confirmText="OK",
cancelText="Cancel",
titleFont=DEFAULT_FONT, titleFontSize=DEFAULT_FONT_SIZE,
mainFont=DEFAULT_FONT, mainFontSize=DEFAULT_FONT_SIZE,
confirmFont=DEFAULT_FONT, confirmFontSize=DEFAULT_FONT_SIZE,
titleFontColor="black", mainFontColor="black",
confirmFontColor="black",
mainTextXPos="center", mainTextYPos="center",
**kwargs):
super().__init__()
self.titleText = titleText
self.mainText = mainText
self.confirmText = confirmText
self.cancelText = cancelText
self.titleFont = titleFont
self.mainFont = mainFont
self.confirmFont = confirmFont
self.titleFontSize = titleFontSize
self.mainFontSize = mainFontSize
self.confirmFontSize = confirmFontSize
self.titleFontColor = titleFontColor
self.mainFontColor = mainFontColor
self.confirmFontColor = confirmFontColor
self.mainTextXPos = mainTextXPos
self.mainTextYPos = mainTextYPos
self.userAction = 0
self.okBtn = None
self.cancelBtn = None
for param in kwargs:
setattr(self, param, kwargs[param])
self.build_layout()
def generator(self,**kwargs):
self.make_area()
super().generator(**kwargs)
def build_layout(self):
titleBtn = Button(
text=self.titleText,
font=self.titleFont,
font_size=self.titleFontSize,
font_color=self.titleFontColor
)
mainBtn = Button(
text=self.mainText,
font=self.mainFont,
font_size=self.mainFontSize,
font_color=self.mainFontColor,
text_xPosition=self.mainTextXPos,
text_yPosition=self.mainTextYPos
)
okBtn = Button(
text=self.confirmText,
font=self.confirmFont,
font_size=self.confirmFontSize,
font_color=self.confirmFontColor,
onclickInside=self.confirm
)
cancelBtn = Button(
text=self.cancelText,
font=self.confirmFont,
font_size=self.confirmFontSize,
font_color=self.confirmFontColor,
onclickInside=self.cancel
)
lM = (None,1)
layout = [
["?*1.5", (titleBtn, "?"), lM],
["?*3", (mainBtn, "?"), lM],
["?*1", (okBtn, "?"), (cancelBtn, "?"), lM]
]
self.layout = layout
return layout
def confirm(self, elt=None, coords=None):
self.userAction = 1
def cancel(self,elt=None, coords=None):
self.userAction = 2
def waitForResponse(self):
while self.userAction == 0:
self.parentPSSMScreen.device.wait(0.01)
self.parentPSSMScreen.OSKHide()
hasConfirmed = self.userAction == 1
self.userAction = 0 # Reset the state
self.parentPSSMScreen.removeElt(self)
return hasConfirmed
# ########################## - Simple Elements - ##############################
class Rectangle(Element):
"""
A rectangle
Args:
background_color (str): The background color
outline_color (str): The border color
"""
def __init__(self, background_color="white", outline_color="gray3",
parentPSSMScreen=None):
super().__init__()
self.background_color = background_color
self.outline_color = outline_color
self.parentPSSMScreen = parentPSSMScreen
def generator(self, area):
[(x, y), (w, h)] = area
self.area = area
colorType = self.parentPSSMScreen.colorType
img = Image.new(
colorType,
(w, h),
color=get_Color("white", colorType)
)
rect = ImageDraw.Draw(img, colorType)
fill_color = get_Color(self.background_color, colorType)
outline_color = get_Color(self.outline_color, colorType)
rect.rectangle(
[(0, 0), (w-1, h-1)],
fill=fill_color,
outline=outline_color
)
self.imgData = img
return self.imgData
class RectangleRounded(Element):
"""
A rectangle, but with rounded corners
"""
def __init__(self, radius=20, background_color="white",
outline_color="gray3", parentPSSMScreen=None):
super().__init__()
self.radius = radius
self.background_color = background_color
self.outline_color = outline_color
self.parentPSSMScreen = parentPSSMScreen
def generator(self, area):
[(x, y), (w, h)] = area
self.area = area
colorType = self.parentPSSMScreen.colorType
rectangle = Image.new(
colorType,
(w, h),
color=get_Color("white", colorType)
)
draw = ImageDraw.Draw(rectangle)
draw.rectangle(
[(0, 0), (w, h)],
fill=get_Color(self.background_color, colorType),
outline=get_Color(self.outline_color, colorType)
)
draw.line(
[(self.radius, h-1), (w-self.radius, h-1)],
fill=get_Color(self.outline_color, colorType),
width=1
)
draw.line(
[(w-1, self.radius), (w-1, h-self.radius)],
fill=get_Color(self.outline_color, colorType),
width=1
)
corner = roundedCorner(
self.radius,
self.background_color,
self.outline_color,
self.parentPSSMScreen.colorType
)
rectangle.paste(corner, (0, 0))
# Rotate the corner and paste it
rectangle.paste(corner.rotate(90), (0, h - self.radius))
rectangle.paste(corner.rotate(180), (w - self.radius, h - self.radius))
rectangle.paste(corner.rotate(270), (w - self.radius, 0))
self.imgData = rectangle
return self.imgData
class Button(Element):
"""
Basically a rectangle (or rounded rectangle) with text printed on it
Args:
text (str): The main text to be written on it
font (str): Path to a font file (ttf file), or one of PSSM built-in
fonts (e.g. "Merriweather-Bold", "default", "Merriweather-Regular",
...) (see the font folder for the complete list)
font_size (int): The font size
font_color (str): The color of the font : "white", "black", "gray0" to
"gray15" or a (red, green, blue, transparency) tuple
wrap_textOverflow (bool): (True by default) Wrap text in order to avoid
it overflowing. The cuts are made between words.
text_xPosition (str or int): can be left, center, right, or an integer
value, or a pssm string dimension
text_yPosition (str or int): can be left, center, right, or an integer
value, or a pssm string dimension
background_color (str): The background color
outline_color (str): The border color
radius (int): If not 0, then add rounded corners of this radius
"""
def __init__(self, text="", font=DEFAULT_FONT, font_size=DEFAULT_FONT_SIZE,
background_color="white", outline_color="black", radius=0,
font_color="black", text_xPosition="center",
text_yPosition="center", wrap_textOverflow=True, **kwargs):
super().__init__()
self.background_color = background_color
self.outline_color = outline_color
self.text = text
self.font = tools_parseKnownFonts(font)
self.font_size = font_size
self.radius = radius
self.font_color = font_color
self.text_xPosition = text_xPosition
self.text_yPosition = text_yPosition
self.wrap_textOverflow = wrap_textOverflow
for param in kwargs:
setattr(self, param, kwargs[param])
self.loaded_font = None
self.convertedText = None
self.imgDraw = None
def generator(self, area=None):
if area is None:
area = self.area
[(x, y), (w, h)] = area
self.area = area
if not isinstance(self.font_size, int):
self.font_size = self.convertDimension(self.font_size)
if not isinstance(self.font_size, int):
# That's a question mark dimension, or an invalid dimension.
# Rollback to default font size
self.font_size = self.convertDimension(DEFAULT_FONT_SIZE)
loaded_font = ImageFont.truetype(self.font, self.font_size)
self.loaded_font = loaded_font
if self.radius > 0:
rect = RectangleRounded(
radius=self.radius,
background_color=self.background_color,
outline_color=self.outline_color,
parentPSSMScreen=self.parentPSSMScreen
)
else:
rect = Rectangle(
background_color=self.background_color,
outline_color=self.outline_color,
parentPSSMScreen=self.parentPSSMScreen
)
rect_img = rect.generator(self.area)
imgDraw = ImageDraw.Draw(rect_img, self.parentPSSMScreen.colorType)
self.imgDraw = imgDraw
if self.wrap_textOverflow:
myText = self.wrapText(self.text, loaded_font, imgDraw)
else:
myText = self.text
self.convertedText = myText
text_w, text_h = imgDraw.textsize(myText, font=loaded_font)
x = tools_convertXArgsToPX(self.text_xPosition, w, text_w, myElt=self)
y = tools_convertYArgsToPX(self.text_yPosition, h, text_h, myElt=self)
imgDraw.text(
(x, y),
myText,
font=loaded_font,
fill=get_Color(self.font_color, self.parentPSSMScreen.colorType)
)
self.imgData = rect_img
return self.imgData
def wrapText(self, text, loaded_font, imgDraw):
def get_text_width(text):
return imgDraw.textsize(text=text, font=loaded_font)[0]
[(x, y), (max_width, h)] = self.area
text_lines = [
' '.join([w.strip() for w in line.split(' ') if w])
for line in text.split('\n')
if line
]
space_width = get_text_width(" ")
wrapped_lines = []
buf = []
buf_width = 0
for line in text_lines:
for word in line.split(' '):
word_width = get_text_width(word)
expected_width = word_width if not buf else \
buf_width + space_width + word_width
if expected_width <= max_width:
# word fits in line
buf_width = expected_width
buf.append(word)
else:
# word doesn't fit in line
wrapped_lines.append(' '.join(buf))
buf = [word]
buf_width = word_width
if buf:
wrapped_lines.append(' '.join(buf))
buf = []
buf_width = 0
return '\n'.join(wrapped_lines)
class Icon(Element):
"""
An icon, built from an image
Args:
file (str): Path to a file, or one of the integrated image (see the
icon folder for the name of each image). 'reboot' for instance
points to the integrated reboot image.
centered (bool): Center the icon?
"""
def __init__(self, file, centered=True, **kwargs):
super().__init__()
self.file = file
self.centered = centered
self.path_to_file = tools_parseKnownImageFile(self.file)
for param in kwargs:
setattr(self, param, kwargs[param])
def generator(self, area):
self.area = area
[(x, y), (w, h)] = area
colorType = self.parentPSSMScreen.colorType
icon_size = min(area[1][0], area[1][1])
loadedImg = Image.open(self.path_to_file)
convImg = loadedImg.convert(colorType)
iconImg = convImg.resize((icon_size, icon_size))
if not self.centered:
self.imgData = iconImg
return iconImg
else:
img = Image.new(
colorType,
(w+1, h+1),
color=get_Color("white", colorType)
)
x = int(0.5*w-0.5*icon_size)
y = int(0.5*h-0.5*icon_size)
img.paste(iconImg, (x, y))
self.imgData = img
return img
class Static(Element):
"""
A very simple element which only displays a pillow image
Args:
pil_image (str or pil image): path to an image or a pillow image
centered (bool): Center the image ?
resize (bool): Make it fit the area ? (proportions are respected)
rotation (int): an integer rotation angle
background_color (str): "white", "black", "gray0" to "gray15" or a
(red, green, blue, transparency) tuple
"""
def __init__(self, pil_image, centered=True, resize=True,
background_color="white", rotation=0, **kwargs):
super().__init__()
if isinstance(pil_image, str):
self.pil_image = Image.open(pil_image)
else:
self.pil_image = pil_image
self.background_color = background_color
self.centered = centered
self.resize = resize
self.rotation = rotation
for param in kwargs:
setattr(self, param, kwargs[param])
def generator(self, area=None):
# TODO : crop or resize the image to make it fit the area
(x, y), (w, h) = area
colorType = self.parentPSSMScreen.colorType
pil_image = self.pil_image.convert(colorType)
if self.resize:
r = min(w/pil_image.width, h/pil_image.height)
size = (int(pil_image.width*r), int(pil_image.height*r))
pil_image = self.pil_image.resize(size)
if self.rotation != 0:
pil_image = pil_image.rotate(self.rotation,
fillcolor=self.background_color)
if not self.centered:
return pil_image
else:
img = Image.new(
colorType,
(w+1, h+1),
color=get_Color(self.background_color, colorType)
)
x = int(0.5*w-0.5*pil_image.width)
y = int(0.5*h-0.5*pil_image.height)
img.paste(pil_image, (x, y))
self.imgData = img
return img
class Line(Element):
"""
Draws a simple line
Args:
color (str or tuple): "white", "black", "gray0" to "gray15" or a
(red, green, blue, transparency) tuple
width (int): The width of the line
type (str): can be "horizontal", "vertical", "diagonal1" (top-left to
bottom right) or "diagonal2" (top-right to bottom-left)
"""
def __init__(self, color="black", width=1, type="horizontal"):
super().__init__()
self.color = color
self.width = width
self.type = type
def generator(self, area):
(x, y), (w, h) = area
self.area = area
colorType = self.parentPSSMScreen.colorType
if self.type == "horizontal":
coo = [(0, 0), (w, 0)]
elif self.type == "vertical":
coo = [(0, 0), (0, h)]
elif self.type == "diagonal1":
coo = [(0, 0), (w, h)]
else: # Assuming diagonal2
coo = [(w, 0), (0, h)]
rectangle = Image.new(
colorType,
(w, h),
color=get_Color("white", colorType)
)
draw = ImageDraw.Draw(rectangle)
draw.line(
coo,
fill=get_Color(self.color, colorType),
width=self.width
)
self.imgData = rectangle
return self.imgData
class Input(Button):
"""
Basically a button, except when you click on it, it displays the keyboard.
It handles typing things for you. so when you click on this element, the
keyboard shows up, and you can start typing.
The main thing it does is that it is able to detect between which
characters the user typed to be able to insert a character between two
others (and that was no easy task)
It has a method to retrieve what was typed :
Input.getInput()
Args:
isMultiline (bool): Allow carriage return
onReturn (function): Function to be executed on carriage return
"""
def __init__(self, isMultiline=True, onReturn=returnFalse, **kwargs):
super().__init__()
self.hideCursorWhenLast = True
self.isMultiline = isMultiline
self.onReturn = onReturn
self.allowSetCursorPos = False
self.isOnTop = True # Let's assume an input elt is always on top
for param in kwargs:
setattr(self, param, kwargs[param])
if 'font' in kwargs:
self.font = tools_parseKnownFonts(kwargs["font"])
self.cursorPosition = len(self.text)
self.typedText = self.text[:]
self.text = self.typedText
def getInput(self):
"""
Returns the text currently written on the Input box.
"""
return self.typedText
def pssmOnClickInside(self, coords):
if not self.parentPSSMScreen.osk:
print(
"[PSSM] Keyboard not initialized, Input element cannot be " +
"properly handled"
)
return None
# Set the callback function to our own
self.parentPSSMScreen.osk.onKeyPress = self.onKeyPress
if not self.parentPSSMScreen.isOSKShown:
# Let's print the on screen keyboard as it is not already here
self.parentPSSMScreen.OSKShow()
elif self.allowSetCursorPos:
cx, cy = coords
[(sx, sy), (w, h)] = self.area
loaded_font = self.loaded_font
myText = self.convertedText
imgDraw = self.imgDraw
text_w, text_h = imgDraw.textsize(myText, font=loaded_font)
x = tools_convertXArgsToPX(self.text_xPosition, w, text_w,
myElt=self)
y = tools_convertYArgsToPX(self.text_yPosition, h, text_h,
myElt=self)
# Then let's linear search
wasFound = False
olines = myText[:].split("\n")
if len(olines) > 0:
lines = [olines[0]]
else:
lines = []
for i in range(len(olines)):
lines.append("\n")
linesBefore = ""
for i in range(len(lines)):
tw1, th1 = imgDraw.textsize(linesBefore, font=loaded_font)
linesBefore += lines[i]
tw2, th2 = imgDraw.textsize(linesBefore, font=loaded_font)
b_correct_y = cy > sy + x + th1 and cy <= sy + y + th2
if b_correct_y:
for j in range(len(linesBefore)):
tw1, th1 = imgDraw.textsize(linesBefore[:j],
font=loaded_font)
tw2, th2 = imgDraw.textsize(linesBefore[:j+1],
font=loaded_font)
b_correct_x = cx > sx + x + tw1 and cx <= sx + x + tw2
if b_correct_x:
pos = j
for line in lines[:i]:
pos += len(line)
self.setCursorPosition(pos+1)
wasFound = True
if not wasFound: # Let's put it at the end of the row
pos = 0
for line in lines[:i+1]:
pos += len(line)
self.setCursorPosition(pos)
wasFound = True
if not wasFound:
self.setCursorPosition(None)
pass
def onKeyPress(self, keyType, keyChar):
"""
Handles each key press.
By default, it will re-display the input element on each keypress ON
TOP OF THE SCREEN (not honoring stack position). This allow for a 30%
speed increase on my basic test. You can change this behaviour by
setting `InputElt.isOnTop = False`
"""
c = self.cursorPosition
if keyType == KTstandardChar:
self.typedText = insertStr(self.typedText, keyChar, c)
self.setCursorPosition(self.cursorPosition+1, skipPrint=True)
elif keyType == KTcarriageReturn:
if self.isMultiline:
self.typedText = insertStr(self.typedText, "\n", c)
self.setCursorPosition(self.cursorPosition+1, skipPrint=True)
else:
self.onReturn()
elif keyType == KTbackspace:
self.typedText = self.typedText[:c-1] + self.typedText[c:]
self.setCursorPosition(self.cursorPosition-1, skipPrint=True)
if self.hideCursorWhenLast:
if self.cursorPosition >= len(self.typedText):
# Don't display the cursor when it is at the last position
self.text = self.typedText[:]
else:
self.text = insertStr(self.typedText, CURSOR_CHAR,
self.cursorPosition)
if self.isOnTop:
self.update(reprintOnTop=True)
else:
self.update()
def setCursorPosition(self, pos, skipPrint=False):
if pos is None:
pos = len(self.typedText)
self.cursorPosition = pos
self.text = insertStr(self.typedText, CURSOR_CHAR, self.cursorPosition)
if not skipPrint:
self.update()
# ########################## - Tools - ##############################
def roundedCorner(radius, fill="white", outline_color="gray3", colorType='L'):
"""
Draw a round corner
"""
corner = Image.new(colorType, (radius, radius), "white")
draw = ImageDraw.Draw(corner)
draw.pieslice(
(0, 0, radius * 2, radius * 2),
180,
270,
fill=get_Color(fill, colorType),
outline=get_Color(outline_color, colorType)
)
return corner
# ############################# - DOCUMENTATION - #############################
__pdoc__ = {} # For the documentation
ignoreList = [
'returnFalse',
'coordsInArea',
'getRectanglesIntersection',
'roundedCorner',
'tools_convertXArgsToPX',
'tools_convertYArgsToPX',
'tools_parseKnownImageFile',
'get_Color',
'PSSMScreen.convertDimension',
'Layout.generator',
'Layout.createImgMatrix',
'Layout.createAreaMatrix',
'Layout.calculate_remainingHeight',
'Layout.calculate_remainingWidth',
'Layout.extract_rowsHeight',
'Layout.extract_colsWidth',
'Layout._dispatchClick',
'Layout._dispatchClick_LINEAR',
'Layout._dispatchClick_DICHOTOMY_colsOnly',
'Layout._dispatchClick_DICHOTOMY_Full_ToBeFixed'
]
for f in ignoreList:
__pdoc__[f] = False
Classes
class Button (text='', font='default', font_size='H*0.036', background_color='white', outline_color='black', radius=0, font_color='black', text_xPosition='center', text_yPosition='center', wrap_textOverflow=True, **kwargs)
-
Basically a rectangle (or rounded rectangle) with text printed on it
Args
text
:str
- The main text to be written on it
font
:str
- Path to a font file (ttf file), or one of PSSM built-in fonts (e.g. "Merriweather-Bold", "default", "Merriweather-Regular", …) (see the font folder for the complete list)
font_size
:int
- The font size
font_color
:str
- The color of the font : "white", "black", "gray0" to "gray15" or a (red, green, blue, transparency) tuple
wrap_textOverflow
:bool
- (True by default) Wrap text in order to avoid
- it overflowing. The cuts are made between words.
text_xPosition
:str
orint
- can be left, center, right, or an integer value, or a pssm string dimension
text_yPosition
:str
orint
- can be left, center, right, or an integer value, or a pssm string dimension
background_color
:str
- The background color
outline_color
:str
- The border color
radius
:int
- If not 0, then add rounded corners of this radius
Expand source code
class Button(Element): """ Basically a rectangle (or rounded rectangle) with text printed on it Args: text (str): The main text to be written on it font (str): Path to a font file (ttf file), or one of PSSM built-in fonts (e.g. "Merriweather-Bold", "default", "Merriweather-Regular", ...) (see the font folder for the complete list) font_size (int): The font size font_color (str): The color of the font : "white", "black", "gray0" to "gray15" or a (red, green, blue, transparency) tuple wrap_textOverflow (bool): (True by default) Wrap text in order to avoid it overflowing. The cuts are made between words. text_xPosition (str or int): can be left, center, right, or an integer value, or a pssm string dimension text_yPosition (str or int): can be left, center, right, or an integer value, or a pssm string dimension background_color (str): The background color outline_color (str): The border color radius (int): If not 0, then add rounded corners of this radius """ def __init__(self, text="", font=DEFAULT_FONT, font_size=DEFAULT_FONT_SIZE, background_color="white", outline_color="black", radius=0, font_color="black", text_xPosition="center", text_yPosition="center", wrap_textOverflow=True, **kwargs): super().__init__() self.background_color = background_color self.outline_color = outline_color self.text = text self.font = tools_parseKnownFonts(font) self.font_size = font_size self.radius = radius self.font_color = font_color self.text_xPosition = text_xPosition self.text_yPosition = text_yPosition self.wrap_textOverflow = wrap_textOverflow for param in kwargs: setattr(self, param, kwargs[param]) self.loaded_font = None self.convertedText = None self.imgDraw = None def generator(self, area=None): if area is None: area = self.area [(x, y), (w, h)] = area self.area = area if not isinstance(self.font_size, int): self.font_size = self.convertDimension(self.font_size) if not isinstance(self.font_size, int): # That's a question mark dimension, or an invalid dimension. # Rollback to default font size self.font_size = self.convertDimension(DEFAULT_FONT_SIZE) loaded_font = ImageFont.truetype(self.font, self.font_size) self.loaded_font = loaded_font if self.radius > 0: rect = RectangleRounded( radius=self.radius, background_color=self.background_color, outline_color=self.outline_color, parentPSSMScreen=self.parentPSSMScreen ) else: rect = Rectangle( background_color=self.background_color, outline_color=self.outline_color, parentPSSMScreen=self.parentPSSMScreen ) rect_img = rect.generator(self.area) imgDraw = ImageDraw.Draw(rect_img, self.parentPSSMScreen.colorType) self.imgDraw = imgDraw if self.wrap_textOverflow: myText = self.wrapText(self.text, loaded_font, imgDraw) else: myText = self.text self.convertedText = myText text_w, text_h = imgDraw.textsize(myText, font=loaded_font) x = tools_convertXArgsToPX(self.text_xPosition, w, text_w, myElt=self) y = tools_convertYArgsToPX(self.text_yPosition, h, text_h, myElt=self) imgDraw.text( (x, y), myText, font=loaded_font, fill=get_Color(self.font_color, self.parentPSSMScreen.colorType) ) self.imgData = rect_img return self.imgData def wrapText(self, text, loaded_font, imgDraw): def get_text_width(text): return imgDraw.textsize(text=text, font=loaded_font)[0] [(x, y), (max_width, h)] = self.area text_lines = [ ' '.join([w.strip() for w in line.split(' ') if w]) for line in text.split('\n') if line ] space_width = get_text_width(" ") wrapped_lines = [] buf = [] buf_width = 0 for line in text_lines: for word in line.split(' '): word_width = get_text_width(word) expected_width = word_width if not buf else \ buf_width + space_width + word_width if expected_width <= max_width: # word fits in line buf_width = expected_width buf.append(word) else: # word doesn't fit in line wrapped_lines.append(' '.join(buf)) buf = [word] buf_width = word_width if buf: wrapped_lines.append(' '.join(buf)) buf = [] buf_width = 0 return '\n'.join(wrapped_lines)
Ancestors
Subclasses
Methods
def wrapText(self, text, loaded_font, imgDraw)
-
Expand source code
def wrapText(self, text, loaded_font, imgDraw): def get_text_width(text): return imgDraw.textsize(text=text, font=loaded_font)[0] [(x, y), (max_width, h)] = self.area text_lines = [ ' '.join([w.strip() for w in line.split(' ') if w]) for line in text.split('\n') if line ] space_width = get_text_width(" ") wrapped_lines = [] buf = [] buf_width = 0 for line in text_lines: for word in line.split(' '): word_width = get_text_width(word) expected_width = word_width if not buf else \ buf_width + space_width + word_width if expected_width <= max_width: # word fits in line buf_width = expected_width buf.append(word) else: # word doesn't fit in line wrapped_lines.append(' '.join(buf)) buf = [word] buf_width = word_width if buf: wrapped_lines.append(' '.join(buf)) buf = [] buf_width = 0 return '\n'.join(wrapped_lines)
Inherited members
class ButtonList (buttons, margins=[0, 0, 0, 0], spacing=0, **kwargs)
-
Generates a Layout with only one item per row, all the same type (buttons) and same height and width
Args
button
:list
- a [{"text":"my text","onclickInside":onclickInside}, someOtherDict, someOtherDict] array. Each dict will contain the parameters of each button of the button list
borders
:list
- a [top, bottom,left,right] array
Expand source code
class ButtonList(Layout): """ Generates a Layout with only one item per row, all the same type (buttons) and same height and width Args: button (list): a [{"text":"my text","onclickInside":onclickInside}, someOtherDict, someOtherDict] array. Each dict will contain the parameters of each button of the button list borders (list): a [top, bottom,left,right] array """ def __init__(self, buttons, margins=[0, 0, 0, 0], spacing=0, **kwargs): self.buttons = buttons self.margins = margins self.spacing = spacing layout = self.build_layoutFromButtons() super().__init__(layout) for param in kwargs: setattr(self, param, kwargs[param]) def build_layoutFromButtons(self): # TODO : must honor min_width,max_width etc [top, bottom, left, right] = self.margins buttonLayout = [[top-self.spacing]] for button in self.buttons: buttonElt = Button(text=button['text']) for param in button: setattr(buttonElt, param, button[param]) row_height = "?" buttonLayout.append([self.spacing]) row = [row_height, (None, left), (buttonElt, "?"), (None, right)] buttonLayout.append(row) buttonLayout.append([bottom]) return buttonLayout
Ancestors
Methods
def build_layoutFromButtons(self)
-
Expand source code
def build_layoutFromButtons(self): # TODO : must honor min_width,max_width etc [top, bottom, left, right] = self.margins buttonLayout = [[top-self.spacing]] for button in self.buttons: buttonElt = Button(text=button['text']) for param in button: setattr(buttonElt, param, button[param]) row_height = "?" buttonLayout.append([self.spacing]) row = [row_height, (None, left), (buttonElt, "?"), (None, right)] buttonLayout.append(row) buttonLayout.append([bottom]) return buttonLayout
Inherited members
class Element (area=None, imgData=None, onclickInside=<function returnFalse>, isInverted=False, data={}, invertOnClick=False, invertDuration=0.2, forcePrintOnTop=False)
-
Everything which is going to be displayed on the screen is an Element.
Args
isInverted
:bool
- Is the element inverted
data
:dict,
orany
- A parameter for you to store whatever you want
area
:list
- a [(x, y), (w, h)] list. If used in a Layout, the layout will take care of calculating the area.
imgData
:PILImage
- the PIL image of the object (None by default, the generator function takes care of generating one)
onclickInside
:function
- A function to be executed when the user clicks on the Element
invertOnClick
:bool
- Invert the element when a click is registered ?
invertDuration
:int
- Duration in seconds of the element invertion after a click is registered (use 0 for infinite)
forcePrintOnTop
:bool
- Force the element to be printed on top of the stack, even if there is an on-screen keyboard or a popup
Expand source code
class Element: """ Everything which is going to be displayed on the screen is an Element. Args: isInverted (bool): Is the element inverted data (dict, or any): A parameter for you to store whatever you want area (list): a [(x, y), (w, h)] list. If used in a Layout, the layout will take care of calculating the area. imgData (PILImage): the PIL image of the object (None by default, the generator function takes care of generating one) onclickInside (function): A function to be executed when the user clicks on the Element invertOnClick (bool): Invert the element when a click is registered ? invertDuration (int): Duration in seconds of the element invertion after a click is registered (use 0 for infinite) forcePrintOnTop (bool): Force the element to be printed on top of the stack, even if there is an on-screen keyboard or a popup """ def __init__(self, area=None, imgData=None, onclickInside=returnFalse, isInverted=False, data={}, invertOnClick=False, invertDuration=DEFAULT_INVERT_DURATION, forcePrintOnTop=False ): global lastUsedId self.id = lastUsedId lastUsedId += 1 self.isLayout = False self.imgData = imgData self.area = area self.onclickInside = onclickInside self.isInverted = isInverted self.user_data = data self.invertOnClick = invertOnClick self.invertDuration = invertDuration self.forcePrintOnTop = forcePrintOnTop self.parentLayouts = [] self.parentPSSMScreen = None def __hash__(self): return hash(self.id) def __eq__(self, other): if isinstance(other, self.__class__): return self.id == other.id return NotImplemented def update(self, newAttributes={}, skipGen=False, skipPrint=False, reprintOnTop=False): """ Pass a dict as argument, and it will update the Element's attributes accordingly (both its attribute and then the screen). Note: Updating an element can be very slow ! It depends on every specific cases, but know there are a few ways to make it faster: - Use `screen.startBatchWriting()` and `screen.stopBatchWriting()` - If you know this specific element is on top of the screen, use: `elt.update(newAttributes=myDict, reprintOnTop=True)` Which will do the same, except it won't rebuild the whole stack image, it will just print this object on top. (On my tests, I could spare up to 0.5s !) Args : newAttributes (dict): The element's new attributes skipGen (bool): Just update the element's attribute, but do not do any generation or printing reprintOnTop (bool): Do not reprint the whole stack, but do print this element on top of the screen. (much faster when possible) skipPrint (bool): Do not update the screen, but do regenerate. """ # First, we set the attributes for param in newAttributes: setattr(self, param, newAttributes[param]) if not skipGen: # we recreate the pillow image of this particular object self.generator() if (not skipPrint) and (not skipGen): # No need to update if no regen isBatch = self.parentPSSMScreen.isBatch if reprintOnTop: self.parentPSSMScreen.simplePrintElt(self) elif not isBatch: hasParent = len(self.parentLayouts) > 0 # We don't want unncesseray generation when printing batch if hasParent: # We recreate the pillow image of the oldest parent # And it is not needed to regenerate standard objects, since oldest_parent = self.parentLayouts[0] oldest_parent.generator(skipNonLayoutGen=True) # Then, let's reprint the stack self.parentPSSMScreen.printStack(area=self.area) return True def generator(self): """ The generator is the function which is called when the container layout wants to build an image. It therefore returns a pillow image. """ return NotImplemented def convertDimension(self, dimension): """ Converts the user dimension input (like "h*0.1") to to proper integer amount of pixels. Basically, you give it a string. And it will change a few characters to their corresponding value, then return the evaluated string. Examples: I HIGHLY recommend doing only simple operation, like "H*0.1", or "W/10", always starting with the corresponding variable. But you can if you want do more complicated things: elt.convertDimension("H+W") -> screen_height + screen_width elt.convertDimension("p*300+max(w, h)") -> 300 + max(element_width, screen_height) Note: When using question mark dimension (like "?*2"), the question mark MUST be at the beginning of the string """ if isinstance(dimension, int): return dimension elif isinstance(dimension, str): nd = "" W = self.parentPSSMScreen.width H = self.parentPSSMScreen.height if self.area: (x, y), (w, h) = self.area else: # area not defined. Instead of being stuck, let's assume the # screen height and width are a decent alternative w, h = W, H for c in dimension: if c == 'p' or c == 'P': nd += '1' elif c == 'W': # screen width nd += str(W) elif c == 'H': # screen height nd += str(H) elif c == 'w': # element width nd += str(w) elif c == 'h': # element height nd += str(h) else: # A standard character nd += c if dimension[0] == '?': # We return the string, another function will take care of # evaluating it return nd else: return int(eval(nd)) # Then we can evaluate the input else: print("[PSSM] Could not parse the dimension") return dimension def pssmOnClickInside(self, coords=None): """ Each Element can also have a pssm function implemented on click. By default, it does nothing. """ return None
Subclasses
Methods
def convertDimension(self, dimension)
-
Converts the user dimension input (like "h*0.1") to to proper integer amount of pixels. Basically, you give it a string. And it will change a few characters to their corresponding value, then return the evaluated string.
Examples
I HIGHLY recommend doing only simple operation, like "H0.1", or "W/10", always starting with the corresponding variable. But you can if you want do more complicated things: elt.convertDimension("H+W") -> screen_height + screen_width elt.convertDimension("p300+max(w, h)") -> 300 + max(element_width, screen_height)
Note
When using question mark dimension (like "?*2"), the question mark MUST be at the beginning of the string
Expand source code
def convertDimension(self, dimension): """ Converts the user dimension input (like "h*0.1") to to proper integer amount of pixels. Basically, you give it a string. And it will change a few characters to their corresponding value, then return the evaluated string. Examples: I HIGHLY recommend doing only simple operation, like "H*0.1", or "W/10", always starting with the corresponding variable. But you can if you want do more complicated things: elt.convertDimension("H+W") -> screen_height + screen_width elt.convertDimension("p*300+max(w, h)") -> 300 + max(element_width, screen_height) Note: When using question mark dimension (like "?*2"), the question mark MUST be at the beginning of the string """ if isinstance(dimension, int): return dimension elif isinstance(dimension, str): nd = "" W = self.parentPSSMScreen.width H = self.parentPSSMScreen.height if self.area: (x, y), (w, h) = self.area else: # area not defined. Instead of being stuck, let's assume the # screen height and width are a decent alternative w, h = W, H for c in dimension: if c == 'p' or c == 'P': nd += '1' elif c == 'W': # screen width nd += str(W) elif c == 'H': # screen height nd += str(H) elif c == 'w': # element width nd += str(w) elif c == 'h': # element height nd += str(h) else: # A standard character nd += c if dimension[0] == '?': # We return the string, another function will take care of # evaluating it return nd else: return int(eval(nd)) # Then we can evaluate the input else: print("[PSSM] Could not parse the dimension") return dimension
def generator(self)
-
The generator is the function which is called when the container layout wants to build an image. It therefore returns a pillow image.
Expand source code
def generator(self): """ The generator is the function which is called when the container layout wants to build an image. It therefore returns a pillow image. """ return NotImplemented
def pssmOnClickInside(self, coords=None)
-
Each Element can also have a pssm function implemented on click. By default, it does nothing.
Expand source code
def pssmOnClickInside(self, coords=None): """ Each Element can also have a pssm function implemented on click. By default, it does nothing. """ return None
def update(self, newAttributes={}, skipGen=False, skipPrint=False, reprintOnTop=False)
-
Pass a dict as argument, and it will update the Element's attributes accordingly (both its attribute and then the screen).
Note
Updating an element can be very slow ! It depends on every specific cases, but know there are a few ways to make it faster: - Use
screen.startBatchWriting()
andscreen.stopBatchWriting()
- If you know this specific element is on top of the screen, use:elt.update(newAttributes=myDict, reprintOnTop=True)
Which will do the same, except it won't rebuild the whole stack image, it will just print this object on top. (On my tests, I could spare up to 0.5s !)Args : newAttributes (dict): The element's new attributes skipGen (bool): Just update the element's attribute, but do not do any generation or printing reprintOnTop (bool): Do not reprint the whole stack, but do print this element on top of the screen. (much faster when possible) skipPrint (bool): Do not update the screen, but do regenerate.
Expand source code
def update(self, newAttributes={}, skipGen=False, skipPrint=False, reprintOnTop=False): """ Pass a dict as argument, and it will update the Element's attributes accordingly (both its attribute and then the screen). Note: Updating an element can be very slow ! It depends on every specific cases, but know there are a few ways to make it faster: - Use `screen.startBatchWriting()` and `screen.stopBatchWriting()` - If you know this specific element is on top of the screen, use: `elt.update(newAttributes=myDict, reprintOnTop=True)` Which will do the same, except it won't rebuild the whole stack image, it will just print this object on top. (On my tests, I could spare up to 0.5s !) Args : newAttributes (dict): The element's new attributes skipGen (bool): Just update the element's attribute, but do not do any generation or printing reprintOnTop (bool): Do not reprint the whole stack, but do print this element on top of the screen. (much faster when possible) skipPrint (bool): Do not update the screen, but do regenerate. """ # First, we set the attributes for param in newAttributes: setattr(self, param, newAttributes[param]) if not skipGen: # we recreate the pillow image of this particular object self.generator() if (not skipPrint) and (not skipGen): # No need to update if no regen isBatch = self.parentPSSMScreen.isBatch if reprintOnTop: self.parentPSSMScreen.simplePrintElt(self) elif not isBatch: hasParent = len(self.parentLayouts) > 0 # We don't want unncesseray generation when printing batch if hasParent: # We recreate the pillow image of the oldest parent # And it is not needed to regenerate standard objects, since oldest_parent = self.parentLayouts[0] oldest_parent.generator(skipNonLayoutGen=True) # Then, let's reprint the stack self.parentPSSMScreen.printStack(area=self.area) return True
class Icon (file, centered=True, **kwargs)
-
An icon, built from an image
Args
file
:str
- Path to a file, or one of the integrated image (see the icon folder for the name of each image). 'reboot' for instance points to the integrated reboot image.
centered
:bool
- Center the icon?
Expand source code
class Icon(Element): """ An icon, built from an image Args: file (str): Path to a file, or one of the integrated image (see the icon folder for the name of each image). 'reboot' for instance points to the integrated reboot image. centered (bool): Center the icon? """ def __init__(self, file, centered=True, **kwargs): super().__init__() self.file = file self.centered = centered self.path_to_file = tools_parseKnownImageFile(self.file) for param in kwargs: setattr(self, param, kwargs[param]) def generator(self, area): self.area = area [(x, y), (w, h)] = area colorType = self.parentPSSMScreen.colorType icon_size = min(area[1][0], area[1][1]) loadedImg = Image.open(self.path_to_file) convImg = loadedImg.convert(colorType) iconImg = convImg.resize((icon_size, icon_size)) if not self.centered: self.imgData = iconImg return iconImg else: img = Image.new( colorType, (w+1, h+1), color=get_Color("white", colorType) ) x = int(0.5*w-0.5*icon_size) y = int(0.5*h-0.5*icon_size) img.paste(iconImg, (x, y)) self.imgData = img return img
Ancestors
Inherited members
class Input (isMultiline=True, onReturn=<function returnFalse>, **kwargs)
-
Basically a button, except when you click on it, it displays the keyboard. It handles typing things for you. so when you click on this element, the keyboard shows up, and you can start typing. The main thing it does is that it is able to detect between which characters the user typed to be able to insert a character between two others (and that was no easy task) It has a method to retrieve what was typed : Input.getInput()
Args
isMultiline
:bool
- Allow carriage return
onReturn
:function
- Function to be executed on carriage return
Expand source code
class Input(Button): """ Basically a button, except when you click on it, it displays the keyboard. It handles typing things for you. so when you click on this element, the keyboard shows up, and you can start typing. The main thing it does is that it is able to detect between which characters the user typed to be able to insert a character between two others (and that was no easy task) It has a method to retrieve what was typed : Input.getInput() Args: isMultiline (bool): Allow carriage return onReturn (function): Function to be executed on carriage return """ def __init__(self, isMultiline=True, onReturn=returnFalse, **kwargs): super().__init__() self.hideCursorWhenLast = True self.isMultiline = isMultiline self.onReturn = onReturn self.allowSetCursorPos = False self.isOnTop = True # Let's assume an input elt is always on top for param in kwargs: setattr(self, param, kwargs[param]) if 'font' in kwargs: self.font = tools_parseKnownFonts(kwargs["font"]) self.cursorPosition = len(self.text) self.typedText = self.text[:] self.text = self.typedText def getInput(self): """ Returns the text currently written on the Input box. """ return self.typedText def pssmOnClickInside(self, coords): if not self.parentPSSMScreen.osk: print( "[PSSM] Keyboard not initialized, Input element cannot be " + "properly handled" ) return None # Set the callback function to our own self.parentPSSMScreen.osk.onKeyPress = self.onKeyPress if not self.parentPSSMScreen.isOSKShown: # Let's print the on screen keyboard as it is not already here self.parentPSSMScreen.OSKShow() elif self.allowSetCursorPos: cx, cy = coords [(sx, sy), (w, h)] = self.area loaded_font = self.loaded_font myText = self.convertedText imgDraw = self.imgDraw text_w, text_h = imgDraw.textsize(myText, font=loaded_font) x = tools_convertXArgsToPX(self.text_xPosition, w, text_w, myElt=self) y = tools_convertYArgsToPX(self.text_yPosition, h, text_h, myElt=self) # Then let's linear search wasFound = False olines = myText[:].split("\n") if len(olines) > 0: lines = [olines[0]] else: lines = [] for i in range(len(olines)): lines.append("\n") linesBefore = "" for i in range(len(lines)): tw1, th1 = imgDraw.textsize(linesBefore, font=loaded_font) linesBefore += lines[i] tw2, th2 = imgDraw.textsize(linesBefore, font=loaded_font) b_correct_y = cy > sy + x + th1 and cy <= sy + y + th2 if b_correct_y: for j in range(len(linesBefore)): tw1, th1 = imgDraw.textsize(linesBefore[:j], font=loaded_font) tw2, th2 = imgDraw.textsize(linesBefore[:j+1], font=loaded_font) b_correct_x = cx > sx + x + tw1 and cx <= sx + x + tw2 if b_correct_x: pos = j for line in lines[:i]: pos += len(line) self.setCursorPosition(pos+1) wasFound = True if not wasFound: # Let's put it at the end of the row pos = 0 for line in lines[:i+1]: pos += len(line) self.setCursorPosition(pos) wasFound = True if not wasFound: self.setCursorPosition(None) pass def onKeyPress(self, keyType, keyChar): """ Handles each key press. By default, it will re-display the input element on each keypress ON TOP OF THE SCREEN (not honoring stack position). This allow for a 30% speed increase on my basic test. You can change this behaviour by setting `InputElt.isOnTop = False` """ c = self.cursorPosition if keyType == KTstandardChar: self.typedText = insertStr(self.typedText, keyChar, c) self.setCursorPosition(self.cursorPosition+1, skipPrint=True) elif keyType == KTcarriageReturn: if self.isMultiline: self.typedText = insertStr(self.typedText, "\n", c) self.setCursorPosition(self.cursorPosition+1, skipPrint=True) else: self.onReturn() elif keyType == KTbackspace: self.typedText = self.typedText[:c-1] + self.typedText[c:] self.setCursorPosition(self.cursorPosition-1, skipPrint=True) if self.hideCursorWhenLast: if self.cursorPosition >= len(self.typedText): # Don't display the cursor when it is at the last position self.text = self.typedText[:] else: self.text = insertStr(self.typedText, CURSOR_CHAR, self.cursorPosition) if self.isOnTop: self.update(reprintOnTop=True) else: self.update() def setCursorPosition(self, pos, skipPrint=False): if pos is None: pos = len(self.typedText) self.cursorPosition = pos self.text = insertStr(self.typedText, CURSOR_CHAR, self.cursorPosition) if not skipPrint: self.update()
Ancestors
Methods
def getInput(self)
-
Returns the text currently written on the Input box.
Expand source code
def getInput(self): """ Returns the text currently written on the Input box. """ return self.typedText
def onKeyPress(self, keyType, keyChar)
-
Handles each key press. By default, it will re-display the input element on each keypress ON TOP OF THE SCREEN (not honoring stack position). This allow for a 30% speed increase on my basic test. You can change this behaviour by setting
InputElt.isOnTop = False
Expand source code
def onKeyPress(self, keyType, keyChar): """ Handles each key press. By default, it will re-display the input element on each keypress ON TOP OF THE SCREEN (not honoring stack position). This allow for a 30% speed increase on my basic test. You can change this behaviour by setting `InputElt.isOnTop = False` """ c = self.cursorPosition if keyType == KTstandardChar: self.typedText = insertStr(self.typedText, keyChar, c) self.setCursorPosition(self.cursorPosition+1, skipPrint=True) elif keyType == KTcarriageReturn: if self.isMultiline: self.typedText = insertStr(self.typedText, "\n", c) self.setCursorPosition(self.cursorPosition+1, skipPrint=True) else: self.onReturn() elif keyType == KTbackspace: self.typedText = self.typedText[:c-1] + self.typedText[c:] self.setCursorPosition(self.cursorPosition-1, skipPrint=True) if self.hideCursorWhenLast: if self.cursorPosition >= len(self.typedText): # Don't display the cursor when it is at the last position self.text = self.typedText[:] else: self.text = insertStr(self.typedText, CURSOR_CHAR, self.cursorPosition) if self.isOnTop: self.update(reprintOnTop=True) else: self.update()
def setCursorPosition(self, pos, skipPrint=False)
-
Expand source code
def setCursorPosition(self, pos, skipPrint=False): if pos is None: pos = len(self.typedText) self.cursorPosition = pos self.text = insertStr(self.typedText, CURSOR_CHAR, self.cursorPosition) if not skipPrint: self.update()
Inherited members
class Layout (layout, area=None, background_color='white', **kwargs)
-
A layout is a quite general kind of Element : If must be given the working area, and a layout, and will generate every element of the layout
Args
layout
:list
- The given layout (see example below). It is basically a
- list of rows. Each row is a list containing : the height of the row,
- then as many tuples as you want, each tuple being a
- (pssm.Element, width) instance
background_color
area
… all other arguments from the pssm.Element class Example of usage: See examples
Expand source code
class Layout(Element): """ A layout is a quite general kind of Element : If must be given the working area, and a layout, and will generate every element of the layout Args: layout (list): The given layout (see example below). It is basically a list of rows. Each row is a list containing : the height of the row, then as many tuples as you want, each tuple being a (pssm.Element, width) instance background_color area ... all other arguments from the pssm.Element class Example of usage: See [examples](examples/index.html) """ def __init__(self, layout, area=None, background_color="white", **kwargs): super().__init__() self.area = area self.layout = layout self.isValid = self.isLayoutValid() self.background_color = background_color self.areaMatrix = None self.imgMatrix = None self.borders = None self.isLayout = True for param in kwargs: setattr(self, param, kwargs[param]) def isLayoutValid(self): # TODO : to be tested layout = self.layout if not isinstance(layout, list): raise Exception("Layout Element is supposed to be a list") for row in layout: if not isinstance(row, list): raise Exception("A layout row is supposed to be a list") elif len(row) == 0: raise Exception("A layout row cannot be empty") elif not isinstance(row[0], str) and not isinstance(row[0], int): raise Exception( "The first element of a row (its height) should be a " + "string or an integer" ) for j in range(1, len(row)): eltTuple = row[j] isTuple = isinstance(eltTuple, tuple) isList = isinstance(eltTuple, list) if not (isTuple or isList): raise Exception( "A layout row should be a list of Tuple " + "(except for its first element)" ) if len(eltTuple) != 2: raise Exception( "A layout element should be a Tuple : " + "(Element, elementWidth)" ) isStr = isinstance(eltTuple[1], str) isInt = isinstance(eltTuple[1], int) if not (isInt or isStr): raise Exception( "An element width should be a string or an integer" ) isElement = isinstance(eltTuple[0], Element) if not (isElement or eltTuple[0] is None): raise Exception( "A layout element should be a Tuple : " + "(Element, elementWidth), with Element designating " + " a PSSM Element" ) return True def generator(self, area=None, skipNonLayoutGen=False): """ Builds one img out of all the Elements it is being given """ if area is not None: self.area = area self.createAreaMatrix() self.createImgMatrix(skipNonLayoutGen=skipNonLayoutGen) [(x, y), (w, h)] = self.area colorType = self.parentPSSMScreen.colorType color = get_Color(self.background_color, colorType) placeholder = Image.new(colorType, (w, h), color=color) for i in range(len(self.areaMatrix)): for j in range(len(self.areaMatrix[i])): [(elt_x, elt_y), (elt_w, elt_h)] = self.areaMatrix[i][j] relative_x = elt_x - x relative_y = elt_y - y elt_img = self.imgMatrix[i][j] if elt_img is not None: pos = (relative_x, relative_y) placeholder.paste(self.imgMatrix[i][j], pos) self.imgData = placeholder return self.imgData def createImgMatrix(self, skipNonLayoutGen=False): matrix = [] if not self.areaMatrix: print("[PSSM Layout] Error, areaMatrix has to be defined first") return None for i in range(len(self.layout)): row = [] for j in range(1, len(self.layout[i])): elt, _ = self.layout[i][j] if elt is None: elt_area = self.areaMatrix[i][j-1] elt_img = None else: elt_area = self.areaMatrix[i][j-1] if not elt.isLayout and skipNonLayoutGen: elt_img = elt.imgData else: elt_img = elt.generator(area=elt_area) row.append(elt_img) matrix.append(row) self.imgMatrix = matrix def createAreaMatrix(self): # TODO : must honor min and max matrix = [] n_rows = len(self.layout) [(x, y), (w, h)] = self.area[:] x0, y0 = x, y for i in range(n_rows): # Lets loop through the rows row = self.layout[i] row_cols = [] # All the columns of this particular row row_height = row[0] converted_height = self.convertDimension(row_height) if isinstance(converted_height, int): true_row_height = converted_height else: remaining_height = self.calculate_remainingHeight() dim = str(remaining_height) + converted_height[1:] true_row_height = int(eval(dim)) for j in range(1, len(row)): (element, element_width) = row[j] converted_width = self.convertDimension(element_width) if element is not None: for parent in self.parentLayouts: self.layout[i][j][0].parentLayouts.append(parent) self.layout[i][j][0].parentLayouts.append(self) self.layout[i][j][0].parentPSSMScreen = \ self.parentPSSMScreen if isinstance(converted_width, int): true_elt_width = converted_width else: remaining_width = self.calculate_remainingWidth(i) dim = str(remaining_width) + converted_width[1:] true_elt_width = int(eval(dim)) self.layout[i][j] = (self.layout[i][j][0], true_elt_width) element_area = [(x0, y0), (true_elt_width, true_row_height)] x0 += true_elt_width row_cols.append(element_area) y0 += true_row_height x0 = x matrix.append(row_cols) self.areaMatrix = matrix def createEltList(self): """ Returns a list of all the elements the Layout Element contains """ eltList = [] for row in self.layout: for i in range(1, len(row)): elt, _ = row[i] if elt is not None: eltList.append(elt) return eltList def calculate_remainingHeight(self): rows = self.extract_rowsHeight() total_questionMarks_weight = 0 total_height = 0 for dimension in rows: converted_dimension = self.convertDimension(dimension) if isinstance(converted_dimension, int): total_height += converted_dimension else: weight = eval("1" + converted_dimension[1:]) total_questionMarks_weight += weight layout_height = self.area[1][1] return int((layout_height - total_height)/total_questionMarks_weight) def calculate_remainingWidth(self, rowIndex): cols = self.extract_colsWidth(rowIndex) total_width = 0 total_questionMarks_weight = 0 for dimension in cols: converted_dimension = self.convertDimension(dimension) if isinstance(converted_dimension, int): total_width += converted_dimension else: weight = eval("1" + converted_dimension[1:]) total_questionMarks_weight += weight layout_width = self.area[1][0] return int((layout_width - total_width)/total_questionMarks_weight) def extract_rowsHeight(self): rows = [] for row in self.layout: rows.append(row[0]) return rows def extract_colsWidth(self, rowIndex): cols = [] for col in self.layout[rowIndex]: if isinstance(col, tuple): cols.append(col[1]) return cols def _dispatchClick(self, coords): """ Finds the element on which the user clicked """ self._dispatchClick_LINEAR(coords) def _dispatchClick_LINEAR(self, coords): """ Linear search throuh both the rows and the columns """ click_x, click_y = coords # Linear search though the rows for i in range(len(self.areaMatrix)): if len(self.areaMatrix[i]) == 0: # That's a fake row (a margin row) continue first_row_elt = self.areaMatrix[i][0] last_row_elt = self.areaMatrix[i][-1] x = first_row_elt[0][0] y = first_row_elt[0][1] w = last_row_elt[0][0] + last_row_elt[1][0] - first_row_elt[0][0] h = last_row_elt[0][1] + last_row_elt[1][1] - first_row_elt[0][1] if coordsInArea(click_x, click_y, [(x, y), (w, h)]): # CLick was in that row for j in range(len(self.areaMatrix[i])): # Linear search through the columns if coordsInArea(click_x, click_y, self.areaMatrix[i][j]): # Click was on that element elt, _ = self.layout[i][j+1] if elt is not None and elt.onclickInside is not None: self.parentPSSMScreen._dispatchClickToElt( coords, elt ) return True return False def _dispatchClick_DICHOTOMY_colsOnly(self, coords): """ Linear search through the rows, dichotomy for the columns (Because of the empty rows, a dichotomy for the rows doesn't work) NEEDS TO BE FIXED TOO (example : two buttons in a row) """ click_x, click_y = coords row_A = -1 for i in range(len(self.areaMatrix)): # Linear search though the rows if len(self.areaMatrix[i]) == 0: # That's a fake row (a margin row) continue first_row_elt = self.areaMatrix[i][0] last_row_elt = self.areaMatrix[i][-1] x = first_row_elt[0][0] y = first_row_elt[0][1] w = last_row_elt[0][0] + last_row_elt[1][0] - first_row_elt[0][0] h = last_row_elt[0][1] + last_row_elt[1][1] - first_row_elt[0][1] if coordsInArea(click_x, click_y, [(x, y), (w, h)]): # CLick was in that row row_A = i break if row_A == -1: return None col_A = 0 col_C = max(len(self.areaMatrix[row_A]) - 1, 0) xA = self.areaMatrix[row_A][col_A][0][0] xC = self.areaMatrix[row_A][col_C][0][0] if click_x < xA: return None if click_x > xC + self.areaMatrix[row_A][col_C][1][0]: return None while col_C > col_A + 1: col_B = int(0.5*(col_A+col_C)) # The average of the two xB = self.areaMatrix[row_A][col_B][0][0] if click_x >= xB or col_B == col_C: col_A = col_B xA = xB else: col_C = col_B xC = xB # Element is at indexes row_A, col_A elt, _ = self.layout[row_A][col_A+1] if elt is not None and elt.onclickInside is not None: self.parentPSSMScreen._dispatchClickToElt(coords, elt) return True def _dispatchClick_DICHOTOMY_Full_ToBeFixed(self, coords): """ Finds the element on which the user clicked Implemented with dichotomy search (with the hope of making things faster, especially the integrated keyboard) """ # TODO : To be fixed # For now it does not work, because there are empty rows which # break the loop click_x, click_y = coords row_A = 0 row_C = max(len(self.areaMatrix) - 1, 0) print(self.areaMatrix[row_C]) while len(self.areaMatrix[row_A]) == 0: row_A += 1 while len(self.areaMatrix[row_C]) == 0: row_C -= 1 # First column THEN first row , [(x, y), (w, h)] THUS first tuple of # list THEN second coordinate of tuple yA = self.areaMatrix[row_A][0][0][1] yC = self.areaMatrix[row_C][0][0][1] if click_y < yA: return None if click_y > yC + self.areaMatrix[row_C][0][1][1]: return None while row_C > row_A+1: row_B = int(0.5*(row_A+row_C)) # The average of the two while len(self.areaMatrix[row_B]) == 0: row_B += 1 yB = self.areaMatrix[row_B][0][0][1] if click_y >= yB or row_B == row_C: row_A = row_B yA = yB else: row_C = row_B yC = yB # User clicked on element ar row of index row_A # Let's do the same for the column col_A = 0 col_C = max(len(self.areaMatrix[row_A]) - 1, 0) xA = self.areaMatrix[row_A][col_A][0][0] xC = self.areaMatrix[row_A][col_C][0][0] if click_x < xA: return None if click_x > xC + self.areaMatrix[row_A][col_C][1][0]: return None while col_C > col_A + 1: col_B = int(0.5*(col_A+col_C)) # The average of the two xB = self.areaMatrix[row_A][col_B][0][0] if click_x >= xB or col_B == col_C: col_A = col_B xA = xB else: col_C = col_B xC = xB # Element is at indexes row_A, col_A elt, _ = self.layout[row_A-2][col_A+1] if elt is not None and elt.onclickInside is not None: self.parentPSSMScreen._dispatchClickToElt(coords, elt) return True
Ancestors
Subclasses
Methods
def createEltList(self)
-
Returns a list of all the elements the Layout Element contains
Expand source code
def createEltList(self): """ Returns a list of all the elements the Layout Element contains """ eltList = [] for row in self.layout: for i in range(1, len(row)): elt, _ = row[i] if elt is not None: eltList.append(elt) return eltList
def isLayoutValid(self)
-
Expand source code
def isLayoutValid(self): # TODO : to be tested layout = self.layout if not isinstance(layout, list): raise Exception("Layout Element is supposed to be a list") for row in layout: if not isinstance(row, list): raise Exception("A layout row is supposed to be a list") elif len(row) == 0: raise Exception("A layout row cannot be empty") elif not isinstance(row[0], str) and not isinstance(row[0], int): raise Exception( "The first element of a row (its height) should be a " + "string or an integer" ) for j in range(1, len(row)): eltTuple = row[j] isTuple = isinstance(eltTuple, tuple) isList = isinstance(eltTuple, list) if not (isTuple or isList): raise Exception( "A layout row should be a list of Tuple " + "(except for its first element)" ) if len(eltTuple) != 2: raise Exception( "A layout element should be a Tuple : " + "(Element, elementWidth)" ) isStr = isinstance(eltTuple[1], str) isInt = isinstance(eltTuple[1], int) if not (isInt or isStr): raise Exception( "An element width should be a string or an integer" ) isElement = isinstance(eltTuple[0], Element) if not (isElement or eltTuple[0] is None): raise Exception( "A layout element should be a Tuple : " + "(Element, elementWidth), with Element designating " + " a PSSM Element" ) return True
Inherited members
class Line (color='black', width=1, type='horizontal')
-
Draws a simple line
Args
color
:str
ortuple
- "white", "black", "gray0" to "gray15" or a (red, green, blue, transparency) tuple
width
:int
- The width of the line
type
:str
- can be "horizontal", "vertical", "diagonal1" (top-left to bottom right) or "diagonal2" (top-right to bottom-left)
Expand source code
class Line(Element): """ Draws a simple line Args: color (str or tuple): "white", "black", "gray0" to "gray15" or a (red, green, blue, transparency) tuple width (int): The width of the line type (str): can be "horizontal", "vertical", "diagonal1" (top-left to bottom right) or "diagonal2" (top-right to bottom-left) """ def __init__(self, color="black", width=1, type="horizontal"): super().__init__() self.color = color self.width = width self.type = type def generator(self, area): (x, y), (w, h) = area self.area = area colorType = self.parentPSSMScreen.colorType if self.type == "horizontal": coo = [(0, 0), (w, 0)] elif self.type == "vertical": coo = [(0, 0), (0, h)] elif self.type == "diagonal1": coo = [(0, 0), (w, h)] else: # Assuming diagonal2 coo = [(w, 0), (0, h)] rectangle = Image.new( colorType, (w, h), color=get_Color("white", colorType) ) draw = ImageDraw.Draw(rectangle) draw.line( coo, fill=get_Color(self.color, colorType), width=self.width ) self.imgData = rectangle return self.imgData
Ancestors
Inherited members
class OSK (keymapPath={'standard': 'D:\\Jehan\\Documents\\Developpement\\KOBO\\dev\\Python-Screen-Stack-Manager\\config\\default-keymap-en_us.json', 'caps': 'D:\\Jehan\\Documents\\Developpement\\KOBO\\dev\\Python-Screen-Stack-Manager\\config\\default-keymap-en_us_CAPS.json', 'alt': 'D:\\Jehan\\Documents\\Developpement\\KOBO\\dev\\Python-Screen-Stack-Manager\\config\\default-keymap-en_us_ALT.json'}, onKeyPress=None, area=None, **kwargs)
-
A PSSM Layout element which builds an on-screen keyboard
Args
keymapPath
:str
- a path to a PSSMOSK keymap (like the one included)
onKeyPress
:function
- A callback function. Will be given keyType and keyChar as argument
Expand source code
class OSK(Layout): """ A PSSM Layout element which builds an on-screen keyboard Args: keymapPath (str): a path to a PSSMOSK keymap (like the one included) onKeyPress (function): A callback function. Will be given keyType and keyChar as argument """ def __init__(self, keymapPath=DEFAULT_KEYMAP_PATH, onKeyPress=None, area=None, **kwargs): if not keymapPath: keymapPath = DEFAULT_KEYMAP_PATH self.keymapPaths = keymapPath self.keymap = {'standard': None, 'caps': None, 'alt': None} self.keymap_layouts = {'standard': None, 'caps': None, 'alt': None} self.keymap_imgs = {'standard': None, 'caps': None, 'alt': None} with open(self.keymapPaths['standard']) as json_file: self.keymap['standard'] = json.load(json_file) with open(self.keymapPaths['caps']) as json_file: self.keymap['caps'] = json.load(json_file) with open(self.keymapPaths['alt']) as json_file: self.keymap['alt'] = json.load(json_file) self.lang = self.keymap['standard']["lang"] self.onKeyPress = onKeyPress for param in kwargs: setattr(self, param, kwargs[param]) self.view = 'standard' self.keymap_layouts['standard'] = self.build_layout( self.keymap['standard']) self.keymap_layouts['caps'] = self.build_layout(self.keymap['caps']) self.keymap_layouts['alt'] = self.build_layout(self.keymap['alt']) # Initialize layout with standard view self.layout = self.keymap_layouts['standard'] super().__init__(self.layout) self.area = area def generator(self, area=None, forceRegenerate=False, skipNonLayoutGen=False): """ This generator is a bit special : we don't want it to regenerate everything everytime we change view. So we will generate all the views at once the first time. Then, unless asked to, we will only return the appropriate image. """ isStDefined = self.keymap_imgs['standard'] isCaDefined = self.keymap_imgs['caps'] isAlDefined = self.keymap_imgs['alt'] areAllDefined = isStDefined and isCaDefined and isAlDefined if forceRegenerate or (not areAllDefined): print("[PSSM OSK] Regenration started") # Let's create all the Images # Standard view is created last, because it is the one which is to # be displayed def generateLayout(name): self.layout = self.keymap_layouts[name] self.keymap_imgs[name] = super(OSK, self).generator(area=area) generateLayout("caps") generateLayout("alt") generateLayout("standard") self.imgData = self.keymap_imgs[self.view] return self.keymap_imgs[self.view] def build_layout(self, keymap): oskLayout = [] spacing = keymap["spacing"] for row in keymap["rows"]: buttonRow = ["?", (None, spacing)] for key in row: label = self.getKeyLabel(key) color_condition = key["keyType"] != KTstandardChar background_color = "gray12" if color_condition else "white" outline_color = "white" if key["isPadding"] else "black" willChangeLayout = key["keyType"] in [ KTcapsLock, KTalt, KTcarriageReturn ] invertOnClick = False if willChangeLayout else True buttonElt = Button( text=label, font_size="H*0.02", background_color=background_color, outline_color=outline_color, onclickInside=self.handleKeyPress, user_data=key, wrap_textOverflow=False, invertOnClick=invertOnClick ) key_width = key["keyWidth"] buttonRow.append((buttonElt, key_width)) buttonRow.append((None, spacing)) oskLayout.append(buttonRow) oskLayout.append([spacing]) return oskLayout def handleKeyPress(self, elt, coords): keyType = elt.user_data["keyType"] keyChar = elt.user_data["char"] if keyType == KTcapsLock: # In this particular case, we can assume the keyboard will always # be on top. # Therefore, no need to print everything self.view = 'caps' if self.view != 'caps' else 'standard' self.layout = self.keymap_layouts[self.view] self.imgData = self.keymap_imgs[self.view] self.parentPSSMScreen.simplePrintElt(self) elif keyType == KTalt: # In this particular case, we can assume the keyboard will always # be on top # Therefore, no need to print everything self.view = 'alt' if self.view != 'alt' else 'standard' self.layout = self.keymap_layouts[self.view] self.imgData = self.keymap_imgs[self.view] self.parentPSSMScreen.simplePrintElt(self) if self.onKeyPress: self.onKeyPress(keyType, keyChar) def getKeyLabel(self, key): kt = key["keyType"] if kt == KTstandardChar: return key["char"] elif kt == KTalt: return "ALT" elif kt == KTbackspace: return "BACK" elif kt == KTcapsLock: return "CAPS" elif kt == KTcarriageReturn: return "RET" elif kt == KTcontrol: return "CTRL" elif kt == KTdelete: return "DEL" return ""
Ancestors
Methods
def build_layout(self, keymap)
-
Expand source code
def build_layout(self, keymap): oskLayout = [] spacing = keymap["spacing"] for row in keymap["rows"]: buttonRow = ["?", (None, spacing)] for key in row: label = self.getKeyLabel(key) color_condition = key["keyType"] != KTstandardChar background_color = "gray12" if color_condition else "white" outline_color = "white" if key["isPadding"] else "black" willChangeLayout = key["keyType"] in [ KTcapsLock, KTalt, KTcarriageReturn ] invertOnClick = False if willChangeLayout else True buttonElt = Button( text=label, font_size="H*0.02", background_color=background_color, outline_color=outline_color, onclickInside=self.handleKeyPress, user_data=key, wrap_textOverflow=False, invertOnClick=invertOnClick ) key_width = key["keyWidth"] buttonRow.append((buttonElt, key_width)) buttonRow.append((None, spacing)) oskLayout.append(buttonRow) oskLayout.append([spacing]) return oskLayout
def generator(self, area=None, forceRegenerate=False, skipNonLayoutGen=False)
-
This generator is a bit special : we don't want it to regenerate everything everytime we change view. So we will generate all the views at once the first time. Then, unless asked to, we will only return the appropriate image.
Expand source code
def generator(self, area=None, forceRegenerate=False, skipNonLayoutGen=False): """ This generator is a bit special : we don't want it to regenerate everything everytime we change view. So we will generate all the views at once the first time. Then, unless asked to, we will only return the appropriate image. """ isStDefined = self.keymap_imgs['standard'] isCaDefined = self.keymap_imgs['caps'] isAlDefined = self.keymap_imgs['alt'] areAllDefined = isStDefined and isCaDefined and isAlDefined if forceRegenerate or (not areAllDefined): print("[PSSM OSK] Regenration started") # Let's create all the Images # Standard view is created last, because it is the one which is to # be displayed def generateLayout(name): self.layout = self.keymap_layouts[name] self.keymap_imgs[name] = super(OSK, self).generator(area=area) generateLayout("caps") generateLayout("alt") generateLayout("standard") self.imgData = self.keymap_imgs[self.view] return self.keymap_imgs[self.view]
def getKeyLabel(self, key)
-
Expand source code
def getKeyLabel(self, key): kt = key["keyType"] if kt == KTstandardChar: return key["char"] elif kt == KTalt: return "ALT" elif kt == KTbackspace: return "BACK" elif kt == KTcapsLock: return "CAPS" elif kt == KTcarriageReturn: return "RET" elif kt == KTcontrol: return "CTRL" elif kt == KTdelete: return "DEL" return ""
def handleKeyPress(self, elt, coords)
-
Expand source code
def handleKeyPress(self, elt, coords): keyType = elt.user_data["keyType"] keyChar = elt.user_data["char"] if keyType == KTcapsLock: # In this particular case, we can assume the keyboard will always # be on top. # Therefore, no need to print everything self.view = 'caps' if self.view != 'caps' else 'standard' self.layout = self.keymap_layouts[self.view] self.imgData = self.keymap_imgs[self.view] self.parentPSSMScreen.simplePrintElt(self) elif keyType == KTalt: # In this particular case, we can assume the keyboard will always # be on top # Therefore, no need to print everything self.view = 'alt' if self.view != 'alt' else 'standard' self.layout = self.keymap_layouts[self.view] self.imgData = self.keymap_imgs[self.view] self.parentPSSMScreen.simplePrintElt(self) if self.onKeyPress: self.onKeyPress(keyType, keyChar)
Inherited members
class PSSMScreen (deviceName, name='screen', stack=[], isInverted=False)
-
This is the class which handles most of the logic.
Args
deviceName
:str
- "Kobo" for Kobo ereaders (and probably all FBInk supported devices)
name
:str
- The name of the class instance (deprecated, I will eventually remove it)
stack
:list
- Do not use it unless you know what you are doing. The list of all the pssm Elements which are on the screen.
isInverted
:bool
- …
Attributes
device
- the device.py module.
You can with it access a few useful functions like
self.device.readBatteryPercentage()
colorType
- "L" for grayscale devices, "RGBA" for others
width
- The screen width
height
:The screen height
view_width
- The width of the screen portion not hidden behind bezels
view_height
- …
name
- …
isInverted
- …
Example of usage: screen = pssm.PSSMScreen("Kobo","Main"))
Expand source code
class PSSMScreen: """ This is the class which handles most of the logic. Args: deviceName (str): "Kobo" for Kobo ereaders (and probably all FBInk supported devices) name (str): The name of the class instance (deprecated, I will eventually remove it) stack (list): Do not use it unless you know what you are doing. The list of all the pssm Elements which are on the screen. isInverted (bool): ... Attributes: device: the device.py module. You can with it access a few useful functions like `self.device.readBatteryPercentage()` colorType: "L" for grayscale devices, "RGBA" for others width: The screen width height : The screen height view_width: The width of the screen portion not hidden behind bezels view_height: ... name: ... isInverted: ... Example of usage: screen = pssm.PSSMScreen("Kobo","Main")) """ def __init__(self, deviceName, name="screen", stack=[], isInverted=False): self.name = name if deviceName == "Kobo": import devices.kobo.device as pssm_device else: import devices.emulator.device as pssm_device self.device = pssm_device self.colorType = self.device.colorType self.width = self.device.screen_width self.height = self.device.screen_height self.view_width = self.device.view_width self.view_height = self.device.view_height self.w_offset = self.device.w_offset self.h_offset = self.device.h_offset self.area = [(0, 0), (self.view_width, self.view_height)] self.stack = stack self.isInverted = isInverted self.isInputThreadStarted = False self.lastX = -1 self.lastY = -1 self.osk = None self.numberEltOnTop = 0 self.isOSKShown = False self.isBatch = False def findEltWithId(self, myElementId, stack=None): """ (Deprecated) Returns the element which has such an ID. Avoid using this function as much as possible: it has terrible performance. (Recursive search through all the elements of the stack) And anyway there is no reason why you should use it. """ if stack is None: stack = self.stack for elt in stack: if elt.id == myElementId: return elt elif elt.isLayout: layoutEltList = elt.createEltList() search = self.findEltWithId(myElementId, stack=layoutEltList) if search is not None: return search return None def printStack(self, area=None, forceLayoutGen=False): """ Prints the stack Elements in the stack order If a area is set, then, we only display the part of the stack which is in this area """ if self.isBatch: # Do not do anything during batch mode return None pil_image = self.capture(area=area, forceLayoutGen=forceLayoutGen) if area: [(x, y), (w, h)] = area else: [(x, y), (w, h)] = self.area self.device.print_pil(pil_image, x, y, isInverted=self.isInverted) def capture(self,area=None, forceLayoutGen=False): """ Returns a screen capture of the current stack state. """ white = get_Color("white", self.colorType) dim = (self.width, self.height) img = Image.new(self.colorType, dim, color=white) for elt in self.stack: [(x, y), (w, h)] = elt.area if elt.isLayout and forceLayoutGen: elt.generator(area=elt.area, skipNonLayoutGen=True) if elt.isInverted: pil_image = ImageOps.invert(elt.imgData) else: pil_image = elt.imgData img.paste(pil_image, (x, y)) if area: [(x, y), (w, h)] = area box = (x, y, x+w, y+h) return img.crop(box=box) else: return img def simplePrintElt(self, myElement, skipGen=False): """ Prints the Element without adding it to the stack. Does not honor isBatch (you can simplePrint even during batch mode) Args: myElement (PSSM Element): The element you want to display skipGen (bool): Do you want to regenerate the image? """ if not skipGen: # First, the element must be generated myElement.generator() # Then, we print it [(x, y), (w, h)] = myElement.area # What follows is a Workaround : self.device.print_pil( myElement.imgData, x, y, isInverted=myElement.isInverted ) def startBatchWriting(self): """ Toggle batch writing: nothing will be displayed on the screen until you use screen.stopBatchWriting() """ self.isBatch = True def stopBatchWriting(self): """ Updates the screen after batch writing """ self.isBatch = False self.printStack(area=self.area, forceLayoutGen=True) def addElt(self, myElement, skipPrint=False, skipRegistration=False): """ Adds Element to the stack and prints it myElement (PSSM Element): The Element you want to add skipPrint (bool): True if you don't want to update the screen skipRegistration (bool): True if you don't want to add the Element to the stack """ for i in range(len(self.stack)): elt = self.stack[i] if elt.id == myElement.id: # There is already an Element in the stack with the same ID. # Let's update the Element in the stack if not skipPrint: self.stack[i] = myElement self.printStack(area=myElement.area) break # The Element is already in the stack else: # the Element is not already in the stack if not skipRegistration: # We append the element to the stack myElement.parentPSSMScreen = self if self.numberEltOnTop > 0: # There is something on top, addnig it at position -2 # (before the last one) pos = - 1 - self.numberEltOnTop self.stack.insert(pos, myElement) else: self.stack.append(myElement) if not skipPrint : if self.numberEltOnTop > 0 and not self.forcePrintOnTop: # TODO : make it faster, we only need to display the # image behind the keyboard, not reprint everything myElement.generator() self.printStack(area=myElement.area) else: # No keyboard on the horizon, let's do it if self.isBatch: # Then we only generate myElement.generator() else: myElement.generator() self.simplePrintElt(myElement, skipGen=True) def removeElt(self, elt=None, eltid=None, skipPrint=False): """ Removes the Element from the stack and hides it from the screen """ if elt: self.stack.remove(elt) if not skipPrint: self.printStack(area=elt.area) elif eltid: elt = self.findEltWithId(eltid) if elt: self.stack.remove(elt) if not skipPrint: self.printStack(area=elt.area) else: print('No element given') def getStackLevel(self, myElementId): elt = self.findEltWithId(myElementId) return self.stack.index(elt) def setStackLevel(self, elt, stackLevel="last"): """ Set the position of said Element Then prints every Element above it (including itself) """ # TODO : Must be able to accept another stackLevel if stackLevel == "last" or stackLevel == -1: stackLevel = len(self.stack) self.removeElt(elt, skipPrint=True) self.stack.insert(stackLevel, elt) self.printStack(area=[elt.xy, elt.xy2]) return True def invertElt(self, elt, invertDuration=-1, useFastPrint=True, skipPrint=False): """ Inverts an Element Args: elt (Element): The PSSM Element to invert invertDuration (int) : -1 or 0 if permanent, else an integer skipPrint (bool): Save only or save + print? useFastPrint (bool): Use FBInk's partial refresh with nightmode (much faster) instead of printing the whole stack. """ if elt is None: print("No element given") return False # First, let's get the Element's initial inverted state Element_initial_state = bool(elt.isInverted) elt.isInverted = not Element_initial_state if not skipPrint: if useFastPrint: # Run as thread to make things a bit faster args = [ elt.area, invertDuration, not Element_initial_state ] invertThread = threading.Thread( target=self._invertArea_helper, args=args ) invertThread.start() # self._invertArea_helper(elt.area, invertDuration, True) else: elt.update() elt.isInverted = Element_initial_state def _invertArea_helper(self, area, invertDuration, isInverted=False): """ Helper function to properly setup the timer. """ # TODO: To be tested initial_mode = isInverted isTemporaryinvertion = bool(invertDuration > 0) self.device.do_screen_refresh( isInverted=isInverted, area=area, isInvertionPermanent=False, isFlashing=False, useFastInvertion=True ) if isTemporaryinvertion: # Now we call this funcion, without starting a timer # And the screen is now in an opposite state as the initial one myTimer = threading.Timer( interval=invertDuration, function=self._invertArea_helper, args=[area, -1, not initial_mode] ) myTimer.start() return True def invert(self): """ Inverts the whole screen """ self.isInverted = not self.isInverted self.device.do_screen_refresh(self.isInverted) return True def refresh(self): """ Refreshes the screeen """ self.device.do_screen_refresh() return True def clear(self): """ Clears the screen """ self.device.do_screen_clear() return True def OSKInit(self, onKeyPress=None, area=None, keymapPath=None): if not area: x = 0 y = int(2*self.view_height/3) w = self.view_width h = int(self.view_height/3) area = [(x, y), (w, h)] self.osk = OSK(onKeyPress=onKeyPress, area=area, keymapPath=keymapPath) def OSKShow(self, onKeyPress=None): if not self.osk: print("OSK not initialized, it can't be shown") return None if onKeyPress: self.osk.onKeyPress = onKeyPress self.addElt(self.osk) # It has already been generated self.numberEltOnTop += 1 self.isOSKShown = True def OSKHide(self): if self.isOSKShown: self.removeElt(elt=self.osk) self.numberEltOnTop -= 1 self.isOSKShown = False def startListenerThread(self, grabInput=False): """ Starts the touch listener as a separate thread Args: grabInput (boolean): Do an EVIOCGRAB IOCTL call to prevent any other software from registering touch events """ self.isInputThreadStarted = True self.device.isInputThreadStarted = True print("[PSSM - Touch handler] : Input thread started") args = [self._clickHandler, True, grabInput] inputThread = threading.Thread( target=self.device.eventBindings, args=args ) inputThread.start() def _clickHandler(self, x, y): n = len(self.stack) for i in range(n): j = n-1-i # We go through the stack in descending order elt = self.stack[j] if elt.area is None: # An object without area, it should not happen, but if it does, # it can be skipped continue if coordsInArea(x, y, elt.area): if elt.onclickInside is not None: self.lastX = x self.lastY = y if elt is not None: self._dispatchClickToElt((x, y), elt) break def _dispatchClickToElt(self, coords, elt): """ Once given an object on which the user clicked, this function calls the appropriate function on the object (ie elt.onclickInside or elt._dispatchClick) It also handles invertion. """ if elt.isLayout: if elt.onclickInside is not None: elt.onclickInside(elt, coords) if elt.invertOnClick: self.invertElt(elt, elt.invertDuration) elt._dispatchClick(coords) else: if elt.invertOnClick: self.invertElt(elt, elt.invertDuration) # Execute PSSM action on click elt.pssmOnClickInside(coords) # Execute user action attached to it too elt.onclickInside(elt, coords) def stopListenerThread(self): self.isInputThreadStarted = False self.device.isInputThreadStarted = False print("[PSSM - Touch handler] : Input thread stopped")
Methods
def OSKHide(self)
-
Expand source code
def OSKHide(self): if self.isOSKShown: self.removeElt(elt=self.osk) self.numberEltOnTop -= 1 self.isOSKShown = False
def OSKInit(self, onKeyPress=None, area=None, keymapPath=None)
-
Expand source code
def OSKInit(self, onKeyPress=None, area=None, keymapPath=None): if not area: x = 0 y = int(2*self.view_height/3) w = self.view_width h = int(self.view_height/3) area = [(x, y), (w, h)] self.osk = OSK(onKeyPress=onKeyPress, area=area, keymapPath=keymapPath)
def OSKShow(self, onKeyPress=None)
-
Expand source code
def OSKShow(self, onKeyPress=None): if not self.osk: print("OSK not initialized, it can't be shown") return None if onKeyPress: self.osk.onKeyPress = onKeyPress self.addElt(self.osk) # It has already been generated self.numberEltOnTop += 1 self.isOSKShown = True
def addElt(self, myElement, skipPrint=False, skipRegistration=False)
-
Adds Element to the stack and prints it myElement (PSSM Element): The Element you want to add skipPrint (bool): True if you don't want to update the screen skipRegistration (bool): True if you don't want to add the Element to the stack
Expand source code
def addElt(self, myElement, skipPrint=False, skipRegistration=False): """ Adds Element to the stack and prints it myElement (PSSM Element): The Element you want to add skipPrint (bool): True if you don't want to update the screen skipRegistration (bool): True if you don't want to add the Element to the stack """ for i in range(len(self.stack)): elt = self.stack[i] if elt.id == myElement.id: # There is already an Element in the stack with the same ID. # Let's update the Element in the stack if not skipPrint: self.stack[i] = myElement self.printStack(area=myElement.area) break # The Element is already in the stack else: # the Element is not already in the stack if not skipRegistration: # We append the element to the stack myElement.parentPSSMScreen = self if self.numberEltOnTop > 0: # There is something on top, addnig it at position -2 # (before the last one) pos = - 1 - self.numberEltOnTop self.stack.insert(pos, myElement) else: self.stack.append(myElement) if not skipPrint : if self.numberEltOnTop > 0 and not self.forcePrintOnTop: # TODO : make it faster, we only need to display the # image behind the keyboard, not reprint everything myElement.generator() self.printStack(area=myElement.area) else: # No keyboard on the horizon, let's do it if self.isBatch: # Then we only generate myElement.generator() else: myElement.generator() self.simplePrintElt(myElement, skipGen=True)
def capture(self, area=None, forceLayoutGen=False)
-
Returns a screen capture of the current stack state.
Expand source code
def capture(self,area=None, forceLayoutGen=False): """ Returns a screen capture of the current stack state. """ white = get_Color("white", self.colorType) dim = (self.width, self.height) img = Image.new(self.colorType, dim, color=white) for elt in self.stack: [(x, y), (w, h)] = elt.area if elt.isLayout and forceLayoutGen: elt.generator(area=elt.area, skipNonLayoutGen=True) if elt.isInverted: pil_image = ImageOps.invert(elt.imgData) else: pil_image = elt.imgData img.paste(pil_image, (x, y)) if area: [(x, y), (w, h)] = area box = (x, y, x+w, y+h) return img.crop(box=box) else: return img
def clear(self)
-
Clears the screen
Expand source code
def clear(self): """ Clears the screen """ self.device.do_screen_clear() return True
def findEltWithId(self, myElementId, stack=None)
-
(Deprecated) Returns the element which has such an ID. Avoid using this function as much as possible: it has terrible performance. (Recursive search through all the elements of the stack) And anyway there is no reason why you should use it.
Expand source code
def findEltWithId(self, myElementId, stack=None): """ (Deprecated) Returns the element which has such an ID. Avoid using this function as much as possible: it has terrible performance. (Recursive search through all the elements of the stack) And anyway there is no reason why you should use it. """ if stack is None: stack = self.stack for elt in stack: if elt.id == myElementId: return elt elif elt.isLayout: layoutEltList = elt.createEltList() search = self.findEltWithId(myElementId, stack=layoutEltList) if search is not None: return search return None
def getStackLevel(self, myElementId)
-
Expand source code
def getStackLevel(self, myElementId): elt = self.findEltWithId(myElementId) return self.stack.index(elt)
def invert(self)
-
Inverts the whole screen
Expand source code
def invert(self): """ Inverts the whole screen """ self.isInverted = not self.isInverted self.device.do_screen_refresh(self.isInverted) return True
def invertElt(self, elt, invertDuration=-1, useFastPrint=True, skipPrint=False)
-
Inverts an Element
Args
elt
:Element
- The PSSM Element to invert
- invertDuration (int) : -1 or 0 if permanent, else an integer
skipPrint
:bool
- Save only or save + print?
useFastPrint
:bool
- Use FBInk's partial refresh with nightmode (much faster) instead of printing the whole stack.
Expand source code
def invertElt(self, elt, invertDuration=-1, useFastPrint=True, skipPrint=False): """ Inverts an Element Args: elt (Element): The PSSM Element to invert invertDuration (int) : -1 or 0 if permanent, else an integer skipPrint (bool): Save only or save + print? useFastPrint (bool): Use FBInk's partial refresh with nightmode (much faster) instead of printing the whole stack. """ if elt is None: print("No element given") return False # First, let's get the Element's initial inverted state Element_initial_state = bool(elt.isInverted) elt.isInverted = not Element_initial_state if not skipPrint: if useFastPrint: # Run as thread to make things a bit faster args = [ elt.area, invertDuration, not Element_initial_state ] invertThread = threading.Thread( target=self._invertArea_helper, args=args ) invertThread.start() # self._invertArea_helper(elt.area, invertDuration, True) else: elt.update() elt.isInverted = Element_initial_state
def printStack(self, area=None, forceLayoutGen=False)
-
Prints the stack Elements in the stack order If a area is set, then, we only display the part of the stack which is in this area
Expand source code
def printStack(self, area=None, forceLayoutGen=False): """ Prints the stack Elements in the stack order If a area is set, then, we only display the part of the stack which is in this area """ if self.isBatch: # Do not do anything during batch mode return None pil_image = self.capture(area=area, forceLayoutGen=forceLayoutGen) if area: [(x, y), (w, h)] = area else: [(x, y), (w, h)] = self.area self.device.print_pil(pil_image, x, y, isInverted=self.isInverted)
def refresh(self)
-
Refreshes the screeen
Expand source code
def refresh(self): """ Refreshes the screeen """ self.device.do_screen_refresh() return True
def removeElt(self, elt=None, eltid=None, skipPrint=False)
-
Removes the Element from the stack and hides it from the screen
Expand source code
def removeElt(self, elt=None, eltid=None, skipPrint=False): """ Removes the Element from the stack and hides it from the screen """ if elt: self.stack.remove(elt) if not skipPrint: self.printStack(area=elt.area) elif eltid: elt = self.findEltWithId(eltid) if elt: self.stack.remove(elt) if not skipPrint: self.printStack(area=elt.area) else: print('No element given')
def setStackLevel(self, elt, stackLevel='last')
-
Set the position of said Element Then prints every Element above it (including itself)
Expand source code
def setStackLevel(self, elt, stackLevel="last"): """ Set the position of said Element Then prints every Element above it (including itself) """ # TODO : Must be able to accept another stackLevel if stackLevel == "last" or stackLevel == -1: stackLevel = len(self.stack) self.removeElt(elt, skipPrint=True) self.stack.insert(stackLevel, elt) self.printStack(area=[elt.xy, elt.xy2]) return True
def simplePrintElt(self, myElement, skipGen=False)
-
Prints the Element without adding it to the stack. Does not honor isBatch (you can simplePrint even during batch mode)
Args
myElement
:PSSM Element
- The element you want to display
skipGen
:bool
- Do you want to regenerate the image?
Expand source code
def simplePrintElt(self, myElement, skipGen=False): """ Prints the Element without adding it to the stack. Does not honor isBatch (you can simplePrint even during batch mode) Args: myElement (PSSM Element): The element you want to display skipGen (bool): Do you want to regenerate the image? """ if not skipGen: # First, the element must be generated myElement.generator() # Then, we print it [(x, y), (w, h)] = myElement.area # What follows is a Workaround : self.device.print_pil( myElement.imgData, x, y, isInverted=myElement.isInverted )
def startBatchWriting(self)
-
Toggle batch writing: nothing will be displayed on the screen until you use screen.stopBatchWriting()
Expand source code
def startBatchWriting(self): """ Toggle batch writing: nothing will be displayed on the screen until you use screen.stopBatchWriting() """ self.isBatch = True
def startListenerThread(self, grabInput=False)
-
Starts the touch listener as a separate thread
Args
grabInput
:boolean
- Do an EVIOCGRAB IOCTL call to prevent any other software from registering touch events
Expand source code
def startListenerThread(self, grabInput=False): """ Starts the touch listener as a separate thread Args: grabInput (boolean): Do an EVIOCGRAB IOCTL call to prevent any other software from registering touch events """ self.isInputThreadStarted = True self.device.isInputThreadStarted = True print("[PSSM - Touch handler] : Input thread started") args = [self._clickHandler, True, grabInput] inputThread = threading.Thread( target=self.device.eventBindings, args=args ) inputThread.start()
def stopBatchWriting(self)
-
Updates the screen after batch writing
Expand source code
def stopBatchWriting(self): """ Updates the screen after batch writing """ self.isBatch = False self.printStack(area=self.area, forceLayoutGen=True)
def stopListenerThread(self)
-
Expand source code
def stopListenerThread(self): self.isInputThreadStarted = False self.device.isInputThreadStarted = False print("[PSSM - Touch handler] : Input thread stopped")
class Popup (layout=[], width='W*0.8', height='H*0.5', xPos=0.5, yPos=0.3, **kwargs)
-
A popup to be displayed above everything else, to simple ask a question
Args
layout
:list
- The list of PSSMElements to be displayed. cf Layout
width
:str
- The width of the popup
height
:str
- The height of the popup
xPos
:float
- Relative position on the x axis of the center point
yPos
:float
- Relative position on the y axis of the center point
Expand source code
class Popup(Layout): """ A popup to be displayed above everything else, to simple ask a question Args: layout (list): The list of PSSMElements to be displayed. cf Layout width (str): The width of the popup height (str): The height of the popup xPos (float): Relative position on the x axis of the center point yPos (float): Relative position on the y axis of the center point """ def __init__(self, layout=[], width="W*0.8", height="H*0.5", xPos=0.5, yPos=0.3, **kwargs): super().__init__(layout=layout) self.width = width self.height = height self.xPos = xPos self.yPos = yPos for param in kwargs: setattr(self, param, kwargs[param]) def make_area(self): w = self.convertDimension(self.width) h = self.convertDimension(self.height) x = self.convertDimension("W*" + str(self.xPos)) - int(0.5*w) y = self.convertDimension("H*" + str(self.yPos)) - int(0.5*h) self.area = [(x, y), (w, h)] return self.area
Ancestors
Subclasses
Methods
def make_area(self)
-
Expand source code
def make_area(self): w = self.convertDimension(self.width) h = self.convertDimension(self.height) x = self.convertDimension("W*" + str(self.xPos)) - int(0.5*w) y = self.convertDimension("H*" + str(self.yPos)) - int(0.5*h) self.area = [(x, y), (w, h)] return self.area
Inherited members
class PopupConfirm (titleText='', mainText='', confirmText='OK', cancelText='Cancel', titleFont='default', titleFontSize='H*0.036', mainFont='default', mainFontSize='H*0.036', confirmFont='default', confirmFontSize='H*0.036', titleFontColor='black', mainFontColor='black', confirmFontColor='black', mainTextXPos='center', mainTextYPos='center', **kwargs)
-
A popup to be displayed above everything else, to simple ask a question
Args
layout
:list
- The list of PSSMElements to be displayed. cf Layout
width
:str
- The width of the popup
height
:str
- The height of the popup
xPos
:float
- Relative position on the x axis of the center point
yPos
:float
- Relative position on the y axis of the center point
Expand source code
class PopupConfirm(Popup): def __init__(self, titleText="", mainText="", confirmText="OK", cancelText="Cancel", titleFont=DEFAULT_FONT, titleFontSize=DEFAULT_FONT_SIZE, mainFont=DEFAULT_FONT, mainFontSize=DEFAULT_FONT_SIZE, confirmFont=DEFAULT_FONT, confirmFontSize=DEFAULT_FONT_SIZE, titleFontColor="black", mainFontColor="black", confirmFontColor="black", mainTextXPos="center", mainTextYPos="center", **kwargs): super().__init__() self.titleText = titleText self.mainText = mainText self.confirmText = confirmText self.cancelText = cancelText self.titleFont = titleFont self.mainFont = mainFont self.confirmFont = confirmFont self.titleFontSize = titleFontSize self.mainFontSize = mainFontSize self.confirmFontSize = confirmFontSize self.titleFontColor = titleFontColor self.mainFontColor = mainFontColor self.confirmFontColor = confirmFontColor self.mainTextXPos = mainTextXPos self.mainTextYPos = mainTextYPos self.userAction = 0 self.okBtn = None self.cancelBtn = None for param in kwargs: setattr(self, param, kwargs[param]) self.build_layout() def generator(self,**kwargs): self.make_area() super().generator(**kwargs) def build_layout(self): titleBtn = Button( text=self.titleText, font=self.titleFont, font_size=self.titleFontSize, font_color=self.titleFontColor ) mainBtn = Button( text=self.mainText, font=self.mainFont, font_size=self.mainFontSize, font_color=self.mainFontColor, text_xPosition=self.mainTextXPos, text_yPosition=self.mainTextYPos ) okBtn = Button( text=self.confirmText, font=self.confirmFont, font_size=self.confirmFontSize, font_color=self.confirmFontColor, onclickInside=self.confirm ) cancelBtn = Button( text=self.cancelText, font=self.confirmFont, font_size=self.confirmFontSize, font_color=self.confirmFontColor, onclickInside=self.cancel ) lM = (None,1) layout = [ ["?*1.5", (titleBtn, "?"), lM], ["?*3", (mainBtn, "?"), lM], ["?*1", (okBtn, "?"), (cancelBtn, "?"), lM] ] self.layout = layout return layout def confirm(self, elt=None, coords=None): self.userAction = 1 def cancel(self,elt=None, coords=None): self.userAction = 2 def waitForResponse(self): while self.userAction == 0: self.parentPSSMScreen.device.wait(0.01) self.parentPSSMScreen.OSKHide() hasConfirmed = self.userAction == 1 self.userAction = 0 # Reset the state self.parentPSSMScreen.removeElt(self) return hasConfirmed
Ancestors
Methods
def build_layout(self)
-
Expand source code
def build_layout(self): titleBtn = Button( text=self.titleText, font=self.titleFont, font_size=self.titleFontSize, font_color=self.titleFontColor ) mainBtn = Button( text=self.mainText, font=self.mainFont, font_size=self.mainFontSize, font_color=self.mainFontColor, text_xPosition=self.mainTextXPos, text_yPosition=self.mainTextYPos ) okBtn = Button( text=self.confirmText, font=self.confirmFont, font_size=self.confirmFontSize, font_color=self.confirmFontColor, onclickInside=self.confirm ) cancelBtn = Button( text=self.cancelText, font=self.confirmFont, font_size=self.confirmFontSize, font_color=self.confirmFontColor, onclickInside=self.cancel ) lM = (None,1) layout = [ ["?*1.5", (titleBtn, "?"), lM], ["?*3", (mainBtn, "?"), lM], ["?*1", (okBtn, "?"), (cancelBtn, "?"), lM] ] self.layout = layout return layout
def cancel(self, elt=None, coords=None)
-
Expand source code
def cancel(self,elt=None, coords=None): self.userAction = 2
def confirm(self, elt=None, coords=None)
-
Expand source code
def confirm(self, elt=None, coords=None): self.userAction = 1
def waitForResponse(self)
-
Expand source code
def waitForResponse(self): while self.userAction == 0: self.parentPSSMScreen.device.wait(0.01) self.parentPSSMScreen.OSKHide() hasConfirmed = self.userAction == 1 self.userAction = 0 # Reset the state self.parentPSSMScreen.removeElt(self) return hasConfirmed
Inherited members
class PoputInput (titleText='', mainText='', confirmText='OK', titleFont='default', titleFontSize='H*0.036', mainFont='default', mainFontSize='H*0.036', inputFont='default', inputFontSize='H*0.036', confirmFont='default', confirmFontSize='H*0.036', titleFontColor='black', mainFontColor='black', inputFontColor='black', confirmFontColor='black', mainTextXPos='center', mainTextYPos='center', isMultiline=False, **kwargs)
-
A popup to be displayed above everything else, to simple ask a question
Args
layout
:list
- The list of PSSMElements to be displayed. cf Layout
width
:str
- The width of the popup
height
:str
- The height of the popup
xPos
:float
- Relative position on the x axis of the center point
yPos
:float
- Relative position on the y axis of the center point
Expand source code
class PoputInput(Popup): def __init__(self, titleText="", mainText="", confirmText="OK", titleFont=DEFAULT_FONT, titleFontSize=DEFAULT_FONT_SIZE, mainFont=DEFAULT_FONT, mainFontSize=DEFAULT_FONT_SIZE, inputFont=DEFAULT_FONT, inputFontSize=DEFAULT_FONT_SIZE, confirmFont=DEFAULT_FONT, confirmFontSize=DEFAULT_FONT_SIZE, titleFontColor="black", mainFontColor="black", inputFontColor="black", confirmFontColor="black", mainTextXPos="center", mainTextYPos="center", isMultiline=False, **kwargs): super().__init__() self.titleText = titleText self.mainText = mainText self.confirmText = confirmText self.isMultiline = isMultiline self.titleFont = titleFont self.mainFont = mainFont self.inputFont = inputFont self.confirmFont = confirmFont self.titleFontSize = titleFontSize self.mainFontSize = mainFontSize self.inputFontSize = inputFontSize self.confirmFontSize = confirmFontSize self.titleFontColor = titleFontColor self.mainFontColor = mainFontColor self.inputFontColor = inputFontColor self.confirmFontColor = confirmFontColor self.mainTextXPos = mainTextXPos self.mainTextYPos = mainTextYPos self.userConfirmed = False self.inputBtn = None self.okBtn = None for param in kwargs: setattr(self, param, kwargs[param]) self.build_layout() def generator(self,**kwargs): self.make_area() super().generator(**kwargs) def build_layout(self): titleBtn = Button( text=self.titleText, font=self.titleFont, font_size=self.titleFontSize, font_color=self.titleFontColor ) mainBtn = Button( text=self.mainText, font=self.mainFont, font_size=self.mainFontSize, font_color=self.mainFontColor, text_xPosition=self.mainTextXPos, text_yPosition=self.mainTextYPos ) if self.isMultiline: onReturn = returnFalse else: onReturn = self.toggleConfirmation inputBtn = Input( font=self.inputFont, font_size=self.inputFontSize, font_color=self.inputFontColor, isMultiline=self.isMultiline, onReturn=onReturn ) okBtn = Button( text=self.confirmText, font=self.confirmFont, font_size=self.confirmFontSize, font_color=self.confirmFontColor, onclickInside=self.toggleConfirmation ) self.inputBtn = inputBtn lM = (None,1) layout = [ ["?*1.5", (titleBtn, "?"), lM], ["?*3", (mainBtn, "?"), lM], ["?*2", (inputBtn, "?"), lM], ["?*1", (okBtn, "?"), lM] ] self.layout = layout return layout def toggleConfirmation(self, elt=None, coords=None): print("Toggling confirmation") self.userConfirmed = True def waitForResponse(self): while not self.userConfirmed: self.parentPSSMScreen.device.wait(0.01) self.parentPSSMScreen.OSKHide() input = self.inputBtn.getInput() self.userConfirmed = False # Reset the state self.parentPSSMScreen.removeElt(self) return input
Ancestors
Methods
def build_layout(self)
-
Expand source code
def build_layout(self): titleBtn = Button( text=self.titleText, font=self.titleFont, font_size=self.titleFontSize, font_color=self.titleFontColor ) mainBtn = Button( text=self.mainText, font=self.mainFont, font_size=self.mainFontSize, font_color=self.mainFontColor, text_xPosition=self.mainTextXPos, text_yPosition=self.mainTextYPos ) if self.isMultiline: onReturn = returnFalse else: onReturn = self.toggleConfirmation inputBtn = Input( font=self.inputFont, font_size=self.inputFontSize, font_color=self.inputFontColor, isMultiline=self.isMultiline, onReturn=onReturn ) okBtn = Button( text=self.confirmText, font=self.confirmFont, font_size=self.confirmFontSize, font_color=self.confirmFontColor, onclickInside=self.toggleConfirmation ) self.inputBtn = inputBtn lM = (None,1) layout = [ ["?*1.5", (titleBtn, "?"), lM], ["?*3", (mainBtn, "?"), lM], ["?*2", (inputBtn, "?"), lM], ["?*1", (okBtn, "?"), lM] ] self.layout = layout return layout
def toggleConfirmation(self, elt=None, coords=None)
-
Expand source code
def toggleConfirmation(self, elt=None, coords=None): print("Toggling confirmation") self.userConfirmed = True
def waitForResponse(self)
-
Expand source code
def waitForResponse(self): while not self.userConfirmed: self.parentPSSMScreen.device.wait(0.01) self.parentPSSMScreen.OSKHide() input = self.inputBtn.getInput() self.userConfirmed = False # Reset the state self.parentPSSMScreen.removeElt(self) return input
Inherited members
class Rectangle (background_color='white', outline_color='gray3', parentPSSMScreen=None)
-
A rectangle
Args
background_color
:str
- The background color
outline_color
:str
- The border color
Expand source code
class Rectangle(Element): """ A rectangle Args: background_color (str): The background color outline_color (str): The border color """ def __init__(self, background_color="white", outline_color="gray3", parentPSSMScreen=None): super().__init__() self.background_color = background_color self.outline_color = outline_color self.parentPSSMScreen = parentPSSMScreen def generator(self, area): [(x, y), (w, h)] = area self.area = area colorType = self.parentPSSMScreen.colorType img = Image.new( colorType, (w, h), color=get_Color("white", colorType) ) rect = ImageDraw.Draw(img, colorType) fill_color = get_Color(self.background_color, colorType) outline_color = get_Color(self.outline_color, colorType) rect.rectangle( [(0, 0), (w-1, h-1)], fill=fill_color, outline=outline_color ) self.imgData = img return self.imgData
Ancestors
Inherited members
class RectangleRounded (radius=20, background_color='white', outline_color='gray3', parentPSSMScreen=None)
-
A rectangle, but with rounded corners
Expand source code
class RectangleRounded(Element): """ A rectangle, but with rounded corners """ def __init__(self, radius=20, background_color="white", outline_color="gray3", parentPSSMScreen=None): super().__init__() self.radius = radius self.background_color = background_color self.outline_color = outline_color self.parentPSSMScreen = parentPSSMScreen def generator(self, area): [(x, y), (w, h)] = area self.area = area colorType = self.parentPSSMScreen.colorType rectangle = Image.new( colorType, (w, h), color=get_Color("white", colorType) ) draw = ImageDraw.Draw(rectangle) draw.rectangle( [(0, 0), (w, h)], fill=get_Color(self.background_color, colorType), outline=get_Color(self.outline_color, colorType) ) draw.line( [(self.radius, h-1), (w-self.radius, h-1)], fill=get_Color(self.outline_color, colorType), width=1 ) draw.line( [(w-1, self.radius), (w-1, h-self.radius)], fill=get_Color(self.outline_color, colorType), width=1 ) corner = roundedCorner( self.radius, self.background_color, self.outline_color, self.parentPSSMScreen.colorType ) rectangle.paste(corner, (0, 0)) # Rotate the corner and paste it rectangle.paste(corner.rotate(90), (0, h - self.radius)) rectangle.paste(corner.rotate(180), (w - self.radius, h - self.radius)) rectangle.paste(corner.rotate(270), (w - self.radius, 0)) self.imgData = rectangle return self.imgData
Ancestors
Inherited members
class Static (pil_image, centered=True, resize=True, background_color='white', rotation=0, **kwargs)
-
A very simple element which only displays a pillow image
Args
pil_image
:str
orpil image
- path to an image or a pillow image
centered
:bool
- Center the image ?
resize
:bool
- Make it fit the area ? (proportions are respected)
rotation
:int
- an integer rotation angle
background_color
:str
- "white", "black", "gray0" to "gray15" or a (red, green, blue, transparency) tuple
Expand source code
class Static(Element): """ A very simple element which only displays a pillow image Args: pil_image (str or pil image): path to an image or a pillow image centered (bool): Center the image ? resize (bool): Make it fit the area ? (proportions are respected) rotation (int): an integer rotation angle background_color (str): "white", "black", "gray0" to "gray15" or a (red, green, blue, transparency) tuple """ def __init__(self, pil_image, centered=True, resize=True, background_color="white", rotation=0, **kwargs): super().__init__() if isinstance(pil_image, str): self.pil_image = Image.open(pil_image) else: self.pil_image = pil_image self.background_color = background_color self.centered = centered self.resize = resize self.rotation = rotation for param in kwargs: setattr(self, param, kwargs[param]) def generator(self, area=None): # TODO : crop or resize the image to make it fit the area (x, y), (w, h) = area colorType = self.parentPSSMScreen.colorType pil_image = self.pil_image.convert(colorType) if self.resize: r = min(w/pil_image.width, h/pil_image.height) size = (int(pil_image.width*r), int(pil_image.height*r)) pil_image = self.pil_image.resize(size) if self.rotation != 0: pil_image = pil_image.rotate(self.rotation, fillcolor=self.background_color) if not self.centered: return pil_image else: img = Image.new( colorType, (w+1, h+1), color=get_Color(self.background_color, colorType) ) x = int(0.5*w-0.5*pil_image.width) y = int(0.5*h-0.5*pil_image.height) img.paste(pil_image, (x, y)) self.imgData = img return img
Ancestors
Inherited members