summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/crashreporter_osx.mm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/client/crashreporter_osx.mm')
-rw-r--r--toolkit/crashreporter/client/crashreporter_osx.mm922
1 files changed, 922 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/crashreporter_osx.mm b/toolkit/crashreporter/client/crashreporter_osx.mm
new file mode 100644
index 000000000..0768d6da3
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_osx.mm
@@ -0,0 +1,922 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import <CoreFoundation/CoreFoundation.h>
+#include "crashreporter.h"
+#include "crashreporter_osx.h"
+#include <crt_externs.h>
+#include <spawn.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <sstream>
+
+using std::string;
+using std::vector;
+using std::ostringstream;
+
+using namespace CrashReporter;
+
+static NSAutoreleasePool* gMainPool;
+static CrashReporterUI* gUI = 0;
+static StringTable gFiles;
+static StringTable gQueryParameters;
+static string gURLParameter;
+static string gSendURL;
+static vector<string> gRestartArgs;
+static bool gDidTrySend = false;
+static bool gRTLlayout = false;
+
+static cpu_type_t pref_cpu_types[2] = {
+#if defined(__i386__)
+ CPU_TYPE_X86,
+#elif defined(__x86_64__)
+ CPU_TYPE_X86_64,
+#elif defined(__ppc__)
+ CPU_TYPE_POWERPC,
+#endif
+ CPU_TYPE_ANY };
+
+#define NSSTR(s) [NSString stringWithUTF8String:(s).c_str()]
+
+static NSString* Str(const char* aName)
+{
+ string str = gStrings[aName];
+ if (str.empty()) str = "?";
+ return NSSTR(str);
+}
+
+static bool RestartApplication()
+{
+ vector<char*> argv(gRestartArgs.size() + 1);
+
+ posix_spawnattr_t spawnattr;
+ if (posix_spawnattr_init(&spawnattr) != 0) {
+ return false;
+ }
+
+ // Set spawn attributes.
+ size_t attr_count = sizeof(pref_cpu_types) / sizeof(pref_cpu_types[0]);
+ size_t attr_ocount = 0;
+ if (posix_spawnattr_setbinpref_np(&spawnattr,
+ attr_count,
+ pref_cpu_types,
+ &attr_ocount) != 0 ||
+ attr_ocount != attr_count) {
+ posix_spawnattr_destroy(&spawnattr);
+ return false;
+ }
+
+ unsigned int i;
+ for (i = 0; i < gRestartArgs.size(); i++) {
+ argv[i] = (char*)gRestartArgs[i].c_str();
+ }
+ argv[i] = 0;
+
+ char **env = NULL;
+ char ***nsEnv = _NSGetEnviron();
+ if (nsEnv)
+ env = *nsEnv;
+ int result = posix_spawnp(NULL,
+ argv[0],
+ NULL,
+ &spawnattr,
+ &argv[0],
+ env);
+
+ posix_spawnattr_destroy(&spawnattr);
+
+ return result == 0;
+}
+
+@implementation CrashReporterUI
+
+-(void)awakeFromNib
+{
+ gUI = self;
+ [mWindow center];
+
+ [mWindow setTitle:[[NSBundle mainBundle]
+ objectForInfoDictionaryKey:@"CFBundleName"]];
+}
+
+-(void)showCrashUI:(const StringTable&)files
+ queryParameters:(const StringTable&)queryParameters
+ sendURL:(const string&)sendURL
+{
+ gFiles = files;
+ gQueryParameters = queryParameters;
+ gSendURL = sendURL;
+
+ [mWindow setTitle:Str(ST_CRASHREPORTERTITLE)];
+ [mHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
+
+ NSRect viewReportFrame = [mViewReportButton frame];
+ [mViewReportButton setTitle:Str(ST_VIEWREPORT)];
+ [mViewReportButton sizeToFit];
+ if (gRTLlayout) {
+ // sizeToFit will keep the left side fixed, so realign
+ float oldWidth = viewReportFrame.size.width;
+ viewReportFrame = [mViewReportButton frame];
+ viewReportFrame.origin.x += oldWidth - viewReportFrame.size.width;
+ [mViewReportButton setFrame: viewReportFrame];
+ }
+
+ [mSubmitReportButton setTitle:Str(ST_CHECKSUBMIT)];
+ [mIncludeURLButton setTitle:Str(ST_CHECKURL)];
+ [mEmailMeButton setTitle:Str(ST_CHECKEMAIL)];
+ [mViewReportOkButton setTitle:Str(ST_OK)];
+
+ [mCommentText setPlaceholder:Str(ST_COMMENTGRAYTEXT)];
+ if (gRTLlayout)
+ [mCommentText toggleBaseWritingDirection:self];
+ [[mEmailText cell] setPlaceholderString:Str(ST_EMAILGRAYTEXT)];
+
+ if (gQueryParameters.find("URL") != gQueryParameters.end()) {
+ // save the URL value in case the checkbox gets unchecked
+ gURLParameter = gQueryParameters["URL"];
+ }
+ else {
+ // no URL specified, hide checkbox
+ [mIncludeURLButton removeFromSuperview];
+ // shrink window to fit
+ NSRect frame = [mWindow frame];
+ NSRect includeURLFrame = [mIncludeURLButton frame];
+ NSRect emailFrame = [mEmailMeButton frame];
+ int buttonMask = [mViewReportButton autoresizingMask];
+ int checkMask = [mSubmitReportButton autoresizingMask];
+ int commentScrollMask = [mCommentScrollView autoresizingMask];
+
+ [mViewReportButton setAutoresizingMask:NSViewMinYMargin];
+ [mSubmitReportButton setAutoresizingMask:NSViewMinYMargin];
+ [mCommentScrollView setAutoresizingMask:NSViewMinYMargin];
+
+ // remove all the space in between
+ frame.size.height -= includeURLFrame.origin.y - emailFrame.origin.y;
+ [mWindow setFrame:frame display: true animate:NO];
+
+ [mViewReportButton setAutoresizingMask:buttonMask];
+ [mSubmitReportButton setAutoresizingMask:checkMask];
+ [mCommentScrollView setAutoresizingMask:commentScrollMask];
+ }
+
+ // resize some buttons horizontally and possibly some controls vertically
+ [self doInitialResizing];
+
+ // load default state of submit checkbox
+ // we don't just do this via IB because we want the default to be
+ // off a certain percentage of the time
+ BOOL submitChecked = NO;
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ if (nil != [userDefaults objectForKey:@"submitReport"]) {
+ submitChecked = [userDefaults boolForKey:@"submitReport"];
+ }
+ else {
+ // use compile-time specified enable percentage
+ submitChecked = ShouldEnableSending();
+ [userDefaults setBool:submitChecked forKey:@"submitReport"];
+ }
+ [mSubmitReportButton setState:(submitChecked ? NSOnState : NSOffState)];
+
+ [self updateSubmit];
+ [self updateURL];
+ [self updateEmail];
+
+ [mWindow makeKeyAndOrderFront:nil];
+}
+
+-(void)showErrorUI:(const string&)message
+{
+ [self setView: mErrorView animate: NO];
+
+ [mErrorHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
+ [self setStringFitVertically:mErrorLabel
+ string:NSSTR(message)
+ resizeWindow:YES];
+ [mErrorCloseButton setTitle:Str(ST_OK)];
+
+ [mErrorCloseButton setKeyEquivalent:@"\r"];
+ [mWindow makeFirstResponder:mErrorCloseButton];
+ [mWindow makeKeyAndOrderFront:nil];
+}
+
+-(void)showReportInfo
+{
+ NSDictionary* boldAttr = [NSDictionary
+ dictionaryWithObject:
+ [NSFont boldSystemFontOfSize:
+ [NSFont smallSystemFontSize]]
+ forKey:NSFontAttributeName];
+ NSDictionary* normalAttr = [NSDictionary
+ dictionaryWithObject:
+ [NSFont systemFontOfSize:
+ [NSFont smallSystemFontSize]]
+ forKey:NSFontAttributeName];
+
+ [mViewReportTextView setString:@""];
+ for (StringTable::iterator iter = gQueryParameters.begin();
+ iter != gQueryParameters.end();
+ iter++) {
+ NSAttributedString* key = [[NSAttributedString alloc]
+ initWithString:NSSTR(iter->first + ": ")
+ attributes:boldAttr];
+ NSAttributedString* value = [[NSAttributedString alloc]
+ initWithString:NSSTR(iter->second + "\n")
+ attributes:normalAttr];
+ [[mViewReportTextView textStorage] appendAttributedString: key];
+ [[mViewReportTextView textStorage] appendAttributedString: value];
+ [key release];
+ [value release];
+ }
+
+ NSAttributedString* extra = [[NSAttributedString alloc]
+ initWithString:NSSTR("\n" + gStrings[ST_EXTRAREPORTINFO])
+ attributes:normalAttr];
+ [[mViewReportTextView textStorage] appendAttributedString: extra];
+ [extra release];
+}
+
+- (void)maybeSubmitReport
+{
+ if ([mSubmitReportButton state] == NSOnState) {
+ [self setStringFitVertically:mProgressText
+ string:Str(ST_REPORTDURINGSUBMIT)
+ resizeWindow:YES];
+ // disable all the controls
+ [self enableControls:NO];
+ [mSubmitReportButton setEnabled:NO];
+ [mRestartButton setEnabled:NO];
+ [mCloseButton setEnabled:NO];
+ [mProgressIndicator startAnimation:self];
+ gDidTrySend = true;
+ [self sendReport];
+ } else {
+ [NSApp terminate:self];
+ }
+}
+
+- (void)closeMeDown:(id)unused
+{
+ [NSApp terminate:self];
+}
+
+-(IBAction)submitReportClicked:(id)sender
+{
+ [self updateSubmit];
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ [userDefaults setBool:([mSubmitReportButton state] == NSOnState)
+ forKey:@"submitReport"];
+ [userDefaults synchronize];
+}
+
+-(IBAction)viewReportClicked:(id)sender
+{
+ [self showReportInfo];
+ [NSApp beginSheet:mViewReportWindow modalForWindow:mWindow
+ modalDelegate:nil didEndSelector:nil contextInfo:nil];
+}
+
+- (IBAction)viewReportOkClicked:(id)sender
+{
+ [mViewReportWindow orderOut:nil];
+ [NSApp endSheet:mViewReportWindow];
+}
+
+-(IBAction)closeClicked:(id)sender
+{
+ [self maybeSubmitReport];
+}
+
+-(IBAction)restartClicked:(id)sender
+{
+ RestartApplication();
+ [self maybeSubmitReport];
+}
+
+- (IBAction)includeURLClicked:(id)sender
+{
+ [self updateURL];
+}
+
+-(IBAction)emailMeClicked:(id)sender
+{
+ [self updateEmail];
+}
+
+-(void)controlTextDidChange:(NSNotification *)note
+{
+ [self updateEmail];
+}
+
+- (void)textDidChange:(NSNotification *)aNotification
+{
+ // update comment parameter
+ if ([[[mCommentText textStorage] mutableString] length] > 0)
+ gQueryParameters["Comments"] = [[[mCommentText textStorage] mutableString]
+ UTF8String];
+ else
+ gQueryParameters.erase("Comments");
+}
+
+// Limit the comment field to 500 bytes in UTF-8
+- (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString
+{
+ // current string length + replacement text length - replaced range length
+ if (([[aTextView string]
+ lengthOfBytesUsingEncoding:NSUTF8StringEncoding]
+ + [replacementString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]
+ - [[[aTextView string] substringWithRange:affectedCharRange]
+ lengthOfBytesUsingEncoding:NSUTF8StringEncoding])
+ > MAX_COMMENT_LENGTH) {
+ return NO;
+ }
+ return YES;
+}
+
+- (void)doInitialResizing
+{
+ NSRect windowFrame = [mWindow frame];
+ NSRect restartFrame = [mRestartButton frame];
+ NSRect closeFrame = [mCloseButton frame];
+ // resize close button to fit text
+ float oldCloseWidth = closeFrame.size.width;
+ [mCloseButton setTitle:Str(ST_QUIT)];
+ [mCloseButton sizeToFit];
+ closeFrame = [mCloseButton frame];
+ // move close button left if it grew
+ if (!gRTLlayout) {
+ closeFrame.origin.x -= closeFrame.size.width - oldCloseWidth;
+ }
+
+ if (gRestartArgs.size() == 0) {
+ [mRestartButton removeFromSuperview];
+ if (!gRTLlayout) {
+ closeFrame.origin.x = restartFrame.origin.x +
+ (restartFrame.size.width - closeFrame.size.width);
+ }
+ else {
+ closeFrame.origin.x = restartFrame.origin.x;
+ }
+ [mCloseButton setFrame: closeFrame];
+ [mCloseButton setKeyEquivalent:@"\r"];
+ } else {
+ [mRestartButton setTitle:Str(ST_RESTART)];
+ // resize "restart" button
+ float oldRestartWidth = restartFrame.size.width;
+ [mRestartButton sizeToFit];
+ restartFrame = [mRestartButton frame];
+ if (!gRTLlayout) {
+ // move left by the amount that the button grew
+ restartFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
+ closeFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
+ }
+ else {
+ // shift the close button right in RTL
+ closeFrame.origin.x += restartFrame.size.width - oldRestartWidth;
+ }
+ [mRestartButton setFrame: restartFrame];
+ [mCloseButton setFrame: closeFrame];
+ // possibly resize window if both buttons no longer fit
+ // leave 20 px from either side of the window, and 12 px
+ // between the buttons
+ float neededWidth = closeFrame.size.width + restartFrame.size.width +
+ 2*20 + 12;
+
+ if (neededWidth > windowFrame.size.width) {
+ windowFrame.size.width = neededWidth;
+ [mWindow setFrame:windowFrame display: true animate: NO];
+ }
+ [mRestartButton setKeyEquivalent:@"\r"];
+ }
+
+ NSButton *checkboxes[] = {
+ mSubmitReportButton,
+ mIncludeURLButton,
+ mEmailMeButton
+ };
+
+ for (int i=0; i<3; i++) {
+ NSRect frame = [checkboxes[i] frame];
+ [checkboxes[i] sizeToFit];
+ if (gRTLlayout) {
+ // sizeToFit will keep the left side fixed, so realign
+ float oldWidth = frame.size.width;
+ frame = [checkboxes[i] frame];
+ frame.origin.x += oldWidth - frame.size.width;
+ [checkboxes[i] setFrame: frame];
+ }
+ // keep existing spacing on left side, + 20 px spare on right
+ float neededWidth = frame.origin.x + frame.size.width + 20;
+ if (neededWidth > windowFrame.size.width) {
+ windowFrame.size.width = neededWidth;
+ [mWindow setFrame:windowFrame display: true animate: NO];
+ }
+ }
+
+ // do this down here because we may have made the window wider
+ // up above
+ [self setStringFitVertically:mDescriptionLabel
+ string:Str(ST_CRASHREPORTERDESCRIPTION)
+ resizeWindow:YES];
+
+ // now pin all the controls (except quit/submit) in place,
+ // if we lengthen the window after this, it's just to lengthen
+ // the progress text, so nothing above that text should move.
+ NSView* views[] = {
+ mSubmitReportButton,
+ mViewReportButton,
+ mCommentScrollView,
+ mIncludeURLButton,
+ mEmailMeButton,
+ mEmailText,
+ mProgressIndicator,
+ mProgressText
+ };
+ for (unsigned int i=0; i<sizeof(views)/sizeof(views[0]); i++) {
+ [views[i] setAutoresizingMask:NSViewMinYMargin];
+ }
+}
+
+-(float)setStringFitVertically:(NSControl*)control
+ string:(NSString*)str
+ resizeWindow:(BOOL)resizeWindow
+{
+ // hack to make the text field grow vertically
+ NSRect frame = [control frame];
+ float oldHeight = frame.size.height;
+
+ frame.size.height = 10000;
+ NSSize oldCellSize = [[control cell] cellSizeForBounds: frame];
+ [control setStringValue: str];
+ NSSize newCellSize = [[control cell] cellSizeForBounds: frame];
+
+ float delta = newCellSize.height - oldCellSize.height;
+ frame.origin.y -= delta;
+ frame.size.height = oldHeight + delta;
+ [control setFrame: frame];
+
+ if (resizeWindow) {
+ NSRect frame = [mWindow frame];
+ frame.origin.y -= delta;
+ frame.size.height += delta;
+ [mWindow setFrame:frame display: true animate: NO];
+ }
+
+ return delta;
+}
+
+-(void)setView: (NSView*)v animate: (BOOL)animate
+{
+ NSRect frame = [mWindow frame];
+
+ NSRect oldViewFrame = [[mWindow contentView] frame];
+ NSRect newViewFrame = [v frame];
+
+ frame.origin.y += oldViewFrame.size.height - newViewFrame.size.height;
+ frame.size.height -= oldViewFrame.size.height - newViewFrame.size.height;
+
+ frame.origin.x += oldViewFrame.size.width - newViewFrame.size.width;
+ frame.size.width -= oldViewFrame.size.width - newViewFrame.size.width;
+
+ [mWindow setContentView:v];
+ [mWindow setFrame:frame display:true animate:animate];
+}
+
+- (void)enableControls:(BOOL)enabled
+{
+ [mViewReportButton setEnabled:enabled];
+ [mIncludeURLButton setEnabled:enabled];
+ [mEmailMeButton setEnabled:enabled];
+ [mCommentText setEnabled:enabled];
+ [mCommentScrollView setHasVerticalScroller:enabled];
+ [self updateEmail];
+}
+
+-(void)updateSubmit
+{
+ if ([mSubmitReportButton state] == NSOnState) {
+ [self setStringFitVertically:mProgressText
+ string:Str(ST_REPORTPRESUBMIT)
+ resizeWindow:YES];
+ [mProgressText setHidden:NO];
+ // enable all the controls
+ [self enableControls:YES];
+ }
+ else {
+ // not submitting, disable all the controls under
+ // the submit checkbox, and hide the status text
+ [mProgressText setHidden:YES];
+ [self enableControls:NO];
+ }
+}
+
+-(void)updateURL
+{
+ if ([mIncludeURLButton state] == NSOnState && !gURLParameter.empty()) {
+ gQueryParameters["URL"] = gURLParameter;
+ } else {
+ gQueryParameters.erase("URL");
+ }
+}
+
+-(void)updateEmail
+{
+ if ([mEmailMeButton state] == NSOnState &&
+ [mSubmitReportButton state] == NSOnState) {
+ NSString* email = [mEmailText stringValue];
+ gQueryParameters["Email"] = [email UTF8String];
+ [mEmailText setEnabled:YES];
+ } else {
+ gQueryParameters.erase("Email");
+ [mEmailText setEnabled:NO];
+ }
+}
+
+-(void)sendReport
+{
+ if (![self setupPost]) {
+ LogMessage("Crash report submission failed: could not set up POST data");
+ [self setStringFitVertically:mProgressText
+ string:Str(ST_SUBMITFAILED)
+ resizeWindow:YES];
+ // quit after 5 seconds
+ [self performSelector:@selector(closeMeDown:) withObject:nil
+ afterDelay:5.0];
+ }
+
+ [NSThread detachNewThreadSelector:@selector(uploadThread:)
+ toTarget:self
+ withObject:mPost];
+}
+
+-(bool)setupPost
+{
+ NSURL* url = [NSURL URLWithString:[NSSTR(gSendURL) stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+ if (!url) return false;
+
+ mPost = [[HTTPMultipartUpload alloc] initWithURL: url];
+ if (!mPost) return false;
+
+ NSMutableDictionary* parameters =
+ [[NSMutableDictionary alloc] initWithCapacity: gQueryParameters.size()];
+ if (!parameters) return false;
+
+ StringTable::const_iterator end = gQueryParameters.end();
+ for (StringTable::const_iterator i = gQueryParameters.begin();
+ i != end;
+ i++) {
+ NSString* key = NSSTR(i->first);
+ NSString* value = NSSTR(i->second);
+ if (key && value) {
+ [parameters setObject: value forKey: key];
+ } else {
+ ostringstream message;
+ message << "Warning: skipping annotation '" << i->first
+ << "' due to malformed UTF-8 encoding";
+ LogMessage(message.str());
+ }
+ }
+
+ for (StringTable::const_iterator i = gFiles.begin();
+ i != gFiles.end();
+ i++) {
+ [mPost addFileAtPath: NSSTR(i->second) name: NSSTR(i->first)];
+ }
+
+ [mPost setParameters: parameters];
+ [parameters release];
+
+ return true;
+}
+
+-(void)uploadComplete:(NSData*)data
+{
+ NSHTTPURLResponse* response = [mPost response];
+ [mPost release];
+
+ bool success;
+ string reply;
+ if (!data || !response || [response statusCode] != 200) {
+ success = false;
+ reply = "";
+
+ // if data is nil, we probably logged an error in uploadThread
+ if (data != nil && response != nil) {
+ ostringstream message;
+ message << "Crash report submission failed: server returned status "
+ << [response statusCode];
+ LogMessage(message.str());
+ }
+ } else {
+ success = true;
+ LogMessage("Crash report submitted successfully");
+
+ NSString* encodingName = [response textEncodingName];
+ NSStringEncoding encoding;
+ if (encodingName) {
+ encoding = CFStringConvertEncodingToNSStringEncoding(
+ CFStringConvertIANACharSetNameToEncoding((CFStringRef)encodingName));
+ } else {
+ encoding = NSISOLatin1StringEncoding;
+ }
+ NSString* r = [[NSString alloc] initWithData: data encoding: encoding];
+ reply = [r UTF8String];
+ [r release];
+ }
+
+ SendCompleted(success, reply);
+
+ [mProgressIndicator stopAnimation:self];
+ if (success) {
+ [self setStringFitVertically:mProgressText
+ string:Str(ST_REPORTSUBMITSUCCESS)
+ resizeWindow:YES];
+ } else {
+ [self setStringFitVertically:mProgressText
+ string:Str(ST_SUBMITFAILED)
+ resizeWindow:YES];
+ }
+ // quit after 5 seconds
+ [self performSelector:@selector(closeMeDown:) withObject:nil
+ afterDelay:5.0];
+}
+
+-(void)uploadThread:(HTTPMultipartUpload*)post
+{
+ NSAutoreleasePool* autoreleasepool = [[NSAutoreleasePool alloc] init];
+ NSError* error = nil;
+ NSData* data = [post send: &error];
+ if (error) {
+ data = nil;
+ NSString* errorDesc = [error localizedDescription];
+ string message = [errorDesc UTF8String];
+ LogMessage("Crash report submission failed: " + message);
+ }
+
+ [self performSelectorOnMainThread: @selector(uploadComplete:)
+ withObject: data
+ waitUntilDone: YES];
+
+ [autoreleasepool release];
+}
+
+// to get auto-quit when we close the window
+-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication
+{
+ return YES;
+}
+
+-(void)applicationWillTerminate:(NSNotification *)aNotification
+{
+ // since we use [NSApp terminate:] we never return to main,
+ // so do our cleanup here
+ if (!gDidTrySend)
+ DeleteDump();
+}
+
+@end
+
+@implementation TextViewWithPlaceHolder
+
+- (BOOL)becomeFirstResponder
+{
+ [self setNeedsDisplay:YES];
+ return [super becomeFirstResponder];
+}
+
+- (void)drawRect:(NSRect)rect
+{
+ [super drawRect:rect];
+ if (mPlaceHolderString && [[self string] isEqualToString:@""] &&
+ self != [[self window] firstResponder])
+ [mPlaceHolderString drawInRect:[self frame]];
+}
+
+- (BOOL)resignFirstResponder
+{
+ [self setNeedsDisplay:YES];
+ return [super resignFirstResponder];
+}
+
+- (void)setPlaceholder:(NSString*)placeholder
+{
+ NSColor* txtColor = [NSColor disabledControlTextColor];
+ NSDictionary* txtDict = [NSDictionary
+ dictionaryWithObjectsAndKeys:txtColor,
+ NSForegroundColorAttributeName, nil];
+ mPlaceHolderString = [[NSMutableAttributedString alloc]
+ initWithString:placeholder attributes:txtDict];
+ if (gRTLlayout)
+ [mPlaceHolderString setAlignment:NSRightTextAlignment
+ range:NSMakeRange(0, [placeholder length])];
+
+}
+
+- (void)insertTab:(id)sender
+{
+ // don't actually want to insert tabs, just tab to next control
+ [[self window] selectNextKeyView:sender];
+}
+
+- (void)insertBacktab:(id)sender
+{
+ [[self window] selectPreviousKeyView:sender];
+}
+
+- (void)setEnabled:(BOOL)enabled
+{
+ [self setSelectable:enabled];
+ [self setEditable:enabled];
+ if (![[self string] isEqualToString:@""]) {
+ NSAttributedString* colorString;
+ NSColor* txtColor;
+ if (enabled)
+ txtColor = [NSColor textColor];
+ else
+ txtColor = [NSColor disabledControlTextColor];
+ NSDictionary *txtDict = [NSDictionary
+ dictionaryWithObjectsAndKeys:txtColor,
+ NSForegroundColorAttributeName, nil];
+ colorString = [[NSAttributedString alloc]
+ initWithString:[self string]
+ attributes:txtDict];
+ [[self textStorage] setAttributedString: colorString];
+ [self setInsertionPointColor:txtColor];
+ [colorString release];
+ }
+}
+
+- (void)dealloc
+{
+ [mPlaceHolderString release];
+ [super dealloc];
+}
+
+@end
+
+/* === Crashreporter UI Functions === */
+
+bool UIInit()
+{
+ gMainPool = [[NSAutoreleasePool alloc] init];
+ [NSApplication sharedApplication];
+
+ if (gStrings.find("isRTL") != gStrings.end() &&
+ gStrings["isRTL"] == "yes")
+ gRTLlayout = true;
+
+ [NSBundle loadNibNamed:(gRTLlayout ? @"MainMenuRTL" : @"MainMenu")
+ owner:NSApp];
+
+ return true;
+}
+
+void UIShutdown()
+{
+ [gMainPool release];
+}
+
+void UIShowDefaultUI()
+{
+ [gUI showErrorUI: gStrings[ST_CRASHREPORTERDEFAULT]];
+ [NSApp run];
+}
+
+bool UIShowCrashUI(const StringTable& files,
+ const StringTable& queryParameters,
+ const string& sendURL,
+ const vector<string>& restartArgs)
+{
+ gRestartArgs = restartArgs;
+
+ [gUI showCrashUI: files
+ queryParameters: queryParameters
+ sendURL: sendURL];
+ [NSApp run];
+
+ return gDidTrySend;
+}
+
+void UIError_impl(const string& message)
+{
+ if (!gUI) {
+ // UI failed to initialize, printing is the best we can do
+ printf("Error: %s\n", message.c_str());
+ return;
+ }
+
+ [gUI showErrorUI: message];
+ [NSApp run];
+}
+
+bool UIGetIniPath(string& path)
+{
+ NSString* tmpPath = [NSString stringWithUTF8String:gArgv[0]];
+ NSString* iniName = [tmpPath lastPathComponent];
+ iniName = [iniName stringByAppendingPathExtension:@"ini"];
+ tmpPath = [tmpPath stringByDeletingLastPathComponent];
+ tmpPath = [tmpPath stringByDeletingLastPathComponent];
+ tmpPath = [tmpPath stringByAppendingPathComponent:@"Resources"];
+ tmpPath = [tmpPath stringByAppendingPathComponent:iniName];
+ path = [tmpPath UTF8String];
+ return true;
+}
+
+bool UIGetSettingsPath(const string& vendor,
+ const string& product,
+ string& settingsPath)
+{
+ FSRef foundRef;
+ OSErr err = FSFindFolder(kUserDomain, kApplicationSupportFolderType,
+ kCreateFolder, &foundRef);
+ if (err != noErr)
+ return false;
+
+ unsigned char path[PATH_MAX];
+ FSRefMakePath(&foundRef, path, sizeof(path));
+ NSString* destPath = [NSString stringWithUTF8String:reinterpret_cast<char*>(path)];
+
+ // Note that MacOS ignores the vendor when creating the profile hierarchy -
+ // all application preferences directories live alongside one another in
+ // ~/Library/Application Support/
+ destPath = [destPath stringByAppendingPathComponent: NSSTR(product)];
+ // Thunderbird stores its profile in ~/Library/Thunderbird,
+ // but we're going to put stuff in ~/Library/Application Support/Thunderbird
+ // anyway, so we have to ensure that path exists.
+ string tempPath = [destPath UTF8String];
+ if (!UIEnsurePathExists(tempPath))
+ return false;
+
+ destPath = [destPath stringByAppendingPathComponent: @"Crash Reports"];
+
+ settingsPath = [destPath UTF8String];
+
+ return true;
+}
+
+bool UIEnsurePathExists(const string& path)
+{
+ int ret = mkdir(path.c_str(), S_IRWXU);
+ int e = errno;
+ if (ret == -1 && e != EEXIST)
+ return false;
+
+ return true;
+}
+
+bool UIFileExists(const string& path)
+{
+ struct stat sb;
+ int ret = stat(path.c_str(), &sb);
+ if (ret == -1 || !(sb.st_mode & S_IFREG))
+ return false;
+
+ return true;
+}
+
+bool UIMoveFile(const string& file, const string& newfile)
+{
+ if (!rename(file.c_str(), newfile.c_str()))
+ return true;
+ if (errno != EXDEV)
+ return false;
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSString *source = [fileManager stringWithFileSystemRepresentation:file.c_str() length:file.length()];
+ NSString *dest = [fileManager stringWithFileSystemRepresentation:newfile.c_str() length:newfile.length()];
+ if (!source || !dest)
+ return false;
+
+ [fileManager moveItemAtPath:source toPath:dest error:NULL];
+ return UIFileExists(newfile);
+}
+
+bool UIDeleteFile(const string& file)
+{
+ return (unlink(file.c_str()) != -1);
+}
+
+std::ifstream* UIOpenRead(const string& filename)
+{
+ return new std::ifstream(filename.c_str(), std::ios::in);
+}
+
+std::ofstream* UIOpenWrite(const string& filename,
+ bool append, // append=false
+ bool binary) // binary=false
+{
+ std::ios_base::openmode mode = std::ios::out;
+
+ if (append) {
+ mode = mode | std::ios::app;
+ }
+
+ if (binary) {
+ mode = mode | std::ios::binary;
+ }
+
+ return new std::ofstream(filename.c_str(), mode);
+}