]> git.mar77i.info Git - zenbook_conf/commitdiff
add all the functionality I wanted to add
authormar77i <mar77i@protonmail.ch>
Tue, 17 Dec 2024 15:34:50 +0000 (16:34 +0100)
committermar77i <mar77i@protonmail.ch>
Tue, 17 Dec 2024 16:00:24 +0000 (17:00 +0100)
bluetooth.py
ease.py [deleted file]
ui.py [new file with mode: 0755]
vectors.py
xinput.py
xrandr.py
zenbook_conf.py

index b9b21855b0c4b77db501197dbf640c2461cdbfcf..214fdde9fea76047b6a1ae745bb9adba40ce6d08 100644 (file)
@@ -1,42 +1,25 @@
-from time import sleep
-from traceback import print_exception
-
-import gi
-import gi.repository.GLib
-import pydbus
+import os
+from subprocess import check_output, run
 
 
 class BluetoothConf:
-    ADAPTER = "org.bluez.Adapter1"
-    RETRIES = 20
-    RETRY_SLEEP = 5
+    DEVICE_TYPE = "bluetooth"
 
     def __init__(self):
-        self.bus = pydbus.SystemBus().get("org.bluez", "/org/bluez/hci0")
-        self.conf = self.bus.Get(self.ADAPTER, "Powered")
+        self.conf = self.get_config()
+
+    def get_config(self):
+        output = check_output(
+            ["rfkill", "-rnoSOFT,HARD", "list", self.DEVICE_TYPE], text=True
+        )
+        state = None
+        for line in output.strip().split(os.linesep):
+            new_state = "blocked" not in line.split()
+            if state is not None and new_state != state:
+                return None
+            state = new_state
+        return state
 
     def update(self, value):
