from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QScrollArea, QSizePolicy
from PyQt6.QtGui import QPixmap, QIcon, QImageReader
from PyQt6.QtCore import Qt, QSize, QTimer
import pygame
import os
import json
from core.resources import ResourceManager
[docs]
class PreviewPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.setMinimumWidth(300)
# Header
self.header_label = QLabel("Preview")
self.header_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.header_label.setStyleSheet("font-weight: bold; padding: 5px; background-color: #333; color: white;")
self.layout.addWidget(self.header_label)
# Container for preview widgets
self.preview_container = QWidget()
self.preview_layout = QVBoxLayout(self.preview_container)
self.preview_layout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.preview_container)
# Current active preview widget
self.current_preview = None
# Supported extensions
self.image_extensions = ['.png', '.jpg', '.jpeg', '.bmp', '.gif']
self.sound_extensions = ['.wav', '.ogg', '.mp3']
self.anim_extensions = ['.anim']
[docs]
def set_file(self, file_path):
self.clear()
if not file_path or not os.path.isfile(file_path):
self.header_label.setText("No Selection")
return
_, ext = os.path.splitext(file_path)
ext = ext.lower()
self.header_label.setText(os.path.basename(file_path))
if ext in self.image_extensions:
self.current_preview = ImagePreview(self)
self.current_preview.load(file_path)
self.preview_layout.addWidget(self.current_preview)
elif ext in self.sound_extensions:
self.current_preview = SoundPreview(self)
self.current_preview.load(file_path)
self.preview_layout.addWidget(self.current_preview)
elif ext in self.anim_extensions:
self.current_preview = AnimationClipPreview(self)
self.current_preview.load(file_path)
self.preview_layout.addWidget(self.current_preview)
else:
label = QLabel("No preview available")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.preview_layout.addWidget(label)
self.current_preview = label
[docs]
def clear(self):
# Remove current preview widget
if self.current_preview:
# If it has a cleanup method (like stopping sound), call it
if hasattr(self.current_preview, 'cleanup'):
self.current_preview.cleanup()
self.preview_layout.removeWidget(self.current_preview)
self.current_preview.deleteLater()
self.current_preview = None
self.header_label.setText("Preview")
[docs]
class ImagePreview(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Use Ignored policy to prevent the image from forcing the widget size
self.image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
self.layout.addWidget(self.image_label, 1) # Give it all available space
self.info_label = QLabel()
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layout.addWidget(self.info_label, 0) # No stretch
self.original_pixmap = None
[docs]
def load(self, path):
self.original_pixmap = QPixmap(path)
if not self.original_pixmap.isNull():
self.update_preview()
self.info_label.setText(f"{self.original_pixmap.width()}x{self.original_pixmap.height()}")
else:
self.image_label.setText("Failed to load image")
self.image_label.setPixmap(QPixmap())
self.info_label.setText("")
[docs]
def resizeEvent(self, event):
self.update_preview()
super().resizeEvent(event)
[docs]
def update_preview(self):
if self.original_pixmap and not self.original_pixmap.isNull():
# Scale to available size
size = self.image_label.size()
if size.width() > 1 and size.height() > 1:
scaled = self.original_pixmap.scaled(size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
self.image_label.setPixmap(scaled)
[docs]
def cleanup(self):
pass
[docs]
class SoundPreview(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.status_label = QLabel("Ready")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layout.addWidget(self.status_label)
controls = QHBoxLayout()
self.play_btn = QPushButton("Play")
self.play_btn.clicked.connect(self.play)
controls.addWidget(self.play_btn)
self.stop_btn = QPushButton("Stop")
self.stop_btn.clicked.connect(self.stop)
controls.addWidget(self.stop_btn)
self.layout.addLayout(controls)
self.sound = None
self.channel = None
[docs]
def load(self, path):
if not pygame.mixer.get_init():
try:
pygame.mixer.init()
except Exception as e:
self.status_label.setText(f"Audio Error: {e}")
self.play_btn.setEnabled(False)
return
try:
self.sound = pygame.mixer.Sound(path)
self.status_label.setText("Loaded")
except Exception as e:
self.status_label.setText("Error loading sound")
print(f"Error loading sound {path}: {e}")
self.play_btn.setEnabled(False)
[docs]
def play(self):
if self.sound:
self.channel = self.sound.play()
self.status_label.setText("Playing...")
[docs]
def stop(self):
if self.channel:
self.channel.stop()
self.status_label.setText("Stopped")
[docs]
def cleanup(self):
self.stop()
[docs]
class AnimationClipPreview(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.preview_label = QLabel("No Preview")
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.preview_label.setMinimumSize(220, 180)
self.layout.addWidget(self.preview_label)
controls = QHBoxLayout()
self.play_btn = QPushButton("Play")
self.pause_btn = QPushButton("Pause")
self.stop_btn = QPushButton("Stop")
self.play_btn.clicked.connect(self.play)
self.pause_btn.clicked.connect(self.pause)
self.stop_btn.clicked.connect(self.stop)
controls.addWidget(self.play_btn)
controls.addWidget(self.pause_btn)
controls.addWidget(self.stop_btn)
self.layout.addLayout(controls)
self.timer = QTimer(self)
self.timer.timeout.connect(self.advance_frame)
self.frames = []
self.index = 0
self.loop = True
self.fps = 12.0
self.anim_path = ""
[docs]
def load(self, path):
self.stop()
self.frames = []
self.index = 0
self.anim_path = path
self.preview_label.setText("No Preview")
try:
with open(path, "r") as f:
data = json.load(f)
except Exception:
self.preview_label.setText("Invalid .anim file")
return
clip_type = data.get("type", "spritesheet")
self.loop = bool(data.get("loop", True))
self.fps = max(0.1, float(data.get("fps", 12.0)))
base_dir = os.path.dirname(path)
if clip_type == "spritesheet":
sheet_path = self._resolve_path(base_dir, data.get("sheet_path", ""))
if sheet_path:
sheet = QPixmap(sheet_path)
if not sheet.isNull():
frame_w = int(max(1, data.get("frame_width", 32)))
frame_h = int(max(1, data.get("frame_height", 32)))
margin = int(max(0, data.get("margin", 0)))
spacing = int(max(0, data.get("spacing", 0)))
all_frames = []
y = margin
while y + frame_h <= sheet.height():
x = margin
while x + frame_w <= sheet.width():
all_frames.append(sheet.copy(x, y, frame_w, frame_h))
x += frame_w + spacing
y += frame_h + spacing
start = int(max(0, data.get("start_frame", 0)))
count = int(max(0, data.get("frame_count", 0)))
if count > 0:
self.frames = all_frames[start:start + count]
else:
self.frames = all_frames[start:]
elif clip_type == "images":
for image_path in data.get("image_paths", []):
resolved = self._resolve_path(base_dir, image_path)
if not resolved:
continue
frame = QPixmap(resolved)
if not frame.isNull():
self.frames.append(frame)
if not self.frames:
self.preview_label.setText("No Preview")
return
self.show_frame()
self.play()
def _resolve_path(self, base_dir, asset_path):
if not asset_path:
return ""
normalized = ResourceManager.to_os_path(asset_path)
if os.path.isabs(normalized) and os.path.exists(normalized):
return normalized
candidate = os.path.normpath(os.path.join(base_dir, normalized))
if os.path.exists(candidate):
return candidate
project_root = os.environ.get("AXISPY_PROJECT_PATH", "")
if project_root:
candidate = os.path.normpath(os.path.join(project_root, normalized))
if os.path.exists(candidate):
return candidate
candidate = os.path.normpath(os.path.join(os.getcwd(), normalized))
if os.path.exists(candidate):
return candidate
return ""
[docs]
def resizeEvent(self, event):
self.show_frame()
super().resizeEvent(event)
[docs]
def show_frame(self):
if not self.frames:
self.preview_label.setText("No Preview")
return
if self.index >= len(self.frames):
self.index = 0
frame = self.frames[self.index]
scaled = frame.scaled(
self.preview_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
self.preview_label.setPixmap(scaled)
[docs]
def advance_frame(self):
if not self.frames:
self.stop()
return
self.index += 1
if self.index >= len(self.frames):
if self.loop:
self.index = 0
else:
self.index = len(self.frames) - 1
self.timer.stop()
self.show_frame()
[docs]
def play(self):
if not self.frames:
return
if not self.loop and self.index >= len(self.frames) - 1:
self.index = 0
self.show_frame()
self.timer.start(int(1000.0 / self.fps))
[docs]
def pause(self):
self.timer.stop()
[docs]
def stop(self):
self.timer.stop()
self.index = 0
self.show_frame()
[docs]
def cleanup(self):
self.timer.stop()