382 lines
14 KiB
Plaintext
382 lines
14 KiB
Plaintext
/**
|
|
* @file llappdelegate-objc.mm
|
|
* @brief Class implementation for the Mac version's application delegate.
|
|
*
|
|
* $LicenseInfo:firstyear=2000&license=viewerlgpl$
|
|
* Second Life Viewer Source Code
|
|
* Copyright (C) 2010, Linden Research, Inc.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation;
|
|
* version 2.1 of the License only.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*
|
|
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
|
|
* $/LicenseInfo$
|
|
*/
|
|
|
|
#import "llappdelegate-objc.h"
|
|
#if defined(LL_BUGSPLAT)
|
|
#include <boost/filesystem.hpp>
|
|
#include <vector>
|
|
@import BugsplatMac;
|
|
// derived from BugsplatMac's BugsplatTester/AppDelegate.m
|
|
@interface LLAppDelegate () <BugsplatStartupManagerDelegate>
|
|
@end
|
|
#endif
|
|
#include "llwindowmacosx-objc.h"
|
|
#include "llappviewermacosx-for-objc.h"
|
|
#include <Carbon/Carbon.h> // Used for Text Input Services ("Safe" API - it's supported)
|
|
|
|
@implementation LLAppDelegate
|
|
|
|
@synthesize window;
|
|
@synthesize inputWindow;
|
|
@synthesize inputView;
|
|
@synthesize currentInputLanguage;
|
|
|
|
- (void)dealloc
|
|
{
|
|
[currentInputLanguage release];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void) applicationWillFinishLaunching:(NSNotification *)notification
|
|
{
|
|
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
|
|
}
|
|
|
|
- (void) applicationDidFinishLaunching:(NSNotification *)notification
|
|
{
|
|
// Call constructViewer() first so our logging subsystem is in place. This
|
|
// risks missing crashes in the LLAppViewerMacOSX constructor, but for
|
|
// present purposes it's more important to get the startup sequence
|
|
// properly logged.
|
|
// Someday I would like to modify the logging system so that calls before
|
|
// it's initialized are cached in a std::ostringstream and then, once it's
|
|
// initialized, "played back" into whatever handlers have been set up.
|
|
constructViewer();
|
|
|
|
#if defined(LL_BUGSPLAT)
|
|
infos("bugsplat setup");
|
|
// Engage BugsplatStartupManager *before* calling initViewer() to handle
|
|
// any crashes during initialization.
|
|
// https://www.bugsplat.com/docs/platforms/os-x#initialization
|
|
[BugsplatStartupManager sharedManager].autoSubmitCrashReport = YES;
|
|
[BugsplatStartupManager sharedManager].askUserDetails = NO;
|
|
[BugsplatStartupManager sharedManager].delegate = self;
|
|
[[BugsplatStartupManager sharedManager] start];
|
|
#endif
|
|
infos("post-bugsplat setup");
|
|
|
|
frameTimer = nil;
|
|
|
|
[self languageUpdated];
|
|
|
|
if (initViewer())
|
|
{
|
|
// Set up recurring calls to oneFrame (repeating timer with timeout 0)
|
|
// until applicationShouldTerminate.
|
|
frameTimer = [NSTimer scheduledTimerWithTimeInterval:0.0 target:self
|
|
selector:@selector(oneFrame) userInfo:nil repeats:YES];
|
|
} else {
|
|
exit(0);
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(languageUpdated) name:@"NSTextInputContextKeyboardSelectionDidChangeNotification" object:nil];
|
|
|
|
// [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
|
|
}
|
|
|
|
- (void) handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
|
|
NSString *url= nil;
|
|
url = [[[[NSAppleEventManager sharedAppleEventManager]// 1
|
|
currentAppleEvent]// 2
|
|
paramDescriptorForKeyword:keyDirectObject]// 3
|
|
stringValue];// 4
|
|
|
|
const char* url_utf8 = [url UTF8String];
|
|
handleUrl(url_utf8);
|
|
}
|
|
|
|
- (void) applicationDidBecomeActive:(NSNotification *)notification
|
|
{
|
|
callWindowFocus();
|
|
}
|
|
|
|
- (void) applicationDidResignActive:(NSNotification *)notification
|
|
{
|
|
callWindowUnfocus();
|
|
}
|
|
|
|
- (void) applicationDidHide:(NSNotification *)notification
|
|
{
|
|
callWindowHide();
|
|
}
|
|
|
|
- (void) applicationDidUnhide:(NSNotification *)notification
|
|
{
|
|
callWindowUnhide();
|
|
}
|
|
|
|
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender
|
|
{
|
|
// run one frame to assess state
|
|
if (!pumpMainLoop())
|
|
{
|
|
// pumpMainLoop() returns true when done, false if it wants to be
|
|
// called again. Since it returned false, do not yet cancel
|
|
// frameTimer.
|
|
handleQuit();
|
|
[[NSApplication sharedApplication] stopModal];
|
|
return NSTerminateCancel;
|
|
} else {
|
|
// pumpMainLoop() returned true: it's done. Okay, done with frameTimer.
|
|
[frameTimer release];
|
|
cleanupViewer();
|
|
return NSTerminateNow;
|
|
}
|
|
}
|
|
|
|
- (void) oneFrame
|
|
{
|
|
bool appExiting = pumpMainLoop();
|
|
if (appExiting)
|
|
{
|
|
// Once pumpMainLoop() reports that we're done, cancel frameTimer:
|
|
// stop the repetitive calls.
|
|
[frameTimer release];
|
|
[[NSApplication sharedApplication] terminate:self];
|
|
}
|
|
}
|
|
|
|
- (void) showInputWindow:(bool)show withEvent:(NSEvent*)textEvent
|
|
{
|
|
if (![self romanScript])
|
|
{
|
|
if (show)
|
|
{
|
|
NSLog(@"Showing input window.");
|
|
[inputWindow makeKeyAndOrderFront:inputWindow];
|
|
if (textEvent != nil)
|
|
{
|
|
[[inputView inputContext] discardMarkedText];
|
|
[[inputView inputContext] handleEvent:textEvent];
|
|
}
|
|
} else {
|
|
NSLog(@"Hiding input window.");
|
|
[inputWindow orderOut:inputWindow];
|
|
[window makeKeyAndOrderFront:window];
|
|
}
|
|
}
|
|
}
|
|
|
|
// This will get called multiple times by NSNotificationCenter.
|
|
// It will be called every time that the window focus changes, and every time that the input language gets changed.
|
|
// The primary use case for this selector is to update our current input language when the user, for whatever reason, changes the input language.
|
|
// This is the more elegant way of handling input language changes instead of checking every time we want to use the input window.
|
|
|
|
- (void) languageUpdated
|
|
{
|
|
TISInputSourceRef currentInput = TISCopyCurrentKeyboardInputSource();
|
|
CFArrayRef languages = (CFArrayRef)TISGetInputSourceProperty(currentInput, kTISPropertyInputSourceLanguages);
|
|
|
|
#if 0 // In the event of ever needing to add new language sources, change this to 1 and watch the terminal for "languages:"
|
|
NSLog(@"languages: %@", TISGetInputSourceProperty(currentInput, kTISPropertyInputSourceLanguages));
|
|
#endif
|
|
|
|
// Typically the language we want is going to be the very first result in the array.
|
|
currentInputLanguage = (NSString*)CFArrayGetValueAtIndex(languages, 0);
|
|
}
|
|
|
|
- (bool) romanScript
|
|
{
|
|
@autoreleasepool {
|
|
// How to add support for new languages with the input window:
|
|
// Simply append this array with the language code (ja for japanese, ko for korean, zh for chinese, etc.)
|
|
NSArray* nonRomanScript = @[@"ja", @"ko", @"zh-Hant", @"zh-Hans"];
|
|
if ([nonRomanScript containsObject:currentInputLanguage])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#if defined(LL_BUGSPLAT)
|
|
|
|
- (NSString *)applicationLogForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager
|
|
{
|
|
CrashMetadata& meta(CrashMetadata_instance());
|
|
// As of BugsplatMac 1.0.6, userName and userEmail properties are now
|
|
// exposed by the BugsplatStartupManager. Set them here, since the
|
|
// defaultUserNameForBugsplatStartupManager and
|
|
// defaultUserEmailForBugsplatStartupManager methods are called later, for
|
|
// the *current* run, rather than for the previous crashed run whose crash
|
|
// report we are about to send.
|
|
infos("applicationLogForBugsplatStartupManager setting userName = '" +
|
|
meta.agentFullname + '"');
|
|
bugsplatStartupManager.userName =
|
|
[NSString stringWithCString:meta.agentFullname.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
// Use the email field for OS version, just as we do on Windows, until
|
|
// BugSplat provides more metadata fields.
|
|
infos("applicationLogForBugsplatStartupManager setting userEmail = '" +
|
|
meta.OSInfo + '"');
|
|
bugsplatStartupManager.userEmail =
|
|
[NSString stringWithCString:meta.OSInfo.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
// This strangely-named override method's return value contributes the
|
|
// User Description metadata field.
|
|
infos("applicationLogForBugsplatStartupManager -> '" + meta.fatalMessage + "'");
|
|
return [NSString stringWithCString:meta.fatalMessage.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
}
|
|
|
|
- (NSString *)applicationKeyForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager signal:(NSString *)signal exceptionName:(NSString *)exceptionName exceptionReason:(NSString *)exceptionReason {
|
|
// TODO: exceptionName, exceptionReason
|
|
|
|
// Windows sends location within region as well, but that's because
|
|
// BugSplat for Windows intercepts crashes during the same run, and that
|
|
// information can be queried once. On the Mac, any metadata we have is
|
|
// written (and rewritten) to the static_debug_info.log file that we read
|
|
// at the start of the next viewer run. It seems ridiculously expensive to
|
|
// rewrite that file on every frame in which the avatar moves.
|
|
std::string regionName(CrashMetadata_instance().regionName);
|
|
infos("applicationKeyForBugsplatStartupManager -> '" + regionName + "'");
|
|
return [NSString stringWithCString:regionName.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
}
|
|
|
|
- (NSString *)defaultUserNameForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager {
|
|
std::string agentFullname(CrashMetadata_instance().agentFullname);
|
|
infos("defaultUserNameForBugsplatStartupManager -> '" + agentFullname + "'");
|
|
return [NSString stringWithCString:agentFullname.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
}
|
|
|
|
- (NSString *)defaultUserEmailForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager {
|
|
// Use the email field for OS version, just as we do on Windows, until
|
|
// BugSplat provides more metadata fields.
|
|
std::string OSInfo(CrashMetadata_instance().OSInfo);
|
|
infos("defaultUserEmailForBugsplatStartupManager -> '" + OSInfo + "'");
|
|
return [NSString stringWithCString:OSInfo.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
}
|
|
|
|
- (void)bugsplatStartupManagerWillSendCrashReport:(BugsplatStartupManager *)bugsplatStartupManager
|
|
{
|
|
infos("bugsplatStartupManagerWillSendCrashReport");
|
|
}
|
|
|
|
struct AttachmentInfo
|
|
{
|
|
AttachmentInfo(const std::string& path, const std::string& type):
|
|
pathname(path),
|
|
basename(boost::filesystem::path(path).filename().string()),
|
|
mimetype(type)
|
|
{}
|
|
|
|
std::string pathname, basename, mimetype;
|
|
};
|
|
|
|
- (NSArray<BugsplatAttachment *> *)attachmentsForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager
|
|
{
|
|
const CrashMetadata& metadata(CrashMetadata_instance());
|
|
|
|
// Since we must do very similar processing for each of several file
|
|
// pathnames, start by collecting them into a vector so we can iterate
|
|
// instead of spelling out the logic for each.
|
|
std::vector<AttachmentInfo> info{
|
|
AttachmentInfo(metadata.logFilePathname, "text/plain"),
|
|
AttachmentInfo(metadata.userSettingsPathname, "text/xml"),
|
|
AttachmentInfo(metadata.accountSettingsPathname, "text/xml"),
|
|
AttachmentInfo(metadata.staticDebugPathname, "text/xml"),
|
|
AttachmentInfo(metadata.attributesPathname, "text/xml")
|
|
};
|
|
|
|
secondLogPath = metadata.secondLogFilePathname;
|
|
if(!secondLogPath.empty())
|
|
{
|
|
info.push_back(AttachmentInfo(secondLogPath, "text/xml"));
|
|
}
|
|
|
|
// We "happen to know" that info[0].basename is "SecondLife.old" -- due to
|
|
// the fact that BugsplatMac only notices a crash during the viewer run
|
|
// following the crash.
|
|
// The Bugsplat service doesn't respect the MIME type above when returning
|
|
// the log data to a browser, so take this opportunity to rename the file
|
|
// from <base>.old to <base>_log.txt
|
|
info[0].basename =
|
|
boost::filesystem::path(info[0].pathname).stem().string() + "_log.txt";
|
|
infos("attachmentsForBugsplatStartupManager attaching log " + info[0].basename);
|
|
|
|
NSMutableArray *attachments = [[NSMutableArray alloc] init];
|
|
|
|
// Iterate over each AttachmentInfo in info vector
|
|
for (const AttachmentInfo& attach : info)
|
|
{
|
|
NSString *nspathname = [NSString stringWithCString:attach.pathname.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
NSString *nsbasename = [NSString stringWithCString:attach.basename.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
NSString *nsmimetype = [NSString stringWithCString:attach.mimetype.c_str()
|
|
encoding:NSUTF8StringEncoding];
|
|
NSData *nsdata = [NSData dataWithContentsOfFile:nspathname];
|
|
|
|
BugsplatAttachment *attachment =
|
|
[[BugsplatAttachment alloc] initWithFilename:nsbasename
|
|
attachmentData:nsdata
|
|
contentType:nsmimetype];
|
|
|
|
[attachments addObject:attachment];
|
|
infos("attachmentsForBugsplatStartupManager attaching " + attach.pathname);
|
|
}
|
|
|
|
return attachments;
|
|
}
|
|
|
|
- (void)bugsplatStartupManagerDidFinishSendingCrashReport:(BugsplatStartupManager *)bugsplatStartupManager
|
|
{
|
|
infos("Sent crash report to BugSplat");
|
|
|
|
if(!secondLogPath.empty())
|
|
{
|
|
boost::filesystem::remove(secondLogPath);
|
|
}
|
|
clearDumpLogsDir();
|
|
}
|
|
|
|
- (void)bugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager didFailWithError:(NSError *)error
|
|
{
|
|
// TODO: message string from NSError
|
|
infos("Could not send crash report to BugSplat");
|
|
}
|
|
|
|
#endif // LL_BUGSPLAT
|
|
|
|
@end
|
|
|
|
@implementation LLApplication
|
|
|
|
- (void)sendEvent:(NSEvent *)event
|
|
{
|
|
[super sendEvent:event];
|
|
if ([event type] == NSKeyUp && ([event modifierFlags] & NSCommandKeyMask))
|
|
{
|
|
[[self keyWindow] sendEvent:event];
|
|
}
|
|
}
|
|
|
|
@end
|