mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
325 lines
11 KiB
Python
325 lines
11 KiB
Python
"""
|
|
GUI Utilities for Jackify Frontend
|
|
"""
|
|
import re
|
|
from typing import Tuple, Optional
|
|
from PySide6.QtWidgets import QApplication, QWidget
|
|
from PySide6.QtCore import QSize, QPoint
|
|
|
|
ANSI_COLOR_MAP = {
|
|
'30': 'black', '31': 'red', '32': 'green', '33': 'yellow', '34': 'blue', '35': 'magenta', '36': 'cyan', '37': 'white',
|
|
'90': 'gray', '91': 'lightcoral', '92': 'lightgreen', '93': 'khaki', '94': 'lightblue', '95': 'violet', '96': 'lightcyan', '97': 'white'
|
|
}
|
|
ANSI_RE = re.compile(r'\x1b\[(\d+)(;\d+)?m')
|
|
|
|
# Pattern to match terminal control codes (cursor movement, line clearing, etc.)
|
|
ANSI_CONTROL_RE = re.compile(
|
|
r'\x1b\[' # CSI sequence start
|
|
r'[0-9;]*' # Parameters
|
|
r'[A-Za-z]' # Command letter
|
|
)
|
|
|
|
def strip_ansi_control_codes(text):
|
|
"""Remove ALL ANSI escape sequences including control codes.
|
|
|
|
This is useful for Hoolamike output which uses terminal control codes
|
|
for progress bars that don't render well in QTextEdit.
|
|
"""
|
|
return ANSI_CONTROL_RE.sub('', text)
|
|
|
|
def ansi_to_html(text):
|
|
"""Convert ANSI color codes to HTML"""
|
|
result = ''
|
|
last_end = 0
|
|
color = None
|
|
for match in ANSI_RE.finditer(text):
|
|
start, end = match.span()
|
|
code = match.group(1)
|
|
if start > last_end:
|
|
chunk = text[last_end:start]
|
|
if color:
|
|
result += f'<span style="color:{color}">{chunk}</span>'
|
|
else:
|
|
result += chunk
|
|
if code == '0':
|
|
color = None
|
|
elif code in ANSI_COLOR_MAP:
|
|
color = ANSI_COLOR_MAP[code]
|
|
last_end = end
|
|
if last_end < len(text):
|
|
chunk = text[last_end:]
|
|
if color:
|
|
result += f'<span style="color:{color}">{chunk}</span>'
|
|
else:
|
|
result += chunk
|
|
result = result.replace('\n', '<br>')
|
|
return result
|
|
|
|
|
|
def get_screen_geometry(widget: Optional[QWidget] = None) -> Tuple[int, int, int, int]:
|
|
"""
|
|
Get available screen geometry for a widget.
|
|
|
|
Args:
|
|
widget: Widget to get screen for (uses primary screen if None)
|
|
|
|
Returns:
|
|
Tuple of (x, y, width, height) for available screen geometry
|
|
"""
|
|
app = QApplication.instance()
|
|
if not app:
|
|
return (0, 0, 1920, 1080) # Fallback
|
|
|
|
if widget:
|
|
screen = None
|
|
window_handle = widget.windowHandle()
|
|
if window_handle and window_handle.screen():
|
|
screen = window_handle.screen()
|
|
else:
|
|
try:
|
|
global_pos = widget.mapToGlobal(widget.rect().center())
|
|
except Exception:
|
|
global_pos = QPoint(0, 0)
|
|
if app:
|
|
screen = app.screenAt(global_pos)
|
|
if not screen and app:
|
|
screen = app.primaryScreen()
|
|
else:
|
|
screen = app.primaryScreen()
|
|
|
|
if screen:
|
|
geometry = screen.availableGeometry()
|
|
return (geometry.x(), geometry.y(), geometry.width(), geometry.height())
|
|
|
|
return (0, 0, 1920, 1080) # Fallback
|
|
|
|
|
|
def calculate_window_size(
|
|
widget: Optional[QWidget] = None,
|
|
width_ratio: float = 0.7,
|
|
height_ratio: float = 0.6,
|
|
min_width: int = 900,
|
|
min_height: int = 500,
|
|
max_width: Optional[int] = None,
|
|
max_height: Optional[int] = None
|
|
) -> Tuple[int, int]:
|
|
"""
|
|
Calculate appropriate window size based on screen geometry.
|
|
|
|
Args:
|
|
widget: Widget to calculate size for (uses primary screen if None)
|
|
width_ratio: Fraction of screen width to use (0.0-1.0)
|
|
height_ratio: Fraction of screen height to use (0.0-1.0)
|
|
min_width: Minimum window width
|
|
min_height: Minimum window height
|
|
max_width: Maximum window width (None = no limit)
|
|
max_height: Maximum window height (None = no limit)
|
|
|
|
Returns:
|
|
Tuple of (width, height)
|
|
"""
|
|
_, _, screen_width, screen_height = get_screen_geometry(widget)
|
|
|
|
# Calculate size based on ratios
|
|
width = int(screen_width * width_ratio)
|
|
height = int(screen_height * height_ratio)
|
|
|
|
# Apply minimums
|
|
width = max(width, min_width)
|
|
height = max(height, min_height)
|
|
|
|
# Apply maximums
|
|
if max_width:
|
|
width = min(width, max_width)
|
|
if max_height:
|
|
height = min(height, max_height)
|
|
|
|
# Ensure we don't exceed screen bounds
|
|
width = min(width, screen_width)
|
|
height = min(height, screen_height)
|
|
|
|
return (width, height)
|
|
|
|
|
|
def calculate_window_position(
|
|
widget: QWidget,
|
|
window_width: int,
|
|
window_height: int,
|
|
parent: Optional[QWidget] = None
|
|
) -> QPoint:
|
|
"""
|
|
Calculate appropriate window position (centered on parent or screen).
|
|
|
|
Args:
|
|
widget: Widget to position
|
|
window_width: Width of window to position
|
|
window_height: Height of window to position
|
|
parent: Parent widget to center on (centers on screen if None)
|
|
|
|
Returns:
|
|
QPoint with x, y coordinates
|
|
"""
|
|
_, _, screen_width, screen_height = get_screen_geometry(widget)
|
|
|
|
if parent:
|
|
parent_geometry = parent.geometry()
|
|
x = parent_geometry.x() + (parent_geometry.width() - window_width) // 2
|
|
y = parent_geometry.y() + (parent_geometry.height() - window_height) // 2
|
|
else:
|
|
# Center on screen
|
|
x = (screen_width - window_width) // 2
|
|
y = (screen_height - window_height) // 2
|
|
|
|
# Ensure window stays on screen
|
|
x = max(0, min(x, screen_width - window_width))
|
|
y = max(0, min(y, screen_height - window_height))
|
|
|
|
return QPoint(x, y)
|
|
|
|
|
|
def set_responsive_minimum(window: Optional[QWidget], min_width: int = 960,
|
|
min_height: int = 520, margin: int = 32):
|
|
"""
|
|
Apply minimum size constraints that respect the current screen bounds.
|
|
|
|
Args:
|
|
window: Target window
|
|
min_width: Desired minimum width
|
|
min_height: Desired minimum height
|
|
margin: Pixels to subtract from available size to avoid full-screen overlap
|
|
"""
|
|
if window is None:
|
|
return
|
|
|
|
_, _, screen_width, screen_height = get_screen_geometry(window)
|
|
|
|
width_cap = min_width
|
|
height_cap = min_height
|
|
|
|
if screen_width:
|
|
available_width = max(640, screen_width - margin)
|
|
available_width = min(available_width, screen_width)
|
|
width_cap = min(min_width, available_width)
|
|
if screen_height:
|
|
available_height = max(520, screen_height - margin)
|
|
available_height = min(available_height, screen_height)
|
|
height_cap = min(min_height, available_height)
|
|
|
|
window.setMinimumSize(QSize(width_cap, height_cap))
|
|
|
|
def load_saved_window_size(window: QWidget) -> Optional[Tuple[int, int]]:
|
|
"""
|
|
Load saved window size from config if available.
|
|
Only returns sizes that are reasonable (compact menu size, not expanded).
|
|
|
|
Args:
|
|
window: Window widget (used to validate size against screen)
|
|
|
|
Returns:
|
|
Tuple of (width, height) if saved size exists and is valid, None otherwise
|
|
"""
|
|
try:
|
|
from ...backend.handlers.config_handler import ConfigHandler
|
|
config_handler = ConfigHandler()
|
|
|
|
saved_width = config_handler.get('window_width')
|
|
saved_height = config_handler.get('window_height')
|
|
|
|
if saved_width and saved_height:
|
|
# Validate saved size is reasonable (not too small, fits on screen)
|
|
_, _, screen_width, screen_height = get_screen_geometry(window)
|
|
min_width = 1200
|
|
min_height = 500
|
|
max_height = int(screen_height * 0.6) # Reject sizes larger than 60% of screen (expanded state)
|
|
|
|
# Ensure saved size is within reasonable bounds (compact menu size)
|
|
# Reject expanded sizes that are too tall
|
|
if (min_width <= saved_width <= screen_width and
|
|
min_height <= saved_height <= max_height):
|
|
return (saved_width, saved_height)
|
|
except Exception:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
def save_window_size(window: QWidget):
|
|
"""
|
|
Save current window size to config.
|
|
|
|
Args:
|
|
window: Window widget to save size for
|
|
"""
|
|
try:
|
|
from ...backend.handlers.config_handler import ConfigHandler
|
|
config_handler = ConfigHandler()
|
|
|
|
size = window.size()
|
|
config_handler.set('window_width', size.width())
|
|
config_handler.set('window_height', size.height())
|
|
config_handler.save_config()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def apply_window_size_and_position(
|
|
window: QWidget,
|
|
width_ratio: float = 0.7,
|
|
height_ratio: float = 0.6,
|
|
min_width: int = 900,
|
|
min_height: int = 500,
|
|
max_width: Optional[int] = None,
|
|
max_height: Optional[int] = None,
|
|
parent: Optional[QWidget] = None,
|
|
preserve_position: bool = False,
|
|
use_saved_size: bool = True
|
|
):
|
|
"""
|
|
Apply dynamic window sizing and positioning based on screen geometry.
|
|
Optionally uses saved window size if user has manually resized before.
|
|
|
|
Args:
|
|
window: Window widget to size/position
|
|
width_ratio: Fraction of screen width to use (if no saved size)
|
|
height_ratio: Fraction of screen height to use (if no saved size)
|
|
min_width: Minimum window width
|
|
min_height: Minimum window height
|
|
max_width: Maximum window width (None = no limit)
|
|
max_height: Maximum window height (None = no limit)
|
|
parent: Parent widget to center on (centers on screen if None)
|
|
preserve_position: If True, preserve current size and position (only set minimums)
|
|
use_saved_size: If True, check for saved window size first
|
|
"""
|
|
# Set minimum size first
|
|
window.setMinimumSize(QSize(min_width, min_height))
|
|
|
|
# If preserve_position is True, don't resize - just ensure minimums are set
|
|
if preserve_position:
|
|
# Only ensure current size meets minimums, don't change size
|
|
current_size = window.size()
|
|
if current_size.width() < min_width:
|
|
window.resize(min_width, current_size.height())
|
|
if current_size.height() < min_height:
|
|
window.resize(window.size().width(), min_height)
|
|
return
|
|
|
|
# Check for saved window size first
|
|
width = None
|
|
height = None
|
|
|
|
if use_saved_size:
|
|
saved_size = load_saved_window_size(window)
|
|
if saved_size:
|
|
width, height = saved_size
|
|
|
|
# If no saved size, calculate dynamically
|
|
if width is None or height is None:
|
|
width, height = calculate_window_size(
|
|
window, width_ratio, height_ratio, min_width, min_height, max_width, max_height
|
|
)
|
|
|
|
# Calculate and set position
|
|
pos = calculate_window_position(window, width, height, parent)
|
|
window.resize(width, height)
|
|
window.move(pos)
|