From 2244051453b882f313a50a2bf08b005d2b8f72ce Mon Sep 17 00:00:00 2001 From: ws909 <37029098+ws909@users.noreply.github.com> Date: Thu, 2 Mar 2023 18:43:27 +0100 Subject: [PATCH] Per-window progress indicator states and values for the MacOS Dock icon --- include/GLFW/glfw3.h | 9 ++- src/cocoa_init.m | 6 +- src/cocoa_platform.h | 12 +++- src/cocoa_window.m | 159 +++++++++++++++++++++++++++++++------------ 4 files changed, 135 insertions(+), 51 deletions(-) diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index a03e7d72..39ebf7f5 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1289,6 +1289,8 @@ extern "C" { * * @remark @x11 @wayland This behaves like @ref GLFW_TASKBAR_PROGRESS_NORMAL. * + * @remark @macos This displays a standard indeterminate `NSProgressIndicator`. + * * Used by @ref window_taskbar_progress. */ #define GLFW_TASKBAR_PROGRESS_INDETERMINATE 1 @@ -1305,7 +1307,7 @@ extern "C" { * * @remark @win32 This displays a red progress bar with 100% progress. * - * @remark @x11 @wayland This behaves like @ref GLFW_TASKBAR_PROGRESS_NORMAL. + * @remark @x11 @wayland @macos This behaves like @ref GLFW_TASKBAR_PROGRESS_NORMAL. * * Used by @ref window_taskbar_progress. */ @@ -1316,7 +1318,7 @@ extern "C" { * * @remark @win32 This displays a yellow filled progress bar. * - * @remark @x11 @wayland This behaves like @ref GLFW_TASKBAR_PROGRESS_NORMAL. + * @remark @x11 @wayland @macos This behaves like @ref GLFW_TASKBAR_PROGRESS_NORMAL. * * Used by @ref window_taskbar_progress. */ @@ -3374,7 +3376,8 @@ GLFWAPI void glfwSetWindowIcon(GLFWwindow* window, int count, const GLFWimage* i * * @remark @win32 On Windows Vista and earlier, this function will emit @ref GLFW_FEATURE_UNAVAILABLE. * - * @remark @macos This function will emit @ref GLFW_FEATURE_UNIMPLEMENTED. + * @remark @macos There exists only one Dock icon progress bar, and this + * displays the combined values of all the windows. * * @remark @x11 @wayland Requires a valid application desktop file with the same name * as the compiled executable. Due to limitations in the Unity Launcher API diff --git a/src/cocoa_init.m b/src/cocoa_init.m index cabc6424..5d199170 100644 --- a/src/cocoa_init.m +++ b/src/cocoa_init.m @@ -650,10 +650,10 @@ void _glfwTerminateCocoa(void) { @autoreleasepool { - if (_glfw.ns.dockProgressIndicator != nil) + if (_glfw.ns.dockProgressIndicator.view != nil) { - [_glfw.ns.dockProgressIndicator removeFromSuperview]; - [_glfw.ns.dockProgressIndicator release]; + [_glfw.ns.dockProgressIndicator.view removeFromSuperview]; + [_glfw.ns.dockProgressIndicator.view release]; } if (_glfw.ns.inputSource) diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h index 59a958a5..aeda201b 100644 --- a/src/cocoa_platform.h +++ b/src/cocoa_platform.h @@ -156,6 +156,11 @@ typedef struct _GLFWwindowNS // since the last cursor motion event was processed // This is kept to counteract Cocoa doing the same internally double cursorWarpDeltaX, cursorWarpDeltaY; + + struct { + int state; + double value; + } dockProgressIndicator; } _GLFWwindowNS; // Cocoa-specific global data @@ -190,7 +195,12 @@ typedef struct _GLFWlibraryNS CFStringRef kPropertyUnicodeKeyLayoutData; } tis; - id dockProgressIndicator; + struct { + id view; + int windowCount; + int indeterminateCount; + double totalValue; + } dockProgressIndicator; } _GLFWlibraryNS; // Cocoa-specific per-monitor data diff --git a/src/cocoa_window.m b/src/cocoa_window.m index b86256ab..ff2e8f9a 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -197,6 +197,75 @@ static NSUInteger translateKeyToModifierFlag(int key) // static const NSRange kEmptyRange = { NSNotFound, 0 }; +static NSProgressIndicator* createProgressIndicator(const NSDockTile* dockTile) +{ + NSView* contentView = [dockTile contentView]; + + NSProgressIndicator* indicator = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(0.0f, 0.0f, contentView.frame.size.width, 15.0f)]; + [indicator setStyle:NSProgressIndicatorStyleBar]; + [indicator setControlSize:NSControlSizeLarge]; + [indicator setMinValue:0.0f]; + [indicator setMaxValue:1.0f]; + + [contentView addSubview:indicator]; + + _glfw.ns.dockProgressIndicator.view = indicator; + + return indicator; +} + +static void setDockProgressIndicator(int progressState, double value) +{ + NSProgressIndicator* indicator = _glfw.ns.dockProgressIndicator.view; + + NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile]; + + if (indicator == nil) + { + if ([dockTile contentView] == nil) + { + NSImageView *iconView = [[NSImageView alloc] init]; + [iconView setImage:[[NSApplication sharedApplication] applicationIconImage]]; + [dockTile setContentView:iconView]; + [iconView release]; + } + + indicator = createProgressIndicator(dockTile); + } + + // ### Switching from INDETERMINATE to NORMAL, PAUSED or ERROR requires 2 invocations in different frames. + // In MacOS 12 (and probably other versions), an indeterminate progress bar is rendered as a normal bar + // with 0.0 progress. So when calling [progressIndicator setIndeterminate:YES], the indicator actually + // sets its doubleValue to 0.0. + // The bug is caused by NSProgressIndicator not immediately updating its value when it's increasing. + // This code illustrates the exact same problem, but this time from NORMAL, PAUSED and ERROR to INDETERMINATE: + // + // if (progressState == GLFW_TASKBAR_PROGRESS_INDETERMINATE) + // [progressIndicator setDoubleValue:0.75]; + // else + // [progressIndicator setDoubleValue:0.25]; + // + // This is likely a bug in Cocoa. + // + // ### Progress increments are delayed + // What this also means, is that each time the progress increments, the bar's progress will be 1 frame delayed, + // and only updated once a higher or similar value is again set the next frame. + + // Workaround for the aforementioned issues. If there's any versions of MacOS where + // this issue is not present, this should be ommitted in those versions. + if ([indicator isIndeterminate] || [indicator doubleValue] < value) + { + [indicator removeFromSuperview]; + [indicator release]; + indicator = createProgressIndicator(dockTile); + } + + [indicator setIndeterminate:progressState == GLFW_TASKBAR_PROGRESS_INDETERMINATE]; + [indicator setHidden:progressState == GLFW_TASKBAR_PROGRESS_DISABLED]; + [indicator setDoubleValue:value]; + + [dockTile display]; +} //------------------------------------------------------------------------ // Delegate for window related notifications @@ -986,6 +1055,8 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, void _glfwDestroyWindowCocoa(_GLFWwindow* window) { @autoreleasepool { + + _glfwSetWindowTaskbarProgressCocoa(window, GLFW_TASKBAR_PROGRESS_DISABLED, 0.0); if (_glfw.ns.disabledCursorWindow == window) _glfw.ns.disabledCursorWindow = NULL; @@ -1032,60 +1103,60 @@ void _glfwSetWindowIconCocoa(_GLFWwindow* window, "Cocoa: Regular windows do not have icons on macOS"); } -// TODO: allow multiple windows to set values. Use the combined progress for all of them; example: [35%, 70%, 90%] => 65%. -// TODO: documentation remarks for MacOS void _glfwSetWindowTaskbarProgressCocoa(_GLFWwindow* window, int progressState, double value) { - NSProgressIndicator* indicator = _glfw.ns.dockProgressIndicator; + if (progressState == GLFW_TASKBAR_PROGRESS_ERROR || progressState == GLFW_TASKBAR_PROGRESS_PAUSED) + progressState = GLFW_TASKBAR_PROGRESS_NORMAL; + + const int oldState = window->ns.dockProgressIndicator.state; + const int state = progressState; - NSDockTile* dockTile = [[NSApplication sharedApplication] dockTile]; + const double oldValue = window->ns.dockProgressIndicator.value; - if (indicator == nil) + if (oldState == state) { - if ([dockTile contentView] == nil) - { - NSImageView *iconView = [[NSImageView alloc] init]; - [iconView setImage:[[NSApplication sharedApplication] applicationIconImage]]; - [dockTile setContentView:iconView]; - [iconView release]; - } - NSView* contentView = [dockTile contentView]; - - indicator = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(0.0f, 0.0f, contentView.frame.size.width, 15.0f)]; - [indicator setStyle:NSProgressIndicatorStyleBar]; - [indicator setControlSize:NSControlSizeLarge]; - [indicator setMinValue:0.0f]; - [indicator setMaxValue:1.0f]; - - [contentView addSubview:indicator]; - - _glfw.ns.dockProgressIndicator = indicator; + if (state == GLFW_TASKBAR_PROGRESS_DISABLED || + state == GLFW_TASKBAR_PROGRESS_INDETERMINATE || + oldValue == value) + return; } - // FIXME: Switching from INDETERMINATE to NORMAL, PAUSED or ERROR requires 2 invocations in different frames. - // In MacOS 12 (and probably other versions), an indeterminate progress bar is rendered as a normal bar - // with 0.0 progress. So when calling [progressIndicator setIndeterminate:YES], the indicator actually - // sets its doubleValue to 0.0. - // The bug is caused by NSProgressIndicator not immediately updating its value when it's increasing. - // This code illustrates the exact same problem, but this time from NORMAL, PAUSED and ERROR to INDETERMINATE: - // - // if (progressState == GLFW_TASKBAR_PROGRESS_INDETERMINATE) - // [progressIndicator setDoubleValue:0.75]; - // else - // [progressIndicator setDoubleValue:0.25]; - // - // This is likely a bug in Cocoa. - // - // FIXME: Progress increments are delayed - // What this also means, is that each time the progress increments, the bar's progress will be 1 frame delayed, - // and only updated once a higher or similar value is again set the next frame. + if (oldState != state) + { + // Reset + if (oldState == GLFW_TASKBAR_PROGRESS_INDETERMINATE) + --_glfw.ns.dockProgressIndicator.indeterminateCount; + if (oldState != GLFW_TASKBAR_PROGRESS_DISABLED) + { + --_glfw.ns.dockProgressIndicator.windowCount; + _glfw.ns.dockProgressIndicator.totalValue -= oldValue; + } + + // Set + if (state == GLFW_TASKBAR_PROGRESS_INDETERMINATE) + ++_glfw.ns.dockProgressIndicator.indeterminateCount; + if (state != GLFW_TASKBAR_PROGRESS_DISABLED) + { + ++_glfw.ns.dockProgressIndicator.windowCount; + _glfw.ns.dockProgressIndicator.totalValue += value; + } + } + else if (state != GLFW_TASKBAR_PROGRESS_DISABLED) + _glfw.ns.dockProgressIndicator.totalValue += (value - oldValue); - [indicator setIndeterminate:progressState == GLFW_TASKBAR_PROGRESS_INDETERMINATE]; - [indicator setHidden:progressState == GLFW_TASKBAR_PROGRESS_DISABLED]; - [indicator setDoubleValue:value]; + if (_glfw.ns.dockProgressIndicator.windowCount > _glfw.ns.dockProgressIndicator.indeterminateCount) + { + const double finalValue = _glfw.ns.dockProgressIndicator.totalValue / _glfw.ns.dockProgressIndicator.windowCount; + setDockProgressIndicator(GLFW_TASKBAR_PROGRESS_NORMAL, finalValue); + } + else if (_glfw.ns.dockProgressIndicator.indeterminateCount > 0) + setDockProgressIndicator(GLFW_TASKBAR_PROGRESS_INDETERMINATE, 0.0f); + else + setDockProgressIndicator(GLFW_TASKBAR_PROGRESS_DISABLED, 0.0f); - [dockTile display]; + window->ns.dockProgressIndicator.state = state; + window->ns.dockProgressIndicator.value = value; } void _glfwGetWindowPosCocoa(_GLFWwindow* window, int* xpos, int* ypos)