Skip to content

Commit bb8baa2

Browse files
committed
added notif baloons
1 parent 4fae61a commit bb8baa2

5 files changed

Lines changed: 378 additions & 14 deletions

File tree

pymcl/animated_widgets.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QRect, QTimer, pyqtProperty, QObject
2+
from PyQt6.QtWidgets import QPushButton, QLineEdit, QGraphicsDropShadowEffect
3+
from PyQt6.QtGui import QColor
4+
5+
class AnimatedButton(QPushButton):
6+
def __init__(self, text="", parent=None, is_secondary=False, is_destructive=False):
7+
super().__init__(text, parent)
8+
self.setCursor(Qt.CursorShape.PointingHandCursor)
9+
10+
self.default_color = QColor("#4a9eff")
11+
self.hover_color = QColor("#5badff")
12+
self.pressed_color = QColor("#3a8eef")
13+
14+
if is_secondary:
15+
self.default_color = QColor("#3c3c3c")
16+
self.hover_color = QColor("#4a4a4a")
17+
self.pressed_color = QColor("#303030")
18+
elif is_destructive:
19+
self.default_color = QColor(200, 50, 50)
20+
self.hover_color = QColor(220, 70, 70)
21+
self.pressed_color = QColor(180, 40, 40)
22+
23+
self._bg_color = self.default_color
24+
25+
# Stylesheet base - we handle background color via animation,
26+
# so remove background-color from stylesheet if we want pure py animation,
27+
# OR we just animate a property that updates stylesheet.
28+
# But QPropertyAnimation on "styleSheet" is inefficient.
29+
# Better: use QObject property and paint event override OR simple qss transition?
30+
# PyQt doesn't support CSS transitions.
31+
# We will use a custom property for background color.
32+
33+
self.setStyleSheet(f"""
34+
QPushButton {{
35+
color: white;
36+
border-radius: 8px;
37+
padding: 0 20px;
38+
font-size: 15px;
39+
font-weight: 600;
40+
border: none;
41+
}}
42+
""")
43+
44+
def _get_bg_color(self):
45+
return self._bg_color
46+
47+
def _set_bg_color(self, color):
48+
self._bg_color = color
49+
self.setStyleSheet(f"""
50+
QPushButton {{
51+
background-color: {color.name()};
52+
color: white;
53+
border-radius: 8px;
54+
padding: 0 20px;
55+
font-size: 15px;
56+
font-weight: 600;
57+
border: none;
58+
}}
59+
""")
60+
61+
bg_color = pyqtProperty(QColor, _get_bg_color, _set_bg_color)
62+
63+
def enterEvent(self, event):
64+
self.animate_color(self.hover_color)
65+
super().enterEvent(event)
66+
67+
def leaveEvent(self, event):
68+
self.animate_color(self.default_color)
69+
super().leaveEvent(event)
70+
71+
def mousePressEvent(self, event):
72+
self.animate_color(self.pressed_color)
73+
super().mousePressEvent(event)
74+
75+
def mouseReleaseEvent(self, event):
76+
if self.underMouse():
77+
self.animate_color(self.hover_color)
78+
else:
79+
self.animate_color(self.default_color)
80+
super().mouseReleaseEvent(event)
81+
82+
def animate_color(self, target_color):
83+
self.anim = QPropertyAnimation(self, b"bg_color")
84+
self.anim.setDuration(150)
85+
self.anim.setStartValue(self._bg_color)
86+
self.anim.setEndValue(target_color)
87+
self.anim.start()
88+
89+
90+
class ShakeWidget(QObject): # Mixin or helper?
91+
pass # implementing directly in widget for now for simplicity
92+
93+
class AnimatedInput(QLineEdit):
94+
def __init__(self, parent=None):
95+
super().__init__(parent)
96+
self._border_color = QColor("#3a3a3a")
97+
self.default_border = QColor("#3a3a3a")
98+
self.focus_border = QColor("#4a9eff")
99+
self.error_border = QColor("#f44336")
100+
101+
# Initial style updates happen via qss usually, but we want to animate border.
102+
# Animatng border in QSS is hard via property.
103+
# We'll just stick to QSS with simple state changes for now,
104+
# OR animate a "borderColor" property.
105+
106+
def shake(self):
107+
# Simple shake animation
108+
key_pos = self.pos()
109+
x = key_pos.x()
110+
y = key_pos.y()
111+
112+
self.anim = QPropertyAnimation(self, b"pos")
113+
self.anim.setDuration(50)
114+
self.anim.setLoopCount(5)
115+
116+
# Shake sequence: left, right, left, right...
117+
# Note: 'pos' animation on a widget in a layout might be fought by layout.
118+
# Better to simple set style to error and flash it.
119+
self.setProperty("error", True)
120+
self.style().unpolish(self)
121+
self.style().polish(self)
122+
123+
QTimer.singleShot(500, self.clear_error)
124+
125+
def clear_error(self):
126+
self.setProperty("error", False)
127+
self.style().unpolish(self)
128+
self.style().polish(self)

