aboutsummaryrefslogtreecommitdiff
path: root/src/libui_sdl/libui/darwin/main.m
blob: 59a8683bffbadc530fa807e697f7761f28c092d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// 6 april 2015
#import "uipriv_darwin.h"

static BOOL canQuit = NO;
static NSAutoreleasePool *globalPool;
static applicationClass *app;
static appDelegate *delegate;

static BOOL (^isRunning)(void);
static BOOL stepsIsRunning;

@implementation applicationClass

- (void)sendEvent:(NSEvent *)e
{
	if (sendAreaEvents(e) != 0)
		return;
	[super sendEvent:e];
}

// NSColorPanel always sends changeColor: to the first responder regardless of whether there's a target set on it
// we can override it here (see colorbutton.m)
// thanks to mikeash in irc.freenode.net/#macdev for informing me this is how the first responder chain is initiated
// it turns out NSFontManager also sends changeFont: through this; let's inhibit that here too (see fontbutton.m)
- (BOOL)sendAction:(SEL)sel to:(id)to from:(id)from
{
	if (colorButtonInhibitSendAction(sel, from, to))
		return NO;
	if (fontButtonInhibitSendAction(sel, from, to))
		return NO;
	return [super sendAction:sel to:to from:from];
}

// likewise, NSFontManager also sends NSFontPanelValidation messages to the first responder, however it does NOT use sendAction:from:to:!
// instead, it uses this one (thanks swillits in irc.freenode.net/#macdev)
// we also need to override it (see fontbutton.m)
- (id)targetForAction:(SEL)sel to:(id)to from:(id)from
{
	id override;

	if (fontButtonOverrideTargetForAction(sel, from, to, &override))
		return override;
	return [super targetForAction:sel to:to from:from];
}

// hey look! we're overriding terminate:!
// we're going to make sure we can go back to main() whether Cocoa likes it or not!
// and just how are we going to do that, hm?
// (note: this is called after applicationShouldTerminate:)
- (void)terminate:(id)sender
{
	// yes that's right folks: DO ABSOLUTELY NOTHING.
	// the magic is [NSApp run] will just... stop.

	// well let's not do nothing; let's actually quit our graceful way
	NSEvent *e;

	if (!canQuit)
		implbug("call to [NSApp terminate:] when not ready to terminate; definitely contact andlabs");

	[realNSApp() stop:realNSApp()];
	// stop: won't register until another event has passed; let's synthesize one
	e = [NSEvent otherEventWithType:NSApplicationDefined
		location:NSZeroPoint
		modifierFlags:0
		timestamp:[[NSProcessInfo processInfo] systemUptime]
		windowNumber:0
		context:[NSGraphicsContext currentContext]
		subtype:0
		data1:0
		data2:0];
	[realNSApp() postEvent:e atStart:NO];		// let pending events take priority (this is what PostQuitMessage() on Windows does so we have to do it here too for parity; thanks to mikeash in irc.freenode.net/#macdev for confirming that this parameter should indeed be NO)

	// and in case uiMainSteps() was called
	stepsIsRunning = NO;
}

@end

@implementation appDelegate

- (void)dealloc
{
	// Apple docs: "Don't Use Accessor Methods in Initializer Methods and dealloc"
	[_menuManager release];
	[super dealloc];
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app
{
	// for debugging
	NSLog(@"in applicationShouldTerminate:");
	if (shouldQuit()) {
		canQuit = YES;
		// this will call terminate:, which is the same as uiQuit()
		return NSTerminateNow;
	}
	return NSTerminateCancel;
}

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app
{
	return NO;
}

@end

uiInitOptions options;

const char *uiInit(uiInitOptions *o)
{
	@autoreleasepool {
		options = *o;
		app = [[applicationClass sharedApplication] retain];
		// don't check for a NO return; something (launch services?) causes running from application bundles to always return NO when asking to change activation policy, even if the change is to the same activation policy!
		// see https://github.com/andlabs/ui/issues/6
		[realNSApp() setActivationPolicy:NSApplicationActivationPolicyRegular];
		delegate = [appDelegate new];
		[realNSApp() setDelegate:delegate];

		initAlloc();

		// always do this so we always have an application menu
		appDelegate().menuManager = [[menuManager new] autorelease];
		[realNSApp() setMainMenu:[appDelegate().menuManager makeMenubar]];

		setupFontPanel();
	}

	globalPool = [[NSAutoreleasePool alloc] init];

	return NULL;
}

void uiUninit(void)
{
	if (!globalPool) {
		userbug("You must call uiInit() first!");
	}
	[globalPool release];

	@autoreleasepool {
		[delegate release];
		[realNSApp() setDelegate:nil];
		[app release];
		uninitAlloc();
	}
}

void uiFreeInitError(const char *err)
{
}

void uiMain(void)
{
	isRunning = ^{
		return [realNSApp() isRunning];
	};
	[realNSApp() run];
}

void uiMainSteps(void)
{
	// SDL does this and it seems to be necessary for the menubar to work (see #182)
	[realNSApp() finishLaunching];
	isRunning = ^{
		return stepsIsRunning;
	};
	stepsIsRunning = YES;
}

int uiMainStep(int wait)
{
	struct nextEventArgs nea;

	nea.mask = NSAnyEventMask;

	// ProPuke did this in his original PR requesting this
	// I'm not sure if this will work, but I assume it will...
	nea.duration = [NSDate distantPast];
	if (wait)		// but this is normal so it will work
		nea.duration = [NSDate distantFuture];

	nea.mode = NSDefaultRunLoopMode;
	nea.dequeue = YES;

	return mainStep(&nea, ^(NSEvent *e) {
		return NO;
	});
}

// see also:
// - http://www.cocoawithlove.com/2009/01/demystifying-nsapplication-by.html
// - https://github.com/gnustep/gui/blob/master/Source/NSApplication.m
int mainStep(struct nextEventArgs *nea, BOOL (^interceptEvent)(NSEvent *e))
{
	NSDate *expire;
	NSEvent *e;
	NSEventType type;

	@autoreleasepool {
		if (!isRunning())
			return 0;

		e = [realNSApp() nextEventMatchingMask:nea->mask
			untilDate:nea->duration
			inMode:nea->mode
			dequeue:nea->dequeue];
		if (e == nil)
			return 1;

		type = [e type];
		if (!interceptEvent(e))
			[realNSApp() sendEvent:e];
		[realNSApp() updateWindows];

		// GNUstep does this
		// it also updates the Services menu but there doesn't seem to be a public API for that so
		if (type != NSPeriodic && type != NSMouseMoved)
			[[realNSApp() mainMenu] update];

		return 1;
	}
}

void uiQuit(void)
{
	canQuit = YES;
	[realNSApp() terminate:realNSApp()];
}

// thanks to mikeash in irc.freenode.net/#macdev for suggesting the use of Grand Central Dispatch for this
// LONGTERM will dispatch_get_main_queue() break after _CFRunLoopSetCurrent()?
void uiQueueMain(void (*f)(void *data), void *data)
{
	// dispatch_get_main_queue() is a serial queue so it will not execute multiple uiQueueMain() functions concurrently
	// the signature of f matches dispatch_function_t
	dispatch_async_f(dispatch_get_main_queue(), data, f);
}