xref: /haiku/src/apps/mediaplayer/MainApp.cpp (revision 62f5ba006a08b0df30631375878effaf67ae5dbc)
1 /*
2  * MainApp.cpp - Media Player for the Haiku Operating System
3  *
4  * Copyright (C) 2006 Marcus Overhagen <marcus@overhagen.de>
5  * Copyright (C) 2008 Stephan Aßmus <superstippi@gmx.de> (MIT Ok)
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * version 2 as published by the Free Software Foundation.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  *
20  */
21 
22 
23 #include "MainApp.h"
24 
25 #include <Alert.h>
26 #include <Autolock.h>
27 #include <Entry.h>
28 #include <FilePanel.h>
29 #include <MediaDefs.h>
30 #include <MediaRoster.h>
31 #include <MimeType.h>
32 #include <Path.h>
33 #include <Resources.h>
34 #include <Roster.h>
35 
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <unistd.h>
39 
40 #include "EventQueue.h"
41 #include "Playlist.h"
42 #include "Settings.h"
43 #include "SettingsWindow.h"
44 
45 
46 MainApp* gMainApp;
47 const char* kAppSig = "application/x-vnd.Haiku-MediaPlayer";
48 
49 static const char* kMediaServerSig = B_MEDIA_SERVER_SIGNATURE;
50 static const char* kMediaServerAddOnSig = "application/x-vnd.Be.addon-host";
51 
52 MainApp::MainApp()
53 	:
54 	BApplication(kAppSig),
55 	fPlayerCount(0),
56 	fSettingsWindow(NULL),
57 
58 	fOpenFilePanel(NULL),
59 	fSaveFilePanel(NULL),
60 	fLastFilePanelFolder(),
61 
62 	fMediaServerRunning(false),
63 	fMediaAddOnServerRunning(false),
64 
65 	fAudioWindowFrameSaved(false),
66 	fLastSavedAudioWindowCreationTime(0)
67 {
68 	mpSettings settings = Settings::CurrentSettings();
69 	fLastFilePanelFolder = settings.filePanelFolder;
70 
71 	// Now tell the application roster, that we're interested
72 	// in getting notifications of apps being launched or quit.
73 	// In this way we are going to detect a media_server restart.
74 	be_roster->StartWatching(BMessenger(this, this),
75 		B_REQUEST_LAUNCHED | B_REQUEST_QUIT);
76 	// we will keep track of the status of media_server
77 	// and media_addon_server
78 	fMediaServerRunning = be_roster->IsRunning(kMediaServerSig);
79 	fMediaAddOnServerRunning = be_roster->IsRunning(kMediaServerAddOnSig);
80 
81 	if (!fMediaServerRunning || !fMediaAddOnServerRunning) {
82 		BAlert* alert = new BAlert("start_media_server",
83 			"It appears the media server is not running.\n"
84 			"Would you like to start it ?", "Quit", "Start media server", NULL,
85 			B_WIDTH_AS_USUAL, B_WARNING_ALERT);
86 		if (alert->Go() == 0) {
87 			PostMessage(B_QUIT_REQUESTED);
88 			return;
89 		}
90 
91 		launch_media_server();
92 
93 		fMediaServerRunning = be_roster->IsRunning(kMediaServerSig);
94 		fMediaAddOnServerRunning = be_roster->IsRunning(kMediaServerAddOnSig);
95 	}
96 }
97 
98 
99 MainApp::~MainApp()
100 {
101 	delete fOpenFilePanel;
102 	delete fSaveFilePanel;
103 }
104 
105 
106 bool
107 MainApp::QuitRequested()
108 {
109 	// Make sure we store the current playlist, if applicable.
110 	for (int32 i = 0; BWindow* window = WindowAt(i); i++) {
111 		MainWin* playerWindow = dynamic_cast<MainWin*>(window);
112 		if (playerWindow == NULL)
113 			continue;
114 
115 		BAutolock _(playerWindow);
116 
117 		BMessage quitMessage;
118 		playerWindow->GetQuitMessage(&quitMessage);
119 
120 		// Store the playlist if there is one. If the user has multiple
121 		// instances playing audio at the this time, the first instance wins.
122 		BMessage playlistArchive;
123 		if (quitMessage.FindMessage("playlist", &playlistArchive) == B_OK) {
124 			_StoreCurrentPlaylist(&playlistArchive);
125 			break;
126 		}
127 	}
128 
129 	// Note: This needs to be done here, SettingsWindow::QuitRequested()
130 	// returns "false" always. (Standard BApplication quit procedure will
131 	// hang otherwise.)
132 	if (fSettingsWindow && fSettingsWindow->Lock())
133 		fSettingsWindow->Quit();
134 	fSettingsWindow = NULL;
135 
136 	// store the current file panel ref in the global settings
137 	mpSettings settings = Settings::CurrentSettings();
138 	settings.filePanelFolder = fLastFilePanelFolder;
139 	Settings::Default()->SaveSettings(settings);
140 
141 	return BApplication::QuitRequested();
142 }
143 
144 
145 MainWin*
146 MainApp::NewWindow(BMessage* message)
147 {
148 	BAutolock _(this);
149 	fPlayerCount++;
150 	return new(std::nothrow) MainWin(fPlayerCount == 1, message);
151 }
152 
153 
154 int32
155 MainApp::PlayerCount() const
156 {
157 	BAutolock _(const_cast<MainApp*>(this));
158 	return fPlayerCount;
159 }
160 
161 
162 // #pragma mark -
163 
164 
165 void
166 MainApp::ReadyToRun()
167 {
168 	// make sure we have at least one window open
169 	if (fPlayerCount == 0) {
170 		MainWin* window = NewWindow();
171 		if (window == NULL) {
172 			PostMessage(B_QUIT_REQUESTED);
173 			return;
174 		}
175 		BMessage lastPlaylistArchive;
176 		if (_RestoreCurrentPlaylist(&lastPlaylistArchive) == B_OK) {
177 			lastPlaylistArchive.what = M_OPEN_PREVIOUS_PLAYLIST;
178 			window->PostMessage(&lastPlaylistArchive);
179 		} else
180 			window->Show();
181 	}
182 
183 	// setup the settings window now, we need to have it
184 	fSettingsWindow = new SettingsWindow(BRect(150, 150, 450, 520));
185 	fSettingsWindow->Hide();
186 	fSettingsWindow->Show();
187 
188 	_InstallPlaylistMimeType();
189 }
190 
191 
192 void
193 MainApp::RefsReceived(BMessage* message)
194 {
195 	// The user dropped a file (or files) on this app's icon,
196 	// or double clicked a file that's handled by this app.
197 	// Command line arguments are also redirected to here by
198 	// ArgvReceived() but without MIME type check.
199 
200 	// If multiple refs are received in short succession we
201 	// combine them into a single window/playlist. Tracker
202 	// will send multiple messages when opening a multi-
203 	// selection for example and we don't want to spawn large
204 	// numbers of windows when someone just tries to open an
205 	// album. We use half a second time and prolong it for
206 	// each new ref received.
207 	static bigtime_t sLastRefsReceived = 0;
208 	static MainWin* sLastRefsWindow = NULL;
209 
210 	if (system_time() - sLastRefsReceived < 500000) {
211 		// Find the last opened window
212 		for (int32 i = CountWindows() - 1; i >= 0; i--) {
213 			MainWin* playerWindow = dynamic_cast<MainWin*>(WindowAt(i));
214 			if (playerWindow == NULL)
215 				continue;
216 
217 			if (playerWindow != sLastRefsWindow) {
218 				// The window has changed since the last refs
219 				sLastRefsReceived = 0;
220 				sLastRefsWindow = NULL;
221 				break;
222 			}
223 
224 			message->AddBool("append to playlist", true);
225 			playerWindow->PostMessage(message);
226 			sLastRefsReceived = system_time();
227 			return;
228 		}
229 	}
230 
231 	sLastRefsWindow = NewWindow(message);
232 	sLastRefsReceived = system_time();
233 }
234 
235 
236 void
237 MainApp::ArgvReceived(int32 argc, char** argv)
238 {
239 	char cwd[B_PATH_NAME_LENGTH];
240 	getcwd(cwd, sizeof(cwd));
241 
242 	BMessage message(B_REFS_RECEIVED);
243 
244 	for (int i = 1; i < argc; i++) {
245 		printf("MainApp::ArgvReceived %s\n", argv[i]);
246 		BPath path;
247 		if (argv[i][0] != '/')
248 			path.SetTo(cwd, argv[i]);
249 		else
250 			path.SetTo(argv[i]);
251 		BEntry entry(path.Path(), true);
252 		if (!entry.Exists() || !entry.IsFile())
253 			continue;
254 
255 		entry_ref ref;
256 		if (entry.GetRef(&ref) == B_OK)
257 			message.AddRef("refs", &ref);
258 	}
259 
260 	if (message.HasRef("refs")) {
261 		printf("MainApp::ArgvReceived calling RefsReceived\n");
262 		RefsReceived(&message);
263 	}
264 }
265 
266 
267 void
268 MainApp::MessageReceived(BMessage* message)
269 {
270 	switch (message->what) {
271 		case M_PLAYER_QUIT:
272 		{
273 			// store the window settings of this instance
274 			MainWin* window = NULL;
275 			bool audioOnly = false;
276 			BRect windowFrame;
277 			bigtime_t creationTime;
278 			if (message->FindPointer("instance", (void**)&window) == B_OK
279 				&& message->FindBool("audio only", &audioOnly) == B_OK
280 				&& message->FindRect("window frame", &windowFrame) == B_OK
281 				&& message->FindInt64("creation time", &creationTime) == B_OK) {
282 				if (audioOnly) {
283 					if (!fAudioWindowFrameSaved
284 						|| creationTime < fLastSavedAudioWindowCreationTime) {
285 						fAudioWindowFrameSaved = true;
286 						fLastSavedAudioWindowCreationTime = creationTime;
287 						mpSettings settings
288 							= Settings::Default()->CurrentSettings();
289 						settings.audioPlayerWindowFrame = windowFrame;
290 						Settings::Default()->SaveSettings(settings);
291 					}
292 				}
293 			}
294 
295 			// Store the playlist if there is one. Since the app is doing
296 			// this, it is "atomic". If the user has multiple instances
297 			// playing audio at the same time, the last instance which is
298 			// quit wins.
299 			BMessage playlistArchive;
300 			if (message->FindMessage("playlist", &playlistArchive) == B_OK)
301 				_StoreCurrentPlaylist(&playlistArchive);
302 
303 			// quit if this was the last player window
304 			fPlayerCount--;
305 			if (fPlayerCount == 0)
306 				PostMessage(B_QUIT_REQUESTED);
307 			break;
308 		}
309 
310 		case B_SOME_APP_LAUNCHED:
311 		case B_SOME_APP_QUIT:
312 		{
313 			const char* mimeSig;
314 			if (message->FindString("be:signature", &mimeSig) < B_OK)
315 				break;
316 
317 			bool isMediaServer = strcmp(mimeSig, kMediaServerSig) == 0;
318 			bool isAddonServer = strcmp(mimeSig, kMediaServerAddOnSig) == 0;
319 			if (!isMediaServer && !isAddonServer)
320 				break;
321 
322 			bool running = (message->what == B_SOME_APP_LAUNCHED);
323 			if (isMediaServer)
324 				fMediaServerRunning = running;
325 			if (isAddonServer)
326 				fMediaAddOnServerRunning = running;
327 
328 			if (!fMediaServerRunning && !fMediaAddOnServerRunning) {
329 				fprintf(stderr, "media server has quit.\n");
330 				// trigger closing of media nodes
331 				BMessage broadcast(M_MEDIA_SERVER_QUIT);
332 				_BroadcastMessage(broadcast);
333 			} else if (fMediaServerRunning && fMediaAddOnServerRunning) {
334 				fprintf(stderr, "media server has launched.\n");
335 				// HACK!
336 				// quit our now invalid instance of the media roster
337 				// so that before new nodes are created,
338 				// we get a new roster (it is a normal looper)
339 				// TODO: This functionality could become part of
340 				// BMediaRoster. It could detect the start/quit of
341 				// the servers like it is done here, and either quit
342 				// itself, or re-establish the connection, and send some
343 				// notification to the app... something along those lines.
344 				BMediaRoster* roster = BMediaRoster::CurrentRoster();
345 				if (roster) {
346 					roster->Lock();
347 					roster->Quit();
348 				}
349 				// give the servers some time to init...
350 				snooze(3000000);
351 				// trigger re-init of media nodes
352 				BMessage broadcast(M_MEDIA_SERVER_STARTED);
353 				_BroadcastMessage(broadcast);
354 			}
355 			break;
356 		}
357 		case M_SETTINGS:
358 			_ShowSettingsWindow();
359 			break;
360 
361 		case M_SHOW_OPEN_PANEL:
362 			_ShowOpenFilePanel(message);
363 			break;
364 		case M_SHOW_SAVE_PANEL:
365 			_ShowSaveFilePanel(message);
366 			break;
367 
368 		case M_OPEN_PANEL_RESULT:
369 			_HandleOpenPanelResult(message);
370 			break;
371 		case M_SAVE_PANEL_RESULT:
372 			_HandleSavePanelResult(message);
373 			break;
374 		case B_CANCEL:
375 		{
376 			// The user canceled a file panel, but store at least the current
377 			// file panel folder.
378 			uint32 oldWhat;
379 			if (message->FindInt32("old_what", (int32*)&oldWhat) != B_OK)
380 				break;
381 			if (oldWhat == M_OPEN_PANEL_RESULT && fOpenFilePanel != NULL)
382 				fOpenFilePanel->GetPanelDirectory(&fLastFilePanelFolder);
383 			else if (oldWhat == M_SAVE_PANEL_RESULT && fSaveFilePanel != NULL)
384 				fSaveFilePanel->GetPanelDirectory(&fLastFilePanelFolder);
385 			break;
386 		}
387 
388 		default:
389 			BApplication::MessageReceived(message);
390 			break;
391 	}
392 }
393 
394 
395 void
396 MainApp::AboutRequested()
397 {
398 	BAlert* alert = new BAlert("about", NAME"\n\nWritten by Marcus Overhagen "
399 		", Stephan Aßmus and Frederik Modéen", "Thanks");
400 	alert->SetFeel(B_FLOATING_ALL_WINDOW_FEEL);
401 		// Make sure it is on top of any player windows that may have the
402 		// floating all window feel.
403 	alert->Go(NULL);
404 		// asynchronous mode
405 }
406 
407 
408 // #pragma mark -
409 
410 
411 void
412 MainApp::_BroadcastMessage(const BMessage& _message)
413 {
414 	for (int32 i = 0; BWindow* window = WindowAt(i); i++) {
415 		BMessage message(_message);
416 		window->PostMessage(&message);
417 	}
418 }
419 
420 
421 void
422 MainApp::_ShowSettingsWindow()
423 {
424 	BAutolock lock(fSettingsWindow);
425 	if (!lock.IsLocked())
426 		return;
427 
428 	// If the window is already showing, don't jerk the workspaces around,
429 	// just pull it to the current one.
430 	uint32 workspace = 1UL << (uint32)current_workspace();
431 	uint32 windowWorkspaces = fSettingsWindow->Workspaces();
432 	if ((windowWorkspaces & workspace) == 0) {
433 		// window in a different workspace, reopen in current
434 		fSettingsWindow->SetWorkspaces(workspace);
435 	}
436 
437 	if (fSettingsWindow->IsHidden())
438 		fSettingsWindow->Show();
439 	else
440 		fSettingsWindow->Activate();
441 }
442 
443 
444 // #pragma mark - file panels
445 
446 
447 void
448 MainApp::_ShowOpenFilePanel(const BMessage* message)
449 {
450 	if (fOpenFilePanel == NULL) {
451 		BMessenger target(this);
452 		fOpenFilePanel = new BFilePanel(B_OPEN_PANEL, &target);
453 	}
454 
455 	_ShowFilePanel(fOpenFilePanel, M_OPEN_PANEL_RESULT, message,
456 		"Open", "Open");
457 }
458 
459 
460 void
461 MainApp::_ShowSaveFilePanel(const BMessage* message)
462 {
463 	if (fSaveFilePanel == NULL) {
464 		BMessenger target(this);
465 		fSaveFilePanel = new BFilePanel(B_SAVE_PANEL, &target);
466 	}
467 
468 	_ShowFilePanel(fSaveFilePanel, M_SAVE_PANEL_RESULT, message,
469 		"Save", "Save");
470 }
471 
472 
473 void
474 MainApp::_ShowFilePanel(BFilePanel* panel, uint32 command,
475 	const BMessage* message, const char* defaultTitle,
476 	const char* defaultLabel)
477 {
478 //	printf("_ShowFilePanel()\n");
479 //	message->PrintToStream();
480 
481 	BMessage panelMessage(command);
482 
483 	if (message != NULL) {
484 		BMessage targetMessage;
485 		if (message->FindMessage("message", &targetMessage) == B_OK)
486 			panelMessage.AddMessage("message", &targetMessage);
487 
488 		BMessenger target;
489 		if (message->FindMessenger("target", &target) == B_OK)
490 			panelMessage.AddMessenger("target", target);
491 
492 		const char* panelTitle;
493 		if (message->FindString("title", &panelTitle) != B_OK)
494 			panelTitle = defaultTitle;
495 		{
496 			BString finalPanelTitle = "MediaPlayer: ";
497 			finalPanelTitle << panelTitle;
498 			BAutolock lock(panel->Window());
499 			panel->Window()->SetTitle(finalPanelTitle.String());
500 		}
501 		const char* buttonLabel;
502 		if (message->FindString("label", &buttonLabel) != B_OK)
503 			buttonLabel = defaultLabel;
504 		panel->SetButtonLabel(B_DEFAULT_BUTTON, buttonLabel);
505 	}
506 
507 //	panelMessage.PrintToStream();
508 	panel->SetMessage(&panelMessage);
509 
510 	if (fLastFilePanelFolder != entry_ref()) {
511 		panel->SetPanelDirectory(&fLastFilePanelFolder);
512 	}
513 
514 	panel->Show();
515 }
516 
517 
518 void
519 MainApp::_HandleOpenPanelResult(const BMessage* message)
520 {
521 	_HandleFilePanelResult(fOpenFilePanel, message);
522 }
523 
524 
525 void
526 MainApp::_HandleSavePanelResult(const BMessage* message)
527 {
528 	_HandleFilePanelResult(fSaveFilePanel, message);
529 }
530 
531 
532 void
533 MainApp::_HandleFilePanelResult(BFilePanel* panel, const BMessage* message)
534 {
535 //	printf("_HandleFilePanelResult()\n");
536 //	message->PrintToStream();
537 
538 	panel->GetPanelDirectory(&fLastFilePanelFolder);
539 
540 	BMessage targetMessage;
541 	if (message->FindMessage("message", &targetMessage) != B_OK)
542 		targetMessage.what = message->what;
543 
544 	BMessenger target;
545 	if (message->FindMessenger("target", &target) != B_OK) {
546 		if (targetMessage.what == M_OPEN_PANEL_RESULT
547 			|| targetMessage.what == M_SAVE_PANEL_RESULT) {
548 			// prevent endless message cycle
549 			return;
550 		}
551 		// send result message to ourselves
552 		target = BMessenger(this);
553 	}
554 
555 	// copy the important contents of the message
556 	// save panel
557 	entry_ref directory;
558 	if (message->FindRef("directory", &directory) == B_OK)
559 		targetMessage.AddRef("directory", &directory);
560 	const char* name;
561 	if (message->FindString("name", &name) == B_OK)
562 		targetMessage.AddString("name", name);
563 	// open panel
564 	entry_ref ref;
565 	for (int32 i = 0; message->FindRef("refs", i, &ref) == B_OK; i++)
566 		targetMessage.AddRef("refs", &ref);
567 
568 	target.SendMessage(&targetMessage);
569 }
570 
571 
572 static const char* kCurrentPlaylistFilename = "MediaPlayer Current Playlist";
573 
574 
575 void
576 MainApp::_StoreCurrentPlaylist(const BMessage* message) const
577 {
578 	BPath path;
579 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
580 		|| path.Append(kCurrentPlaylistFilename) != B_OK) {
581 		return;
582 	}
583 
584 	BFile file(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
585 	if (file.InitCheck() != B_OK)
586 		return;
587 
588 	message->Flatten(&file);
589 }
590 
591 
592 status_t
593 MainApp::_RestoreCurrentPlaylist(BMessage* message) const
594 {
595 	BPath path;
596 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
597 		|| path.Append(kCurrentPlaylistFilename) != B_OK) {
598 		return B_ERROR;
599 	}
600 
601 	BFile file(path.Path(), B_READ_ONLY);
602 	if (file.InitCheck() != B_OK)
603 		return B_ERROR;
604 
605 	return message->Unflatten(&file);
606 }
607 
608 void
609 MainApp::_InstallPlaylistMimeType()
610 {
611 	// install mime type of documents
612 	BMimeType mime(kBinaryPlaylistMimeString);
613 	status_t ret = mime.InitCheck();
614 	if (ret != B_OK) {
615 		fprintf(stderr, "Could not init native document mime type (%s): %s.\n",
616 			kBinaryPlaylistMimeString, strerror(ret));
617 		return;
618 	}
619 
620 	if (mime.IsInstalled() && !(modifiers() & B_SHIFT_KEY)) {
621 		// mime is already installed, and the user is not
622 		// pressing the shift key to force a re-install
623 		return;
624 	}
625 
626 	ret = mime.Install();
627 	if (ret != B_OK && ret != B_FILE_EXISTS) {
628 		fprintf(stderr, "Could not install native document mime type (%s): %s.\n",
629 			kBinaryPlaylistMimeString, strerror(ret));
630 		return;
631 	}
632 	// set preferred app
633 	ret = mime.SetPreferredApp(kAppSig);
634 	if (ret != B_OK) {
635 		fprintf(stderr, "Could not set native document preferred app: %s\n",
636 			strerror(ret));
637 	}
638 
639 	// set descriptions
640 	ret = mime.SetShortDescription("MediaPlayer playlist");
641 	if (ret != B_OK) {
642 		fprintf(stderr, "Could not set short description of mime type: %s\n",
643 			strerror(ret));
644 	}
645 	ret = mime.SetLongDescription("MediaPlayer binary playlist file");
646 	if (ret != B_OK) {
647 		fprintf(stderr, "Could not set long description of mime type: %s\n",
648 			strerror(ret));
649 	}
650 
651 	// set extensions
652 	BMessage message('extn');
653 	message.AddString("extensions", "playlist");
654 	ret = mime.SetFileExtensions(&message);
655 	if (ret != B_OK) {
656 		fprintf(stderr, "Could not set extensions of mime type: %s\n",
657 			strerror(ret));
658 	}
659 
660 	// set sniffer rule
661 	char snifferRule[32];
662 	uint32 bigEndianMagic = B_HOST_TO_BENDIAN_INT32(kPlaylistMagicBytes);
663 	sprintf(snifferRule, "0.9 ('%4s')", (const char*)&bigEndianMagic);
664 	ret = mime.SetSnifferRule(snifferRule);
665 	if (ret != B_OK) {
666 		BString parseError;
667 		BMimeType::CheckSnifferRule(snifferRule, &parseError);
668 		fprintf(stderr, "Could not set sniffer rule of mime type: %s\n",
669 			parseError.String());
670 	}
671 
672 	// set playlist icon
673 	BResources* resources = AppResources();
674 		// does not need to be freed (belongs to BApplication base)
675 	if (resources != NULL) {
676 		size_t size;
677 		const void* iconData = resources->LoadResource('VICN', "PlaylistIcon",
678 			&size);
679 		if (iconData != NULL && size > 0) {
680 			if (mime.SetIcon(reinterpret_cast<const uint8*>(iconData), size)
681 				!= B_OK) {
682 				fprintf(stderr, "Could not set vector icon of mime type.\n");
683 			}
684 		} else {
685 			fprintf(stderr, "Could not find icon in app resources "
686 				"(data: %p, size: %ld).\n", iconData, size);
687 		}
688 	} else
689 		fprintf(stderr, "Could not find app resources.\n");
690 }
691 
692 
693 // #pragma mark - main
694 
695 
696 int
697 main()
698 {
699 	EventQueue::CreateDefault();
700 
701 	srand(system_time());
702 
703 	gMainApp = new MainApp;
704 	gMainApp->Run();
705 	delete gMainApp;
706 
707 	EventQueue::DeleteDefault();
708 
709 	return 0;
710 }
711