1/* 2 PPPanelController.m 3 4 Copyright 2013-2018,2020 Josh Freeman 5 http://www.twilightedge.com 6 7 This file is part of PikoPixel for Mac OS X and GNUstep. 8 PikoPixel is a graphical application for drawing & editing pixel-art images. 9 10 PikoPixel is free software: you can redistribute it and/or modify it under 11 the terms of the GNU Affero General Public License as published by the 12 Free Software Foundation, either version 3 of the License, or (at your 13 option) any later version approved for PikoPixel by its copyright holder (or 14 an authorized proxy). 15 16 PikoPixel is distributed in the hope that it will be useful, but WITHOUT ANY 17 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 19 details. 20 21 You should have received a copy of the GNU Affero General Public License 22 along with this program. If not, see <http://www.gnu.org/licenses/>. 23*/ 24 25#import "PPPanelController.h" 26 27#import "PPUserDefaults.h" 28#import "PPDocument.h" 29#import "NSDocument_PPUtilities.h" 30#import "NSObject_PPUtilities.h" 31#import "NSWindow_PPUtilities.h" 32#import "PPGeometry.h" 33#import "PPSRGBUtilities.h" 34 35 36#define kScreenBoundsPinningMargin_Left 10.0f 37#define kScreenBoundsPinningMargin_Right 20.0f 38#define kScreenBoundsPinningMargin_Top 35.0f 39#define kScreenBoundsPinningMargin_Bottom 20.0f 40 41 42static NSRect ScreenBoundsForPinningDefaultWindowFrame(void); 43 44 45@interface PPPanelController (PrivateMethods) 46 47- (void) updatePanelVisibility; 48- (void) showPanel; 49- (void) hidePanel; 50- (void) hidePanelIfDocumentIsInvalid; 51 52- (void) disablePanel; 53 54- (void) setupPanelStateFromUserDefaults; 55- (NSRect) defaultPinnedWindowFrame; 56 57@end 58 59#if PP_SDK_REQUIRES_PROTOCOLS_FOR_DELEGATES_AND_DATASOURCES 60 61@interface PPPanelController (RequiredProtocols) <NSWindowDelegate> 62@end 63 64#endif 65 66@implementation PPPanelController 67 68+ controller 69{ 70 NSString *panelNibName = [self panelNibName]; 71 72 if (!panelNibName) 73 { 74 return nil; 75 } 76 77 return [[[self alloc] initWithWindowNibName: panelNibName] autorelease]; 78} 79 80- (id) initWithWindowNibName: (NSString *) windowNibName 81{ 82 self = [super initWithWindowNibName: windowNibName]; 83 84 if (!self) 85 goto ERROR; 86 87 _shouldStorePanelStateInUserDefaults = [self shouldStorePanelStateInUserDefaults]; 88 89 if (_shouldStorePanelStateInUserDefaults) 90 { 91 [PPUserDefaults registerDefaultEnabledState: [self defaultPanelEnabledState] 92 forPanelWithNibName: windowNibName]; 93 94 if ([PPUserDefaults enabledStateForPanelWithNibName: windowNibName]) 95 { 96 // user defaults setting wants the panel enabled - requesting the window will 97 // force the controller to load it immediately 98 [self window]; 99 } 100 } 101 102 return self; 103 104ERROR: 105 [self release]; 106 107 return nil; 108} 109 110- (void) dealloc 111{ 112 [self setPPDocument: nil]; 113 114 [super dealloc]; 115} 116 117+ (NSString *) panelNibName 118{ 119 return nil; 120} 121 122- (void) setPPDocument: (PPDocument *) ppDocument 123{ 124 if (_ppDocument == ppDocument) 125 { 126 return; 127 } 128 129 if (_ppDocument) 130 { 131 [self removeAsObserverForPPDocumentNotifications]; 132 } 133 134 [_ppDocument release]; 135 _ppDocument = [ppDocument retain]; 136 137 if (_ppDocument && _panelDidLoad) 138 { 139 [self addAsObserverForPPDocumentNotifications]; 140 141 [self setupPanelForCurrentPPDocument]; 142 } 143 else 144 { 145 [self updatePanelVisibility]; 146 } 147} 148 149- (void) setPanelVisibilityAllowed: (bool) allowPanelVisibility 150{ 151 allowPanelVisibility = (allowPanelVisibility) ? YES : NO; 152 153 if (_allowPanelVisibility != allowPanelVisibility) 154 { 155 _allowPanelVisibility = allowPanelVisibility; 156 157 if (_allowPanelVisibility) 158 { 159 // setPanelVisibilityAllowed: can be called while switching document windows, 160 // so _ppDocument may not yet point to the new active document - if the panel 161 // becomes visible immediately, it will briefly flicker the old document's state 162 // before updating with the new document, so delay showing the panel until 163 // _ppDocument is definitely valid (next stack frame) 164 165 [self ppPerformSelectorAtomicallyFromNewStackFrame: 166 @selector(updatePanelVisibility)]; 167 } 168 else 169 { 170 [self updatePanelVisibility]; 171 } 172 } 173} 174 175- (void) setPanelEnabled: (bool) enablePanel 176{ 177 enablePanel = (enablePanel) ? YES : NO; 178 179 if (_panelIsEnabled == enablePanel) 180 { 181 return; 182 } 183 184 _panelIsEnabled = enablePanel; 185 186 [self updatePanelVisibility]; 187 188 if (_shouldStorePanelStateInUserDefaults) 189 { 190 [PPUserDefaults setEnabledState: _panelIsEnabled 191 forPanelWithNibName: [self windowNibName]]; 192 } 193} 194 195- (void) togglePanelEnabledState 196{ 197 [self setPanelEnabled: (_panelIsEnabled) ? NO : YES]; 198} 199 200- (bool) panelIsVisible 201{ 202 if (!_panelDidLoad) 203 { 204 return NO; 205 } 206 207 return [[self window] isVisible] ? YES : NO; 208} 209 210- (bool) mouseLocationIsInsideVisiblePanel: (NSPoint) mouseLocation 211{ 212 NSWindow *panel; 213 214 if (!_panelDidLoad || !_ppDocument || !_allowPanelVisibility || !_panelIsEnabled) 215 { 216 return NO; 217 } 218 219 panel = [self window]; 220 221 return ([panel isVisible] 222 && NSMouseInRect(mouseLocation, [panel frame], NO)) 223 ? YES : NO; 224} 225 226- (void) addAsObserverForPPDocumentNotifications 227{ 228} 229 230- (void) removeAsObserverForPPDocumentNotifications 231{ 232} 233 234- (bool) allowPanelToBecomeKey 235{ 236 return NO; 237} 238 239- (bool) shouldStorePanelStateInUserDefaults 240{ 241 return YES; 242} 243 244- (bool) defaultPanelEnabledState 245{ 246 return NO; 247} 248 249- (PPFramePinningType) pinningTypeForDefaultWindowFrame 250{ 251 return kPPFramePinningType_Invalid; 252} 253 254- (void) setupPanelForCurrentPPDocument 255{ 256 [self updatePanelVisibility]; 257} 258 259- (void) setupPanelBeforeMakingVisible 260{ 261} 262 263- (void) setupPanelAfterVisibilityChange 264{ 265} 266 267#pragma mark NSWindowController overrides 268 269- (void) windowDidLoad 270{ 271 NSPanel *panel; 272 273 [super windowDidLoad]; 274 275 panel = (NSPanel *) [self window]; 276 [panel setDelegate: self]; 277 [panel setBecomesKeyOnlyIfNeeded: YES]; 278 279 [panel ppSetSRGBColorSpace]; 280 [panel ppDisableWindowAnimation]; 281 282 if (_shouldStorePanelStateInUserDefaults) 283 { 284 [self setupPanelStateFromUserDefaults]; 285 } 286 287 _panelDidLoad = YES; 288 289 if (_ppDocument) 290 { 291 [self addAsObserverForPPDocumentNotifications]; 292 293 [self setupPanelForCurrentPPDocument]; 294 } 295} 296 297#pragma mark NSWindow delegate methods 298 299- (void) windowDidBecomeKey: (NSNotification *) notification 300{ 301 if (![self allowPanelToBecomeKey]) 302 { 303 [_ppDocument ppMakeWindowKey]; 304 } 305} 306 307- (BOOL) windowShouldClose: (id) sender 308{ 309 [self ppPerformSelectorFromNewStackFrame: @selector(disablePanel)]; 310 311 return NO; 312} 313 314#pragma mark Private methods 315 316- (void) updatePanelVisibility 317{ 318 bool panelIsVisible, panelShouldBeVisible; 319 320 panelIsVisible = ([self panelIsVisible]) ? YES : NO; 321 322 panelShouldBeVisible = (_allowPanelVisibility && _panelIsEnabled && _ppDocument) ? YES : NO; 323 324 if (panelIsVisible == panelShouldBeVisible) 325 { 326 return; 327 } 328 329 if (panelShouldBeVisible) 330 { 331 [self showPanel]; 332 } 333 else 334 { 335 if (_ppDocument) 336 { 337 [self hidePanel]; 338 } 339 else 340 { 341 // when _ppDocument is invalid, delay hiding the panel until the next stack 342 // frame - this keeps the panel from flickering off/on when switching to a 343 // different document window (_ppDocument is nil temporarily, but will be valid 344 // by the next frame) 345 346 [self ppPerformSelectorAtomicallyFromNewStackFrame: 347 @selector(hidePanelIfDocumentIsInvalid)]; 348 } 349 } 350} 351 352- (void) showPanel 353{ 354 NSWindow *panel; 355 356 if ([self panelIsVisible]) 357 { 358 return; 359 } 360 361 panel = [self window]; // make sure window is loaded before setup 362 363 [self setupPanelBeforeMakingVisible]; 364 365 [panel orderFront: self]; 366 367 [self setupPanelAfterVisibilityChange]; 368} 369 370- (void) hidePanel 371{ 372 if (![self panelIsVisible]) 373 { 374 return; 375 } 376 377 [[self window] orderOut: self]; 378 379 [self setupPanelAfterVisibilityChange]; 380} 381 382- (void) hidePanelIfDocumentIsInvalid 383{ 384 if (!_ppDocument) 385 { 386 [self hidePanel]; 387 } 388} 389 390- (void) disablePanel 391{ 392 [self setPanelEnabled: NO]; 393} 394 395- (void) setupPanelStateFromUserDefaults 396{ 397 NSRect defaultWindowFrame; 398 NSString *windowNibName; 399 400 if (!_shouldStorePanelStateInUserDefaults) 401 return; 402 403 defaultWindowFrame = [self defaultPinnedWindowFrame]; 404 405 if (!NSIsEmptyRect(defaultWindowFrame)) 406 { 407 [[self window] setFrame: defaultWindowFrame display: NO]; 408 } 409 410 windowNibName = [self windowNibName]; 411 412 [[self window] setFrameAutosaveName: windowNibName]; 413 414 // Panel won't be visible after nib is loaded (set to remain hidden), so enable if needed 415 416 if ([PPUserDefaults enabledStateForPanelWithNibName: windowNibName]) 417 { 418 [self setPanelEnabled: YES]; 419 } 420} 421 422- (NSRect) defaultPinnedWindowFrame 423{ 424 NSRect screenBoundsForWindowFrame, windowFrame; 425 PPFramePinningType framePinningType; 426 427 screenBoundsForWindowFrame = ScreenBoundsForPinningDefaultWindowFrame(); 428 429 windowFrame = [[self window] frame]; 430 431 framePinningType = [self pinningTypeForDefaultWindowFrame]; 432 433 if (!PPFramePinningType_IsValid(framePinningType)) 434 { 435 goto ERROR; 436 } 437 438 // horizontal pinning 439 440 switch (framePinningType) 441 { 442 case kPPFramePinningType_TopLeft: 443 case kPPFramePinningType_CenterLeft: 444 case kPPFramePinningType_BottomLeft: 445 { 446 windowFrame.origin.x = screenBoundsForWindowFrame.origin.x; 447 } 448 break; 449 450 case kPPFramePinningType_TopRight: 451 case kPPFramePinningType_CenterRight: 452 case kPPFramePinningType_BottomRight: 453 { 454 windowFrame.origin.x = screenBoundsForWindowFrame.origin.x 455 + screenBoundsForWindowFrame.size.width 456 - windowFrame.size.width; 457 } 458 break; 459 460 default: 461 break; 462 } 463 464 // vertical pinning 465 466 switch (framePinningType) 467 { 468 case kPPFramePinningType_TopLeft: 469 case kPPFramePinningType_TopRight: 470 { 471 windowFrame.origin.y = screenBoundsForWindowFrame.origin.y 472 + screenBoundsForWindowFrame.size.height 473 - windowFrame.size.height; 474 } 475 break; 476 477 case kPPFramePinningType_CenterLeft: 478 case kPPFramePinningType_CenterRight: 479 { 480 windowFrame.origin.y = 481 roundf(screenBoundsForWindowFrame.origin.y 482 + (screenBoundsForWindowFrame.size.height - windowFrame.size.height) 483 / 2.0f); 484 } 485 break; 486 487 case kPPFramePinningType_BottomLeft: 488 case kPPFramePinningType_BottomRight: 489 { 490 windowFrame.origin.y = screenBoundsForWindowFrame.origin.y; 491 } 492 break; 493 494 default: 495 break; 496 } 497 498 return windowFrame; 499 500ERROR: 501 if (!NSIsEmptyRect(screenBoundsForWindowFrame) && !NSIsEmptyRect(windowFrame)) 502 { 503 windowFrame.origin = 504 PPGeometry_OriginPointForConfiningRectInsideRect(windowFrame, 505 screenBoundsForWindowFrame); 506 } 507 508 return windowFrame; 509} 510 511@end 512 513#pragma mark Private functions 514 515static NSRect ScreenBoundsForPinningDefaultWindowFrame(void) 516{ 517 NSRect screenBounds = [[NSScreen mainScreen] visibleFrame]; 518 519 screenBounds.origin.x += kScreenBoundsPinningMargin_Left; 520 screenBounds.origin.y += kScreenBoundsPinningMargin_Bottom; 521 522 screenBounds.size.width -= 523 kScreenBoundsPinningMargin_Left + kScreenBoundsPinningMargin_Right; 524 screenBounds.size.height -= 525 kScreenBoundsPinningMargin_Top + kScreenBoundsPinningMargin_Bottom; 526 527 return PPGeometry_PixelBoundsCoveredByRect(screenBounds); 528} 529