From 22734848ea2124d2e8c75a472f71ae20a69fb116 Mon Sep 17 00:00:00 2001 From: nealsid Date: Tue, 21 Jul 2009 00:10:57 +0000 Subject: Port fixes from internal Google Breakpad to SVN. A=preston, nealsid R=Stuart, Preston git-svn-id: http://google-breakpad.googlecode.com/svn/trunk@360 4c0a9323-5329-0410-9bdc-e9ce6186880e --- src/client/mac/Breakpad.xcodeproj/project.pbxproj | 4 + src/client/mac/Framework/Breakpad.h | 11 +- src/client/mac/Framework/Breakpad.mm | 14 +- src/client/mac/crash_generation/Inspector.mm | 21 +- src/client/mac/sender/Breakpad.nib/classes.nib | 14 +- src/client/mac/sender/Breakpad.nib/info.nib | 6 +- .../mac/sender/Breakpad.nib/keyedobjects.nib | Bin 13747 -> 14674 bytes .../mac/sender/English.lproj/Localizable.strings | Bin 1488 -> 2270 bytes src/client/mac/sender/crash_report_sender.h | 49 ++++- src/client/mac/sender/crash_report_sender.m | 215 ++++++++++++++++++--- src/client/mac/sender/goArrow.png | Bin 0 -> 3591 bytes src/client/mac/tests/BreakpadFramework_Test.mm | 5 +- 12 files changed, 287 insertions(+), 52 deletions(-) create mode 100644 src/client/mac/sender/goArrow.png (limited to 'src') diff --git a/src/client/mac/Breakpad.xcodeproj/project.pbxproj b/src/client/mac/Breakpad.xcodeproj/project.pbxproj index 97659d32..2dbbdb40 100644 --- a/src/client/mac/Breakpad.xcodeproj/project.pbxproj +++ b/src/client/mac/Breakpad.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ F93DE33F0F82C66B00608B94 /* string_utilities.cc in Sources */ = {isa = PBXBuildFile; fileRef = F92C53820ECCE635009BE4BA /* string_utilities.cc */; }; F93DE3410F82C68300608B94 /* exception_handler_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F93DE3400F82C68300608B94 /* exception_handler_test.cc */; }; F945849E0F280E3C009A47BF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F945849C0F280E3C009A47BF /* Localizable.strings */; }; + F9B630A0100FF96B00D0F4AC /* goArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = F9B6309F100FF96B00D0F4AC /* goArrow.png */; }; F9C44DB20EF07288003AEBAA /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = F9C44DAC0EF07288003AEBAA /* Controller.m */; }; F9C44DB30EF07288003AEBAA /* crashduringload in Resources */ = {isa = PBXBuildFile; fileRef = F9C44DAD0EF07288003AEBAA /* crashduringload */; }; F9C44DB40EF07288003AEBAA /* crashInMain in Resources */ = {isa = PBXBuildFile; fileRef = F9C44DAE0EF07288003AEBAA /* crashInMain */; }; @@ -288,6 +289,7 @@ F93DE3400F82C68300608B94 /* exception_handler_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = exception_handler_test.cc; path = handler/exception_handler_test.cc; sourceTree = ""; }; F945849D0F280E3C009A47BF /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = sender/English.lproj/Localizable.strings; sourceTree = ""; }; F945859D0F78241E009A47BF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Framework/Info.plist; sourceTree = ""; }; + F9B6309F100FF96B00D0F4AC /* goArrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = goArrow.png; path = sender/goArrow.png; sourceTree = ""; }; F9C44DA50EF060A8003AEBAA /* BreakpadTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BreakpadTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; F9C44DAC0EF07288003AEBAA /* Controller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Controller.m; path = testapp/Controller.m; sourceTree = ""; }; F9C44DAD0EF07288003AEBAA /* crashduringload */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = crashduringload; path = testapp/crashduringload; sourceTree = ""; }; @@ -535,6 +537,7 @@ F92C56A60ECE04B6009BE4BA /* sender */ = { isa = PBXGroup; children = ( + F9B6309F100FF96B00D0F4AC /* goArrow.png */, F92C56A70ECE04C5009BE4BA /* crash_report_sender.h */, F92C56A80ECE04C5009BE4BA /* crash_report_sender.m */, F945849C0F280E3C009A47BF /* Localizable.strings */, @@ -813,6 +816,7 @@ 4084699D0F5D9CF900FDCA37 /* crash_report_sender.icns in Resources */, 33880C800F9E097100817F82 /* InfoPlist.strings in Resources */, 3329D4ED0FA16D820007BBC5 /* Breakpad.nib in Resources */, + F9B630A0100FF96B00D0F4AC /* goArrow.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/client/mac/Framework/Breakpad.h b/src/client/mac/Framework/Breakpad.h index ab4d6220..6b5ce66a 100644 --- a/src/client/mac/Framework/Breakpad.h +++ b/src/client/mac/Framework/Breakpad.h @@ -89,6 +89,7 @@ extern "C" { #define BREAKPAD_PROCESS_CRASH_TIME "BreakpadProcessCrashTime" #define BREAKPAD_LOGFILE_KEY_PREFIX "BreakpadAppLogFile" #define BREAKPAD_SERVER_PARAMETER_PREFIX "BreakpadServerParameterPrefix_" +#define BREAKPAD_ON_DEMAND "BreakpadOnDemand" // Optional user-defined function to dec to decide if we should handle // this crash or forward it along. @@ -215,7 +216,7 @@ typedef bool (*BreakpadFilterCallback)(int exception_type, //============================================================================= // The following are NOT user-supplied but are documented here for // completeness. They are calculated by Breakpad during initialization & -// crash-dump generation. +// crash-dump generation, or entered in by the user. // // BREAKPAD_PROCESS_START_TIME The time the process started. // @@ -242,6 +243,12 @@ typedef bool (*BreakpadFilterCallback)(int exception_type, // server without leaking Breakpad's // internal values. // +// BREAKPAD_ON_DEMAND Used internally to indicate to the +// Reporter that we're sending on-demand, +// not as result of a crash. +// +// BREAKPAD_COMMENTS The text the user provided as comments. +// Only used in crash_report_sender. // Returns a new BreakpadRef object on success, NULL otherwise. BreakpadRef BreakpadCreate(NSDictionary *parameters); @@ -286,7 +293,7 @@ void BreakpadRemoveKeyValue(BreakpadRef ref, NSString *key); // necessary. Note that as mentioned above there are limits on both // the number of keys and their length. void BreakpadAddUploadParameter(BreakpadRef ref, NSString *key, - NSString *value); + NSString *value); // This method will remove a previously-added parameter from the // upload parameter set. diff --git a/src/client/mac/Framework/Breakpad.mm b/src/client/mac/Framework/Breakpad.mm index 551f9785..2528d99f 100644 --- a/src/client/mac/Framework/Breakpad.mm +++ b/src/client/mac/Framework/Breakpad.mm @@ -409,7 +409,6 @@ bool Breakpad::ExtractParameters(NSDictionary *parameters) { NSString *timeout = [parameters objectForKey:@BREAKPAD_CONFIRM_TIMEOUT]; NSArray *logFilePaths = [parameters objectForKey:@BREAKPAD_LOGFILES]; NSString *logFileTailSize = [parameters objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE]; - NSString *reportEmail = [parameters objectForKey:@BREAKPAD_EMAIL]; NSString *requestUserText = [parameters objectForKey:@BREAKPAD_REQUEST_COMMENTS]; NSString *requestEmail = [parameters objectForKey:@BREAKPAD_REQUEST_EMAIL]; @@ -451,7 +450,7 @@ bool Breakpad::ExtractParameters(NSDictionary *parameters) { vendor = @"Vendor not specified"; } - // Normalize the values + // Normalize the values. if (skipConfirm) { skipConfirm = [skipConfirm uppercaseString]; @@ -504,7 +503,7 @@ bool Breakpad::ExtractParameters(NSDictionary *parameters) { [resourcePath stringByAppendingPathComponent:@"Inspector"]; } - // Verify that there is an Inspector tool + // Verify that there is an Inspector tool. if (![[NSFileManager defaultManager] fileExistsAtPath:inspectorPathString]) { DEBUGLOG(stderr, "Cannot find Inspector tool\n"); return false; @@ -517,7 +516,7 @@ bool Breakpad::ExtractParameters(NSDictionary *parameters) { reporterPathString = [[NSBundle bundleWithPath:reporterPathString] executablePath]; } - // Verify that there is a Reporter application + // Verify that there is a Reporter application. if (![[NSFileManager defaultManager] fileExistsAtPath:reporterPathString]) { DEBUGLOG(stderr, "Cannot find Reporter tool\n"); @@ -588,11 +587,6 @@ bool Breakpad::ExtractParameters(NSDictionary *parameters) { } } - if (reportEmail) { - dictionary.SetKeyValue(BREAKPAD_EMAIL, - [reportEmail UTF8String]); - } - if (serverParameters) { // For each key-value pair, call BreakpadAddUploadParameter() NSEnumerator *keyEnumerator = [serverParameters keyEnumerator]; @@ -633,7 +627,9 @@ void Breakpad::RemoveKeyValue(NSString *key) { //============================================================================= void Breakpad::GenerateAndSendReport() { + config_params_->SetKeyValue(BREAKPAD_ON_DEMAND, "YES"); HandleException(0, 0, 0, mach_thread_self()); + config_params_->SetKeyValue(BREAKPAD_ON_DEMAND, "NO"); } //============================================================================= diff --git a/src/client/mac/crash_generation/Inspector.mm b/src/client/mac/crash_generation/Inspector.mm index 5c5c2218..d328ee5d 100644 --- a/src/client/mac/crash_generation/Inspector.mm +++ b/src/client/mac/crash_generation/Inspector.mm @@ -319,6 +319,12 @@ kern_return_t Inspector::ReadMessages() { printf("parameter count = %d\n", info.parameter_count); #endif + // In certain situations where multiple crash requests come + // through quickly, we can end up with the mach IPC messages not + // coming through correctly. Since we don't know what parameters + // we've missed, we can't do much besides abort the crash dump + // situation in this case. + unsigned int parameters_read = 0; // The initial message contains the number of key value pairs that // we are expected to read. // Read each key/value pair, one mach message per key/value pair. @@ -329,6 +335,14 @@ kern_return_t Inspector::ReadMessages() { if(result == KERN_SUCCESS) { KeyValueMessageData &key_value_data = (KeyValueMessageData&)*message.GetData(); + // If we get a blank key, make sure we don't increment the + // parameter count; in some cases (notably on-demand generation + // many times in a short period of time) caused the Mach IPC + // messages to not come through correctly. + if (strlen(key_value_data.key) == 0) { + continue; + } + parameters_read++; config_params_.SetKeyValue(key_value_data.key, key_value_data.value); } else { @@ -336,6 +350,11 @@ kern_return_t Inspector::ReadMessages() { break; } } + if (parameters_read != info.parameter_count) { + DEBUGLOG(stderr, "Only read %d parameters instead of %d, aborting crash " + "dump generation.", parameters_read, info.parameter_count); + return KERN_FAILURE; + } } return result; @@ -379,7 +398,7 @@ bool Inspector::InspectTask() { SetCrashTimeParameters(); // If the client app has not specified a minidump directory, // use a default of Library// - if (0 == strlen(minidumpDirectory)) { + if (!minidumpDirectory || 0 == strlen(minidumpDirectory)) { NSArray *libraryDirectories = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, diff --git a/src/client/mac/sender/Breakpad.nib/classes.nib b/src/client/mac/sender/Breakpad.nib/classes.nib index ebc5ab40..c9d43a38 100644 --- a/src/client/mac/sender/Breakpad.nib/classes.nib +++ b/src/client/mac/sender/Breakpad.nib/classes.nib @@ -4,6 +4,14 @@ IBClasses + + CLASS + LengthLimitingTextField + LANGUAGE + ObjC + SUPERCLASS + NSTextField + ACTIONS @@ -26,10 +34,14 @@ NSButton commentMessage_ NSTextField + commentsEntryField_ + LengthLimitingTextField + countdownLabel_ + NSTextField dialogTitle_ NSTextField emailEntryField_ - NSView + LengthLimitingTextField emailLabel_ NSTextField emailMessage_ diff --git a/src/client/mac/sender/Breakpad.nib/info.nib b/src/client/mac/sender/Breakpad.nib/info.nib index 21bd6319..e39a8e54 100644 --- a/src/client/mac/sender/Breakpad.nib/info.nib +++ b/src/client/mac/sender/Breakpad.nib/info.nib @@ -3,17 +3,17 @@ IBFramework Version - 672 + 676 IBLastKnownRelativeProjectPath ../Breakpad.xcodeproj IBOldestOS 5 IBOpenObjects - 2 + 132 IBSystem Version - 9G55 + 9J61 targetFramework IBCocoaFramework diff --git a/src/client/mac/sender/Breakpad.nib/keyedobjects.nib b/src/client/mac/sender/Breakpad.nib/keyedobjects.nib index d0cbd3f2..e370206c 100644 Binary files a/src/client/mac/sender/Breakpad.nib/keyedobjects.nib and b/src/client/mac/sender/Breakpad.nib/keyedobjects.nib differ diff --git a/src/client/mac/sender/English.lproj/Localizable.strings b/src/client/mac/sender/English.lproj/Localizable.strings index efc832ac..70626567 100644 Binary files a/src/client/mac/sender/English.lproj/Localizable.strings and b/src/client/mac/sender/English.lproj/Localizable.strings differ diff --git a/src/client/mac/sender/crash_report_sender.h b/src/client/mac/sender/crash_report_sender.h index f62e7613..ca5b3079 100644 --- a/src/client/mac/sender/crash_report_sender.h +++ b/src/client/mac/sender/crash_report_sender.h @@ -41,6 +41,24 @@ extern NSString *const kGoogleServerType; extern NSString *const kSocorroServerType; extern NSString *const kDefaultServerType; + +// We're sublcassing NSTextField in order to override a particular +// method (see the implementation) that lets us reject changes if they +// are longer than a particular length. Bindings would normally solve +// this problem, but when we implemented a validation method, and +// returned NO for strings that were too long, the UI was not updated +// right away, which was a poor user experience. The UI would be +// updated as soon as the text field lost first responder status, +// which isn't soon enough. It is a known bug that the UI KVO didn't +// work in the middle of a validation. +@interface LengthLimitingTextField : NSTextField { + @private + unsigned int maximumLength_; +} + +- (void) setMaximumLength:(unsigned int)maxLength; +@end + @interface Reporter : NSObject { @public IBOutlet NSWindow *alertWindow_; // The alert window @@ -50,26 +68,34 @@ extern NSString *const kDefaultServerType; IBOutlet NSBox *preEmailBox_; IBOutlet NSBox *emailSectionBox_; // Localized elements (or things that need to be moved during localization). - IBOutlet NSTextField *dialogTitle_; - IBOutlet NSTextField *commentMessage_; - IBOutlet NSTextField *emailMessage_; - IBOutlet NSTextField *emailLabel_; - IBOutlet NSTextField *privacyLinkLabel_; - IBOutlet NSButton *sendButton_; - IBOutlet NSButton *cancelButton_; - IBOutlet NSView *emailEntryField_; - IBOutlet NSView *privacyLinkArrow_; + IBOutlet NSTextField *dialogTitle_; + IBOutlet NSTextField *commentMessage_; + IBOutlet NSTextField *emailMessage_; + IBOutlet NSTextField *emailLabel_; + IBOutlet NSTextField *privacyLinkLabel_; + IBOutlet NSButton *sendButton_; + IBOutlet NSButton *cancelButton_; + IBOutlet LengthLimitingTextField *emailEntryField_; + IBOutlet LengthLimitingTextField *commentsEntryField_; + IBOutlet NSTextField *countdownLabel_; + IBOutlet NSView *privacyLinkArrow_; // Text field bindings, for user input. NSString *commentsValue_; // Comments from the user NSString *emailValue_; // Email from the user - + NSString *countdownMessage_; // Message indicating time + // left for input. @private int configFile_; // File descriptor for config file NSMutableDictionary *parameters_; // Key value pairs of data (STRONG) NSData *minidumpContents_; // The data in the minidump (STRONG) NSData *logFileData_; // An NSdata for the tar, // bz2'd log file. + NSTimeInterval remainingDialogTime_; // Keeps track of how long + // we have until we cancel + // the dialog + NSTimer *messageTimer_; // Timer we use to update + // the dialog NSMutableDictionary *serverDictionary_; // The dictionary mapping a // server type name to a // dictionary of server @@ -107,4 +133,7 @@ extern NSString *const kDefaultServerType; - (NSString *)emailValue; - (void)setEmailValue:(NSString *)value; +- (NSString *)countdownMessage; +- (void)setCountdownMessage:(NSString *)value; + @end diff --git a/src/client/mac/sender/crash_report_sender.m b/src/client/mac/sender/crash_report_sender.m index 5d76290c..31c35fe5 100644 --- a/src/client/mac/sender/crash_report_sender.m +++ b/src/client/mac/sender/crash_report_sender.m @@ -42,8 +42,11 @@ #define kLastSubmission @"LastSubmission" const int kMinidumpFileLengthLimit = 800000; +const int kUserCommentsMaxLength = 1500; +const int kEmailMaxLength = 64; -#define kApplePrefsSyncExcludeAllKey @"com.apple.PreferenceSync.ExcludeAllSyncKeys" +#define kApplePrefsSyncExcludeAllKey \ + @"com.apple.PreferenceSync.ExcludeAllSyncKeys" NSString *const kGoogleServerType = @"google"; NSString *const kSocorroServerType = @"socorro"; @@ -174,6 +177,9 @@ NSString *const kDefaultServerType = @"google"; // Returns YES if we should send the report without asking the user first. - (BOOL)shouldSubmitSilently; +// Returns YES if the minidump was generated on demand. +- (BOOL)isOnDemand; + // Returns YES if we should ask the user to provide comments. - (BOOL)shouldRequestComments; @@ -187,11 +193,11 @@ NSString *const kDefaultServerType = @"google"; // Returns the short description of the crash, suitable for use as a dialog // title (e.g., "The application Foo has quit unexpectedly"). -- (NSString*)shortCrashDialogMessage; +- (NSString*)shortDialogMessage; // Return explanatory text about the crash and the reporter, suitable for the // body text of a dialog. -- (NSString*)explanatoryCrashDialogText; +- (NSString*)explanatoryDialogText; // Returns the amount of time the UI should be shown before timing out. - (NSTimeInterval)messageTimeout; @@ -207,7 +213,7 @@ NSString *const kDefaultServerType = @"google"; - (void)removeEmailPrompt; // Run an alert window with the given timeout. Returns -// NSAlertButtonDefault if the timeout is exceeded. A timeout of 0 +// NSRunStoppedResponse if the timeout is exceeded. A timeout of 0 // queues the message immediately in the modal run loop. - (int)runModalWindow:(NSWindow*)window withTimeout:(NSTimeInterval)timeout; @@ -235,6 +241,16 @@ NSString *const kDefaultServerType = @"google"; // will be uploaded to the crash server. - (void)addServerParameter:(id)value forKey:(NSString *)key; +// This method is used to periodically update the UI with how many +// seconds are left in the dialog display. +- (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer; + +// When we receive this notification, it means that the user has +// begun editing the email address or comments field, and we disable +// the timers so that the user has as long as they want to type +// in their comments/email. +- (void)controlTextDidBeginEditing:(NSNotification *)aNotification; + @end @implementation Reporter @@ -261,6 +277,7 @@ NSString *const kDefaultServerType = @"google"; - (id)initWithConfigurationFD:(int)fd { if ((self = [super init])) { configFile_ = fd; + remainingDialogTime_ = 0; } // Because the reporter is embedded in the framework (and many copies @@ -545,8 +562,8 @@ NSString *const kDefaultServerType = @"google"; } } else { // Create an alert panel to tell the user something happened - NSPanel* alert = NSGetAlertPanel([self shortCrashDialogMessage], - [self explanatoryCrashDialogText], + NSPanel* alert = NSGetAlertPanel([self shortDialogMessage], + [self explanatoryDialogText], NSLocalizedString(@"sendReportButton", @""), NSLocalizedString(@"cancelButton", @""), nil); @@ -566,11 +583,11 @@ NSString *const kDefaultServerType = @"google"; // "fall" as text areas are shrunk from their overly-large IB sizes. // Localize the header. No resizing needed, as it has plenty of room. - [dialogTitle_ setStringValue:[self shortCrashDialogMessage]]; + [dialogTitle_ setStringValue:[self shortDialogMessage]]; // Localize the explanatory text field. [commentMessage_ setStringValue:[NSString stringWithFormat:@"%@\n\n%@", - [self explanatoryCrashDialogText], + [self explanatoryDialogText], NSLocalizedString(@"commentsMsg", @"")]]; float commentHeightDelta = [commentMessage_ breakpad_adjustHeightToFit]; [headerBox_ breakpad_shiftVertically:commentHeightDelta]; @@ -615,9 +632,13 @@ NSString *const kDefaultServerType = @"google"; - (int)runModalWindow:(NSWindow*)window withTimeout:(NSTimeInterval)timeout { // Queue a |stopModal| message to be performed in |timeout| seconds. if (timeout > 0.001) { - [NSApp performSelector:@selector(stopModal) - withObject:nil - afterDelay:timeout]; + remainingDialogTime_ = timeout; + SEL updateSelector = @selector(updateSecondsLeftInDialogDisplay:); + messageTimer_ = [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self + selector:updateSelector + userInfo:nil + repeats:YES]; } // Run the window modally and wait for either a |stopModal| message or a @@ -625,12 +646,6 @@ NSString *const kDefaultServerType = @"google"; [NSApp activateIgnoringOtherApps:YES]; int returnMethod = [NSApp runModalForWindow:window]; - // Cancel the pending |stopModal| message. - if (returnMethod != NSRunStoppedResponse) { - [NSObject cancelPreviousPerformRequestsWithTarget:NSApp - selector:@selector(stopModal) - object:nil]; - } return returnMethod; } @@ -667,8 +682,10 @@ NSString *const kDefaultServerType = @"google"; textView:(NSTextView*)textView doCommandBySelector:(SEL)commandSelector { BOOL result = NO; - // If the user has entered text, don't end editing on "return" - if (commandSelector == @selector(insertNewline:) + // If the user has entered text on the comment field, don't end + // editing on "return". + if (control == commentsEntryField_ && + commandSelector == @selector(insertNewline:) && [[textView string] length] > 0) { [textView insertNewlineIgnoringFieldEditor:self]; result = YES; @@ -676,6 +693,50 @@ doCommandBySelector:(SEL)commandSelector { return result; } +- (void)controlTextDidBeginEditing:(NSNotification *)aNotification { + [messageTimer_ invalidate]; + [self setCountdownMessage:@""]; +} + +- (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer { + remainingDialogTime_ -= 1; + + NSString *countdownMessage; + NSString *formatString; + + int displayedTimeLeft; // This can be either minutes or seconds. + + if (remainingDialogTime_ > 59) { + // calculate minutes remaining for UI purposes + displayedTimeLeft = (remainingDialogTime_ / 60); + + if (displayedTimeLeft == 1) { + formatString = NSLocalizedString(@"countdownMsgMinuteSingular", @""); + } else { + formatString = NSLocalizedString(@"countdownMsgMinutesPlural", @""); + } + } else { + displayedTimeLeft = remainingDialogTime_; + if (remainingDialogTime_ == 1) { + formatString = NSLocalizedString(@"countdownMsgSecondSingular", @""); + } else { + formatString = NSLocalizedString(@"countdownMsgSecondsPlural", @""); + } + } + countdownMessage = [NSString stringWithFormat:formatString, + displayedTimeLeft]; + if (remainingDialogTime_ <= 30) { + [countdownLabel_ setTextColor:[NSColor redColor]]; + } + [self setCountdownMessage:countdownMessage]; + if (remainingDialogTime_ <= 0) { + [messageTimer_ invalidate]; + [NSApp stopModal]; + } +} + + + #pragma mark Accessors #pragma mark - //============================================================================= @@ -702,6 +763,17 @@ doCommandBySelector:(SEL)commandSelector { } } +- (NSString *)countdownMessage { + return [[countdownMessage_ retain] autorelease]; +} + +- (void)setCountdownMessage:(NSString *)value { + if (countdownMessage_ != value) { + [countdownMessage_ release]; + countdownMessage_ = [value copy]; + } +} + #pragma mark - //============================================================================= - (BOOL)reportIntervalElapsed { @@ -730,6 +802,11 @@ doCommandBySelector:(SEL)commandSelector { return YES; } +- (BOOL)isOnDemand { + return [[parameters_ objectForKey:@BREAKPAD_ON_DEMAND] + isEqualToString:@"YES"]; +} + - (BOOL)shouldSubmitSilently { return [[parameters_ objectForKey:@BREAKPAD_SKIP_CONFIRM] isEqualToString:@"YES"]; @@ -745,21 +822,40 @@ doCommandBySelector:(SEL)commandSelector { isEqualToString:@"YES"]; } -- (NSString*)shortCrashDialogMessage { +- (NSString*)shortDialogMessage { NSString *displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; if (![displayName length]) displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT]; - return [NSString stringWithFormat:NSLocalizedString(@"headerFmt", @""), - displayName]; + if ([self isOnDemand]) { + return [NSString + stringWithFormat:NSLocalizedString(@"noCrashDialogHeader", @""), + displayName]; + } else { + return [NSString + stringWithFormat:NSLocalizedString(@"crashDialogHeader", @""), + displayName]; + } } -- (NSString*)explanatoryCrashDialogText { +- (NSString*)explanatoryDialogText { + NSString *displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; + if (![displayName length]) + displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT]; + NSString *vendor = [parameters_ objectForKey:@BREAKPAD_VENDOR]; if (![vendor length]) vendor = @"unknown vendor"; - return [NSString stringWithFormat:NSLocalizedString(@"msgFmt", @""), vendor]; + if ([self isOnDemand]) { + return [NSString + stringWithFormat:NSLocalizedString(@"noCrashDialogMsg", @""), + vendor, displayName]; + } else { + return [NSString + stringWithFormat:NSLocalizedString(@"crashDialogMsg", @""), + vendor]; + } } - (NSTimeInterval)messageTimeout { @@ -940,6 +1036,77 @@ doCommandBySelector:(SEL)commandSelector { [super dealloc]; } +- (void)awakeFromNib { + [emailEntryField_ setMaximumLength:kEmailMaxLength]; + [commentsEntryField_ setMaximumLength:kUserCommentsMaxLength]; +} + +@end + +//============================================================================= +@implementation LengthLimitingTextField + +- (void) setMaximumLength:(unsigned int)maxLength { + maximumLength_ = maxLength; +} + +// This is the method we're overriding in NSTextField, which lets us +// limit the user's input if it makes the string too long. +- (BOOL) textView:(NSTextView *)textView +shouldChangeTextInRange:(NSRange)affectedCharRange + replacementString:(NSString *)replacementString { + + // Sometimes the range comes in invalid, so reject if we can't + // figure out if the replacement text is too long. + if (affectedCharRange.location == NSNotFound) { + return NO; + } + // Figure out what the new string length would be, taking into + // account user selections. + int newStringLength = + [[textView string] length] - affectedCharRange.length + + [replacementString length]; + if (newStringLength > maximumLength_) { + return NO; + } else { + return YES; + } +} + +// Cut, copy, and paste have to be caught specifically since there is no menu. +- (BOOL)performKeyEquivalent:(NSEvent*)event { + // Only handle the key equivalent if |self| is the text field with focus. + NSText* fieldEditor = [self currentEditor]; + if (fieldEditor != nil) { + // Check for a single "Command" modifier + unsigned int modifiers = [event modifierFlags]; + modifiers &= NSDeviceIndependentModifierFlagsMask; + if (modifiers == NSCommandKeyMask) { + // Now, check for Select All, Cut, Copy, or Paste key equivalents. + NSString* characters = [event characters]; + // Select All is Command-A. + if ([characters isEqualToString:@"a"]) { + [fieldEditor selectAll:self]; + return YES; + // Cut is Command-X. + } else if ([characters isEqualToString:@"x"]) { + [fieldEditor cut:self]; + return YES; + // Copy is Command-C. + } else if ([characters isEqualToString:@"c"]) { + [fieldEditor copy:self]; + return YES; + // Paste is Command-V. + } else if ([characters isEqualToString:@"v"]) { + [fieldEditor paste:self]; + return YES; + } + } + } + // Let the super class handle the rest (e.g. Command-Period will cancel). + return [super performKeyEquivalent:event]; +} + @end //============================================================================= diff --git a/src/client/mac/sender/goArrow.png b/src/client/mac/sender/goArrow.png new file mode 100644 index 00000000..f318a567 Binary files /dev/null and b/src/client/mac/sender/goArrow.png differ diff --git a/src/client/mac/tests/BreakpadFramework_Test.mm b/src/client/mac/tests/BreakpadFramework_Test.mm index cd163630..2ea103c6 100644 --- a/src/client/mac/tests/BreakpadFramework_Test.mm +++ b/src/client/mac/tests/BreakpadFramework_Test.mm @@ -209,8 +209,9 @@ const mach_port_t kNoLastExceptionThread = MACH_PORT_NULL; @"Last exception type is not 0 for on demand"); STAssertEquals(last_exception_code_, 0, @"Last exception code is not 0 for on demand"); - STAssertEquals(last_exception_thread_, (mach_port_t)0, - @"Last exception thread is not 0 for on demand"); + STAssertEquals(last_exception_thread_, mach_thread_self(), + @"Last exception thread is not mach_thread_self() " + "for on demand"); } @end -- cgit v1.2.1