1/*
2    PPGNUstepGlue_Miscellaneous.m
3
4    Copyright 2014-2018 Josh Freeman
5    http://www.twilightedge.com
6
7    This file is part of PikoPixel for 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// Miscellaneous standalone (single-method) GNUstep workarounds & tweaks
26
27#ifdef GNUSTEP
28
29#import <Cocoa/Cocoa.h>
30#import "NSObject_PPUtilities.h"
31#import "PPAppBootUtilities.h"
32#import "PPApplication.h"
33#import "NSFileManager_PPUtilities.h"
34#import "PPPopupPanel.h"
35#import "PPToolsPanelController.h"
36#import "PPDocument.h"
37#import "PPCanvasView.h"
38#import "PPToolButtonMatrix.h"
39#import "NSPasteboard_PPUtilities.h"
40#import "NSDocumentController_PPUtilities.h"
41#import "PPUserFolderPaths.h"
42#import "PPGeometry.h"
43
44
45@implementation NSObject (PPGNUstepGlue_Miscellaneous)
46
47+ (void) ppGSGlue_Miscellaneous_InstallPatches
48{
49    macroSwizzleInstanceMethod(PPApplication, validateMenuItem:, ppGSPatch_ValidateMenuItem:);
50
51
52    macroSwizzleInstanceMethod(NSFileManager, ppVerifySupportFileDirectory,
53                                ppGSPatch_VerifySupportFileDirectory);
54
55
56    macroSwizzleInstanceMethod(NSBrowser, setMaxVisibleColumns:,
57                                ppGSPatch_SetMaxVisibleColumns:);
58
59
60    macroSwizzleInstanceMethod(NSImage, copyWithZone:, ppGSPatch_CopyWithZone:);
61
62
63    macroSwizzleInstanceMethod(PPPopupPanel, canBecomeKeyWindow, ppGSPatch_CanBecomeKeyWindow);
64
65
66    macroSwizzleInstanceMethod(PPToolsPanelController, defaultPinnedWindowFrame,
67                                ppGSPatch_DefaultPinnedWindowFrame);
68
69
70    macroSwizzleInstanceMethod(PPDocument, setupNewPPDocumentWithCanvasSize:,
71                                ppGSPatch_SetupNewPPDocumentWithCanvasSize:);
72
73
74    macroSwizzleInstanceMethod(PPCanvasView, drawRect:, ppGSPatch_DrawRect:);
75
76
77    macroSwizzleClassMethod(PPToolButtonMatrix, cellClass, ppGSPatch_CellClass);
78
79
80    macroSwizzleInstanceMethod(NSColorWell, performClick:, ppGSPatch_PerformClick:);
81}
82
83+ (void) load
84{
85    macroPerformNSObjectSelectorAfterAppLoads(ppGSGlue_Miscellaneous_InstallPatches);
86}
87
88@end
89
90@implementation PPApplication (PPGNUstepGlue_Miscellaneous)
91
92// PATCH: -[PPApplication validateMenuItem:]
93//  GNUstep doesn't support comparing selectors with ==, so patched to use sel_isEqual().
94
95- (BOOL) ppGSPatch_ValidateMenuItem: (id <NSMenuItem>) menuItem
96{
97    SEL menuItemAction = [menuItem action];
98
99    if (sel_isEqual(menuItemAction, @selector(newDocumentFromPasteboard:)))
100    {
101        return [NSPasteboard ppPasteboardHasBitmap] ? YES : NO;
102    }
103
104    // printing is currently disabled, so both printing-related menu items (print & page setup)
105    // use runPageLayout: as their action for convenience when invalidating them
106
107    if (sel_isEqual(menuItemAction, @selector(runPageLayout:)))
108    {
109        return NO;
110    }
111
112
113    if (sel_isEqual(menuItemAction, @selector(activateNextDocumentWindow:))
114        || sel_isEqual(menuItemAction, @selector(activatePreviousDocumentWindow:)))
115    {
116        return [[NSDocumentController sharedDocumentController] ppHasMultipleDocuments];
117    }
118
119    return YES;
120}
121
122@end
123
124@implementation NSFileManager (PPGNUstepGlue_Miscellaneous)
125
126// PATCH: -[NSFileManager ppVerifySupportFileDirectory]
127//  On GNUstep, the ApplicationSupport directory may not exist yet, and if it's not there, then
128// creating the PikoPixel support folder (in the ApplicationSupport directory) will fail,
129// because ppVerifySupportFileDirectory calls -[NSFileManager createDirectoryAtPath:...] with
130// the createIntermediates parameter set to NO.
131//  Patch calls -[NSFileManager createDirectoryAtPath:...] with the createIntermediates
132// parameter set to YES.
133
134- (bool) ppGSPatch_VerifySupportFileDirectory
135{
136    NSString *supportFolderPath = PPUserFolderPaths_ApplicationSupport();
137    BOOL isDirectory = NO;
138
139    if (![supportFolderPath length])
140    {
141        goto ERROR;
142    }
143
144    if ([self fileExistsAtPath: supportFolderPath isDirectory: &isDirectory])
145    {
146        if (!isDirectory)
147            goto ERROR;
148
149        return YES;
150    }
151
152    return [self createDirectoryAtPath: supportFolderPath
153                    withIntermediateDirectories: YES
154                    attributes: nil
155                    error: NULL];
156
157ERROR:
158    return NO;
159}
160
161@end
162
163@implementation NSBrowser (PPGNUstepGlue_Miscellaneous)
164
165// PATCH: -[NSBrowser setMaxVisibleColumns:]
166//  Patch allows browser columns on NSSavePanels & NSOpenPanels to be wider than the default
167// max-width on GNUstep (140).
168
169#define kNSBrowserMaxColumnWidth    190
170
171- (void) ppGSPatch_SetMaxVisibleColumns: (NSInteger) columnCount
172{
173    static Class NSSavePanelClass = nil;
174
175    if (!NSSavePanelClass)
176    {
177        NSSavePanelClass = [[NSSavePanel class] retain];
178    }
179
180    if ([[self window] isKindOfClass: NSSavePanelClass])
181    {
182        columnCount = [self frame].size.width / kNSBrowserMaxColumnWidth;
183
184        if (columnCount < 1)
185        {
186            columnCount = 1;
187        }
188    }
189
190    [self ppGSPatch_SetMaxVisibleColumns: columnCount];
191}
192
193@end
194
195@implementation NSImage (PPGNUstepGlue_Miscellaneous)
196
197// PATCH: -[NSImage copyWithZone:]
198//  GNUstep's implementation of copyWithZone: only copies image representations that are
199// non-cached, so if the source image only contains NSCachedImageReps, the copy will be empty.
200//  Patch checks whether the copy is a valid image; If not - and the source image is valid -
201// a valid representation is created by manually drawing the source image into the copy.
202
203- (id) ppGSPatch_CopyWithZone: (NSZone *) zone
204{
205    NSImage *copiedImage = [self ppGSPatch_CopyWithZone: zone];
206
207    if (copiedImage
208        && ![copiedImage isValid]
209        && [self isValid])
210    {
211        NSRect imageFrame = PPGeometry_OriginRectOfSize([self size]);
212
213        [copiedImage lockFocus];
214
215        [self drawInRect: imageFrame
216                fromRect: imageFrame
217                operation: NSCompositeCopy
218                fraction: 1.0f];
219
220        [copiedImage unlockFocus];
221    }
222
223    return copiedImage;
224}
225
226@end
227
228@implementation PPPopupPanel (PPGNUstepGlue_Miscellaneous)
229
230// PATCH: -[PPPopupPanel canBecomeKeyWindow]
231//  GNUstep's implementation of -[NSWindow canBecomeKeyWindow] doesn't automatically return NO
232// when the window has no title bar or resize bar (as on OS X), so need PPPopupPanel override
233// to prevent popup panels from becoming key.
234
235- (BOOL) ppGSPatch_CanBecomeKeyWindow
236{
237    return NO;
238}
239
240@end
241
242@implementation PPToolsPanelController (PPGNUstepGlue_Miscellaneous)
243
244// PATCH: -[PPToolsPanelController defaultPinnedWindowFrame]
245//  Prevents the tools panel's default position from overlapping the main menu.
246
247#define kMinDistanceBetweenToolsPanelAndMainMenu    20
248
249- (NSRect) ppGSPatch_DefaultPinnedWindowFrame
250{
251    NSRect panelFrame, mainMenuFrame, mainMenuFrameMargin;
252
253    panelFrame = [self ppGSPatch_DefaultPinnedWindowFrame];
254    mainMenuFrame = [[[NSApp mainMenu] window] frame];
255    mainMenuFrameMargin = NSInsetRect(mainMenuFrame, -kMinDistanceBetweenToolsPanelAndMainMenu,
256                                        -kMinDistanceBetweenToolsPanelAndMainMenu);
257
258    if (!NSIsEmptyRect(NSIntersectionRect(panelFrame, mainMenuFrameMargin)))
259    {
260        panelFrame.origin.y = NSMinY(mainMenuFrameMargin) - panelFrame.size.height;
261    }
262
263    return panelFrame;
264}
265
266@end
267
268@implementation PPDocument (PPGNUstepGlue_Miscellaneous)
269
270// PATCH: -[PPDocument setupNewPPDocumentWithCanvasSize:]
271//  On GNUstep, closing a new, unmodified PPDocument will display a confirmation dialog, due to
272// unsaved changes (whereas OS X can close an unmodified PPDocument immediately).
273//  This is because a document's change count isn't cleared when its undoManager is sent a
274// removeAllActions message (a new PPDocument is changed during setup, so removeAllActions is
275// called at the end of setupNewPPDocumentWithCanvasSize:).
276//  The workaround is to manually clear the new document's change count.
277
278- (bool) ppGSPatch_SetupNewPPDocumentWithCanvasSize: (NSSize) canvasSize
279{
280    bool returnValue = [self ppGSPatch_SetupNewPPDocumentWithCanvasSize: canvasSize];
281
282    if (returnValue)
283    {
284        [self updateChangeCount: NSChangeCleared];
285    }
286
287    return returnValue;
288}
289
290@end
291
292@implementation PPCanvasView (PPGNUstepGlue_Miscellaneous)
293
294// PATCH: -[PPCanvasView drawRect:]
295//  Fix for PPCanvasView zoomout artifacts that appear when there's only one scrollbar visible
296// (but not both) - workaround is to draw over the artifacts by manually filling the area
297// outside the visible canvas with the enclosing scrollview's background color (normally
298// the canvasview doesn't draw this area and the underlying scrollview's background shows
299// through automatically).
300
301- (void) ppGSPatch_DrawRect: (NSRect) rect
302{
303    // redraw the background surrounding the visible canvas if:
304    // - drawing is allowed (zoomed-images draw-mode is 0 (normal))
305    // - and the dirty-rect covers some area outside the visible canvas
306    // - and at least one scrollbar is visible (should only be one at this point - if they were
307    // both visible, the dirty-rect would not be outside the visible canvas)
308    if ((_zoomedImagesDrawMode == 0)
309        && (!NSContainsRect(_offsetZoomedVisibleCanvasBounds, rect))
310        && ((_offsetZoomedCanvasFrame.origin.x == 0.0)
311            || (_offsetZoomedCanvasFrame.origin.y == 0.0)))
312    {
313        static NSColor *backgroundColor = nil;
314        NSRect rect1 = NSZeroRect, rect2 = NSZeroRect;
315
316        if (!backgroundColor)
317        {
318            backgroundColor = [[[self enclosingScrollView] backgroundColor] retain];
319        }
320
321        if (_offsetZoomedCanvasFrame.origin.x == 0.0)
322        {
323            // horizontal scrollbar - background-fill areas are above & below visible canvas
324
325            CGFloat canvasMaxY = NSMaxY(_offsetZoomedCanvasFrame);
326
327            rect1 = NSMakeRect(rect.origin.x,
328                                canvasMaxY,
329                                rect.size.width,
330                                NSMaxY(rect) - canvasMaxY);
331
332            rect2 = NSMakeRect(rect.origin.x,
333                                rect.origin.y,
334                                rect.size.width,
335                                _offsetZoomedCanvasFrame.origin.y - rect.origin.y);
336        }
337        else if (_offsetZoomedCanvasFrame.origin.y == 0.0)
338        {
339            // vertical scrollbar - background-fill areas are on left & right of visible canvas
340
341            CGFloat canvasMaxX = NSMaxX(_offsetZoomedCanvasFrame);
342
343            rect1 = NSMakeRect(canvasMaxX,
344                                rect.origin.y,
345                                NSMaxX(rect) - canvasMaxX,
346                                rect.size.height);
347
348            rect2 = NSMakeRect(rect.origin.x,
349                                rect.origin.y,
350                                _offsetZoomedCanvasFrame.origin.x - rect.origin.x,
351                                rect.size.height);
352        }
353
354        [backgroundColor set];
355        NSRectFill(rect1);
356        NSRectFill(rect2);
357    }
358
359    [self ppGSPatch_DrawRect: rect];
360}
361
362@end
363
364@implementation PPToolButtonMatrix (PPGNUstepGlue_Miscellaneous)
365
366// PATCH: +[PPToolButtonMatrix cellClass]
367//  GNUstep's nib loader has an issue when loading instances of NSMatrix subclasses (but not
368// instances of NSMatrix itself): All of the NSMatrix-subclass' cells become NSActionCells,
369// regardless of what their actual class is in the nib file. This is because the nib loader
370// mistakenly forces the loaded cells (which originally have the correct class) to be
371// reallocated by the class object returned by +cellClass (+[NSMatrix cellClass] returns
372// NSActionCell).
373//  The patch returns the correct class for allocating PPToolButtonMatrix's cells: NSButtonCell.
374
375+ (Class) ppGSPatch_CellClass
376{
377    static Class buttonCellClass = NULL;
378
379    if (!buttonCellClass)
380    {
381        buttonCellClass = [[NSButtonCell class] retain];
382    }
383
384    return buttonCellClass;
385}
386
387@end
388
389@implementation NSColorWell (PPGNUstepGlue_Miscellaneous)
390
391// PATCH: -[NSColorWell performClick:]
392//  On GNUstep, -[NSColorWell performClick:] is unimplemented (calling the method falls through
393// to inherited implementation which doesn't activate/deactivate the well). (This is now fixed
394// in the GNUstep trunk, 2016-11-21).
395//  Patch implements correct behavior for color wells: clicking deactivates if it's already
396// active, & activates if it's not active.
397
398- (void) ppGSPatch_PerformClick: (id) sender
399{
400    if ([self isActive])
401    {
402        [self deactivate];
403    }
404    else
405    {
406        [self activate: YES];
407    }
408}
409
410@end
411
412#endif  // GNUSTEP
413
414