Last active
August 16, 2022 13:54
-
-
Save Onefabis/21bce0bd7f1c58b5c222a348d898a25e to your computer and use it in GitHub Desktop.
The color widget that indicates current active layer in QMK keyboard via HID raw data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PyQt5.QtCore import * | |
from PyQt5.QtGui import * | |
from PyQt5.QtWidgets import * | |
import sys | |
import pywinusb.hid as hid | |
from functools import partial | |
import base64 | |
import os | |
import re | |
import win32gui | |
import win32process | |
import psutil | |
import ast | |
class Device(object): | |
def __init__(self): | |
pass | |
def sample_handler(self, data): | |
print("Raw data: {0}".format(data)) | |
def hid_devices(self): | |
all_hids = hid.find_all_hid_devices() # Get a list of HID objects | |
# Convert to a dictionary of Names:Objects | |
hids_dict = {} | |
for device in all_hids: | |
device_name = str( | |
"{0.vendor_name} {0.product_name}" | |
"(vID=0x{1:04x}, pID=0x{2:04x})" | |
"".format(device, device.vendor_id, device.product_id) | |
) | |
hids_dict[device_name] = device | |
return hids_dict | |
def hid_read(self, hids_dict, menu_item): | |
device = hids_dict[menu_item] # Match the selection to the HID object | |
device.open() # Open the HID device for communication | |
device.set_raw_data_handler(self.sample_handler) # Set raw data callback | |
return device # Return the HID device | |
class QMKColorWidget(QWidget): | |
def __init__(self, parent=None): | |
super(QMKColorWidget, self).__init__(parent) | |
self.isMove = False | |
self.isResize = False | |
self.new_menu_pos = 0 | |
self.new_pos = None | |
self.new_size = None | |
self.device_sel = None | |
self.thread_raw = None | |
self.thread_app = None | |
self.selected_item = None | |
self.language = "en" | |
self.language_dict = {"en":["Opacity", "Roundness", "pix.", "Exclude", "Ignore list", "Language", "Pause", "Close"], | |
"ru":["Прозрачность", "Скругление", "пикс.", "Исключить", "Чёрный список", "Язык", "Пауза", "Закрыть"]} | |
self.active_apps = [] | |
self.apps_blacklist = [] | |
self.colors = ["blue", "red", "orange", "purple", "green", "yellow"] | |
self.roundness = 3 | |
self.opacity = 0.5 | |
self.init_pos = [60, 60] | |
self.init_size = [600, 15] | |
self.read_settings() | |
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SplashScreen) | |
self.setWindowModality(Qt.WindowModal | Qt.ApplicationModal) | |
self.setStyleSheet("""QMKColorWidget {border-radius: %s px; border: 3px solid red}""" %self.roundness) | |
self.setWindowOpacity(self.opacity) | |
ly = QVBoxLayout(self) | |
ly.setContentsMargins(0, 0, 0, 0) | |
self.toolbar = QLabel() | |
self.toolbar_ly = QHBoxLayout(self.toolbar) | |
self.toolbar_ly.setContentsMargins(0, 0, 0, 0) | |
self.toolbar_ly.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) | |
ly.addWidget(self.toolbar) | |
self.toolbar.setAttribute(Qt.WA_StyledBackground, True) | |
self.setContextMenuPolicy(Qt.CustomContextMenu) | |
self.customContextMenuRequested.connect(self.on_context_menu) | |
self.currPos = self.startPos = 0 | |
self.curSize = self.size() | |
self.setGeometry(self.init_pos[0], self.init_pos[1], self.init_size[0], self.init_size[1]) | |
base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyVpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LmIwZjhiZTkwLCAyMDIxLzEyLzE1LTIxOjI1OjE1ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjMuMiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RThCMzEwMTkxQTVCMTFFRDlBNTBCMkJGRUU1NDAxQjkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RThCMzEwMUExQTVCMTFFRDlBNTBCMkJGRUU1NDAxQjkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFOEIzMTAxNzFBNUIxMUVEOUE1MEIyQkZFRTU0MDFCOSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFOEIzMTAxODFBNUIxMUVEOUE1MEIyQkZFRTU0MDFCOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pi9hm8EAAAA5SURBVHjafNAxDgAwCAJA5f9/pnUw0SiykHAbTtJU3HGYkTjsNw7buNjgbp2HFd4sWVhIzPqZJ8AAcWoWD0ghUMEAAAAASUVORK5CYII=" | |
img = base64.b64decode(base64_image) | |
c_img = QPixmap() | |
c_img.loadFromData(img) | |
self.palette = QPalette() | |
self.palette.setBrush(self.backgroundRole(), QBrush(c_img)) | |
self.setPalette(self.palette) | |
self.createMenu() | |
def resizeEvent(self, event): | |
bmp = QBitmap(self.size()) | |
bmp.clear() | |
painter = QPainter(bmp) | |
painter.setRenderHint(QPainter.Antialiasing, True) | |
painter.setPen(Qt.NoPen) | |
painter.setBrush(Qt.NoBrush) | |
path = QPainterPath() | |
path.addRoundedRect(0, 0, self.geometry().width(), self.geometry().height(), self.roundness, self.roundness) | |
painter.fillPath(path, Qt.red) | |
painter.end() | |
self.setMask(bmp) | |
def on_context_menu(self, point): | |
# show context menu | |
if self.new_menu_pos != 0: | |
new_pos = QPoint(self.new_menu_pos.x() + point.x(), self.new_menu_pos.y() + point.y()) | |
point = new_pos | |
self.popMenu.exec_(point) | |
def closeEvent(self, event: QCloseEvent) -> None: | |
try: | |
self.stop_device() | |
except: | |
pass | |
self.save_settings() | |
qApp.quit() | |
def mouse_moved(self, event): | |
if self.isMove: | |
new = event.globalPos() | |
self.new_pos = new + self.currPos - self.startPos | |
self.new_menu_pos = self.new_pos | |
self.move(self.new_pos.x(), self.new_pos.y()) | |
elif self.isResize: | |
self.new_pos = event.globalPos() - self.startPos | |
self.new_size = self.curSize + QSize(self.new_pos.x(), self.new_pos.y()) | |
self.resize(self.new_size.width(), self.new_size.height()) | |
def mousePressEvent(self, event): | |
if event.button() == 1: | |
self.isMove = True | |
self.startPos = event.globalPos() | |
self.currPos = self.pos() | |
elif event.button() == 4: | |
self.isResize = True | |
self.curSize = self.size() | |
self.startPos = event.globalPos() | |
else: | |
self.isMove = False | |
self.isResize = False | |
QWidget.mousePressEvent(self, event) | |
def mouseMoveEvent(self, event): | |
self.mouse_moved(event) | |
def mouseReleaseEvent(self, event): | |
if event.button() == Qt.MiddleButton: | |
self.isResize = False | |
if event.button() == Qt.LeftButton: | |
self.isMove = False | |
QWidget.mouseReleaseEvent(self, event) | |
def createMenu(self): | |
items = self.language_dict[self.language] | |
self.popMenu = QMenu(self) | |
self.popMenu.setStyleSheet(""" | |
QMenu{background-color: white;color: black;} | |
QMenu::item:selected{background-color: rgb(220, 220, 220);color: black;} | |
QMenu::item:default { color: rgb(20, 20, 20); } | |
""") | |
self.devices = Device() | |
self.hids_dict = self.devices.hid_devices() # Get a dictionary of HID devices | |
device_names = list(self.hids_dict.keys()) | |
for s in range(len(device_names)): | |
action = self.popMenu.addAction(device_names[s]) | |
if self.selected_item is not None and self.selected_item == s: | |
self.popMenu.setDefaultAction(action) | |
action.triggered.connect(partial(self.start_device, s)) | |
action = self.popMenu.addMenu(items[0]) | |
for m in range(10): | |
opacity = action.addAction("%d" %(m*10+10)) | |
opacity.triggered.connect(partial(self.set_opacity, m)) | |
action = self.popMenu.addMenu(items[1]) | |
for r in range(10): | |
opacity = action.addAction("%d %s" % (r, items[2])) | |
opacity.triggered.connect(partial(self.set_roundhess, r)) | |
if QSysInfo().productType() == "windows": | |
win32gui.EnumWindows(self.winEnumHandler, None ) | |
if len(self.active_apps) > 0: | |
action = self.popMenu.addMenu(items[3]) | |
active_apps = list(set(self.active_apps)) | |
for s in range(len(active_apps)): | |
if active_apps[s] not in self.apps_blacklist: | |
app = action.addAction(active_apps[s].rsplit("\\", 1)[-1].rsplit(".")[0]) | |
app.triggered.connect(partial(self.add_to_blacklist, active_apps[s])) | |
if len(self.apps_blacklist) > 0: | |
action = self.popMenu.addMenu(items[4]) | |
for s in range(len(self.apps_blacklist)): | |
blacklist_app = action.addAction(self.apps_blacklist[s].rsplit("\\", 1)[-1].rsplit(".")[0]) | |
blacklist_app.triggered.connect(partial(self.remove_from_blacklist, self.apps_blacklist[s])) | |
action = self.popMenu.addMenu(items[5]) | |
app = action.addAction("RU") | |
app.triggered.connect(partial(self.change_language, "ru")) | |
app = action.addAction("EN") | |
app.triggered.connect(partial(self.change_language, "en")) | |
action = self.popMenu.addAction(items[6]) | |
action.triggered.connect(lambda: self.stop_device()) | |
action = self.popMenu.addAction(items[7]) | |
action.triggered.connect(lambda: self.close()) | |
def winEnumHandler(self, hwnd, ctx): | |
if win32gui.IsWindowVisible(hwnd): | |
_, pid = win32process.GetWindowThreadProcessId(hwnd) | |
self.active_apps.append(psutil.Process(pid).exe()) | |
def set_opacity(self, o): | |
self.setWindowOpacity((o+1)*0.1) | |
self.opacity = (o+1)*0.1 | |
def set_roundhess(self, r): | |
self.roundness = r | |
size = self.geometry() | |
self.setGeometry(QRect(size.x(), size.y(), size.width(), size.height()+1)) | |
self.setGeometry(size) | |
def change_language(self, l): | |
self.language = l | |
self.popMenu.deleteLater() | |
self.createMenu() | |
def add_to_blacklist(self, b): | |
self.apps_blacklist.append(b) | |
self.apps_blacklist = list(set(self.apps_blacklist)) | |
self.createMenu() | |
def remove_from_blacklist(self, b): | |
self.apps_blacklist.remove(b) | |
self.createMenu() | |
def start_device(self, data, part): | |
self.selected_item = data | |
try: | |
self.popMenu.deleteLater() | |
self.createMenu() | |
self.start_thread() | |
except: | |
pass | |
def start_thread(self): | |
if self.thread_raw is not None: | |
self.getRawDataHandler.stop() | |
self.thread_raw.quit() | |
self.thread_raw.wait() | |
if self.thread_app is not None: | |
self.getActiveAppHandler.stop() | |
self.thread_app.quit() | |
self.thread_app.wait() | |
self.thread_raw = QThread() | |
self.getRawDataHandler = getRawDataHandler(self.selected_item) | |
self.getRawDataHandler.moveToThread(self.thread_raw) | |
self.thread_raw.started.connect(self.getRawDataHandler.run) | |
self.getRawDataHandler.newLayer.connect(self.edit_color) | |
self.thread_raw.start() | |
self.thread_app = QThread() | |
self.getActiveAppHandler = getActiveAppHandler(self.apps_blacklist) | |
self.getActiveAppHandler.moveToThread(self.thread_app) | |
self.thread_app.started.connect(self.getActiveAppHandler.run) | |
self.getActiveAppHandler.visible.connect(self.get_visible) | |
self.thread_app.start() | |
def stop_device(self): | |
self.setStyleSheet("background-color: none;") | |
self.setPalette(self.palette) | |
self.getRawDataHandler.stop() | |
self.thread_raw.quit() | |
self.thread_raw.wait() | |
self.thread_raw = None | |
self.getActiveAppHandler.stop() | |
self.thread_app.quit() | |
self.thread_app.wait() | |
self.thread_app = None | |
def save_settings(self): | |
script_path = os.path.realpath(__file__) | |
settings_path = (script_path.rsplit("\\", 1)[0] + "\\settings.txt") | |
data_to_save = [self.roundness, self.opacity] | |
data_to_save.extend([self.new_pos.x(), self.new_pos.y()]) if self.new_pos else data_to_save.extend([self.init_pos[0], self.init_pos[1]]) | |
data_to_save.extend([self.new_size.width(), self.new_size.height()]) if self.new_size else data_to_save.extend([self.init_size[0], self.init_size[1]]) | |
data_to_save.append(self.language) | |
text_to_save = " ".join([str(x) for x in data_to_save]) | |
f = open(settings_path, 'w+') | |
f.write("Colors:\n" + "\n".join(self.colors) + "\n\n") | |
f.write("Window:\n" + text_to_save + "\n\n") | |
f.write("Device:\n" + "\n\n") if self.selected_item is None else f.write("Device:\n" + str(self.selected_item) + "\n\n") | |
f.write("Blacklist:\n" + str(self.apps_blacklist)) | |
f.close() | |
def read_settings(self): | |
script_path = os.path.realpath(__file__) | |
settings_path = (script_path.rsplit("\\", 1)[0] + "\\settings.txt") | |
if os.path.isfile(settings_path): | |
try: | |
delimiters = "Colors:", "Window:", "Device:", "Blacklist:" | |
regexPattern = '|'.join(map(re.escape, delimiters)) | |
f = open(settings_path, 'r') | |
lines = f.read() | |
data_to_parse = re.split(regexPattern, lines) | |
self.colors = list(filter(None, data_to_parse[1].split("\n"))) | |
window_data = list(filter(None, data_to_parse[2].split("\n")))[0].split(" ") | |
self.language = window_data.pop(6) | |
window_data = [self.int_or_float(x) for x in window_data] | |
self.roundness = window_data[0] | |
self.opacity = window_data[1] | |
self.init_pos = [window_data[2], window_data[3]] | |
self.init_size = [window_data[4], window_data[5]] | |
self.new_menu_pos = QPoint(self.init_pos[0], self.init_pos[1]) | |
device_idx = list(filter(None, data_to_parse[3].split("\n"))) | |
if len(device_idx)>0: | |
self.selected_item = device_idx[0] | |
blacklist = list(filter(None, data_to_parse[4].split("\n"))) | |
if len(blacklist): | |
self.apps_blacklist.extend(ast.literal_eval(blacklist[0])) | |
finally: | |
print("close") | |
f.close() | |
def int_or_float(self, s): | |
try: | |
return int(s) | |
except ValueError: | |
return float(s) | |
@pyqtSlot(int) | |
def edit_color(self, layer): | |
if layer < len(self.colors): | |
color = str(self.colors[layer]) | |
if "," in self.colors[layer]: | |
color = "rgb(%s)" % str(self.colors[layer]) | |
if "%" in self.colors[layer]: | |
color = "hsl(%s)" % str(self.colors[layer]) | |
self.setStyleSheet("background-color: %s;" % color) | |
@pyqtSlot(int) | |
def get_visible(self, visible): | |
if visible == 0: | |
self.hide() | |
else: | |
self.show() | |
class getActiveAppHandler(QObject): | |
visible = pyqtSignal(int) | |
def __init__(self, blacklist, parent=None): | |
QThread.__init__(self, parent) | |
self.blacklist = blacklist | |
self._isRunning = True | |
self.old_active_app = None | |
def run(self): | |
while True: | |
QThread.msleep(100) | |
if QSysInfo().productType() == "windows": | |
hwnd = win32gui.GetForegroundWindow() | |
if hwnd: | |
_, pid = win32process.GetWindowThreadProcessId(hwnd) | |
if pid: | |
path = psutil.Process(pid).exe() | |
QThread.msleep(100) | |
if path != self.old_active_app: | |
if path in self.blacklist: | |
self.visible.emit(0) | |
else: | |
self.visible.emit(1) | |
self.old_active_app = path | |
if self._isRunning == False: | |
break | |
return | |
def stop(self): | |
self._isRunning = False | |
print("stop") | |
class getRawDataHandler(QObject): | |
newLayer = pyqtSignal(int) | |
def __init__(self, id, parent=None): | |
QThread.__init__(self, parent) | |
self.id = id | |
self._isRunning = True | |
def sample_handler(self, data): | |
self.newLayer.emit(int(data[1])) | |
def run(self): | |
self.device = hid.find_all_hid_devices()[self.id] | |
self.device.open() | |
self.device.set_raw_data_handler(self.sample_handler) | |
try: | |
while self.device.is_plugged(): | |
QThread.msleep(300) | |
if self._isRunning == False: | |
break | |
return | |
finally: | |
print("device close") | |
self.device.close() | |
def stop(self): | |
self._isRunning = False | |
print("stop") | |
qApp = QApplication(sys.argv) | |
qApp.setQuitOnLastWindowClosed(True) | |
window = QMKColorWidget() | |
window.show() | |
qApp.exec() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you want to compile exe by yourself just follow this steps:
pip install git+https://github.com/pyinstaller/pyinstaller
, but first delete the old pyinstaller, in case if you have it alreadypyinstaller -F --onefile --windowed qmk_color_widget.py
Если хотите скомпилировать exe файл самостоятельно, следуйте следующим шагам:
pip install git+https://github.com/pyinstaller/pyinstaller
, удалите старый pyinstaller, если такой уже установленpyinstaller -F --onefile --windowed qmk_color_widget.py