1/*
2 *  tracker/cocoa/AppDelegate.mm
3 *
4 *  Copyright 2014 Dale Whinham
5 *
6 *  This file is part of Milkytracker.
7 *
8 *  Milkytracker is free software: you can redistribute it and/or modify
9 *  it under the terms of the GNU General Public License as published by
10 *  the Free Software Foundation, either version 3 of the License, or
11 *  (at your option) any later version.
12 *
13 *  Milkytracker is distributed in the hope that it will be useful,
14 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 *  GNU General Public License for more details.
17 *
18 *  You should have received a copy of the GNU General Public License
19 *  along with Milkytracker.  If not, see <http://www.gnu.org/licenses/>.
20 *
21 */
22
23// -------- Cocoa/OS X --------
24#import <Cocoa/Cocoa.h>
25#import <dispatch/dispatch.h>
26
27// ---------- Tracker ---------
28#import "DisplayDevice_COCOA.h"
29#import "MidiReceiver_CoreMIDI.h"
30#import "PPMutex.h"
31#import "Screen.h"
32#import "Tracker.h"
33
34@implementation AppDelegate
35
36// ---------- Display ---------
37@synthesize myWindow;
38@synthesize myTrackerView;
39@synthesize myProgressWindow;
40@synthesize myProgressIndicator;
41
42// ------ Tracker Globals -----
43static PPScreen*			myTrackerScreen;
44static Tracker*				myTracker;
45static PPDisplayDevice*		myDisplayDevice;
46static MidiReceiver*		myMidiReceiver;
47
48static PPMutex*				globalMutex;
49
50static BOOL					startupAfterFullScreen;
51static BOOL					startupComplete;
52static NSMutableArray*		filesToLoad;
53
54static CVDisplayLinkRef		displayLink;
55
56// TODO: Crash handler
57
58void RaiseEventSynchronized(PPEvent* event)
59{
60	if (myTrackerScreen && globalMutex->tryLock())
61	{
62		myTrackerScreen->raiseEvent(event);
63		globalMutex->unlock();
64	}
65}
66
67static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now,
68									const CVTimeStamp* outputTime, CVOptionFlags flagsIn,
69									CVOptionFlags* flagsOut, void* displayLinkContext)
70{
71	// Raise the event on the main thread
72	dispatch_async(dispatch_get_main_queue(), ^
73	{
74		if (!myTrackerScreen)
75			return;
76
77		PPEvent e = PPEvent(eTimer);
78		RaiseEventSynchronized(&e);
79	});
80
81	return kCVReturnSuccess;
82}
83
84- (void)initTracker
85{
86	[myWindow setTitle:@"Loading MilkyTracker..."];
87	[myWindow display];
88
89	// Instantiate the tracker
90	myTracker = new Tracker();
91
92	// Retrieve and apply display settings
93	PPSize windowSize = myTracker->getWindowSizeFromDatabase();
94	pp_int32 scaleFactor = myTracker->getScreenScaleFactorFromDatabase();
95	bool fullScreen = myTracker->getFullScreenFlagFromDatabase();
96
97	// Bring up display device
98	myDisplayDevice = new PPDisplayDevice(myWindow,
99										  myTrackerView,
100										  windowSize.width,
101										  windowSize.height,
102										  scaleFactor,
103										  32);
104
105	// Enable fullscreen mode if necessary
106	myDisplayDevice->goFullScreen(fullScreen);
107
108	// Should we wait for fullscreen transition before completing startup?
109	startupAfterFullScreen = fullScreen;
110
111	// Attach display to tracker
112	myTrackerScreen = new PPScreen(myDisplayDevice, myTracker);
113	myTracker->setScreen(myTrackerScreen);
114
115	// Init MIDI
116	myMidiReceiver = new MidiReceiver(*myTracker, *globalMutex);
117	myMidiReceiver->init();
118}
119
120- (void)trackerStartUp
121{
122	// Force immediate screen updates during splash screen because Cocoa loop is blocked
123	myDisplayDevice->setImmediateUpdates(true);
124
125	// Perform startup
126	myTracker->startUp();
127
128	// Allow Cocoa to handle refresh again (keeps event processing smooth and responsive)
129	myDisplayDevice->setImmediateUpdates(false);
130
131	// CVDisplayLink gives us a callback synchronised with vertical blanking
132	CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
133	CVDisplayLinkSetOutputCallback(displayLink, &DisplayLinkCallback, NULL);
134	CVDisplayLinkStart(displayLink);
135
136	// Signal startup complete
137	startupComplete = YES;
138
139	// Handle deferred file loading
140	for (NSString* filename in filesToLoad)
141		[self application: NSApp openFile:filename];
142
143	[filesToLoad removeAllObjects];
144	filesToLoad = nil;
145}
146
147- (void)timerCallback:(NSTimer*)theTimer
148{
149	if (!myTrackerScreen)
150		return;
151
152	PPEvent e = PPEvent(eTimer);
153	RaiseEventSynchronized(&e);
154}
155
156- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
157{
158	// Initialisation
159	globalMutex = new PPMutex();
160	[self initTracker];
161
162	if (!startupAfterFullScreen)
163		[self trackerStartUp];
164}
165
166#pragma mark Progress window
167- (void)showProgress:(BOOL)yes
168{
169	if (yes)
170	{
171		[myProgressWindow makeKeyAndOrderFront:nil];
172		[myProgressIndicator startAnimation:nil];
173	}
174	else
175	{
176		[myProgressIndicator stopAnimation:nil];
177		[myProgressWindow orderOut:nil];
178	}
179}
180
181#pragma mark Application events
182- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
183{
184	return myTracker->shutDown() ? NSTerminateNow : NSTerminateCancel;
185}
186
187- (void)applicationWillTerminate:(NSNotification *)aNotification
188{
189	myMidiReceiver->close();
190	delete myMidiReceiver;
191
192	CVDisplayLinkStop(displayLink);
193	CVDisplayLinkRelease(displayLink);
194
195	delete myTracker;
196	delete myTrackerScreen;
197	delete myDisplayDevice;
198	delete globalMutex;
199}
200
201#pragma mark File open events
202- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
203{
204	// Startup not complete; hold onto the file path and load it later
205	if (!startupComplete)
206	{
207		if (!filesToLoad)
208			filesToLoad = [[NSMutableArray alloc] initWithObjects:filename, nil];
209		else
210			[filesToLoad addObject:filename];
211	}
212	else
213	{
214		// Temp buffer for file path
215		char filePath[PATH_MAX + 1];
216
217		// Convert to C string
218		[filename getCString:filePath maxLength:PATH_MAX encoding:NSUTF8StringEncoding];
219
220		// Create system string from C string
221		PPSystemString sysString(filePath);
222		PPSystemString* sysStrPtr = &sysString;
223
224		// Raise file drop event
225		PPEvent event(eFileDragDropped, &sysStrPtr, sizeof(PPSystemString*));
226		RaiseEventSynchronized(&event);
227	}
228
229	return YES;
230}
231
232- (void)application:(NSApplication *)theApplication openFiles:(NSArray *)filenames
233{
234	// Call the single openFile delegate method when multiple files are opened
235	for (NSString* filename in filenames)
236		[[NSApp delegate] application:NSApp openFile:filename];
237}
238
239#pragma mark Window events
240- (BOOL)windowShouldClose:(id)sender
241{
242	[NSApp terminate:self];
243	return NO;
244}
245
246- (void)windowDidResignKey:(NSNotification *)notification
247{
248	// Clear modifier keys if window loses focus
249	clearKeyModifier(KeyModifierCTRL);
250	clearKeyModifier(KeyModifierALT);
251	clearKeyModifier(KeyModifierSHIFT);
252}
253
254- (void)windowWillEnterFullScreen:(NSNotification *)notification
255{
256	PPEvent event(eFullScreen);
257	RaiseEventSynchronized(&event);
258}
259
260- (void)windowWillExitFullScreen:(NSNotification *)notification
261{
262	PPEvent event(eFullScreen);
263	RaiseEventSynchronized(&event);
264}
265
266- (void)windowDidEnterFullScreen:(NSNotification *)notification
267{
268	if (startupAfterFullScreen)
269	{
270		[self trackerStartUp];
271		startupAfterFullScreen = NO;
272	}
273}
274@end
275