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