pymcl/launch_page.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
QPushButton,
1010
QComboBox,
1111
QProgressBar,
12+
QComboBox,
13+
QProgressBar,
1214
QScrollArea,
1315
)
1416

17+
from .animated_widgets import AnimatedButton, AnimatedInput
18+
1519
class LaunchPage(QWidget):
1620
def __init__(self, parent=None):
1721
super().__init__(parent)
@@ -34,6 +38,7 @@ def __init__(self, parent=None):
3438
self.auth_method_combo = QComboBox()
3539
self.auth_method_combo.addItems(["Offline", "Microsoft"])
3640
self.auth_method_combo.setMinimumHeight(55)
41+
self.auth_method_combo.setToolTip("Choose 'Microsoft' for online play or 'Offline' for local play.")
3742
layout.addWidget(self.auth_method_combo)
3843

3944
layout.addSpacing(15)
@@ -44,15 +49,16 @@ def __init__(self, parent=None):
4449

4550
layout.addSpacing(5)
4651

47-
self.username_input = QLineEdit()
52+
self.username_input = AnimatedInput()
4853
self.username_input.setPlaceholderText("Enter your username")
4954
self.username_input.setText(f"Player{uuid.uuid4().hex[:6]}")
5055
self.username_input.setMinimumHeight(55)
56+
self.username_input.setToolTip("Enter the username you want to use in-game (Offline mode only).")
5157
layout.addWidget(self.username_input)
5258

53-
self.microsoft_login_button = QPushButton("Login with Microsoft")
54-
self.microsoft_login_button.setCursor(Qt.CursorShape.PointingHandCursor)
59+
self.microsoft_login_button = AnimatedButton("Login with Microsoft")
5560
self.microsoft_login_button.setMinimumHeight(55)
61+
self.microsoft_login_button.setToolTip("Sign in with your Microsoft account to play online.")
5662
layout.addWidget(self.microsoft_login_button)
5763

5864
layout.addSpacing(15)
@@ -66,6 +72,7 @@ def __init__(self, parent=None):
6672
self.version_combo = QComboBox()
6773
self.version_combo.setPlaceholderText("Loading versions...")
6874
self.version_combo.setMinimumHeight(55)
75+
self.version_combo.setToolTip("Select the Minecraft version to launch.")
6976
layout.addWidget(self.version_combo)
7077

7178
layout.addSpacing(15)
@@ -80,22 +87,23 @@ def __init__(self, parent=None):
8087
self.mod_loader_combo = QComboBox()
8188
self.mod_loader_combo.addItems(["Vanilla", "Fabric", "Forge", "NeoForge", "Quilt"])
8289
self.mod_loader_combo.setMinimumHeight(55)
90+
self.mod_loader_combo.setToolTip("Choose the mod loader (e.g., Fabric, Forge) or use Vanilla.")
8391
mod_layout.addWidget(self.mod_loader_combo)
8492

8593
mod_layout.addStretch(1)
8694

