Skip to content

Instantly share code, notes, and snippets.

@Onefabis
Last active August 16, 2022 13:54
Show Gist options
  • Save Onefabis/21bce0bd7f1c58b5c222a348d898a25e to your computer and use it in GitHub Desktop.
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
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()
@Onefabis
Copy link
Author

Onefabis commented Aug 16, 2022

If you want to compile exe by yourself just follow this steps:

  1. Save the code above as 'qmk_color_widget.py'
  2. Install the next libraries, please use the python 3.8 and above:
pip install PyQt5
pip install pywinusb
pip install psutil
pip install pywin32
  1. Install pip install git+https://github.com/pyinstaller/pyinstaller, but first delete the old pyinstaller, in case if you have it already
  2. In command line change the work path (cd) to where the python file is located and launch the next code:
    pyinstaller -F --onefile --windowed qmk_color_widget.py
  3. Take the 'qmk_color_widget.exe' in dist subfolder.

Если хотите скомпилировать exe файл самостоятельно, следуйте следующим шагам:

  1. Сохраните код выше как 'qmk_color_widget.py'
  2. Установите следующие библиотеки, желательно использовать python не ниже версии 3.8:
pip install PyQt5
pip install pywinusb
pip install psutil
pip install pywin32
  1. Установите pip install git+https://github.com/pyinstaller/pyinstaller, удалите старый pyinstaller, если такой уже установлен
  2. В командной строке замените рабочий путь (cd) на тот в котором размещается python файл и запустите следующий код:
    pyinstaller -F --onefile --windowed qmk_color_widget.py
  3. Возьмите 'qmk_color_widget.exe' в dist подпапке.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment