AppKit: Implement window minimize with restore from Windows menu

Order minimized window off screen and keep it in Windows menu for restore. Enable minimize button for miniaturizable windows.

* CPWindow.j:
  - Order window out with attached sheet and visible child windows on miniaturize:; keep first responder and child windows for restore; ignore miniaturize: on minimized window.
  - Restore window, first responder and child windows when window orders front; post CPWindowDidDeminiaturizeNotification after restore.
  - Restore minimized window through makeKeyAndOrderFront: in deminiaturize:.
  - Remove calls to platform window's miniaturize: and deminiaturize:.
  - Minimize through performMiniaturize: only when window can minimize; enable performMiniaturize: and miniaturize: menu items under same condition.

* _CPStandardWindowView.j: Enable minimize button for miniaturizable windows and target window's miniaturize:.

* CPWindowTest.j: Add tests for restore, notification order, first responder restore, performMiniaturize: on non-miniaturizable window, minimize button action and menu validation.

* CPApplicationWindowsMenuTest.j: Add tests for minimized window listed in mixed state and restored from Windows menu.

* DropInCompatibilityTest.j: Replace test for minimize button never enabled with test for minimize button enabled only for miniaturizable windows.
This commit is contained in:
David Richardson committed 2026-11-08 14:37:23 -06:00
1 parent af4d9dd9b9
commit af2edf6c97
5 files changed
+237 -33

No files matched your search

