Main cleanup

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-08-18 15:24:00 +02:00
parent 789daa7b48
commit fd4f00041a
+148 -290
View File
@@ -23,15 +23,10 @@ import "src/windows/window_args.dart";
const Size _mainSize = Size(275, 116);
const Size _shadeSize = Size(275, 14);
// Toggle to enable extra drag diagnostics (kept off by default).
const bool _dragDiagnostics = false;
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
// Sub-windows (Equalizer/Playlist) are separate Flutter engines that re-run
// this entrypoint; the plugin passes each one its launch arguments so we can
// build the matching app.
final subWindowArgs = await _resolveSubWindowArgs();
if (subWindowArgs != null) {
await windowManager.ensureInitialized();
@@ -56,6 +51,7 @@ Future<void> main(List<String> args) async {
final plOpen = await PlayerState.loadPlOpen();
final uiScale = await PlayerState.loadUiScale();
final skinManager = await SkinManager.load();
await windowManager.ensureInitialized();
runApp(
DartampApp(
@@ -74,9 +70,8 @@ Future<SubWindowArgs?> _resolveSubWindowArgs() async {
final controller = await WindowController.fromCurrentEngine();
return SubWindowArgs.tryDecode(controller.arguments);
} catch (_) {
// ignore: the plugin may not be ready; treat as the main window
}
return null;
}
}
class DartampApp extends StatelessWidget {
@@ -140,6 +135,7 @@ class _WinampRootState extends State<WinampRoot> {
late final SkinManager _skins;
late final DockController _dock;
late final _MainWindowDragController _mainDrag;
bool _windowReady = false;
bool _wasShade = false;
double _wasScale = 1.0;
@@ -147,35 +143,41 @@ class _WinampRootState extends State<WinampRoot> {
bool _wasEqOpen = false;
bool _wasPlOpen = false;
String _wasSkinName = baseSkinName;
WindowController? _eqController;
WindowController? _plController;
WindowController? _presetsController;
StreamSubscription<void>? _windowsSubscription;
/// Initial rects queued for a sub-window that has not booted yet; pushed
/// when it reports `subReady`.
final Map<DockWindow, Rect> _pendingSubBounds = {};
/// The initial rect queued for the presets dialog (not docked), pushed once
/// its control channel registers.
Rect? _pendingPresetsBounds;
/// The latest desired top-left per window, coalesced by [_flushMoves].
final Map<DockWindow, Offset> _pendingMoves = {};
Timer? _moveFlushTimer;
Timer? _presetsTimer;
/// How often pending window moves are flushed to the native side.
static const Duration _moveFlushInterval = Duration(milliseconds: 16);
/// Saved top-left positions of the sub-windows, loaded at startup.
final Map<DockWindow, Rect> _pendingSubBounds = {};
Rect? _pendingPresetsBounds;
final Map<DockWindow, Offset> _pendingMoves = {};
final Map<SubWindowType, (double, double)> _savedSubPositions = {};
/// Saved sizes (width, height) of sub-windows, loaded at startup.
final Map<SubWindowType, (double, double)> _savedSubSizes = {};
@override
void initState() {
super.initState();
_initializeState();
winampChannel.setMethodCallHandler(_handleMethodCall);
unawaited(subWindowChannel.setMethodCallHandler(_onSubWindowCall));
_windowsSubscription = onWindowsChanged.listen((_) {
unawaited(_reconcileSubWindows());
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_readyWindow();
});
}
void _initializeState() {
_player = PlayerState(
windowShade: widget.initialWindowShade,
alwaysOnTop: widget.initialAlwaysOnTop,
@@ -189,37 +191,22 @@ class _WinampRootState extends State<WinampRoot> {
persistRects: _persistRects,
);
_mainDrag = _MainWindowDragController(this);
_wasShade = widget.initialWindowShade;
_wasScale = widget.initialUiScale;
_wasAlwaysOnTop = widget.initialAlwaysOnTop;
_wasEqOpen = widget.initialEqOpen;
_wasPlOpen = widget.initialPlOpen;
_wasSkinName = _skins.active.name;
_player.addListener(_onPlayerChanged);
_skins.addListener(_onSkinChanged);
// Set up method channel for macOS menu actions
winampChannel.setMethodCallHandler(_handleMethodCall);
// Sub-windows report shade toggles and close requests back here.
unawaited(
subWindowChannel.setMethodCallHandler(_onSubWindowCall),
);
// Track native sub-window lifetime so externally-closed windows don't leave
// the Equalizer/Playlist menu checkmarks stuck on.
_windowsSubscription = onWindowsChanged.listen((_) {
unawaited(_reconcileSubWindows());
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_readyWindow();
});
}
@override
void dispose() {
_moveFlushTimer?.cancel();
_presetsTimer?.cancel();
_windowsSubscription?.cancel();
_player.removeListener(_onPlayerChanged);
_skins.removeListener(_onSkinChanged);
@@ -229,19 +216,23 @@ class _WinampRootState extends State<WinampRoot> {
Future<void> _readyWindow() async {
if (!mounted || _windowReady) return;
try {
await windowManager.setPreventClose(false);
await windowManager.setResizable(false);
await windowManager.setAlwaysOnTop(_player.alwaysOnTop);
await windowManager.setTitle("Winamp");
await _loadScreenBounds();
// Try to restore a saved size first (user-resized main window), falling
// back to the default scaled size when none saved.
if (!mounted) return;
final savedMainSize = await PlayerState.loadWindowSize("main");
final sizeToUse = savedMainSize == null
? _scaledSize()
: Size(savedMainSize.$1, savedMainSize.$2);
await windowManager.setSize(sizeToUse);
final savedMain = await PlayerState.loadWindowPos("main");
if (savedMain != null) {
final saved = _clampToScreen(
@@ -255,9 +246,6 @@ class _WinampRootState extends State<WinampRoot> {
);
await windowManager.setPosition(saved.topLeft);
} else {
// No saved position: pick a centered-on-screen default so the window
// always starts fully visible (no off-screen sliver that breaks pointer
// hit-testing against the drag surface).
final screen = _dock.screenRect;
final size = _scaledSize();
if (screen != Rect.zero) {
@@ -266,7 +254,10 @@ class _WinampRootState extends State<WinampRoot> {
await windowManager.setPosition(Offset(x, y));
}
}
final mainBounds = await windowManager.getBounds();
if (!mounted) return;
_dock.setRect(
DockWindow.main,
Rect.fromLTWH(
@@ -276,52 +267,48 @@ class _WinampRootState extends State<WinampRoot> {
mainBounds.height,
),
);
if (mounted) setState(() => _windowReady = true);
debugPrint(
"main window ready: "
"${mainBounds.left},${mainBounds.top} "
"${mainBounds.width}x${mainBounds.height} "
"screen=${_dock.screenRect.left},${_dock.screenRect.top} "
"${_dock.screenRect.width}x${_dock.screenRect.height}",
);
setState(() => _windowReady = true);
} catch (e, st) {
debugPrint("_readyWindow retry: $e\n$st");
// The native window may not be ready yet on macOS; defer to the next
// frame so later gestures (drag, shade toggle) still work once it is.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _readyWindow();
});
return;
}
unawaited(_syncMenuToNative());
await _loadSubWindowStates();
}
Future<void> _loadSubWindowStates() async {
final savedEq = await PlayerState.loadWindowPos("eq");
final savedPl = await PlayerState.loadWindowPos("pl");
final savedEqSize = await PlayerState.loadWindowSize("eq");
final savedPlSize = await PlayerState.loadWindowSize("pl");
if (savedEq != null) _savedSubPositions[SubWindowType.eq] = savedEq;
if (savedPl != null) _savedSubPositions[SubWindowType.pl] = savedPl;
if (savedEqSize != null) {
if (savedEqSize != null)
_savedSubSizes[SubWindowType.eq] = (savedEqSize.$1, savedEqSize.$2);
}
if (savedPlSize != null) {
if (savedPlSize != null)
_savedSubSizes[SubWindowType.pl] = (savedPlSize.$1, savedPlSize.$2);
}
if (_player.eqOpen) unawaited(_openSubWindow(SubWindowType.eq));
if (_player.plOpen) unawaited(_openSubWindow(SubWindowType.pl));
}
/// Clamps [rect] so its top-left stays within the visible screen, keeping at
/// least the window's top-left corner on screen. Returns [rect] unchanged
/// when the screen bounds are not known yet (`screenRect` is zero).
Rect _clampToScreen(Rect rect, Size size) {
final screen = _dock.screenRect;
if (screen == Rect.zero) return rect;
final maxX = screen.width < size.width
? screen.left
: screen.right - size.width;
final maxY = screen.height < size.height
? screen.top
: screen.bottom - size.height;
return Rect.fromLTWH(
rect.left.clamp(screen.left, maxX).toDouble(),
rect.top.clamp(screen.top, maxY).toDouble(),
@@ -331,34 +318,25 @@ class _WinampRootState extends State<WinampRoot> {
}
Future<void> _loadScreenBounds() async {
// The native `winamp` channel handler may not be registered on the very
// first frame, so retry briefly before giving up: with screenRect zero, the
// drag loop snaps the main window to the screen origin and it cannot move.
for (var attempt = 0; attempt < 5; attempt++) {
try {
final result = await winampChannel.invokeMethod<Map<dynamic, dynamic>>(
"getScreenBounds",
);
if (result != null) {
final rect = Rect.fromLTWH(
_dock.screenRect = Rect.fromLTWH(
(result["x"] as num).toDouble(),
(result["y"] as num).toDouble(),
(result["width"] as num).toDouble(),
(result["height"] as num).toDouble(),
);
_dock.screenRect = rect;
debugPrint(
"getScreenBounds: ${rect.left},${rect.top} "
"${rect.width}x${rect.height}",
);
return;
}
} catch (_) {
// ignore: retry; see above
// Retry logic expected
}
await Future<void>.delayed(const Duration(milliseconds: 100));
}
debugPrint("getScreenBounds: all attempts failed");
}
Size _scaledSize() {
@@ -369,15 +347,19 @@ class _WinampRootState extends State<WinampRoot> {
void _onPlayerChanged() {
if (!_windowReady) return;
final shadeChanged = _player.windowShade != _wasShade;
final scaleChanged = _player.uiScale != _wasScale;
if (shadeChanged || scaleChanged) {
_wasShade = _player.windowShade;
_wasScale = _player.uiScale;
final newSize = _scaledSize();
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(windowManager.setSize(newSize));
});
final mainRect = _dock.rectOf(DockWindow.main);
if (mainRect != null) {
_dock.repinResized(
@@ -385,57 +367,53 @@ class _WinampRootState extends State<WinampRoot> {
Rect.fromLTWH(
mainRect.left,
mainRect.top,
_scaledSize().width,
_scaledSize().height,
newSize.width,
newSize.height,
),
);
}
}
if (scaleChanged) {
_invokeSub(DockWindow.eq, "setUiScale", _player.uiScale);
_invokeSub(DockWindow.pl, "setUiScale", _player.uiScale);
unawaited(_pushDockContext());
}
unawaited(windowManager.setAlwaysOnTop(_player.alwaysOnTop));
if (_player.alwaysOnTop != _wasAlwaysOnTop) {
_wasAlwaysOnTop = _player.alwaysOnTop;
_invokeSub(DockWindow.eq, "setAlwaysOnTop", _player.alwaysOnTop);
_invokeSub(DockWindow.pl, "setAlwaysOnTop", _player.alwaysOnTop);
_invokePresets("setAlwaysOnTop", _player.alwaysOnTop);
}
if (_player.eqOpen != _wasEqOpen) {
_wasEqOpen = _player.eqOpen;
if (_player.eqOpen) {
unawaited(_openSubWindow(SubWindowType.eq));
} else {
unawaited(_eqController?.hide());
}
_player.eqOpen
? unawaited(_openSubWindow(SubWindowType.eq))
: unawaited(_eqController?.hide());
}
if (_player.plOpen != _wasPlOpen) {
_wasPlOpen = _player.plOpen;
if (_player.plOpen) {
unawaited(_openSubWindow(SubWindowType.pl));
} else {
unawaited(_plController?.hide());
}
_player.plOpen
? unawaited(_openSubWindow(SubWindowType.pl))
: unawaited(_plController?.hide());
}
unawaited(_syncMenuToNative());
}
void _toggleEqWindow() {
_player.setEqOpen(!_player.eqOpen);
}
void _togglePlaylistWindow() {
_player.setPlOpen(!_player.plOpen);
}
void _onSkinChanged() {
final name = _skins.active.name;
if (name == _wasSkinName) return;
_wasSkinName = name;
final skin = _skins.skin;
if (skin != null) unawaited(registerSkinCursors(skin));
_invokeSub(DockWindow.eq, "setSkin", name);
_invokeSub(DockWindow.pl, "setSkin", name);
}
@@ -448,29 +426,30 @@ class _WinampRootState extends State<WinampRoot> {
);
final path = result?.files.single.path;
if (path == null) return;
try {
final bytes = await File(path).readAsBytes();
await _skins.importArchive(bytes, fileName: path.split("/").last);
} catch (_) {
// ignore: a failed import leaves the active skin unchanged
// Ignored: active skin remains unchanged
}
}
Future<void> _openSubWindow(SubWindowType type) async {
final isEq = type == SubWindowType.eq;
var controller = isEq ? _eqController : _plController;
if (controller != null) {
try {
await controller.show();
} catch (_) {
// The native window is gone (its engine was destroyed); forget it so
// the next open creates a fresh one.
_handleSubWindowLost(isEq ? DockWindow.eq : DockWindow.pl);
return;
}
_setInitialSubRect(type);
return;
}
try {
controller = await WindowController.create(
WindowConfiguration(
@@ -485,15 +464,10 @@ class _WinampRootState extends State<WinampRoot> {
),
);
} catch (_) {
if (isEq) {
_player.setEqOpen(false);
} else {
_player.setPlOpen(false);
}
isEq ? _player.setEqOpen(false) : _player.setPlOpen(false);
return;
}
// The sub-window shows itself once configured; queue its initial position
// (restored or default-docked) so it spawns docked, not centered.
_setInitialSubRect(type);
if (isEq) {
_eqController = controller;
@@ -502,60 +476,48 @@ class _WinampRootState extends State<WinampRoot> {
}
}
/// Positions a sub-window at its saved position or docked to the main window
/// (EQ to the right, PL below), clamped on screen, and queues the rect for
/// when it boots.
void _setInitialSubRect(SubWindowType type) {
final window = type == SubWindowType.eq ? DockWindow.eq : DockWindow.pl;
final mainRect = _dock.rectOf(DockWindow.main);
if (mainRect == null) return;
final shade = type == SubWindowType.eq ? _player.eqShade : _player.plShade;
var size = DockController.scaledSize(window, shade, _player.uiScale);
// If a saved size exists for this sub-window, prefer it over the
// computed default so user resizes persist across launches.
final savedSize = _savedSubSizes[type];
if (savedSize != null) {
size = Size(savedSize.$1, savedSize.$2);
}
var rect = _defaultSubRect(type, mainRect, size);
final saved = _savedSubPositions[type];
if (saved != null) {
rect = Rect.fromLTWH(saved.$1, saved.$2, size.width, size.height);
// If the saved position is near the main window's bottom edge within
// the snap tolerance, snap it exactly so the window restores docked
// rather than merely appearing visually aligned but detached.
rect = DockLogic.snapWindowBelow(rect, mainRect, snapLen: _dock.snapLen);
} else if (_pendingSubBounds.containsKey(window)) {
rect = _pendingSubBounds[window]!;
}
rect = _clampToScreen(rect, size);
_pendingSubBounds[window] = rect;
_dock.setRect(window, rect);
unawaited(_pushPendingSubBounds(window));
}
Rect _defaultSubRect(
SubWindowType type,
Rect mainRect,
Size size,
) {
Rect _defaultSubRect(SubWindowType type, Rect mainRect, Size size) {
final window = type == SubWindowType.eq ? DockWindow.eq : DockWindow.pl;
final siblings = Map<DockWindow, Rect>.of(_dock.rects)..remove(window);
return DockLogic.defaultSubRect(window, mainRect, size, siblings);
}
/// Pushes a queued initial bounds to a sub-window that has booted.
///
/// The bounds stay pending until the invoke succeeds: a sub-window's control
/// channel is only registered once its engine reports `subReady`, and an
/// early `setBounds` otherwise races the registration and fails. A lost
/// channel (the window's engine was destroyed) clears the window from the
/// dock instead.
Future<void> _pushPendingSubBounds(DockWindow window) async {
final rect = _pendingSubBounds[window];
if (rect == null) return;
final controller = window == DockWindow.eq ? _eqController : _plController;
if (controller == null) return;
try {
await controller.invokeMethod("setBounds", {
"x": rect.left,
@@ -571,8 +533,6 @@ class _WinampRootState extends State<WinampRoot> {
}
}
/// Opens the EQ presets dialog (spawned by the EQ presets button), switching
/// an already-open dialog to [mode].
Future<void> _openPresetsWindow(String mode) async {
final existing = _presetsController;
if (existing != null) {
@@ -580,10 +540,7 @@ class _WinampRootState extends State<WinampRoot> {
await existing.invokeMethod("setMode", mode);
await existing.show();
return;
} on WindowChannelException {
_handlePresetsWindowLost();
} catch (_) {
// The window is mid-boot; fall through and show it once ready.
try {
await existing.show();
return;
@@ -592,8 +549,10 @@ class _WinampRootState extends State<WinampRoot> {
}
}
}
final mainRect = _dock.rectOf(DockWindow.main);
final eqRect = _dock.rectOf(DockWindow.eq);
try {
final controller = await WindowController.create(
WindowConfiguration(
@@ -612,18 +571,17 @@ class _WinampRootState extends State<WinampRoot> {
_setInitialPresetsRect(eqRect ?? mainRect);
unawaited(_pushPresetsBounds());
} catch (_) {
// ignore: failed to open the presets window
// Ignore open failure
}
}
/// Positions the presets dialog next to [anchor] (the Equalizer window, or
/// the main window when the Equalizer is closed), kept fully on screen.
void _setInitialPresetsRect(Rect? anchor) {
const size = presetsWindowSize;
if (anchor == null) {
_pendingPresetsBounds = Rect.fromLTWH(120, 120, size.width, size.height);
return;
}
final screen = _dock.screenRect;
if (screen == Rect.zero) {
_pendingPresetsBounds = Rect.fromLTWH(
@@ -634,17 +592,17 @@ class _WinampRootState extends State<WinampRoot> {
);
return;
}
final x = anchor.right.clamp(screen.left, screen.right - size.width);
final y = anchor.top.clamp(screen.top, screen.bottom - size.height);
_pendingPresetsBounds = Rect.fromLTWH(x, y, size.width, size.height);
}
/// Pushes the queued initial bounds to the presets dialog, retrying until
/// its control channel registers.
Future<void> _pushPresetsBounds() async {
final rect = _pendingPresetsBounds;
final controller = _presetsController;
if (rect == null || controller == null) return;
try {
await controller.invokeMethod("setBounds", {
"x": rect.left,
@@ -657,7 +615,8 @@ class _WinampRootState extends State<WinampRoot> {
_handlePresetsWindowLost();
} catch (_) {
if (mounted && _presetsController != null) {
Timer(const Duration(milliseconds: 120), () {
_presetsTimer?.cancel();
_presetsTimer = Timer(const Duration(milliseconds: 120), () {
if (mounted && _presetsController != null) {
unawaited(_pushPresetsBounds());
}
@@ -666,25 +625,17 @@ class _WinampRootState extends State<WinampRoot> {
}
}
/// Invokes [method] on the presets dialog's control channel, dropping the
/// controller when its channel is gone (the window was closed).
void _invokePresets(String method, [Object? args]) {
final controller = _presetsController;
if (controller == null) return;
unawaited(
controller.invokeMethod(method, args).catchError((Object e) {
_presetsController?.invokeMethod(method, args).catchError((Object e) {
if (e is WindowChannelException && e.code == "CHANNEL_UNREGISTERED") {
_handlePresetsWindowLost();
}
return null;
}),
);
});
}
/// Drops the presets dialog whose native window/channel is gone.
void _handlePresetsWindowLost() {
if (_presetsController == null) return;
debugPrint("presets window lost");
_presetsController = null;
}
@@ -705,20 +656,15 @@ class _WinampRootState extends State<WinampRoot> {
final mode = args["mode"] is String ? args["mode"] as String : "load";
unawaited(_openPresetsWindow(mode));
case "applyPreset":
// Forward a preset loaded from the presets dialog to the Equalizer.
_invokeSub(DockWindow.eq, "applyPreset", {
"preamp": args["preamp"],
"bands": args["bands"],
});
case "getEqSettings":
// The presets dialog pulls the Equalizer's current settings to save.
final controller = _eqController;
if (controller == null) return null;
if (_eqController == null) return null;
try {
return await controller.invokeMethod("getEqSettings");
} on WindowChannelException {
return null;
} on MissingPluginException {
return await _eqController!.invokeMethod("getEqSettings");
} catch (_) {
return null;
}
case "subReady":
@@ -750,19 +696,14 @@ class _WinampRootState extends State<WinampRoot> {
return null;
}
/// Pushes the current screen rect, snap settings and all window rects to the
/// open sub-windows so they can snap locally during their own drags.
Future<void> _pushDockContext() async {
final rectsJson = _dock.rects.map(
(window, rect) => MapEntry(
window.name,
{
(window, rect) => MapEntry(window.name, {
"x": rect.left,
"y": rect.top,
"w": rect.width,
"h": rect.height,
},
),
}),
);
final args = <String, dynamic>{
"screen": {
@@ -775,25 +716,21 @@ class _WinampRootState extends State<WinampRoot> {
"snapLen": _dock.snapLen,
"rects": rectsJson,
};
_invokeSub(DockWindow.eq, "setDockContext", args);
_invokeSub(DockWindow.pl, "setDockContext", args);
_invokePresets("setDockContext", args);
}
/// Invokes [method] on a sub-window's control channel without ever leaking
/// an unhandled channel error, and drops the window from the dock when its
/// channel is gone (the native window was destroyed).
void _invokeSub(DockWindow window, String method, [Object? args]) {
final controller = window == DockWindow.eq ? _eqController : _plController;
if (controller == null) return;
unawaited(
controller.invokeMethod(method, args).catchError((Object e) {
if (e is WindowChannelException && e.code == "CHANNEL_UNREGISTERED") {
_handleSubWindowLost(window, error: e);
} else if (e is MissingPluginException) {
// The window's engine is mid-boot; its channel is not registered
// yet. Swallow: the next push reaches it once it is.
} else {
} else if (e is! MissingPluginException) {
debugPrint("sub invoke $method failed for $window: $e");
}
return null;
@@ -801,34 +738,22 @@ class _WinampRootState extends State<WinampRoot> {
);
}
/// Drops a sub-window whose native window/channel is gone (its engine was
/// destroyed), so its rect and menu checkmark do not linger.
void _handleSubWindowLost(DockWindow window, {Object? error}) {
final isEq = window == DockWindow.eq;
if ((isEq ? _eqController : _plController) == null) return;
debugPrint("sub window lost ($window): $error");
if (isEq) {
_eqController = null;
} else {
_plController = null;
}
_dock.removeWindow(window);
if (isEq) {
_player.setEqOpen(false);
} else {
_plController = null;
_player.setPlOpen(false);
}
_dock.removeWindow(window);
unawaited(_pushDockContext());
}
/// Applies a window rect natively: the main window positions itself;
/// sub-windows are moved over their control channel.
///
/// Moves are coalesced: each call only records the latest position and a
/// single flush applies them up to [_moveFlushInterval] later. A fast drag
/// emits hundreds of positions per second; sending them all would saturate
/// the platform thread (a sub-window move crosses three channel hops) and
/// the windows would lag the cursor and catch up in a cascade of jumps.
void _moveWindow(DockWindow window, Rect rect) {
_pendingMoves[window] = rect.topLeft;
_moveFlushTimer ??= Timer(_moveFlushInterval, _flushMoves);
@@ -838,17 +763,14 @@ class _WinampRootState extends State<WinampRoot> {
_moveFlushTimer = null;
final moves = Map.of(_pendingMoves);
_pendingMoves.clear();
for (final entry in moves.entries) {
final window = entry.key;
final topLeft = entry.value;
if (window == DockWindow.main) {
windowManager.setPosition(topLeft);
} else {
_invokeSub(
window,
"setBounds",
{"x": topLeft.dx, "y": topLeft.dy},
);
_invokeSub(window, "setBounds", {"x": topLeft.dx, "y": topLeft.dy});
}
}
}
@@ -867,19 +789,13 @@ class _WinampRootState extends State<WinampRoot> {
PlayerState.saveWindowPos(name, entry.value.left, entry.value.top),
);
unawaited(
PlayerState.saveWindowSize(
name,
entry.value.width,
entry.value.height,
),
PlayerState.saveWindowSize(name, entry.value.width, entry.value.height),
);
}
}
Future<void> _reconcileSubWindows() async {
WindowController? eq;
WindowController? pl;
WindowController? presets;
WindowController? eq, pl, presets;
try {
for (final window in await WindowController.getAll()) {
final args = SubWindowArgs.tryDecode(window.arguments);
@@ -891,9 +807,11 @@ class _WinampRootState extends State<WinampRoot> {
} catch (_) {
return;
}
_eqController = eq;
_plController = pl;
_presetsController = presets;
if (eq == null && _player.eqOpen) {
_dock.removeWindow(DockWindow.eq);
_player.setEqOpen(false);
@@ -902,17 +820,15 @@ class _WinampRootState extends State<WinampRoot> {
_dock.removeWindow(DockWindow.pl);
_player.setPlOpen(false);
}
if (eq == null || pl == null) {
unawaited(_pushDockContext());
}
if (eq == null || pl == null) unawaited(_pushDockContext());
}
Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case "toggleEq":
_toggleEqWindow();
_player.setEqOpen(!_player.eqOpen);
case "togglePlaylist":
_togglePlaylistWindow();
_player.setPlOpen(!_player.plOpen);
case "toggleAlwaysOnTop":
_player.setAlwaysOnTop(!_player.alwaysOnTop);
case "toggleWindowShade":
@@ -925,8 +841,6 @@ class _WinampRootState extends State<WinampRoot> {
}
}
/// Reports state back to the macOS menu bar so checkmarks stay in sync with
/// what the in-window buttons do. Best-effort; native may not be up yet.
Future<void> _syncMenuToNative() async {
try {
await winampChannel.invokeMethod("setAlwaysOnTop", {
@@ -940,26 +854,24 @@ class _WinampRootState extends State<WinampRoot> {
await winampChannel.invokeMethod("setUiScale", {
"scale": _player.uiScale,
});
} on PlatformException {
// ignore: menu sync is best-effort
} on MissingPluginException {
// ignore: menu sync is best-effort
} catch (_) {
// Menu sync is best-effort
}
}
@override
Widget build(BuildContext context) {
final skin = _skins.skin;
final Widget body;
if (skin == null) {
body = const ColoredBox(color: Colors.black);
} else if (_skins.lastError != null) {
body = ColoredBox(
if (skin == null) return const ColoredBox(color: Colors.black);
if (_skins.lastError != null) {
return ColoredBox(
color: const Color(0xFF00FF00),
child: Center(child: Text("Skin load failed: ${_skins.lastError}")),
);
} else {
body = AnimatedBuilder(
}
return AnimatedBuilder(
animation: Listenable.merge([_player, _skins]),
builder: (context, _) {
final scale = _player.uiScale;
@@ -990,18 +902,8 @@ class _WinampRootState extends State<WinampRoot> {
},
);
}
return body;
}
}
/// Drives the main window's custom drag loop through the [DockController].
///
/// The cursor's global screen position is fetched from the native winamp
/// channel on every update; absolute positions make the window track the
/// cursor exactly with no feedback loop or accumulated error. If that fetch
/// ever fails (channel down), the drag falls back to a relative mode: the
/// window-local grab offset plus the current window position, which needs no
/// native round-trip at all.
class _MainWindowDragController implements WindowDragController {
_MainWindowDragController(this._state);
@@ -1021,12 +923,7 @@ class _MainWindowDragController implements WindowDragController {
_begun = false;
_last = null;
_relGrab = globalPosition;
_useNative = null; // undecided until first fetch
debugPrint(
"main drag start (window-local: "
"${globalPosition.dx.toStringAsFixed(1)},"
"${globalPosition.dy.toStringAsFixed(1)})",
);
_useNative = null;
_fetch(globalPosition);
}
@@ -1037,84 +934,55 @@ class _MainWindowDragController implements WindowDragController {
Future<void> _fetch(ui.Offset localPoint) async {
final seq = ++_seq;
// Decide once whether to use native absolute coordinates or the local
// relative fallback for the whole drag. This avoids mid-drag source
// switching that causes visible jumps when the native channel is
// intermittently available.
if (_useNative == null) {
if (_useNative == null || _useNative == true) {
try {
final result = await winampChannel.invokeMethod<Map<dynamic, dynamic>>(
"getMouseLocation",
);
if (!_active || seq != _seq || result == null) return;
_useNative = true;
final point = Offset(
(result["x"] as num).toDouble(),
(result["y"] as num).toDouble(),
);
_applyNativeDrag(point);
return;
} catch (_) {
if (_useNative == true) return; // Prevent mid-drag switching
_useNative = false;
}
}
if (_useNative == false) {
_applyRelativeDrag(localPoint, seq);
}
}
void _applyNativeDrag(Offset point) {
if (!_begun) {
_begun = true;
_state._dock.beginMainDrag(point);
if (_dragDiagnostics)
debugPrint("main drag begun at $point (native)");
}
if (point != _last) {
_last = point;
_state._dock.mainDragTo(point);
if (_dragDiagnostics) debugPrint("main drag to $point (native)");
}
return;
} catch (_) {
_useNative = false;
if (_dragDiagnostics)
debugPrint("main drag: native unavailable, using relative mode");
// fall through to relative path below
}
}
if (_useNative == true) {
// Native already chosen; attempt to fetch latest but ignore errors.
try {
final result = await winampChannel.invokeMethod<Map<dynamic, dynamic>>(
"getMouseLocation",
);
if (!_active || seq != _seq || result == null) return;
final point = Offset(
(result["x"] as num).toDouble(),
(result["y"] as num).toDouble(),
);
if (!_begun) {
_begun = true;
_state._dock.beginMainDrag(point);
if (_dragDiagnostics)
debugPrint("main drag begun at $point (native)");
}
if (point == _last) return;
_last = point;
_state._dock.mainDragTo(point);
if (_dragDiagnostics) debugPrint("main drag to $point (native)");
} catch (_) {
// Ignore transient failures once native was chosen; avoid switching
// to relative mid-drag to prevent jumps.
}
return;
}
// Relative fallback path (chosen because native was unavailable at drag
// start). Use the window-local grab offset consistently for the whole
// drag so coordinates don't oscillate.
void _applyRelativeDrag(ui.Offset localPoint, int seq) {
if (!_active || seq != _seq) return;
final rect = _state._dock.rectOf(DockWindow.main);
final relGrab = _relGrab;
if (rect == null || relGrab == null) return;
if (!_begun) {
_begun = true;
_state._dock.beginMainDrag(rect.topLeft + relGrab);
if (_dragDiagnostics)
debugPrint(
"main drag begun (relative) at ${rect.topLeft.dx + relGrab.dx},${rect.topLeft.dy + relGrab.dy}",
);
}
_state._dock.moveAttached(
DockWindow.main,
DockLogic.moveBy(
@@ -1123,10 +991,6 @@ class _MainWindowDragController implements WindowDragController {
localPoint.dy - relGrab.dy,
),
);
if (_dragDiagnostics)
debugPrint(
"main drag to (relative) ${localPoint.dx - relGrab.dx},${localPoint.dy - relGrab.dy}",
);
}
@override
@@ -1135,14 +999,8 @@ class _MainWindowDragController implements WindowDragController {
unawaited(_finalize());
}
/// Applies one last cursor position so the window settles exactly where the
/// pointer was released (the in-flight [update] fetches are dropped once
/// [_active] is false), then ends the drag.
Future<void> _finalize() async {
try {
// Only query native if we chose native mode for this drag. If we
// started in relative mode, skip the native fetch to avoid switching
// sources at the last moment.
if (_useNative != false) {
final result = await winampChannel.invokeMethod<Map<dynamic, dynamic>>(
"getMouseLocation",
@@ -1159,7 +1017,7 @@ class _MainWindowDragController implements WindowDragController {
}
}
} catch (_) {
// ignore: end the drag with the position already applied
// Drag ends with the last position already applied
}
_state._dock.endMainDrag();
}