diff --git a/g13gui/g13gui/bitwidgets/button.py b/g13gui/g13gui/bitwidgets/button.py index 522a127..6612dd3 100644 --- a/g13gui/g13gui/bitwidgets/button.py +++ b/g13gui/g13gui/bitwidgets/button.py @@ -4,6 +4,7 @@ from builtins import property from g13gui.bitwidgets import DISPLAY_WIDTH from g13gui.bitwidgets import DISPLAY_HEIGHT from g13gui.bitwidgets.widget import Widget +from g13gui.bitwidgets.rectangle import Rectangle from g13gui.observer.subject import ChangeType @@ -16,6 +17,9 @@ class Glyphs(enum.Enum): UP_ARROW = [(2, 4), (2, 0), (4, 2), (0, 2), (2, 0)] CHECKMARK = [(0, 3), (1, 4), (4, 1), (1, 4)] XMARK = [(0, 0), (4, 4), (2, 2), (4, 0), (0, 4)] + BLANK = [] + + BOUNDS = (5, 5) def transformTo(self, offsetx, offsety): return [(offsetx + x, offsety + y) for (x, y) in self.value] @@ -31,6 +35,17 @@ class ButtonBar(Widget): self.position = (0, ButtonBar.TOP_LINE) self.bounds = (DISPLAY_WIDTH, DISPLAY_HEIGHT - ButtonBar.TOP_LINE) + def _buttonBounds(self): + width = (DISPLAY_WIDTH // ButtonBar.MAX_BUTTONS) - 2 + height = DISPLAY_HEIGHT - ButtonBar.TOP_LINE + return (width, height) + + def _positionForSlot(self, buttonNum): + slotWidth = DISPLAY_WIDTH / ButtonBar.MAX_BUTTONS + slotX = (buttonNum * slotWidth) + slotY = ButtonBar.TOP_LINE + 1 + return (int(slotX), int(slotY)) + def button(self, buttonNum): return self._children[buttonNum] @@ -39,8 +54,8 @@ class ButtonBar(Widget): self.removeChild(self._children[buttonNum]) self._children[buttonNum] = button - position = self._positionForButton(buttonNum) - button.position = position + button.position = self._positionForSlot(buttonNum) + button.bounds = self._buttonBounds() button.parent = self self.addChange(ChangeType.ADD, 'child', button) @@ -61,24 +76,6 @@ class ButtonBar(Widget): self.addChange(ChangeType.REMOVE, 'child', button) self.notifyChanged() - def _positionForSlot(self, buttonNum): - slotWidth = DISPLAY_WIDTH / ButtonBar.MAX_BUTTONS - slotX = (buttonNum * slotWidth) - slotY = ButtonBar.TOP_LINE + 2 - - return (slotX, slotY) - - def _positionForButton(self, buttonNum): - (slotX, slotY) = self._positionForSlot(buttonNum) - slotWidth = DISPLAY_WIDTH / ButtonBar.MAX_BUTTONS - slotHeight = DISPLAY_HEIGHT - ButtonBar.TOP_LINE - 2 - - (width, height) = self._children[buttonNum].bounds - x_pos = int(slotX + (slotWidth / 2) - (width / 2)) - y_pos = int(slotY + (slotHeight / 2) - (height / 2)) - - return (x_pos, y_pos) - def draw(self, ctx): if self.visible: for child in self._children: @@ -92,23 +89,84 @@ class ButtonBar(Widget): # Dividing lines for slot in range(0, ButtonBar.MAX_BUTTONS): - position = list(self._positionForSlot(slot)) - position[0] -= 2 - ctx.line(tuple(position) + (position[0], DISPLAY_HEIGHT), + (x, y) = self._positionForSlot(slot) + x -= 1 + ctx.line((x, y) + (x, DISPLAY_HEIGHT), fill=1) -class Button(Widget): - def __init__(self, glyph, fill=True): +class Glyph(Widget): + def __init__(self, x, y, glyph=Glyphs.BLANK, fill=True): Widget.__init__(self) self.glyph = glyph + self.position = (x, y) self.fill = fill - self.bounds = (5, 5) - - def draw(self, ctx): - if self._visible: - xformedGlyph = self._glyph.transformTo(*self._position) - ctx.line(xformedGlyph, fill=self.fill) + + @property + def glyph(self): + return self._glyph + + @glyph.setter + def glyph(self, glyph): + self.setProperty('glyph', glyph) + + @property + def bounds(self): + return Glyphs.BOUNDS.value + + def draw(self, ctx): + if self.visible: + (x, y) = self.position + xformedGlyph = self.glyph.transformTo(x, y) + ctx.line(xformedGlyph, fill=self._fill) + + +class Button(Widget): + def __init__(self, glyph, fill=True): + Widget.__init__(self) + self._rect = Rectangle(*self.position, *self.bounds, fill=False) + self._rect.show() + self.addChild(self._rect) + + self._glyph = Glyph(*self.position, glyph=glyph) + self._glyph.fill = fill + self._glyph.show() + self.addChild(self._glyph) + + self.pressed = False + self.fill = fill + + self.registerObserver(self) + self.changeTrigger(self._updatePositionAndBounds, + changeType=ChangeType.MODIFY, + keys={'position', 'bounds'}) + self.changeTrigger(self._updateStates, + changeType=ChangeType.MODIFY, + keys={'pressed'}) + + def _updatePositionAndBounds(self, subject, changeType, key, data): + self._rect.position = self.position + self._rect.bounds = self.bounds + + (x, y) = self.position + (w, h) = self.bounds + (glyphW, glyphH) = self._glyph.bounds + glyphX = x + (w // 2) - (glyphW // 2) + glyphY = y + (h // 2) - (glyphH // 2) + + self._glyph.position = (glyphX, glyphY) + + def _updateStates(self, subject, changeType, key, data): + self._rect.fill = self.pressed + self._glyph.fill = not self.pressed + + @property + def pressed(self): + return self._pressed + + @pressed.setter + def pressed(self, pressed): + self.setProperty('pressed', pressed) @property def glyph(self): diff --git a/g13gui/g13gui/bitwidgets/button_tests.py b/g13gui/g13gui/bitwidgets/button_tests.py index 9e33f6d..ed91d7e 100644 --- a/g13gui/g13gui/bitwidgets/button_tests.py +++ b/g13gui/g13gui/bitwidgets/button_tests.py @@ -6,6 +6,7 @@ from g13gui.bitwidgets.x11displaydevice import X11DisplayDevice from g13gui.bitwidgets.screen import Screen from g13gui.bitwidgets.button import Button from g13gui.bitwidgets.button import Glyphs +from g13gui.bitwidgets.button import Glyph from g13gui.bitwidgets.label import Label from g13gui.bitwidgets.fonts import Fonts @@ -23,7 +24,18 @@ class ButtonTests(unittest.TestCase): self.dd.shutdown() self.dd.join() + def testGlyph(self): + self.dd.name = 'testGlyph' + ctx = self.display.getContext() + glyph = Glyph(10, 10, Glyphs.CHECKMARK) + glyph.fill = True + glyph.show() + glyph.draw(ctx) + + self.display.commit() + def testExButton(self): + self.dd.name = 'testExButton' ctx = self.display.getContext() exButton = Button(Glyphs.XMARK) exButton.show() @@ -44,6 +56,7 @@ class ButtonTests(unittest.TestCase): self.display.commit() def testButtonBar(self): + self.dd.name = 'testButtonBar' exButton = Button(Glyphs.XMARK) upButton = Button(Glyphs.UP_ARROW) downButton = Button(Glyphs.DOWN_ARROW) @@ -58,6 +71,7 @@ class ButtonTests(unittest.TestCase): self.screen.nextFrame() def testLabelButton(self): + self.dd.name = 'testLabelButton' testButton = Label(0, 0, "Test", font=Fonts.TINY) self.screen.buttonBar.addChild(testButton) self.screen.buttonBar.showAll() diff --git a/g13gui/g13gui/bitwidgets/listview.py b/g13gui/g13gui/bitwidgets/listview.py new file mode 100644 index 0000000..f014b52 --- /dev/null +++ b/g13gui/g13gui/bitwidgets/listview.py @@ -0,0 +1,195 @@ +from g13gui.bitwidgets.widget import Widget +from g13gui.bitwidgets.button import ButtonBar +from g13gui.bitwidgets.button import Glyph +from g13gui.bitwidgets.button import Glyphs +from g13gui.bitwidgets.rectangle import Rectangle +from g13gui.bitwidgets.label import Label +from g13gui.bitwidgets.fonts import Fonts +from g13gui.bitwidgets.fonts import FontManager +from g13gui.bitwidgets import DISPLAY_WIDTH +from g13gui.bitwidgets import DISPLAY_HEIGHT +from g13gui.observer.subject import ChangeType + + +class ListView(Widget): + def __init__(self, model, markedIdx=None, font=Fonts.SMALL): + Widget.__init__(self) + self.model = model + self._font = font + + self.position = (0, 0) + self.bounds = (DISPLAY_WIDTH, ButtonBar.TOP_LINE - 1) + + self._markedIdx = markedIdx + self._selectionIdx = 0 + self._visibilityOffset = 0 + + self._setup() + self.update() + + @property + def model(self): + return self._model + + @model.setter + def model(self, model): + self._model = sorted(list(model)) + + @property + def selectionIndex(self): + return self._selectionIdx + + @selectionIndex.setter + def selectionIndex(self, value): + self._selectionIdx = value + + def selection(self): + items = sorted(self._model) + return items[self._selectionIdx] + + def markedItem(self): + items = sorted(self._model) + return items[self._markedIdx] + + def nextSelection(self): + maxIdx = len(self._model) - 1 + idx = self.selectionIndex + + idx += 1 + + if idx > maxIdx: + idx = maxIdx + + maxVisibleItem = self._visibilityOffset + self._numVisibleItems - 1 + if idx > maxVisibleItem: + self._visibilityOffset += 1 + + self.selectionIndex = idx + self.update() + + def prevSelection(self): + idx = self.selectionIndex + idx -= 1 + if idx < 0: + idx = 0 + + if idx < self._visibilityOffset: + self._visibilityOffset -= 1 + + self.selectionIndex = idx + self.update() + + def markSelection(self): + self._markedIdx = self.selectionIndex + + @property + def markedIndex(self): + return self._markedIdx + + @markedIndex.setter + def markedIndex(self, value): + self._markedIdx = value + + def update(self): + items = sorted(self._model) + startIdx = self._visibilityOffset + endIdx = self._visibilityOffset + self._numVisibleItems + maxItemIdx = len(items) - 1 + + for idx in range(startIdx, endIdx): + name = '' + if idx <= maxItemIdx: + name = items[idx] + + itemIdx = idx - startIdx + item = self._items[itemIdx] + item.text = name + item.isSelected = (idx == self._markedIdx) + item.isHighlighted = (idx == self.selectionIndex) + + def _setup(self): + self._items = [] + + li = ListItem(0, 'Wqpj', font=self._font) + (_, self._liHeight) = li.bounds + self._numVisibleItems = self._bounds[1] // self._liHeight + + for i in range(0, self._numVisibleItems): + y = int(self._liHeight * i) + li = ListItem(y, '', font=self._font) + li.show() + self.addChild(li) + self._items.append(li) + + self._items[self.selectionIndex].isHighlighted = True + + +class ListItem(Widget): + def __init__(self, ypos, text, + is_selected=False, + is_highlighted=False, + font=Fonts.SMALL): + Widget.__init__(self) + + self._text = text + self._isSelected = is_selected + self._isHighlighted = is_highlighted + + self._font = font + (_, self._fontHeight) = FontManager.getFont(self._font).getsize('Wqpj') + + self.position = (0, ypos) + self.bounds = (DISPLAY_WIDTH, self._fontHeight + 3) + + self._setup() + + @property + def text(self): + return self._text + + @text.setter + def text(self, text): + self._text = text + self._label.text = text + + @property + def isHighlighted(self): + return self._isHighlighted + + @isHighlighted.setter + def isHighlighted(self, is_highlighted): + self._isHighlighted = is_highlighted + self._updateStates() + + @property + def isSelected(self): + return self._isSelected + + @isSelected.setter + def isSelected(self, is_selected): + self._isSelected = is_selected + self._updateStates() + + def _updateStates(self): + self._rect.fill = self.isHighlighted + self._label.fill = not self.isHighlighted + + self._indicator.fill = self.isHighlighted ^ self.isSelected + + def _setup(self): + self._rect = Rectangle(*self.position, *self.bounds, + fill=self.isHighlighted) + self._rect.show() + self.addChild(self._rect) + + self._indicator = Glyph(*self.position, Glyphs.CHECKMARK) + self._indicator.position = (2, (self.position[1] + self._fontHeight // 2) - 1) + self._indicator.show() + self.addChild(self._indicator) + + self._label = Label(10, self.position[1] + 2, self._text, + fill=not self.isHighlighted, font=self._font) + self._label.show() + self.addChild(self._label) + + self._updateStates() diff --git a/g13gui/g13gui/bitwidgets/listview_tests.py b/g13gui/g13gui/bitwidgets/listview_tests.py new file mode 100644 index 0000000..c177e50 --- /dev/null +++ b/g13gui/g13gui/bitwidgets/listview_tests.py @@ -0,0 +1,41 @@ +import unittest +import time + +from g13gui.bitwidgets.display import Display +from g13gui.bitwidgets.x11displaydevice import X11DisplayDevice +from g13gui.bitwidgets.screen import Screen +from g13gui.bitwidgets.listview import ListView + + +class ListViewTests(unittest.TestCase): + def setUp(self): + self.dd = X11DisplayDevice(self.__class__.__name__) + self.dd.start() + time.sleep(0.25) + self.display = Display(self.dd) + self.screen = Screen(self.display) + + def tearDown(self): + time.sleep(1) + self.dd.shutdown() + self.dd.join() + + def testEmptyList(self): + lv = ListView([]) + lv.show() + self.screen.addChild(lv) + self.screen.nextFrame() + + def testFullLists(self): + lv = ListView([ + 'One', + 'Two', + 'Three' + ]) + lv.show() + self.screen.addChild(lv) + self.screen.nextFrame() + + +if __name__ == '__main__': + unittest.main() diff --git a/g13gui/g13gui/bitwidgets/rectangle.py b/g13gui/g13gui/bitwidgets/rectangle.py index bf2bde5..2d7b15e 100644 --- a/g13gui/g13gui/bitwidgets/rectangle.py +++ b/g13gui/g13gui/bitwidgets/rectangle.py @@ -2,11 +2,10 @@ from g13gui.bitwidgets.widget import Widget class Rectangle(Widget): - def __init__(self, x, y, w, h, radius=0, fill=True): + def __init__(self, x, y, w, h, fill=True): Widget.__init__(self) self.position = (x, y) self.bounds = (w, h) - self.radius = radius self.fill = fill def draw(self, ctx): @@ -14,14 +13,5 @@ class Rectangle(Widget): points = (self._position[0], self._position[1], self._position[0] + self._bounds[0], self._position[1] + self._bounds[1]) - ctx.rounded_rectangle(*points, - radius=self._radius, - fill=self._fill) - - @property - def radius(self): - return self._radius - - @radius.setter - def radius(self, radius): - self._setProperty('radius', radius) + ctx.rectangle(points, + fill=self._fill) diff --git a/g13gui/g13gui/bitwidgets/rectangle_tests.py b/g13gui/g13gui/bitwidgets/rectangle_tests.py new file mode 100644 index 0000000..7cac429 --- /dev/null +++ b/g13gui/g13gui/bitwidgets/rectangle_tests.py @@ -0,0 +1,36 @@ +import unittest +import time + +from g13gui.bitwidgets import DISPLAY_WIDTH +from g13gui.bitwidgets import DISPLAY_HEIGHT +from g13gui.bitwidgets.display import Display +from g13gui.bitwidgets.x11displaydevice import X11DisplayDevice +from g13gui.bitwidgets.screen import Screen +from g13gui.bitwidgets.rectangle import Rectangle + + +class RectangleTests(unittest.TestCase): + def setUp(self): + self.dd = X11DisplayDevice(self.__class__.__name__) + self.dd.start() + time.sleep(0.25) + self.display = Display(self.dd) + self.screen = Screen(self.display) + + def tearDown(self): + time.sleep(1) + self.dd.shutdown() + self.dd.join() + + def testRect(self): + self.dd.name = 'testRect' + ctx = self.display.getContext() + rect = Rectangle(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT - 1, fill=True) + rect.show() + rect.draw(ctx) + + self.display.commit() + + +if __name__ == '__main__': + unittest.main() diff --git a/g13gui/g13gui/bitwidgets/widget.py b/g13gui/g13gui/bitwidgets/widget.py index 4baf036..eb33ae3 100644 --- a/g13gui/g13gui/bitwidgets/widget.py +++ b/g13gui/g13gui/bitwidgets/widget.py @@ -11,12 +11,12 @@ class Widget(Subject, Observer): Observer.__init__(self) self._children = [] - self.parent = None - self.visible = False - self.valid = False - self.position = (0, 0) - self.bounds = (0, 0) - self.fill = False + self._parent = None + self._visible = False + self._valid = False + self._position = (0, 0) + self._bounds = (0, 0) + self._fill = 0 @property def position(self): @@ -28,7 +28,8 @@ class Widget(Subject, Observer): len(xy) != 2 or \ type(xy[0]) != int or \ type(xy[1]) != int: - raise ValueError('Position must be a tuple of length 2') + raise ValueError('Position must be a tuple of length 2 (got %s)' + % (repr(xy))) self.setProperty('position', xy) @property @@ -41,7 +42,8 @@ class Widget(Subject, Observer): len(wh) != 2 or \ type(wh[0]) != int or \ type(wh[1]) != int: - raise ValueError('Position must be a tuple of length 2') + raise ValueError('Bounds must be a tuple of length 2 (got: %s)' + % (repr(wh))) self.setProperty('bounds', wh) @property diff --git a/g13gui/g13gui/bitwidgets/x11displaydevice.py b/g13gui/g13gui/bitwidgets/x11displaydevice.py index f6153d8..546c556 100644 --- a/g13gui/g13gui/bitwidgets/x11displaydevice.py +++ b/g13gui/g13gui/bitwidgets/x11displaydevice.py @@ -14,6 +14,21 @@ class X11DisplayDevice(DisplayDevice, threading.Thread): self._running = False self._name = name + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + if self._win: + self._setName() + + def _setName(self): + self._win.set_wm_name(self._name) + self._win.set_wm_icon_name(self._name) + self._win.set_wm_class('bitwidgets', self._name) + def run(self): self._display = display.Display() self.createWindow() @@ -55,9 +70,8 @@ class X11DisplayDevice(DisplayDevice, threading.Thread): foreground=self._screen.black_pixel, background=self._screen.white_pixel) - self._win.set_wm_name(self._name) - self._win.set_wm_icon_name(self._name) - self._win.set_wm_class('bitwidgets', self._name) + self._setName() + self._win.set_wm_normal_hints( flags=(Xutil.PPosition | Xutil.PSize | Xutil.PMinSize), min_width=160,