503 lines
12 KiB
Python
503 lines
12 KiB
Python
# latest
|
|
import ctypes
|
|
import logging
|
|
import signal
|
|
import sys
|
|
|
|
from PySide6.QtCore import QPoint, Qt
|
|
from PySide6.QtGui import QColor
|
|
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QCheckBox,
|
|
QColorDialog,
|
|
QLabel,
|
|
QPushButton,
|
|
QSlider,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
|
|
try:
|
|
ctypes.windll.user32.SetProcessDPIAware()
|
|
except Exception:
|
|
pass
|
|
|
|
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
|
DWMWA_WINDOW_CORNER_PREFERENCE = 33
|
|
DWMWCP_ROUND = 2
|
|
|
|
|
|
class WindowConfig:
|
|
width = 800
|
|
height = 500
|
|
opacity = 0.99
|
|
radius = 11
|
|
resize_margin = 8
|
|
minimum_width = 300
|
|
minimum_height = 200
|
|
background_alpha = 190
|
|
border_alpha = 190
|
|
|
|
|
|
class GlassWindow(QWidget):
|
|
def __init__(self):
|
|
super().__init__(
|
|
None,
|
|
Qt.FramelessWindowHint | Qt.Window | Qt.WindowStaysOnTopHint,
|
|
)
|
|
|
|
self.cfg = WindowConfig()
|
|
|
|
self.background_color = QColor(155, 155, 155)
|
|
|
|
self._drag_offset = None
|
|
self._resize_edges = None
|
|
self._resize_start_pos = QPoint()
|
|
self._resize_start_geo = self.geometry()
|
|
|
|
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
|
self.setWindowOpacity(self.cfg.opacity)
|
|
self.resize(self.cfg.width, self.cfg.height)
|
|
self.setMinimumSize(
|
|
self.cfg.minimum_width,
|
|
self.cfg.minimum_height,
|
|
)
|
|
self.setMouseTracking(True)
|
|
|
|
self.panel = QWidget(self)
|
|
self.panel.setObjectName("panel")
|
|
|
|
layout = QVBoxLayout(self.panel)
|
|
layout.setContentsMargins(24, 24, 24, 24)
|
|
layout.setSpacing(16)
|
|
|
|
title = QLabel("")
|
|
title.setStyleSheet(
|
|
"background:transparent;color:white;font-size:24px;font-weight:600;"
|
|
)
|
|
|
|
body = QLabel("opacity")
|
|
body.setStyleSheet("background:transparent;color:transparent;")
|
|
|
|
layout.addWidget(title)
|
|
layout.addWidget(body)
|
|
|
|
layout.addStretch()
|
|
|
|
self.slider = QSlider(
|
|
Qt.Horizontal,
|
|
self.panel,
|
|
)
|
|
self.slider.setRange(0, 255)
|
|
self.slider.setFixedWidth(255)
|
|
self.slider.setValue(
|
|
self.cfg.background_alpha,
|
|
)
|
|
self.slider.valueChanged.connect(
|
|
self.update_panel_style,
|
|
)
|
|
|
|
self.color_button = QPushButton(
|
|
self.panel,
|
|
)
|
|
self.color_button.setFixedSize(22, 22)
|
|
self.color_button.clicked.connect(
|
|
self.pick_background_color,
|
|
)
|
|
|
|
self.pin_button = QPushButton(
|
|
"🗗",
|
|
self.panel,
|
|
)
|
|
self.pin_button.setCheckable(True)
|
|
self.pin_button.setChecked(True)
|
|
self.pin_button.setFixedSize(28, 28)
|
|
self.pin_button.setCursor(Qt.ArrowCursor)
|
|
self.pin_button.setToolTip("Always on Top")
|
|
self.pin_button.toggled.connect(
|
|
self.toggle_always_on_top,
|
|
)
|
|
|
|
self.setCursor(Qt.ArrowCursor)
|
|
self.panel.setCursor(Qt.ArrowCursor)
|
|
self.slider.setCursor(Qt.ArrowCursor)
|
|
self.color_button.setCursor(Qt.ArrowCursor)
|
|
self.pin_button.setCursor(Qt.ArrowCursor)
|
|
self.pin_button.setObjectName("pinButton")
|
|
self.update_panel_style()
|
|
|
|
def toggle_always_on_top(
|
|
self,
|
|
enabled: bool,
|
|
):
|
|
geometry = self.geometry()
|
|
|
|
self.setWindowFlag(
|
|
Qt.WindowStaysOnTopHint,
|
|
enabled,
|
|
)
|
|
|
|
self.show()
|
|
self.setGeometry(geometry)
|
|
|
|
def pick_background_color(self):
|
|
dialog = QColorDialog(
|
|
self.background_color,
|
|
self,
|
|
)
|
|
|
|
dialog.setOption(
|
|
QColorDialog.DontUseNativeDialog,
|
|
True,
|
|
)
|
|
|
|
dialog.setWindowTitle(
|
|
"background",
|
|
)
|
|
|
|
dialog.setStyleSheet("""
|
|
QColorDialog{
|
|
background:#030303;
|
|
color:#f2f2f2;
|
|
}
|
|
|
|
QWidget{
|
|
background:#090909;
|
|
color:#f2f2f2;
|
|
}
|
|
|
|
QLabel{
|
|
background:transparent;
|
|
color:#f2f2f2;
|
|
}
|
|
|
|
QGroupBox{
|
|
color:#f2f2f2;
|
|
border:1px solid #3a3a3a;
|
|
margin-top:8px;
|
|
}
|
|
|
|
QGroupBox::title{
|
|
left:8px;
|
|
padding:0 4px;
|
|
}
|
|
|
|
QPushButton{
|
|
background:#2a2a2a;
|
|
color:white;
|
|
border:1px solid #444;
|
|
border-radius:4px;
|
|
padding:5px 12px;
|
|
}
|
|
|
|
QPushButton:hover{
|
|
background:#343434;
|
|
}
|
|
|
|
QPushButton:pressed{
|
|
background:#404040;
|
|
}
|
|
|
|
QLineEdit,
|
|
QSpinBox,
|
|
QComboBox{
|
|
background:#2a2a2a;
|
|
color:white;
|
|
border:1px solid #444;
|
|
selection-background-color:#5DFFCE;
|
|
}
|
|
|
|
QTabBar::tab{
|
|
background:#2a2a2a;
|
|
color:white;
|
|
padding:6px 10px;
|
|
}
|
|
|
|
QTabBar::tab:selected{
|
|
background:#404040;
|
|
}
|
|
""")
|
|
|
|
if dialog.exec():
|
|
self.background_color = dialog.currentColor()
|
|
self.update_panel_style()
|
|
|
|
# v4
|
|
def update_panel_style(self, value=None):
|
|
if value is not None:
|
|
self.cfg.background_alpha = value
|
|
|
|
r = self.background_color.red()
|
|
g = self.background_color.green()
|
|
b = self.background_color.blue()
|
|
|
|
luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255.0
|
|
|
|
groove = f"rgba({r},{g},{b},{max(28, self.cfg.background_alpha - 135)})"
|
|
|
|
fill = f"rgba({r},{g},{b},{min(255, self.cfg.background_alpha + 45)})"
|
|
|
|
if luminance > 0.60:
|
|
handle = "#1b1b1d"
|
|
handle_hover = "#121214"
|
|
handle_pressed = "#09090b"
|
|
handle_border = "rgba(255,255,255,32)"
|
|
else:
|
|
handle = "#f7f7f8"
|
|
handle_hover = "#ffffff"
|
|
handle_pressed = "#ececee"
|
|
handle_border = "rgba(0,0,0,28)"
|
|
|
|
self.setStyleSheet(f"""
|
|
#panel {{
|
|
background: rgba({r},{g},{b},{self.cfg.background_alpha});
|
|
border: 1px solid rgba(255,255,255,.2);
|
|
border-radius: {self.cfg.radius}px;
|
|
}}
|
|
|
|
QPushButton {{
|
|
margin: 2px;
|
|
background: rgb({r},{g},{b});
|
|
border: 1px solid rgba(255,255,255,24);
|
|
border-radius: 4px;
|
|
}}
|
|
|
|
QPushButton:hover {{
|
|
border: 1px solid rgba(255,255,255,48);
|
|
}}
|
|
|
|
QSlider {{
|
|
background: transparent;
|
|
}}
|
|
|
|
QSlider::groove:horizontal {{
|
|
height: 4px;
|
|
background: {groove};
|
|
border: none;
|
|
border-radius: 2px;
|
|
}}
|
|
|
|
QSlider::sub-page:horizontal {{
|
|
background: {fill};
|
|
border-radius: 2px;
|
|
}}
|
|
|
|
QSlider::add-page:horizontal {{
|
|
background: {groove};
|
|
border-radius: 2px;
|
|
}}
|
|
|
|
QSlider::handle:horizontal {{
|
|
width: 14px;
|
|
height: 14px;
|
|
margin: -5px 0;
|
|
background: {handle};
|
|
border: 1px solid {handle_border};
|
|
border-radius: 7px;
|
|
}}
|
|
|
|
QSlider::handle:horizontal:hover {{
|
|
background: {handle_hover};
|
|
}}
|
|
|
|
QSlider::handle:horizontal:pressed {{
|
|
background: {handle_pressed};
|
|
}}
|
|
|
|
QPushButton {{
|
|
margin: 2px;
|
|
background: rgb({r},{g},{b});
|
|
border: 1px solid rgba(255,255,255,24);
|
|
border-radius: 4px;
|
|
}}
|
|
|
|
|
|
QPushButton#pinButton {{
|
|
color: rgba(255,255,255,180);
|
|
}}
|
|
|
|
QPushButton#pinButton:checked {{
|
|
background: #064C2C1C;
|
|
border: 1px solid rgba(255,255,255,40);
|
|
color: white;
|
|
font-weight: 600;
|
|
}}
|
|
""")
|
|
|
|
def resizeEvent(self, event):
|
|
self.panel.setGeometry(self.rect())
|
|
|
|
margin = 24
|
|
|
|
slider_y = self.panel.height() - self.slider.height() - margin
|
|
|
|
spacing_slider_color = 10
|
|
spacing_color_pin = 8
|
|
|
|
self.slider.move(
|
|
self.panel.width() - self.slider.width() - 67 - margin,
|
|
slider_y,
|
|
)
|
|
|
|
self.color_button.move(
|
|
self.slider.x() + self.slider.width() + spacing_slider_color,
|
|
slider_y + (self.slider.height() - self.color_button.height()) // 2,
|
|
)
|
|
|
|
self.pin_button.move(
|
|
self.color_button.x() + self.color_button.width() + spacing_color_pin,
|
|
slider_y + (self.slider.height() - self.pin_button.height()) // 2,
|
|
)
|
|
|
|
super().resizeEvent(event)
|
|
|
|
def showEvent(self, event):
|
|
super().showEvent(event)
|
|
|
|
hwnd = int(self.winId())
|
|
|
|
try:
|
|
dark = ctypes.c_int(1)
|
|
|
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
|
hwnd,
|
|
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
|
ctypes.byref(dark),
|
|
ctypes.sizeof(dark),
|
|
)
|
|
|
|
corner = ctypes.c_int(DWMWCP_ROUND)
|
|
|
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
|
hwnd,
|
|
DWMWA_WINDOW_CORNER_PREFERENCE,
|
|
ctypes.byref(corner),
|
|
ctypes.sizeof(corner),
|
|
)
|
|
|
|
except Exception as ex:
|
|
logging.warning("DWM setup failed: %s", ex)
|
|
|
|
# v2
|
|
def _hit_test(self, pos):
|
|
m = self.cfg.resize_margin + 6
|
|
|
|
x = pos.x()
|
|
y = pos.y()
|
|
|
|
w = self.width()
|
|
h = self.height()
|
|
|
|
left = x <= m
|
|
right = x >= w - m
|
|
top = y <= m
|
|
bottom = y >= h - m
|
|
|
|
return left, top, right, bottom
|
|
|
|
def mousePressEvent(self, e):
|
|
if e.button() != Qt.LeftButton:
|
|
return super().mousePressEvent(e)
|
|
|
|
edges = self._hit_test(e.position().toPoint())
|
|
|
|
if any(edges):
|
|
self._resize_edges = edges
|
|
self._resize_start_pos = e.globalPosition().toPoint()
|
|
self._resize_start_geo = self.geometry()
|
|
return
|
|
|
|
self._drag_offset = (
|
|
e.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
|
)
|
|
|
|
def mouseMoveEvent(self, e):
|
|
pos = e.position().toPoint()
|
|
|
|
if self._resize_edges is not None:
|
|
dx = e.globalPosition().toPoint().x() - self._resize_start_pos.x()
|
|
dy = e.globalPosition().toPoint().y() - self._resize_start_pos.y()
|
|
|
|
geo = self._resize_start_geo
|
|
|
|
x = geo.x()
|
|
y = geo.y()
|
|
w = geo.width()
|
|
h = geo.height()
|
|
|
|
left, top, right, bottom = self._resize_edges
|
|
|
|
if left:
|
|
x += dx
|
|
w -= dx
|
|
|
|
if right:
|
|
w += dx
|
|
|
|
if top:
|
|
y += dy
|
|
h -= dy
|
|
|
|
if bottom:
|
|
h += dy
|
|
|
|
if w < self.minimumWidth():
|
|
if left:
|
|
x -= self.minimumWidth() - w
|
|
w = self.minimumWidth()
|
|
|
|
if h < self.minimumHeight():
|
|
if top:
|
|
y -= self.minimumHeight() - h
|
|
h = self.minimumHeight()
|
|
|
|
self.setGeometry(x, y, w, h)
|
|
return
|
|
|
|
if self._drag_offset is not None:
|
|
self.move(e.globalPosition().toPoint() - self._drag_offset)
|
|
return
|
|
|
|
left, top, right, bottom = self._hit_test(pos)
|
|
|
|
if (left and top) or (right and bottom):
|
|
self.setCursor(Qt.SizeFDiagCursor)
|
|
elif (right and top) or (left and bottom):
|
|
self.setCursor(Qt.SizeBDiagCursor)
|
|
elif left or right:
|
|
self.setCursor(Qt.SizeHorCursor)
|
|
elif top or bottom:
|
|
self.setCursor(Qt.SizeVerCursor)
|
|
else:
|
|
self.setCursor(Qt.ArrowCursor)
|
|
|
|
# v1
|
|
def mouseReleaseEvent(self, e):
|
|
self._drag_offset = None
|
|
self._resize_edges = None
|
|
super().mouseReleaseEvent(e)
|
|
|
|
def keyPressEvent(self, e):
|
|
if e.key() == Qt.Key_Escape:
|
|
self.close()
|
|
else:
|
|
super().keyPressEvent(e)
|
|
|
|
|
|
def main():
|
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
w = GlassWindow()
|
|
w.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|