Skip to content

Commit

Permalink
Added type annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
samclane committed Jan 30, 2022
1 parent 7bf5134 commit fe4737f
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 93 deletions.
2 changes: 1 addition & 1 deletion lifx_control_panel/_constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = "2.2.1"
BUILD_DATE = "2021-12-22T20:21:34.343113"
BUILD_DATE = "2022-01-28T08:52:57.205948"
AUTHOR = "Sawyer McLane"
DEBUGGING = False
33 changes: 19 additions & 14 deletions lifx_control_panel/frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
from lifx_control_panel.ui.colorscale import ColorScale
from lifx_control_panel.ui.settings import config
from lifx_control_panel.utilities import color_thread
from lifx_control_panel.utilities.color_thread import getScreenAsImage, normalizeRects
from lifx_control_panel.utilities.color_thread import (
get_screen_as_image,
normalize_rectangles,
)
from lifx_control_panel.utilities.utils import (
Color,
tuple2hex,
hsbk_to_rgb,
hue_to_rgb,
hsv_to_rgb,
kelvin_to_rgb,
get_primary_monitor,
str2list,
Expand Down Expand Up @@ -256,7 +259,9 @@ def start_audio():
self.special_functions_lf,
text="Music Color",
command=start_audio,
state="normal" if self.master.audio_interface.initialized else "disabled",
state="disabled"
if not self.master.audio_interface.initialized
else "normal",
)
self.music_button.grid(row=8, column=0)
self.threads["eyedropper"] = color_thread.ColorThreadRunner(
Expand Down Expand Up @@ -334,12 +339,12 @@ def setup_color_controls(self, init_color: Color):
self.hsbk_labels: Tuple[
tkinter.Label, tkinter.Label, tkinter.Label, tkinter.Label
] = (
tkinter.Label(self, text="%.3g" % (360 * (self.hsbk[0].get() / 65535))),
tkinter.Label(self, text=f"{360 * (self.hsbk[0].get() / 65535):.3g}"),
tkinter.Label(
self, text=str("%.3g" % (100 * self.hsbk[1].get() / 65535)) + "%"
self, text=str(f"{100 * self.hsbk[1].get() / 65535:.3g}") + "%"
),
tkinter.Label(
self, text=str("%.3g" % (100 * self.hsbk[2].get() / 65535)) + "%"
self, text=str(f"{100 * self.hsbk[2].get() / 65535:.3g}") + "%"
),
tkinter.Label(self, text=str(self.hsbk[3].get()) + " K"),
)
Expand Down Expand Up @@ -381,7 +386,7 @@ def setup_color_controls(self, init_color: Color):
] = (
tkinter.Canvas(
self,
background=tuple2hex(hue_to_rgb(360 * (init_color.hue / 65535))),
background=tuple2hex(hsv_to_rgb(360 * (init_color.hue / 65535))),
width=20,
height=20,
borderwidth=3,
Expand Down Expand Up @@ -529,13 +534,13 @@ def update_label(self, key: int):
""" Update scale labels, formatted accordingly. """
return [
self.hsbk_labels[0].config(
text=str("%.3g" % (360 * (self.hsbk[0].get() / 65535)))
text=str(f"{360 * (self.hsbk[0].get() / 65535):.3g}")
),
self.hsbk_labels[1].config(
text=str("%.3g" % (100 * (self.hsbk[1].get() / 65535))) + "%"
text=str(f"{100 * (self.hsbk[1].get() / 65535):.3g}") + "%"
),
self.hsbk_labels[2].config(
text=str("%.3g" % (100 * (self.hsbk[2].get() / 65535))) + "%"
text=str(f"{100 * (self.hsbk[2].get() / 65535):.3g}") + "%"
),
self.hsbk_labels[3].config(text=str(self.hsbk[3].get()) + " K"),
][key]
Expand All @@ -545,7 +550,7 @@ def update_display(self, key: int):
h, s, b, k = self.get_color_values_hsbk() # pylint: disable=invalid-name
if key == 0:
self.hsbk_display[0].config(
background=tuple2hex(hue_to_rgb(360 * (h / 65535)))
background=tuple2hex(hsv_to_rgb(360 * (h / 65535)))
)
elif key == 1:
s = 65535 - s # pylint: disable=invalid-name
Expand Down Expand Up @@ -585,7 +590,7 @@ def get_color_from_palette(self):
def update_status_from_bulb(self, run_once=False):
"""
Periodically update status from the bulb to keep UI in sync.
:param run_once: Don't call `after` statement at end. Keeps a million workers from being instanced.
run_once - Don't call `after` statement at end. Keeps a million workers from being instanced.
"""
require_icon_update = False
if not self.master.bulb_interface.power_queue[self.label].empty():
Expand Down Expand Up @@ -628,10 +633,10 @@ def eyedropper(self, *_, **__):
break
lifxlan.sleep(0.001)
# tkinter.Button state changed
screen_img = getScreenAsImage()
screen_img = get_screen_as_image()
cursor_pos = mouse.get_position()
# Convert display coords to image coords
cursor_pos = normalizeRects(
cursor_pos = normalize_rectangles(
get_display_rects() + [(cursor_pos[0], cursor_pos[1], 0, 0)]
)[-1][:2]
color = screen_img.getpixel(cursor_pos)
Expand Down
61 changes: 36 additions & 25 deletions lifx_control_panel/ui/colorscale.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
import tkinter as tk
from typing import List

from ..utilities.utils import tuple2hex, hue_to_rgb, kelvin_to_rgb
from ..utilities.utils import tuple2hex, hsv_to_rgb, kelvin_to_rgb


class ColorScale(tk.Canvas):
"""
A canvas that displays a color scale.
"""

def __init__(
self,
parent,
Expand Down Expand Up @@ -53,7 +57,7 @@ def __init__(
self._variable.set(val)
self._variable.trace("w", self._update_val)

self.gradient = tk.PhotoImage(master=self, width=width, height=height)
self.gradient = tk.PhotoImage(master=self, width=int(width), height=int(height))

self.bind("<Configure>", lambda _: self._draw_gradient(val))
self.bind("<ButtonPress-1>", self._on_click)
Expand All @@ -71,7 +75,9 @@ def _draw_gradient(self, val):
self.gradient = tk.PhotoImage(master=self, width=width, height=height)

line: List[str] = []
gradfunc = lambda x_coord: line.append(tuple2hex((0, 0, 0)))

def gradfunc(x_coord):
return line.append(tuple2hex((0, 0, 0)))

if self.color_grad == "bw":

Expand All @@ -97,46 +103,51 @@ def gradfunc(x_coord):
elif self.color_grad == "hue":

def gradfunc(x_coord):
line.append(tuple2hex(hue_to_rgb(float(x_coord) / width * 360)))
line.append(tuple2hex(hsv_to_rgb(float(x_coord) / width * 360)))

else:
raise ValueError(f"gradient value {self.color_grad} not recognized")

for x in range(width):
gradfunc(x)
for x_coord in range(width):
gradfunc(x_coord)
line: str = "{" + " ".join(line) + "}"
self.gradient.put(" ".join([line for _ in range(height)]))
self.create_image(0, 0, anchor="nw", tags="gradient", image=self.gradient)
self.lower("gradient")

x_start: float = self.min
try:
x = (val - self.min) / float(self.range) * width
x_start = (val - self.min) / float(self.range) * width
except ZeroDivisionError:
x = self.min
self.create_line(x, 0, x, height, width=4, fill="white", tags="cursor")
self.create_line(x, 0, x, height, width=2, tags="cursor")
x_start = self.min
self.create_line(
x_start, 0, x_start, height, width=4, fill="white", tags="cursor"
)
self.create_line(x_start, 0, x_start, height, width=2, tags="cursor")

def _on_click(self, event):
"""Move selection cursor on click."""
x = event.x
if x >= 0:
x_coord = event.x
if x_coord >= 0:
width = self.winfo_width()
self.update_slider_value(width, x)
self.update_slider_value(width, x_coord)

def update_slider_value(self, width, x):
for s in self.find_withtag("cursor"):
self.coords(s, x, 0, x, self.winfo_height())
self._variable.set(round((float(self.range) * x) / width + self.min, 2))
def update_slider_value(self, width, x_coord):
"""Update the slider value based on slider x coordinate."""
height = self.winfo_height()
for x_start in self.find_withtag("cursor"):
self.coords(x_start, x_coord, 0, x_coord, height)
self._variable.set(round((float(self.range) * x_coord) / width + self.min, 2))
if self.command is not None:
self.command()

def _on_move(self, event):
"""Make selection cursor follow the cursor."""
x = event.x
if x >= 0:
w = self.winfo_width()
x = min(max(abs(x), 0), w)
self.update_slider_value(w, x)
x_coord = event.x
if x_coord >= 0:
width = self.winfo_width()
x_coord = min(max(abs(x_coord), 0), width)
self.update_slider_value(width, x_coord)

def _update_val(self, *_):
val = int(self._variable.get())
Expand All @@ -154,9 +165,9 @@ def set(self, val):
"""Set cursor position on the color corresponding to the value"""
width = self.winfo_width()
try:
x = (val - self.min) / float(self.range) * width
x_coord = (val - self.min) / float(self.range) * width
except ZeroDivisionError:
return
for s in self.find_withtag("cursor"):
self.coords(s, x, 0, x, self.winfo_height())
for x_start in self.find_withtag("cursor"):
self.coords(x_start, x_coord, 0, x_coord, self.winfo_height())
self._variable.set(val)
2 changes: 1 addition & 1 deletion lifx_control_panel/ui/icon_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __init__(self, *args, is_group: bool = False, **kwargs):
)
self.scroll_x = 0
self.scroll_y = 0
self.bulb_dict = {}
self.bulb_dict: dict[str, tuple[tkinter.PhotoImage, int, int]] = {}
self.canvas = tkinter.Canvas(
self,
width=self.settings.window_width,
Expand Down
37 changes: 20 additions & 17 deletions lifx_control_panel/utilities/color_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging
import threading
from functools import lru_cache
from typing import List, Tuple

import mss
import numexpr as ne
Expand All @@ -26,7 +27,8 @@ def get_monitor_bounds(func):
return func() or config["AverageColor"]["DefaultMonitor"]


def getScreenAsImage():
def get_screen_as_image():
"""Grabs the entire primary screen as an image"""
with mss.mss() as sct:
monitor = sct.monitors[0]

Expand All @@ -40,7 +42,8 @@ def getScreenAsImage():
return Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")


def getRectAsImage(bounds):
def get_rect_as_image(bounds: Tuple[int, int, int, int]):
""" Grabs a rectangular area of the primary screen as an image """
with mss.mss() as sct:
monitor = {
"left": bounds[0],
Expand All @@ -52,36 +55,38 @@ def getRectAsImage(bounds):
return Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")


def normalizeRects(rects):
smallestX = min(rect[0] for rect in rects)
smallestY = min(rect[1] for rect in rects)
def normalize_rectangles(rects: List[Tuple[int, int, int, int]]):
""" Normalize the rectangles to the monitor size """
x_min = min(rect[0] for rect in rects)
y_min = min(rect[1] for rect in rects)
return [
(-smallestX + left, -smallestY + top, -smallestX + right, -smallestY + bottom,)
(-x_min + left, -y_min + top, -x_min + right, -y_min + bottom,)
for left, top, right, bottom in rects
]


def avg_screen_color(initial_color, func_bounds=lambda: None):
""" Capture an image of the monitor defined by func_bounds, then get the average color of the image in HSBK"""
def avg_screen_color(initial_color_hsbk, func_bounds=lambda: None):
""" Capture an image of the monitor defined by func_bounds, then get the average color of the image in HSBK """
monitor = get_monitor_bounds(func_bounds)
if "full" in monitor:
screenshot = getScreenAsImage()
screenshot = get_screen_as_image()
else:
screenshot = getRectAsImage(str2list(monitor, int))
screenshot = get_rect_as_image(str2list(monitor, int))
# Resizing the image to 1x1 pixel will give us the average for the whole image (via HAMMING interpolation)
color = screenshot.resize((1, 1), Image.HAMMING).getpixel((0, 0))
return list(utils.RGBtoHSBK(color, temperature=initial_color[3]))
return list(utils.RGBtoHSBK(color, temperature=initial_color_hsbk[3]))


def dominant_screen_color(initial_color, func_bounds=lambda: None):
"""
Gets the dominant color of the screen defined by func_bounds
https://stackoverflow.com/questions/50899692/most-dominant-color-in-rgb-image-opencv-numpy-python
"""
monitor = get_monitor_bounds(func_bounds)
if "full" in monitor:
screenshot = getScreenAsImage()
screenshot = get_screen_as_image()
else:
screenshot = getRectAsImage(str2list(monitor, int))
screenshot = get_rect_as_image(str2list(monitor, int))

downscale_width, downscale_height = screenshot.width // 4, screenshot.height // 4
screenshot = screenshot.resize((downscale_width, downscale_height), Image.HAMMING)
Expand All @@ -99,9 +104,7 @@ def dominant_screen_color(initial_color, func_bounds=lambda: None):
a1D = ne.evaluate("a0*s0*s1+a1*s0+a2", eval_params)
color = np.unravel_index(np.bincount(a1D).argmax(), col_range)

color_hsbk = list(utils.RGBtoHSBK(color, temperature=initial_color[3]))
# color_hsbk[2] = initial_color[2] # TODO Decide this
return color_hsbk
return list(utils.RGBtoHSBK(color, temperature=initial_color[3]))


class ColorThread(threading.Thread):
Expand Down Expand Up @@ -129,7 +132,7 @@ def __init__(self, bulb, color_function, parent, continuous=True, **kwargs):
self.kwargs = kwargs
self.parent = parent # couple to parent frame
self.logger = logging.getLogger(
parent.logger.name + ".Thread({})".format(color_function.__name__)
parent.logger.name + f".Thread({color_function.__name__})"
)
self.prev_color = parent.get_color_values_hsbk()
self.continuous = continuous
Expand Down
Loading

0 comments on commit fe4737f

Please sign in to comment.