224 lines
7.6 KiB
Python
224 lines
7.6 KiB
Python
"""Main PySide6 window with custom title bar."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
from PySide6 import QtCore, QtGui, QtWidgets
|
||
|
||
|
||
class TitleBar(QtWidgets.QWidget):
|
||
"""Custom title bar mimicking modern Windows applications."""
|
||
|
||
HEIGHT = 40
|
||
|
||
def __init__(self, window: "MainWindow") -> None:
|
||
super().__init__(window)
|
||
self.window = window
|
||
self.setFixedHeight(self.HEIGHT)
|
||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||
|
||
self.setAutoFillBackground(True)
|
||
palette = self.palette()
|
||
palette.setColor(QtGui.QPalette.Window, QtGui.QColor("#16171d"))
|
||
self.setPalette(palette)
|
||
|
||
layout = QtWidgets.QHBoxLayout(self)
|
||
layout.setContentsMargins(12, 8, 12, 8)
|
||
layout.setSpacing(8)
|
||
|
||
logo_path = Path(__file__).resolve().parents[1] / "assets" / "logo.png"
|
||
if logo_path.exists():
|
||
pixmap = QtGui.QPixmap(str(logo_path))
|
||
logo_label = QtWidgets.QLabel()
|
||
logo_label.setPixmap(pixmap.scaled(24, 24, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
|
||
layout.addWidget(logo_label)
|
||
|
||
title_label = QtWidgets.QLabel("Interactive Color Range Analyzer")
|
||
title_label.setStyleSheet("color: #f7f7fb; font-weight: 600;")
|
||
layout.addWidget(title_label)
|
||
|
||
layout.addStretch(1)
|
||
|
||
self.min_btn = self._create_button("–", "Minimise window")
|
||
self.min_btn.clicked.connect(window.showMinimized)
|
||
layout.addWidget(self.min_btn)
|
||
|
||
self.max_btn = self._create_button("❐", "Maximise / Restore")
|
||
self.max_btn.clicked.connect(window.toggle_maximise)
|
||
layout.addWidget(self.max_btn)
|
||
|
||
close_btn = self._create_button("✕", "Close")
|
||
close_btn.clicked.connect(window.close)
|
||
close_btn.setStyleSheet(
|
||
"""
|
||
QPushButton { background-color: transparent; color: #f7f7fb; border: none; padding: 4px 10px; }
|
||
QPushButton:hover { background-color: #d0342c; }
|
||
"""
|
||
)
|
||
layout.addWidget(close_btn)
|
||
|
||
def _create_button(self, text: str, tooltip: str) -> QtWidgets.QPushButton:
|
||
btn = QtWidgets.QPushButton(text)
|
||
btn.setToolTip(tooltip)
|
||
btn.setFixedSize(36, 24)
|
||
btn.setCursor(QtCore.Qt.ArrowCursor)
|
||
btn.setStyleSheet(
|
||
"""
|
||
QPushButton {
|
||
background-color: transparent;
|
||
color: #f7f7fb;
|
||
border: none;
|
||
padding: 4px 10px;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: rgba(255, 255, 255, 0.12);
|
||
}
|
||
"""
|
||
)
|
||
return btn
|
||
|
||
def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
|
||
if event.button() == QtCore.Qt.LeftButton:
|
||
self.window.toggle_maximise()
|
||
event.accept()
|
||
|
||
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
||
if event.button() == QtCore.Qt.LeftButton:
|
||
self.window.start_system_move(event.globalPosition())
|
||
event.accept()
|
||
super().mousePressEvent(event)
|
||
|
||
|
||
class ImageView(QtWidgets.QLabel):
|
||
"""Simple image display widget that keeps aspect ratio."""
|
||
|
||
def __init__(self) -> None:
|
||
super().__init__()
|
||
self.setAlignment(QtCore.Qt.AlignCenter)
|
||
self._pixmap: QtGui.QPixmap | None = None
|
||
|
||
def set_image(self, pixmap: QtGui.QPixmap | None) -> None:
|
||
self._pixmap = pixmap
|
||
self._rescale()
|
||
|
||
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
|
||
super().resizeEvent(event)
|
||
self._rescale()
|
||
|
||
def _rescale(self) -> None:
|
||
if self._pixmap is None:
|
||
self.clear()
|
||
self.setText("<No image loaded>")
|
||
self.setStyleSheet("color: rgba(255,255,255,0.5); font-size: 14px;")
|
||
return
|
||
target = self._pixmap.scaled(
|
||
self.size(),
|
||
QtCore.Qt.KeepAspectRatio,
|
||
QtCore.Qt.SmoothTransformation,
|
||
)
|
||
self.setPixmap(target)
|
||
self.setStyleSheet("")
|
||
|
||
|
||
class MainWindow(QtWidgets.QMainWindow):
|
||
"""Main application window containing custom chrome and content area."""
|
||
|
||
def __init__(self) -> None:
|
||
super().__init__()
|
||
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
|
||
self.setWindowFlag(QtCore.Qt.Window)
|
||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, False)
|
||
self.setMinimumSize(900, 600)
|
||
|
||
container = QtWidgets.QWidget()
|
||
container_layout = QtWidgets.QVBoxLayout(container)
|
||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||
container_layout.setSpacing(0)
|
||
|
||
self.title_bar = TitleBar(self)
|
||
container_layout.addWidget(self.title_bar)
|
||
|
||
self.content = QtWidgets.QWidget()
|
||
self.content.setStyleSheet("background-color: #111216;")
|
||
content_layout = QtWidgets.QVBoxLayout(self.content)
|
||
content_layout.setContentsMargins(24, 24, 24, 24)
|
||
content_layout.setSpacing(18)
|
||
|
||
toolbar = QtWidgets.QHBoxLayout()
|
||
toolbar.setSpacing(12)
|
||
|
||
self.open_button = QtWidgets.QPushButton("Open Image…")
|
||
self.open_button.setCursor(QtCore.Qt.PointingHandCursor)
|
||
self.open_button.setStyleSheet(
|
||
"""
|
||
QPushButton {
|
||
background: linear-gradient(135deg, #5168ff, #9a4dff);
|
||
border: none;
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
padding: 10px 16px;
|
||
border-radius: 10px;
|
||
}
|
||
QPushButton:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
"""
|
||
)
|
||
self.open_button.clicked.connect(self.open_image)
|
||
toolbar.addWidget(self.open_button)
|
||
|
||
toolbar.addStretch(1)
|
||
|
||
self.status_label = QtWidgets.QLabel("No image loaded")
|
||
self.status_label.setStyleSheet("color: rgba(255,255,255,0.7); font-weight: 500;")
|
||
toolbar.addWidget(self.status_label)
|
||
|
||
content_layout.addLayout(toolbar)
|
||
|
||
self.image_view = ImageView()
|
||
self.image_view.setStyleSheet("border: 1px solid rgba(255,255,255,0.08); border-radius: 12px;")
|
||
content_layout.addWidget(self.image_view, 1)
|
||
|
||
container_layout.addWidget(self.content, 1)
|
||
self.setCentralWidget(container)
|
||
|
||
self._is_maximised = False
|
||
self._current_image_path: Path | None = None
|
||
|
||
# Window control helpers -------------------------------------------------
|
||
|
||
def toggle_maximise(self) -> None:
|
||
handle = self.windowHandle()
|
||
if handle is None:
|
||
return
|
||
if self._is_maximised:
|
||
self.showNormal()
|
||
self._is_maximised = False
|
||
self.title_bar.max_btn.setText("❐")
|
||
else:
|
||
self.showMaximized()
|
||
self._is_maximised = True
|
||
self.title_bar.max_btn.setText("⧉")
|
||
|
||
def start_system_move(self, _global_position: QtCore.QPointF) -> None:
|
||
handle = self.windowHandle()
|
||
if handle:
|
||
handle.startSystemMove()
|
||
|
||
# Image handling ---------------------------------------------------------
|
||
|
||
def open_image(self) -> None:
|
||
filters = "Images (*.png *.jpg *.jpeg *.bmp *.webp)"
|
||
path_str, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select image", "", filters)
|
||
if not path_str:
|
||
return
|
||
path = Path(path_str)
|
||
pixmap = QtGui.QPixmap(str(path))
|
||
if pixmap.isNull():
|
||
QtWidgets.QMessageBox.warning(self, "ICRA", "Unable to open the selected image.")
|
||
return
|
||
self.image_view.set_image(pixmap)
|
||
self._current_image_path = path
|
||
self.status_label.setText(f"{path.name} · {pixmap.width()}×{pixmap.height()}")
|