87-
self.mod_manager_button = QPushButton("Manage Mods")
88-
self.mod_manager_button.setObjectName("secondary_button")
95+
self.mod_manager_button = AnimatedButton("Manage Mods", is_secondary=True)
8996
self.mod_manager_button.setCursor(Qt.CursorShape.PointingHandCursor)
97+
self.mod_manager_button.setToolTip("Open the Mod Manager to add or remove mods.")
9098
mod_layout.addWidget(self.mod_manager_button)
9199

92100
layout.addLayout(mod_layout)
93101

94102
layout.addSpacing(15)
95103

96-
self.launch_button = QPushButton("🚀 LAUNCH GAME")
97-
self.launch_button.setCursor(Qt.CursorShape.PointingHandCursor)
104+
self.launch_button = AnimatedButton("🚀 LAUNCH GAME")
98105
self.launch_button.setMinimumHeight(55)
106+
self.launch_button.setToolTip("Start Minecraft with the selected configuration.")
99107
layout.addWidget(self.launch_button)
100108

101109
layout.addSpacing(10)

pymcl/main_window.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
QVBoxLayout,
2121
QWidget,
2222
QStackedLayout,
23+
QScrollArea,
2324
)
2425

2526
from .constants import (
@@ -44,6 +45,7 @@
4445
from .console_window import ConsoleWindow
4546
from .servers_page import ServersPage
4647
from .skin_manager import SkinManagerPage
48+
from .toast_manager import ToastManager
4749

4850

4951
class MainWindow(QMainWindow):
@@ -77,7 +79,12 @@ def __init__(self):
7779
self.microsoft_auth.login_failed.connect(self.update_status)
7880

7981
self.init_ui()
82+
83+
# Initialize Toast Manager
84+
self.toast_manager = ToastManager(self)
85+
8086
self.load_settings()
87+
8188
self.apply_styles()
8289
self.add_shadow_effects()
8390
self.populate_versions()
@@ -217,6 +224,7 @@ def init_ui(self):
217224
self.launch_page = LaunchPage()
218225
self.settings_page = SettingsPage()
219226
self.settings_page.settings_saved.connect(self.reload_background_settings)
227+
self.settings_page.settings_saved.connect(lambda: self.toast_manager.show_toast("Settings have been saved.", "Settings Saved", "SUCCESS"))
220228
self.mods_page = ModsPage()
221229
self.mod_browser_page = ModBrowserPage()
222230
self.servers_page = ServersPage()
@@ -303,21 +311,27 @@ def _create_slide_animation(self, new_index, old_index):
303311
anim_new = QPropertyAnimation(new_widget, b"pos")
304312
anim_new.setDuration(300)
305313
anim_new.setEndValue(self.stacked_widget.rect().topLeft())
306-
anim_new.setEasingCurve(QEasingCurve.Type.OutCubic)
314+
anim_new.setEasingCurve(QEasingCurve.Type.InOutQuart)
307315

308316
# Old widget to slide out
309317
old_widget = self.stacked_widget.widget(old_index)
310318
anim_old = QPropertyAnimation(old_widget, b"pos")
311319
anim_old.setDuration(300)
312320
anim_old.setEndValue(QPoint(width if new_index < old_index else -width, 0))
313-
anim_old.setEasingCurve(QEasingCurve.Type.OutCubic)
321+
anim_old.setEasingCurve(QEasingCurve.Type.InOutQuart)
314322

315323
anim_group = QParallelAnimationGroup()
316324
anim_group.addAnimation(anim_new)
317325
anim_group.addAnimation(anim_old)
318326

319327
anim_group.finished.connect(lambda: self.on_animation_finished(new_index, old_widget))
320328
return anim_group
329+
330+
def resizeEvent(self, event):
331+
super().resizeEvent(event)
332+
# Reposition toasts
333+
if hasattr(self, 'toast_manager'):
334+
self.toast_manager.reposition_toasts()
321335

322336
def on_animation_finished(self, new_index, old_widget):
323337
self.stacked_widget.setCurrentIndex(new_index)
@@ -340,6 +354,7 @@ def start_microsoft_login(self):
340354

341355
def on_login_success(self, info: MicrosoftInfo):
342356
self.minecraft_info = info
357+
self.toast_manager.show_toast(f"Logged in as {info['username']}", "Login Successful", "SUCCESS")
343358
self.update_status(f"Logged in as {info['username']}")
344359
self.launch_page.microsoft_login_button.setText(f"Logged in as {info['username']}")
345360
self.skin_manager_page.set_microsoft_info(info)
@@ -600,6 +615,8 @@ def start_launch(self):
600615
username = self.launch_page.username_input.text().strip()
601616
if not username:
602617
self.update_status("⚠️ Please enter a username")
618+
self.toast_manager.show_toast("Please enter a username to continue.", "Username Required", "WARNING")
619+
self.launch_page.username_input.shake()
603620
return
604621
options["username"] = username
605622
options["uuid"] = str(uuid.uuid4())
@@ -667,9 +684,15 @@ def on_launch_finished(self, success, message):
667684
self.launch_page.progress_bar.setValue(1 if success else 0)
668685
self.launch_page.progress_bar.setFormat("%p%")
669686

670-
if success and "Game closed" in message:
671-
self.launch_page.progress_bar.setValue(0)
672-
self.launch_page.status_label.setText("✓ Ready to launch")
687+
if success:
688+
if "Game closed" in message:
689+
self.launch_page.progress_bar.setValue(0)
690+
self.launch_page.status_label.setText("✓ Ready to launch")
691+
self.toast_manager.show_toast("Minecraft session ended.", "Game Closed", "INFO")
692+
else:
693+
self.toast_manager.show_toast("Minecraft launched successfully!", "Launch Success", "SUCCESS")
694+
else:
695+
self.toast_manager.show_toast(message, "Launch Failed", "ERROR")
673696

674697
def cancel_launch(self):
675698
if self.worker:
@@ -697,9 +720,9 @@ def clear_cache(self):
697720
try:
698721
if os.path.exists(ICON_CACHE_DIR):
699722
shutil.rmtree(ICON_CACHE_DIR)
700-
QMessageBox.information(self, "Cache Cleared", "The icon cache has been cleared.")
723+
self.toast_manager.show_toast("The icon cache has been cleared.", "Cache Cleared", "SUCCESS")
701724
else:
702-
QMessageBox.information(self, "Cache Cleared", "No icon cache to clear.")
725+
self.toast_manager.show_toast("No icon cache to clear.", "Cache Empty", "INFO")
703726
except Exception as e:
704727
QMessageBox.critical(self, "Error", f"Could not clear cache: {e}")
705728

pymcl/stylesheet.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,58 @@
429429
border-bottom: 1px solid #252525; /* Blend with pane */
430430
}
431431
432+
432433
QTabBar::tab:hover:!selected {
433434
background: #2a2a2a;
434435
color: #ccc;
435436
}
437+
438+
/* Toast Notifications */
439+
QWidget[class^="Toast_"] {
440+
background-color: #252525;
441+
border: 1px solid #333;
442+
border-radius: 8px;
443+
min-height: 50px;
444+
}
445+
446+
QWidget#Toast_INFO {
447+
border-left: 4px solid #4a9eff;
448+
}
449+
450+
QWidget#Toast_SUCCESS {
451+
border-left: 4px solid #4caf50;
452+
background-color: #1e261e; /* Subtle green tint */
453+
}
454+
455+
QWidget#Toast_WARNING {
456+
border-left: 4px solid #ff9800;
457+
background-color: #26221e; /* Subtle orange tint */
458+
}
459+
460+
QWidget#Toast_ERROR {
461+
border-left: 4px solid #f44336;
462+
background-color: #261e1e; /* Subtle red tint */
463+
}
464+
465+
QLabel#toast_title {
466+
font-size: 14px;
467+
font-weight: 700;
468+
color: #ffffff;
469+
}
470+
471+
QLabel#toast_message {
472+
font-size: 13px;
473+
color: #ccc;
474+
}
475+
476+
QPushButton#toast_close {
477+
background: transparent;
478+
color: #666;
479+
border: none;
480+
font-weight: bold;
481+
font-size: 16px;
482+
}
483+
QPushButton#toast_close:hover {
484+
color: #fff;
485+
}
436486
"""

0 commit comments

Comments
 (0)