diff --git a/app.py b/app.py new file mode 100644 index 0000000..5d61f5f --- /dev/null +++ b/app.py @@ -0,0 +1,502 @@ +# 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()