Per-window progress indicator states and values for the MacOS Dock icon

This commit is contained in:
ws909 2023-03-02 18:43:27 +01:00
parent a9b36d48d7
commit 2244051453
4 changed files with 135 additions and 51 deletions

View File

@ -1289,6 +1289,8 @@ extern "C" {
* *
* @remark @x11 @wayland This behaves like @ref GLFW_TASKBAR_PROGRESS_NORMAL. * @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. * Used by @ref window_taskbar_progress.
*/ */
#define GLFW_TASKBAR_PROGRESS_INDETERMINATE 1 #define GLFW_TASKBAR_PROGRESS_INDETERMINATE 1
@ -1305,7 +1307,7 @@ extern "C" {
* *
* @remark @win32 This displays a red progress bar with 100% progress. * @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. * Used by @ref window_taskbar_progress.
*/ */
@ -1316,7 +1318,7 @@ extern "C" {
* *
* @remark @win32 This displays a yellow filled progress bar. * @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. * 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 @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 * @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 * as the compiled executable. Due to limitations in the Unity Launcher API

View File

@ -650,10 +650,10 @@ void _glfwTerminateCocoa(void)
{ {
@autoreleasepool { @autoreleasepool {
if (_glfw.ns.dockProgressIndicator != nil) if (_glfw.ns.dockProgressIndicator.view != nil)
{ {
[_glfw.ns.dockProgressIndicator removeFromSuperview]; [_glfw.ns.dockProgressIndicator.view removeFromSuperview];
[_glfw.ns.dockProgressIndicator release]; [_glfw.ns.dockProgressIndicator.view release];
} }
if (_glfw.ns.inputSource) if (_glfw.ns.inputSource)

View File

@ -156,6 +156,11 @@ typedef struct _GLFWwindowNS
// since the last cursor motion event was processed // since the last cursor motion event was processed
// This is kept to counteract Cocoa doing the same internally // This is kept to counteract Cocoa doing the same internally
double cursorWarpDeltaX, cursorWarpDeltaY; double cursorWarpDeltaX, cursorWarpDeltaY;
struct {
int state;
double value;
} dockProgressIndicator;
} _GLFWwindowNS; } _GLFWwindowNS;
// Cocoa-specific global data // Cocoa-specific global data
@ -190,7 +195,12 @@ typedef struct _GLFWlibraryNS
CFStringRef kPropertyUnicodeKeyLayoutData; CFStringRef kPropertyUnicodeKeyLayoutData;
} tis; } tis;
id dockProgressIndicator; struct {
id view;
int windowCount;
int indeterminateCount;
double totalValue;
} dockProgressIndicator;
} _GLFWlibraryNS; } _GLFWlibraryNS;
// Cocoa-specific per-monitor data // Cocoa-specific per-monitor data

View File

@ -197,6 +197,75 @@ static NSUInteger translateKeyToModifierFlag(int key)
// //
static const NSRange kEmptyRange = { NSNotFound, 0 }; 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 // Delegate for window related notifications
@ -986,6 +1055,8 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window,
void _glfwDestroyWindowCocoa(_GLFWwindow* window) void _glfwDestroyWindowCocoa(_GLFWwindow* window)
{ {
@autoreleasepool { @autoreleasepool {
_glfwSetWindowTaskbarProgressCocoa(window, GLFW_TASKBAR_PROGRESS_DISABLED, 0.0);
if (_glfw.ns.disabledCursorWindow == window) if (_glfw.ns.disabledCursorWindow == window)
_glfw.ns.disabledCursorWindow = NULL; _glfw.ns.disabledCursorWindow = NULL;
@ -1032,60 +1103,60 @@ void _glfwSetWindowIconCocoa(_GLFWwindow* window,
"Cocoa: Regular windows do not have icons on macOS"); "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) 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) if (state == GLFW_TASKBAR_PROGRESS_DISABLED ||
{ state == GLFW_TASKBAR_PROGRESS_INDETERMINATE ||
NSImageView *iconView = [[NSImageView alloc] init]; oldValue == value)
[iconView setImage:[[NSApplication sharedApplication] applicationIconImage]]; return;
[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;
} }
// FIXME: Switching from INDETERMINATE to NORMAL, PAUSED or ERROR requires 2 invocations in different frames. if (oldState != state)
// 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 // Reset
// sets its doubleValue to 0.0. if (oldState == GLFW_TASKBAR_PROGRESS_INDETERMINATE)
// The bug is caused by NSProgressIndicator not immediately updating its value when it's increasing. --_glfw.ns.dockProgressIndicator.indeterminateCount;
// This code illustrates the exact same problem, but this time from NORMAL, PAUSED and ERROR to INDETERMINATE: if (oldState != GLFW_TASKBAR_PROGRESS_DISABLED)
// {
// if (progressState == GLFW_TASKBAR_PROGRESS_INDETERMINATE) --_glfw.ns.dockProgressIndicator.windowCount;
// [progressIndicator setDoubleValue:0.75]; _glfw.ns.dockProgressIndicator.totalValue -= oldValue;
// else }
// [progressIndicator setDoubleValue:0.25];
// // Set
// This is likely a bug in Cocoa. if (state == GLFW_TASKBAR_PROGRESS_INDETERMINATE)
// ++_glfw.ns.dockProgressIndicator.indeterminateCount;
// FIXME: Progress increments are delayed if (state != GLFW_TASKBAR_PROGRESS_DISABLED)
// 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. ++_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]; if (_glfw.ns.dockProgressIndicator.windowCount > _glfw.ns.dockProgressIndicator.indeterminateCount)
[indicator setHidden:progressState == GLFW_TASKBAR_PROGRESS_DISABLED]; {
[indicator setDoubleValue:value]; 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) void _glfwGetWindowPosCocoa(_GLFWwindow* window, int* xpos, int* ypos)