-        assert isinstance(value, bool)
-        if value == self.conf:
-            return
-        for retry in range(self.RETRIES + 1):
-            try:
-                self.bus.Set(self.ADAPTER, "Powered", pydbus.Variant("b", value))
-            except gi.repository.GLib.GError as e:
-                if retry >= self.RETRIES:
-                    raise
-                print_exception(e)
-                print(
-                    f"Sleeping {self.RETRY_SLEEP} seconds to try again "
-                    f"({retry + 1} / {self.RETRIES})..."
-                )
-                sleep(self.RETRY_SLEEP)
-            else:
-                break
-        # dbus-send --system
-        # --dest=org.bluez --print-reply /org/bluez/hci0
-        # org.freedesktop.DBus.Properties.Set
-        # string:org.bluez.Adapter1
-        # string:Powered
-        # variant:boolean:false
-        self.conf = value
+        run(["rfkill", f"{'un' if value else ''}block", self.DEVICE_TYPE])
+        self.conf = self.get_config()
diff --git a/ease.py b/ease.py
deleted file mode 100755 (executable)
index a18527a..0000000
--- a/ease.py
+++ /dev/null
@@ -1,169 +0,0 @@
-#!/usr/bin/env python3
-
-from math import asin, atan2, ceil, cos, floor, pi, sin
-
-import pygame
-
-tau = 2 * pi
-FPS = 60
-
-
-def make_line_shape(p1, p2, n, shape_r):
-    angle = atan2(p1[1] - p2[1], p1[0] - p2[0])
-    if angle < 0:
-        angle += tau
-    # 0 <= angle < tau
-    initial_corner = ceil((angle + tau * 3 / 4) * n / tau) % n
-    opposing_corner = (floor((angle + tau / 4) * n / tau) - initial_corner) % n
-    shape = [
-        (cos(a) * shape_r, sin(a) * shape_r)
-        for a in ((i + initial_corner) * tau / n for i in range(n))
-    ]
-    left = (cos(angle + tau * 3 / 4) * shape_r, sin(angle + tau * 3 / 4) * shape_r)
-    right = (cos(angle + tau / 4) * shape_r, sin(angle + tau / 4) * shape_r)
-    yield (p2[0] + left[0], p2[1] + left[1])
-    yield (p1[0] + left[0], p1[1] + left[1])
-    p = p1
-    for i in range(n):
-        s = shape[i]
-        yield (p[0] + s[0], p[1] + s[1])
-        if i == opposing_corner and p == p1 and p1 != p2:
-            yield (p[0] + right[0], p[1] + right[1])
-            p = p2
-            yield (p[0] + right[0], p[1] + right[1])
-
-
-class EaseGraph:
-    CYCLE = {"length": 4, "pause": 1}
-    STEPS = 256
-    WIDTH = 2
-    LINES_COLOR = "blue"
-    GRAPH_COLOR = "lightblue"
-
-    def __init__(self, rect, line, func):
-        self.rect = rect
-        self.line = line
-        self.current_frame = 0
-        self.current_setting = 0
-        self.func = func
-
-    def draw(self, surf):
-        pygame.draw.rect(surf, self.LINES_COLOR, self.rect, 1)
-        pygame.draw.line(surf, self.LINES_COLOR, *self.line)
-        pos = (
-            self.line[0][0],
-            self.line[0][1] + (
-                self.line[1][1] - self.line[0][1]
-            ) * (1 - self.func(self.current_setting)),
-        )
-        pygame.draw.circle(surf, self.GRAPH_COLOR, pos, 32)
-        prev = self.rect.bottomleft
-        for i in range(1, self.STEPS + 1):
-            i_fract = i / self.STEPS
-            pos = (
-                self.rect.left + (i_fract * self.rect.width),
-                self.rect.bottom - self.func(i_fract) * self.rect.height,
-            )
-            pygame.draw.polygon(
-                surf, self.GRAPH_COLOR, list(make_line_shape(prev, pos, 8, self.WIDTH))
-            )
-            prev = pos
-
-    def update(self):
-        self.current_frame += 1
-        full_cycle = sum(self.CYCLE.values())
-        elapsed = self.current_frame / FPS % (full_cycle * 2)
-        if elapsed > full_cycle:
-            elapsed = 2 * full_cycle - elapsed
-        start = self.CYCLE["pause"] / 2
-        end = start + self.CYCLE["length"]
-        if elapsed < start:
-            current_setting = 0
-        elif elapsed > end:
-            current_setting = 1
-        else:
-            current_setting = (elapsed - start) / self.CYCLE["length"]
-        if current_setting == self.current_setting:
-            return False
-        self.current_setting = current_setting
-        return True
-
-
-def ease_in_out_elastic(mag):
-    p = 1 - mag
-    s = p / tau * asin(1)
-    def inner(x):
-        if x == 0:
-            return 0
-        elif x == 1:
-            return 1
-        elif x < 0 or x > 1:
-            raise ValueError(f"x must be between 0 and 1: got {x}")
-        st = x * 2
-        st1 = st - 1
-        sgn = (st >= 1) * 2 - 1
-        return 2 ** (-sgn * 10 * st1 - 1) * sin((st1 - s) * tau / p) * sgn + (sgn > 0)
-    return inner
-
-
-def main():
-    pygame.init()
-    surf = pygame.display.set_mode((800, 600))
-    clock = pygame.time.Clock()
-    running = True
-    dirty = False
-    #lerp = lambda start, end, x: start + (end - start) * x
-    ease_in = lambda x: x * x
-    flip = lambda x: 1 - x
-    mirror = lambda f, x: (f(2 * x) if x < 0.5 else flip(f(2 * x - 2)) + 1) / 2
-    #ease_out = lambda x: flip(ease_in(flip(x)))
-    #ease_in_out = lambda x: lerp(ease_in(x), ease_out(x), x)
-    #ease_in_out = lambda x: ease_in(x) if x < .5 else ease_out(x)
-    ease_in_out = lambda x: mirror(ease_in, x)
-    #one_minus_cos = lambda x: .5 - cos(tau * x / 2) / 2
-    eg = EaseGraph(
-        pygame.Rect((64, 64), (472, 472)),
-        ((600, 64), (600, 536)),
-        ease_in_out_elastic(5 ** .5 / 2 - 0.5)
-    )
-    frame = 0
-    while True:
-        for ev in pygame.event.get():
-            if ev.type == pygame.QUIT:
-                running = False
-                break
-            elif ev.type == pygame.WINDOWEXPOSED:
-                dirty = True
-            elif ev.type == pygame.KEYDOWN:
-                if ev.key == pygame.K_ESCAPE:
-                    running = False
-                    break
-        if not running:
-            break
-        dirty |= eg.update()
-        dirty = True
-        if dirty:
-            surf.fill("black")
-            angle = frame / (tau * 30)
-            a = (300 + cos(angle) * 200, 300 + sin(angle) * 200)
-            b = (300 + cos(angle + pi) * 200, 300 + sin(angle + pi) * 200)
-            width = 100
-            n = 5
-            pygame.draw.polygon(
-                surf, "purple", list(make_line_shape(a, b, n, width / 2)), 1
-            )
-            shape = [
-                (cos(a) * width / 2, sin(a) * width / 2)
-                for a in (i * tau / n for i in range(n))
-            ]
-            pygame.draw.polygon(surf, "yellow", [(s[0] + a[0], s[1] + a[1]) for s in shape], 1)
-            pygame.draw.polygon(surf, "yellow", [(s[0] + b[0], s[1] + b[1]) for s in shape], 1)
-            frame += 1
-            eg.draw(surf)
-            pygame.display.update()
-            dirty = False
-        clock.tick(FPS)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/ui.py b/ui.py
new file mode 100755 (executable)
index 0000000..c70307c
--- /dev/null
+++ b/ui.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+
+from math import asin, nan, pi, sin
+from time import time
+from colorsys import hsv_to_rgb
+
+import pygame
+
+from bluetooth import BluetoothConf
+
+tau = 2 * pi
+
+
+def ease_in_out_elastic(mag):
+    p = 1 - mag
+    s = p / tau * asin(1)
+    def inner(x):
+        if x == 0:
+            return 0
+        elif x == 1:
+            return 1
+        elif x < 0 or x > 1:
+            raise ValueError(f"x must be between 0 and 1: got {x}")
+        st = x * 2
+        st1 = st - 1
+        sgn = (st >= 1) * 2 - 1
+        return 2 ** (-sgn * 10 * st1 - 1) * sin((st1 - s) * tau / p) * sgn + (sgn > 0)
+    return inner
+
+
+class Switch:
+    MOVE_FOR_SEC = 1
+    EASE = staticmethod(ease_in_out_elastic((5 ** .5 - 1) / 2))
+
+    def __init__(self, rect, update_callback, setting=False):
+        self.rect = rect
+        self.update_callback = update_callback
+        if setting is not None and not isinstance(setting, bool):
+            setting = bool(setting)
+        self.setting = setting
+        self.moving_since = nan
+        self.flip_again = False
+
+    def handle_mousebuttondown(self, ev):
+        if ev.button == 1 and self.rect.collidepoint(ev.pos):
+            if self.moving_since is not nan:
+                self.flip_again = True
+            else:
+                offset = self.MOVE_FOR_SEC / 2 if self.setting is None else 0
+                self.setting = bool(self.setting) ^ True
+                self.moving_since = time() - offset
+
+    def update(self, new_value=None):
+        if new_value is not None and new_value != self.setting:
+            self.setting = new_value
+            self.moving_since = time()
+        return self.moving_since is not nan
+
+    def draw(self, surf):
+        pygame.draw.rect(surf, "gray", self.rect, 8)
+        t = time()
+        if t > self.moving_since + self.MOVE_FOR_SEC:
+            self.update_callback(self.setting)
+            if self.flip_again:
+                self.setting = bool(self.setting) ^ True
+                self.moving_since = t
+                self.flip_again = False
+            else:
+                self.moving_since = nan
+        if self.moving_since is nan:
+            if self.setting is None:
+                current = 0.5
+            else:
+                current = min(max(self.setting, 0), 1)
+        else:
+            current = (t - self.moving_since) / self.MOVE_FOR_SEC
+            if not self.setting:
+                current = 1 - current
+        eased_current = self.EASE(current)
+        base_radius = min(self.rect.height, self.rect.width / 4)
+        base_left = self.rect.left + base_radius
+        movement_width = self.rect.width - 2 * base_radius
+        normalized_current = min(max(current, 0), 1)
+        if current < 0.5:
+            args = (0, 1 - 2 * normalized_current, 1 - normalized_current)
+        else:
+            normalized_current -= 0.5
+            args = (1 / 3, 2 * normalized_current, 0.5 + normalized_current * 0.5)
+        rgb = hsv_to_rgb(*args)
+        pygame.draw.circle(
+            surf,
+            pygame.Color(*(int(x * 255) for x in rgb)),
+            (base_left + eased_current * movement_width, self.rect.top + base_radius),
+            base_radius
+        )
+
+
+class Button:
+    def __init__(self, rect, font, label, callback):
+        self.rect = rect
+        self.font = font
+        self.label = label
+        self.pushed = False
+        self.callback = callback
+
+    def draw(self, surf):
+        if not self.pushed:
+            pygame.draw.rect(surf, "gray", self.rect, 8)
+            fs = self.font.render(self.label, True, "gray")
+        else:
+            pygame.draw.rect(surf, "darkgray", self.rect)
+            pygame.draw.rect(surf, "lightgray", self.rect, 4)
+            fs = self.font.render(self.label, True, "black")
+        fs_size = fs.get_size()
+        center = self.rect.center
+        surf.blit(fs, (center[0] - fs_size[0] // 2, center[1] - fs_size[1] // 2))
+
+    def handle_event(self, ev):
+        if ev.type == pygame.MOUSEBUTTONDOWN:
+            if ev.button == 1 and self.rect.collidepoint(ev.pos):
+                self.pushed = True
+                return True
+        elif ev.type == pygame.MOUSEBUTTONUP:
+            if ev.button == 1 and self.pushed and self.rect.collidepoint(ev.pos):
+                self.pushed = False
+                self.callback()
+                return True
+        elif ev.type == pygame.MOUSEMOTION:
+            if ev.buttons[0] and self.pushed and not self.rect.collidepoint(ev.pos):
+                self.pushed = False
+                return True
+        return False
+
+
+def main():
+    pygame.init()
+    surf = pygame.display.set_mode((800, 600))
+    clock = pygame.time.Clock()
+    font = pygame.font.Font(None, size=64)
+    bc = BluetoothConf()
+    switch = Switch(pygame.Rect((64, 64), (256, 128)), bc.update, bc.conf)
+
+    def cb():
+        switch.update(not bc.conf)
+        bc.update(not bc.conf)
+
+    button = Button(pygame.Rect((64, 256), (384, 128)), font, "Apply", cb)
+    running = True
+    dirty = False
+    while True:
+        for ev in pygame.event.get():
+            if ev.type == pygame.QUIT:
+                running = False
+                break
+            elif ev.type == pygame.WINDOWEXPOSED:
+                dirty = True
+            elif ev.type == pygame.KEYDOWN:
+                if ev.key == pygame.K_ESCAPE:
+                    running = False
+                    break
+            elif ev.type == pygame.MOUSEBUTTONDOWN:
+                switch.handle_mousebuttondown(ev)
+            dirty |= button.handle_event(ev)
+        if not running:
+            break
+        dirty |= switch.update()
+        if dirty:
+            surf.fill("black")
+            switch.draw(surf)
+            button.draw(surf)
+            pygame.display.update()
+            dirty = False
+        clock.tick(60)
+
+
+if __name__ == "__main__":
+    main()
index 20880fda5c9aa6021751b4eb1e5cc6005752f259..679a7c2ccceae74b83ce91cbb6ea9ce42be079a9 100755 (executable)
@@ -1,8 +1,7 @@
 #!/usr/bin/env python3
 
-from collections.abc import Sequence
 from itertools import chain
-from math import atan2, cos, pi, sin
+from math import atan2, ceil, cos, floor, pi, sin
 
 import pygame
 
@@ -76,94 +75,99 @@ class StrokeLine(Polygon):
         if angle < 0:
             angle += tau
         # 0 <= angle < tau
-        # corner below 270°
-        initial_corner = int((angle + pi * 3 / 2) * n / tau + .5) % n
-        # corner below 90°
-        opposing_corner = int((angle + pi / 2) * n / tau + .5) % n
+        initial_corner = ceil((angle + tau * 3 / 4) * n / tau) % n
+        opposing_corner = (floor((angle + tau / 4) * n / tau) - initial_corner) % n
         shape = [
-            (cos(i * tau / n) * shape_r, sin(i * tau / n) * shape_r)
-            for i in range(n)
+            (cos(a) * shape_r, sin(a) * shape_r)
+            for a in ((i + initial_corner) * tau / n for i in range(n))
         ]
+        left = (cos(angle + tau * 3 / 4) * shape_r, sin(angle + tau * 3 / 4) * shape_r)
+        right = (cos(angle + tau / 4) * shape_r, sin(angle + tau / 4) * shape_r)
+        yield (p2[0] + left[0], p2[1] + left[1])
+        yield (p1[0] + left[0], p1[1] + left[1])
         p = p1
-        i = initial_corner
-        while i <= n + initial_corner:
-            mod_i = i % n
-            s = shape[mod_i]
+        for i in range(n):
+            s = shape[i]
             yield (p[0] + s[0], p[1] + s[1])
-            if mod_i == opposing_corner and p == p1 and p1 != p2:
+            if i == opposing_corner and p == p1 and p1 != p2:
+                yield (p[0] + right[0], p[1] + right[1])
                 p = p2
-            else:
-                i += 1
+                yield (p[0] + right[0], p[1] + right[1])
 
 
 class Shapes(Shape):
     def __init__(self, shapes):
-        self.shapes = shapes
+        self.shapes = list(shapes)
 
     def draw(self, surf, pos, unit, color):
         for shape in self.shapes:
             shape.draw(surf, pos, unit, color)
 
 
-class StrokeCircle(Shapes):
+class StrokePath(Shapes):
+    def __init__(self, points, closed, width):
+        self._points = list(points)
+        self.closed = bool(closed)
+        self.width = width
+        super().__init__(self.get_shapes())
+
+    def get_shapes(self):
+        if self.closed:
+            yield StrokeLine(self._points[-1], self._points[0], self.width)
+        iter_points = iter(self._points)
+        old = next(iter_points)
+        for new in iter_points:
+            yield StrokeLine(old, new, self.width)
+            old = new
+
+    @property
+    def points(self):
+        return self._points[:]
+
+    @points.setter
+    def points(self, points):
+        self._points.clear()
+        self._points.extend(points)
+        self._points = points
+        self.shapes.clear()
+        self.shapes.extend(self.get_shapes())
+
+
+class StrokeCircle(StrokePath):
     def __init__(self, center, radius, width):
-        super().__init__([
-            StrokeLine(
-                (
-                    center[0] + cos(a) * radius,
-                    center[1] + sin(a) * radius,
-                ),
-                (
-                    center[0] + cos(b) * radius,
-                    center[1] + sin(b) * radius,
-                ),
-                width,
-            )
-            for a, b in self.get_angle_segments()
-        ])
-
-    @staticmethod
-    def get_angle_segments():
         num_vertices = StrokeLine.num_vertices
-        a = 0
-        for i in range(1, num_vertices + 1):
-            b = tau * i / num_vertices
-            yield a, b
-            a = b
+        super().__init__(
+            [
+                (center[0] + cos(a) * radius, center[1] + sin(a) * radius)
+                for a in (tau * i / num_vertices for i in range(num_vertices))
+            ],
+            True,
+            width,
+        )
 
 
-class StrokeCircleSegment(Shapes):
+class StrokeCircleSegment(StrokePath):
     def __init__(self, center, radius, start_angle, end_angle, width):
-        super().__init__([
-            StrokeLine(
-                (
-                    center[0] + cos(a) * radius,
-                    center[1] + sin(a) * radius,
-                ),
-                (
-                    center[0] + cos(b) * radius,
-                    center[1] + sin(b) * radius,
-                ),
-                width,
-            )
-            for a, b in self.get_angle_segments(start_angle, end_angle)
-        ])
+        super().__init__(
+            [
+                (center[0] + cos(a) * radius, center[1] + sin(a) * radius)
+                for a in self.get_angle_segments(start_angle, end_angle)
+            ],
+            False,
+            width,
+        )
 
     @classmethod
     def get_angle_segments(cls, start_angle, end_angle):
         num_vertices = StrokeLine.num_vertices
-        start_angle, end_angle = (
-            min(start_angle, end_angle), max(start_angle, end_angle)
-        )
+        start_angle, end_angle = sorted((start_angle, end_angle))
         diff_angle = end_angle - start_angle
-        a = 0
-        for i in range(1, num_vertices + 1):
-            b = tau * i / num_vertices
-            if b >= diff_angle:
-                yield a + start_angle, end_angle
+        for i in range(num_vertices):
+            a = tau * i / num_vertices
+            if a >= diff_angle:
+                yield end_angle
                 break
-            yield a + start_angle, b + start_angle
-            a = b
+            yield start_angle + a
 
 
 laptop_single = Shapes(
index 1176f551e1f2f52ba2621dc2259fcd85ab50e3aa..6a78d0db57e5d128c581d12f7c9261b522f77752 100644 (file)
--- a/xinput.py
+++ b/xinput.py
@@ -36,7 +36,7 @@ class XinputConf:
 
     def __init__(self, xrandr_conf):
         self.xrandr_conf = xrandr_conf
-        self.current_conf = self.get_conf()
+        self.conf = self.get_conf()
 
     def get_conf(self):
         output = subprocess.check_output(["xinput", "list"], text=True)
@@ -58,17 +58,49 @@ class XinputConf:
                 print("line", repr(line))
         return devices
 
-    def update_device(self, device, enable=True):
+    def update_device(self, device, enable=None):
+        if enable is None:
+            enable = device["enabled"]
         if enable:
             subprocess.run(["xinput", "enable", device["id"]])
             subprocess.run(["xinput", "map-to-output", device["id"], device["output"]])
         else:
             subprocess.run(["xinput", "disable", device["id"]])
+        self.conf = self.get_conf()
 
     def update(self, mode):
         enable_second = mode != "single"
-        for device in self.current_conf:
+        for device in self.conf:
             if device["output"] == self.xrandr_conf.OUTPUTS[0]:
                 self.update_device(device)
             elif device["output"] == self.xrandr_conf.OUTPUTS[1]:
                 self.update_device(device, enable_second)
+
+    def update_by_type(self, device_type, state):
+        outputs = {o["name"]: o for o in self.xrandr_conf.get_relevant_outputs()}
+        for device in self.conf:
+            if device["type"] != device_type:
+                continue
+            s = state
+            if not outputs[device["output"]]["active"]:
+                s = False
+            self.update_device(device, s)
+        return state
+
+    def conf_by_type(self, device_type):
+        outputs = {o["name"]: o for o in self.xrandr_conf.get_relevant_outputs()}
+        state = None
+        for device in self.conf:
+            if device["type"] != device_type or not outputs[device["output"]]["active"]:
+                continue
+            new_state = device["enabled"]
+            if state is not None and new_state != state:
+                return None
+            state = new_state
+        return state
+
+    def reapply_by_type(self, device_type):
+        for device in self.conf:
+            if device["type"] != device_type:
+                continue
+            self.update_device(device, device["enabled"])
index c203b79e0c3b7ec96246500b5888bd80457746a0..9263586b922dc71a3d010591c5372e52f9215e5a 100644 (file)
--- a/xrandr.py
+++ b/xrandr.py
@@ -139,7 +139,6 @@ class XrandrConf:
                 if line[3] == " " or "modes" not in output:
                     continue
                 output["modes"].append(cls.parse_mode(*lines[i:i + 3]))
-        #print(json.dumps(screens, indent=4))
         return screens
 
     @classmethod
@@ -162,8 +161,8 @@ class XrandrConf:
             self.call(1, "auto", rotate="left", left_of=self.OUTPUTS[0])
         self.current_conf = self.get_conf()
 
-    def get_relevant_outputs(self):
-        screen = next(s for s in self.current_conf if s["screen"] == 0)
+    def get_relevant_outputs(self, screen_id=0):
+        screen = next(s for s in self.current_conf if s["screen"] == screen_id)
         outputs = [None, None]
         to_finds = list(self.OUTPUTS)
         for output in screen["outputs"]:
@@ -175,16 +174,54 @@ class XrandrConf:
                 return outputs
         raise ValueError(f"Outputs not found: {', '.join(self.OUTPUTS)}")
 
+    @staticmethod
+    def compare_output(outputs, expecteds):
+        for output, expected in zip(outputs, expecteds):
+            for key, value in expected.items():
+                if key == "pos":
+                    for p, q in zip(output[key], value):
+                        if p != q:
+                            return False
+                elif output[key] != value:
+                    return False
+        return True
+
     def is_active(self, mode):
         outputs = self.get_relevant_outputs()
-        if mode == "vertical":
-            expected_direction = "left"
-        else:
-            expected_direction = "normal"
-        second_active = mode != "single"
-        return (
-            outputs[0]["active"]
-            and outputs[0]["direction"] == expected_direction
-            and outputs[1]["active"] == second_active
-            and (not second_active or outputs[1]["direction"] == expected_direction)
-        )
+        expected = {
+            "single": [
+                {
+                    "active": True,
+                    "pos": [0, 0],
+                    "direction": "normal",
+                },
+                {
+                    "active": False,
+                },
+            ],
+            "double": [
+                {
+                    "active": True,
+                    "pos": [0, 0],
+                    "direction": "normal",
+                },
+                {
+                    "active": True,
+                    "pos": [0, outputs[0].get("size", [None])[-1]],
+                    "direction": "normal",
+                },
+            ],
+            "vertical": [
+                {
+                    "active": True,
+                    "pos": [outputs[1].get("size", [None])[0], 0],
+                    "direction": "left",
+                },
+                {
+                    "active": True,
+                    "pos": [0, 0],
+                    "direction": "left",
+                },
+            ],
+        }
+        return self.compare_output(outputs, expected[mode])
index f270af9f4582d9124910e77ac2b5d3f85555ece9..86b262de18297bf2d63ed588d3ca71920efcac1a 100755 (executable)
@@ -1,25 +1,15 @@
 #!/usr/bin/env python
 
+from functools import partial
+
 import pygame
 
 from bluetooth import BluetoothConf
-from vectors import laptop_double, laptop_single, laptop_vertical
+from ui import Button, Switch
+from vectors import laptop_double, laptop_single, laptop_vertical, touchscreen, stylus
 from xinput import XinputConf
 from xrandr import XrandrConf
 
-# - slider button with "undecided" state
-# - slider button for bluetooth
-#  - that springs on for the 2-screen modes, because the keyboard needs it
-# - slider button for touchscreen (undecided if disabled on second screen)
-#  - extra button to calibrate
-# - slider button for stylus (undecided if disabled on second screen)
-#  - extra button to calibrate
-# - recognize current setting for touchscreen and stylus
-# - ability to display messages
-#   - below the three screen buttons
-#   - next to the slider button for touchscreen
-#   - next to the slider button for stylus
-
 
 class ZenbookConf:
     SHAPES = (
@@ -42,8 +32,8 @@ class ZenbookConf:
 
     def __init__(self):
         pygame.init()
-        self.surf = pygame.display.set_mode((1536, 768))
-        self.font = pygame.font.Font(None, size=64)
+        self.surf = pygame.display.set_mode((1536, 1200))
+        self.font = pygame.font.Font(None, size=96)
         self.clock = pygame.time.Clock()
         self.running = True
         self.dirty = False
@@ -51,6 +41,33 @@ class ZenbookConf:
         self.xinput_conf = XinputConf(self.xrandr_conf)
         self.bluetooth_conf = BluetoothConf()
         self.current_fps = -1
+        self.bt_switch = Switch(
+            pygame.Rect((128, 552), (256, 128)),
+            self.bluetooth_conf.update,
+            self.bluetooth_conf.conf
+        )
+        self.touch_switch = Switch(
+            pygame.Rect((128, 748), (256, 128)),
+            partial(self.xinput_conf.update_by_type, "touchpad"),
+            self.xinput_conf.conf_by_type("touchpad"),
+        )
+        self.touch_button = Button(
+            pygame.Rect((784, 748), (384, 128)),
+            self.font,
+            "Re-apply",
+            partial(self.xinput_conf.reapply_by_type, "touchpad")
+        )
+        self.stylus_switch = Switch(
+            pygame.Rect((128, 944), (256, 128)),
+            partial(self.xinput_conf.update_by_type, "stylus"),
+            self.xinput_conf.conf_by_type("stylus"),
+        )
+        self.stylus_button = Button(
+            pygame.Rect((784, 944), (384, 128)),
+            self.font,
+            "Re-apply",
+            partial(self.xinput_conf.reapply_by_type, "stylus")
+        )
 
     def handle_event(self, ev):
         if ev.type == pygame.QUIT:
@@ -66,13 +83,24 @@ class ZenbookConf:
             for item in self.SHAPES:
                 if item["rect"].collidepoint(ev.pos):
                     self.xrandr_conf.update(item["name"])
-                    self.xinput_conf.update(item["name"])
                     if item["name"] != "single":
                         self.bluetooth_conf.update(True)
+                        self.bt_switch.update(True)
+                    self.xinput_conf.update_touch(self.touch_switch.setting)
+                    self.xinput_conf.update_stylus(self.stylus_switch.setting)
                     self.dirty = True
+            else:
+                self.bt_switch.handle_mousebuttondown(ev)
+                self.touch_switch.handle_mousebuttondown(ev)
+                self.stylus_switch.handle_mousebuttondown(ev)
+        self.dirty |= self.touch_button.handle_event(ev)
+        self.dirty |= self.stylus_button.handle_event(ev)
         return True
 
     def update(self):
+        self.dirty |= self.bt_switch.update()
+        self.dirty |= self.touch_switch.update()
+        self.dirty |= self.stylus_switch.update()
         if self.clock.get_fps() != self.current_fps:
             self.current_fps = int(self.clock.get_fps())
             self.dirty = True
@@ -91,6 +119,29 @@ class ZenbookConf:
                 "white",
             )
 
+        self.bt_switch.draw(self.surf)
+        fs = self.font.render("Bluetooth", True, "gray")
+        r = self.bt_switch.rect
+        self.surf.blit(fs, (r.right + 68, r.centery - fs.get_height() // 2))
+
+        self.touch_switch.draw(self.surf)
+        touchscreen.draw(
+            self.surf,
+            (self.touch_switch.rect.right + 68, self.touch_switch.rect.centery - 132),
+            (12, 12),
+            "gray",
+        )
+        self.touch_button.draw(self.surf)
+
+        self.stylus_switch.draw(self.surf)
+        stylus.draw(
+            self.surf,
+            (self.stylus_switch.rect.right + 68, self.stylus_switch.rect.centery - 132),
+            (12, 12),
+            "gray",
+        )
+        self.stylus_button.draw(self.surf)
+
     def run(self):
         while True:
             for ev in pygame.event.get():