+90 -26
View File
@@ -187,6 +187,8 @@ var CPWindowActionMessageKeys = [
BOOL _isVisible;
BOOL _hasBeenOrderedIn @accessors;
BOOL _isMiniaturized;
CPResponder _miniaturizedFirstResponder;
CPArray _miniaturizedChildWindows;
CGRect _userStateFrame;
BOOL _isExcludedFromWindowsMenu;
BOOL _isAnimating @accessors(setter=_setAnimating:);
@@ -997,6 +999,14 @@ CPTexturedBackgroundWindowMask
- (void)_orderFront
{
/*
Ordering a minimized window front restores it, so choosing it in the Windows menu,
which sends makeKeyAndOrderFront:, brings it back. The minimized state stayed set, so
a window ordered front still counted as minimized.
*/
var wasMiniaturized = _isMiniaturized;
_isMiniaturized = NO;
if (CPDOMAvailable)
{
@@ -1018,6 +1028,9 @@ CPTexturedBackgroundWindowMask
[self _setFrame:_frame display:YES animate:NO constrainWidth:YES constrainHeight:YES];
}
if (wasMiniaturized)
[self _restoreMiniaturizedState];
if (!CPApp._keyWindow)
[self makeKeyWindow];
@@ -1031,6 +1044,9 @@ CPTexturedBackgroundWindowMask
if ([self attachedSheet])
[_platformWindow order:CPWindowAbove window:[self attachedSheet] relativeTo:nil];
if (wasMiniaturized)
[[CPNotificationCenter defaultCenter] postNotificationName:CPWindowDidDeminiaturizeNotification object:self];
}
/*
@@ -2495,49 +2511,97 @@ CPTexturedBackgroundWindowMask
// Minimizing Windows
/*!
Simulates the user clicking the minimize button. A page cannot minimize its browser
window, so the minimize button is always disabled and this method does nothing.
Simulates the user clicking the minimize button. Minimizes the window when its minimize
button is enabled, and does nothing otherwise. The minimize button is enabled when the
window is miniaturizable and is neither a sheet nor a full platform window.
@param aSender the object making this request
*/
- (void)performMiniaturize:(id)aSender
{
/*
It called -miniaturize:, whose platform window does nothing in the browser, so the
window stayed on screen but counted as minimized.
*/
if ([self _canMiniaturize])
[self miniaturize:aSender];
}
/*!
Minimizes the window. Posts a \c CPWindowWillMiniaturizeNotification to the
notification center before minimizing the window.
Minimizes the window. The window leaves the screen with its attached sheet and its
visible child windows, and stays in the Windows menu, from which the user restores it.
Posts \c CPWindowWillMiniaturizeNotification before the window leaves the screen and
\c CPWindowDidMiniaturizeNotification after.
@param sender the object making this request
*/
- (void)miniaturize:(id)sender
{
if (_isMiniaturized)
return;
[[CPNotificationCenter defaultCenter] postNotificationName:CPWindowWillMiniaturizeNotification object:self];
[[self platformWindow] miniaturize:sender];
/*
The window leaves the screen and keeps its first responder and visible child windows
for the restore. It called the platform window's miniaturize:, which does nothing in
the browser, so the window stayed on screen but counted as minimized.
*/
_isMiniaturized = YES;
_miniaturizedFirstResponder = _firstResponder;
_miniaturizedChildWindows = _childWindows.filter(function(aChildWindow)
{
return [aChildWindow isVisible];
});
[self _updateMainAndKeyWindows];
var sheet = [self attachedSheet];
if (sheet)
{
[_platformWindow order:CPWindowOut window:sheet relativeTo:nil];
[sheet _updateMainAndKeyWindows];
}
[self _orderOutRecursively:YES];
[self _updateWindowsMenuItem];
[[CPNotificationCenter defaultCenter] postNotificationName:CPWindowDidMiniaturizeNotification object:self];
_isMiniaturized = YES;
[self _updateWindowsMenuItem];
}
/*!
Restores a minimized window to it's original size.
Restores a minimized window: orders it front with its attached sheet and the child
windows it took off the screen, and makes it the key and main window. Ordering a
minimized window front restores it in the same way, which is how the Windows menu
restores it.
@param sender the object making this request
*/
- (void)deminiaturize:(id)sender
{
[[self platformWindow] deminiaturize:sender];
if (_isMiniaturized)
[self makeKeyAndOrderFront:sender];
}
[[CPNotificationCenter defaultCenter] postNotificationName:CPWindowDidDeminiaturizeNotification object:self];
/*
The window can minimize when it shows an enabled minimize button.
*/
- (BOOL)_canMiniaturize
{
return !!(_styleMask & CPMiniaturizableWindowMask) && !_isSheet && !_isFullPlatformWindow;
}
_isMiniaturized = NO;
/*
Restores the first responder and the child windows that the window had when it was
minimized.
*/
- (void)_restoreMiniaturizedState
{
if (_miniaturizedFirstResponder && _miniaturizedFirstResponder !== self)
[self makeFirstResponder:_miniaturizedFirstResponder];
[self _updateWindowsMenuItem];
for (var i = 0, count = [_miniaturizedChildWindows count]; i < count; i++)
{
var childWindow = _miniaturizedChildWindows[i];
if ([childWindow parentWindow] === self)
[childWindow orderWindow:[childWindow childOrdering] relativeTo:_windowNumber];
}
_miniaturizedFirstResponder = nil;
_miniaturizedChildWindows = nil;
}
/*!
@@ -2951,9 +3015,9 @@ CPTexturedBackgroundWindowMask
For \c -toggleToolbarShown:, the item's title becomes "Hide Toolbar" or "Show Toolbar"
to reflect the toolbar's visibility, and the item is enabled only when the window has
a toolbar. \c -undo: and \c -redo: are enabled only when the window's undo manager has a
change to undo or redo. \c -performMiniaturize: is disabled, because a page cannot
minimize its browser window. \c -performZoom: and \c -zoom: are enabled only when the
window's zoom button is enabled. Every other item is enabled.
change to undo or redo. \c -performMiniaturize: and \c -miniaturize: are enabled only
when the window's minimize button is enabled, and \c -performZoom: and \c -zoom: only
when its zoom button is enabled. Every other item is enabled.
@param anItem the item to validate
@return \c YES if the item should be enabled
*/
@@ -2968,11 +3032,11 @@ CPTexturedBackgroundWindowMask
return [[self undoManager] canRedo];
/*
The Minimize and Zoom items agree with the window's buttons. Minimize was enabled,
and left the window on screen but counted as minimized.
The Minimize and Zoom items agree with the window's buttons. Minimize was enabled
for every window, including a window that cannot minimize.
*/
if (action === @selector(performMiniaturize:))
return NO;
if (action === @selector(performMiniaturize:) || action === @selector(miniaturize:))
return [self _canMiniaturize];
if (action === @selector(performZoom:) || action === @selector(zoom:))
return [self _canZoom];
+5 -5
View File
@@ -209,16 +209,16 @@ var _CPStandardWindowViewDividerViewHeight = 1.0;
/*
A standard titled window shows close, minimize and zoom, and enables each only
when it is available, as Apple's guidelines ask. Only the available buttons
were shown, so an unavailable control was hidden instead of disabled. Minimize
is never available, because a page cannot minimize its browser window. Zoom
is available when the window is resizable, as in macOS.
were shown, so an unavailable control was hidden instead of disabled. As in
macOS, minimize is available when the window is miniaturizable, and zoom when
it is resizable.
*/
_closeButton = [self _windowButton];
_minimizeButton = [self _windowButton];
_zoomButton = [self _windowButton];
[_closeButton setEnabled:!!(_styleMask & CPClosableWindowMask)];
[_minimizeButton setEnabled:NO];
[_minimizeButton setEnabled:!!(_styleMask & CPMiniaturizableWindowMask)];
[_zoomButton setEnabled:!!(_styleMask & CPResizableWindowMask)];
[self _updateWindowButtons];
@@ -250,7 +250,7 @@ var _CPStandardWindowViewDividerViewHeight = 1.0;
[_closeButton setAction:@selector(performClose:)];
[_minimizeButton setTarget:[self window]];
[_minimizeButton setAction:@selector(performMiniaturize:)];
[_minimizeButton setAction:@selector(miniaturize:)];
[_zoomButton setTarget:[self window]];
[_zoomButton setAction:@selector(zoom:)];
@@ -157,6 +157,34 @@
[self assert:@"Report" equals:[[windowsMenu itemAtIndex:2] title]];
}
- (void)testMinimizedWindowStaysListedInTheMixedState
{
var aWindow = [self visibleWindowWithTitle:@"Report"];
[aWindow makeMainWindow];
[aWindow miniaturize:self];
var item = [windowsMenu itemAtIndex:2];
[self assert:aWindow equals:[item target]];
[self assert:CPMixedState equals:[item state]];
}
- (void)testChoosingAMinimizedWindowRestoresIt
{
var aWindow = [self visibleWindowWithTitle:@"Report"];
[aWindow makeMainWindow];
[aWindow miniaturize:self];
var item = [windowsMenu itemAtIndex:2];
[[item target] performSelector:[item action] withObject:item];
[self assertFalse:[aWindow isMiniaturized]];
[self assert:CPOnState equals:[item state]];
}
- (void)testSetWindowsMenuMovesWindowItems
{
var aWindow = [self visibleWindowWithTitle:@"Report"],
+111
View File
@@ -309,6 +309,117 @@
}
}
/*
There is no DOM in the test runner, so ordering a window in or out does not change its
visibility. The minimize tests start from a window marked visible.
*/
- (CPWindow)visibleWindowWithStyleMask:(unsigned)aStyleMask
{
var window = [[CPWindow alloc] initWithContentRect:CGRectMake(100.0, 100.0, 400.0, 300.0) styleMask:aStyleMask];
window._isVisible = YES;
return window;
}
- (void)testDeminiaturizeRestoresAMinimizedWindow
{
var window = [self visibleWindowWithStyleMask:CPTitledWindowMask | CPMiniaturizableWindowMask];
[window miniaturize:self];
[self assertTrue:[window isMiniaturized]];
[window deminiaturize:self];
[self assertFalse:[window isMiniaturized]];
}
- (void)testMinimizingAndRestoringPostNotificationsInOrder
{
var window = [self visibleWindowWithStyleMask:CPTitledWindowMask | CPMiniaturizableWindowMask],
center = [CPNotificationCenter defaultCenter],
names = [],
record = function(aNotification) { names.push([aNotification name]); },
observers = [CPWindowWillMiniaturizeNotification, CPWindowDidMiniaturizeNotification, CPWindowDidDeminiaturizeNotification].map(function(aName)
{
return [center addObserverForName:aName object:window queue:nil usingBlock:record];
});
[window miniaturize:self];
[window miniaturize:self];
[window deminiaturize:self];
observers.forEach(function(anObserver) { [center removeObserver:anObserver]; });
[self assert:[CPWindowWillMiniaturizeNotification, CPWindowDidMiniaturizeNotification, CPWindowDidDeminiaturizeNotification] equals:names];
}
- (void)testRestoringAMinimizedWindowRestoresItsFirstResponder
{
var window = [self visibleWindowWithStyleMask:CPTitledWindowMask | CPMiniaturizableWindowMask],
responder = [[CPWindowTestResponderView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
/*
The window is key before it is minimized, so that restoring it does not make it key
for the first time, which would choose a first responder of its own.
*/
[[window contentView] addSubview:responder];
[window makeFirstResponder:responder];
[window makeKeyWindow];
[window miniaturize:self];
[window deminiaturize:self];
[self assert:responder equals:[window firstResponder]];
}
- (void)testPerformMiniaturizeMinimizesOnlyAMiniaturizableWindow
{
var miniaturizableWindow = [self visibleWindowWithStyleMask:CPTitledWindowMask | CPMiniaturizableWindowMask],
fixedWindow = [self visibleWindowWithStyleMask:CPTitledWindowMask];
[miniaturizableWindow performMiniaturize:self];
[fixedWindow performMiniaturize:self];
[self assertTrue:[miniaturizableWindow isMiniaturized]];
[self assertFalse:[fixedWindow isMiniaturized]];
}
- (void)testMinimizeButtonMinimizesTheWindow
{
var window = [self visibleWindowWithStyleMask:CPTitledWindowMask | CPMiniaturizableWindowMask],
button = [window _windowView]._minimizeButton;
[button sendAction:[button action] to:[button target]];
[self assertTrue:[window isMiniaturized]];
}
- (void)testMinimizeItemsAreEnabledOnlyForAMiniaturizableWindow
{
var miniaturizableWindow = [self visibleWindowWithStyleMask:CPTitledWindowMask | CPMiniaturizableWindowMask],
fixedWindow = [self visibleWindowWithStyleMask:CPTitledWindowMask];
[@selector(performMiniaturize:), @selector(miniaturize:)].forEach(function(anAction)
{
var item = [[CPMenuItem alloc] initWithTitle:@"Minimize" action:anAction keyEquivalent:@""];
[self assertTrue:[miniaturizableWindow validateUserInterfaceItem:item] message:CPStringFromSelector(anAction) + " in a miniaturizable window"];
[self assertFalse:[fixedWindow validateUserInterfaceItem:item] message:CPStringFromSelector(anAction) + " in a window that cannot minimize"];
});
}
@end
/*
A view that accepts first responder status.
*/
@implementation CPWindowTestResponderView : CPView
- (BOOL)acceptsFirstResponder
{
return YES;
}
@end
/*
+3 -2
View File
@@ -74,9 +74,10 @@ function DropInWindowButton(aStyleMask, aName)
[self assertFalse:[DropInWindowButton(CPTitledWindowMask, @"_closeButton") isEnabled]];
}
- (void)testMinimizeButtonIsNeverEnabled
- (void)testMinimizeButtonIsEnabledOnlyForAMiniaturizableWindow
{
[self assertFalse:[DropInWindowButton(CPTitledWindowMask | CPClosableWindowMask | CPMiniaturizableWindowMask, @"_minimizeButton") isEnabled]];
[self assertTrue:[DropInWindowButton(CPTitledWindowMask | CPMiniaturizableWindowMask, @"_minimizeButton") isEnabled]];
[self assertFalse:[DropInWindowButton(CPTitledWindowMask | CPClosableWindowMask, @"_minimizeButton") isEnabled]];
}
- (void)testZoomButtonIsEnabledOnlyForAResizableWindow