1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/hung_renderer_controller.h"
7 #import <Cocoa/Cocoa.h>
9 #include "base/mac/bundle_locations.h"
10 #include "base/process/process.h"
11 #include "base/strings/sys_string_conversions.h"
12 #import "chrome/browser/ui/cocoa/multi_key_equivalent_button.h"
13 #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
14 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
15 #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
16 #include "chrome/common/logging_chrome.h"
17 #include "content/public/browser/render_process_host.h"
18 #include "content/public/browser/render_view_host.h"
19 #include "content/public/browser/web_contents.h"
20 #include "content/public/common/result_codes.h"
21 #include "grit/theme_resources.h"
22 #include "skia/ext/skia_utils_mac.h"
23 #include "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
25 #include "ui/base/resource/resource_bundle.h"
26 #include "ui/gfx/image/image.h"
28 using content::WebContents;
31 // We only support showing one of these at a time per app. The
32 // controller owns itself and is released when its window is closed.
33 HungRendererController* g_instance = NULL;
36 class HungRendererWebContentsObserverBridge
37 : public content::WebContentsObserver {
39 HungRendererWebContentsObserverBridge(WebContents* web_contents,
40 HungRendererController* controller)
41 : content::WebContentsObserver(web_contents),
42 controller_(controller) {
46 // WebContentsObserver overrides:
47 void RenderProcessGone(base::TerminationStatus status) override {
48 [controller_ renderProcessGone];
50 void WebContentsDestroyed() override { [controller_ renderProcessGone]; }
53 HungRendererController* controller_; // weak
55 DISALLOW_COPY_AND_ASSIGN(HungRendererWebContentsObserverBridge);
58 @implementation HungRendererController
60 - (id)initWithWindowNibName:(NSString*)nibName {
61 NSString* nibpath = [base::mac::FrameworkBundle() pathForResource:nibName
63 self = [super initWithWindowNibPath:nibpath owner:self];
65 [tableView_ setDataSource:self];
72 [tableView_ setDataSource:nil];
73 [tableView_ setDelegate:nil];
74 [killButton_ setTarget:nil];
75 [waitButton_ setTarget:nil];
79 - (void)awakeFromNib {
81 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
82 NSImage* backgroundImage =
83 rb.GetNativeImageNamed(IDR_FROZEN_TAB_ICON).ToNSImage();
84 [imageView_ setImage:backgroundImage];
86 // Make the message fit.
87 CGFloat messageShift =
88 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:messageView_];
90 // Move the graphic up to be top even with the message.
91 NSRect graphicFrame = [imageView_ frame];
92 graphicFrame.origin.y += messageShift;
93 [imageView_ setFrame:graphicFrame];
95 // Make the window taller to fit everything.
96 NSSize windowDelta = NSMakeSize(0, messageShift);
97 [GTMUILocalizerAndLayoutTweaker
98 resizeWindowWithoutAutoResizingSubViews:[self window]
101 // Make the "wait" button respond to additional keys. By setting this to
102 // @"\e", it will respond to both Esc and Command-. (period).
103 KeyEquivalentAndModifierMask key;
104 key.charCode = @"\e";
105 [waitButton_ addKeyEquivalent:key];
108 + (void)showForWebContents:(content::WebContents*)contents {
109 if (!logging::DialogsAreSuppressed()) {
111 g_instance = [[HungRendererController alloc]
112 initWithWindowNibName:@"HungRendererDialog"];
113 [g_instance showForWebContents:contents];
117 + (void)endForWebContents:(content::WebContents*)contents {
118 if (!logging::DialogsAreSuppressed() && g_instance)
119 [g_instance endForWebContents:contents];
122 - (IBAction)kill:(id)sender {
124 base::Process process = base::Process::DeprecatedGetProcessFromHandle(
125 hungContents_->GetRenderProcessHost()->GetHandle());
126 process.Terminate(content::RESULT_CODE_HUNG, false);
128 // Cannot call performClose:, because the close button is disabled.
132 - (IBAction)wait:(id)sender {
133 if (hungContents_ && hungContents_->GetRenderViewHost())
134 hungContents_->GetRenderViewHost()->RestartHangMonitorTimeout();
135 // Cannot call performClose:, because the close button is disabled.
139 - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView {
140 return [hungTitles_ count];
143 - (id)tableView:(NSTableView*)aTableView
144 objectValueForTableColumn:(NSTableColumn*)column
145 row:(NSInteger)rowIndex {
146 return [NSNumber numberWithInt:NSOffState];
149 - (NSCell*)tableView:(NSTableView*)tableView
150 dataCellForTableColumn:(NSTableColumn*)tableColumn
151 row:(NSInteger)rowIndex {
152 NSCell* cell = [tableColumn dataCellForRow:rowIndex];
154 if ([[tableColumn identifier] isEqualToString:@"title"]) {
155 DCHECK([cell isKindOfClass:[NSButtonCell class]]);
156 NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
157 [buttonCell setTitle:[hungTitles_ objectAtIndex:rowIndex]];
158 [buttonCell setImage:[hungFavicons_ objectAtIndex:rowIndex]];
159 [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button.
160 [buttonCell setHighlightsBy:NSNoCellMask];
165 - (void)windowWillClose:(NSNotification*)notification {
166 // We have to reset g_instance before autoreleasing the window,
167 // because we want to avoid reusing the same dialog if someone calls
168 // chrome::ShowHungRendererDialog() between the autorelease call and the
172 // Prevent kills from happening after close if the user had the
173 // button depressed just when new activity was detected.
174 hungContents_ = NULL;
179 // TODO(shess): This could observe all of the tabs referenced in the
180 // loop, updating the dialog and keeping it up so long as any remain.
181 // Tabs closed by their renderer will close the dialog (that's
182 // activity!), so it would not add much value. Also, the views
183 // implementation only monitors the initiating tab.
184 - (void)showForWebContents:(WebContents*)contents {
186 hungContents_ = contents;
187 hungContentsObserver_.reset(
188 new HungRendererWebContentsObserverBridge(contents, self));
189 base::scoped_nsobject<NSMutableArray> titles([[NSMutableArray alloc] init]);
190 base::scoped_nsobject<NSMutableArray> favicons([[NSMutableArray alloc] init]);
191 for (TabContentsIterator it; !it.done(); it.Next()) {
192 if (it->GetRenderProcessHost() == hungContents_->GetRenderProcessHost()) {
193 base::string16 title = it->GetTitle();
195 title = CoreTabHelper::GetDefaultTitle();
196 [titles addObject:base::SysUTF16ToNSString(title)];
197 [favicons addObject:mac::FaviconForWebContents(*it)];
200 hungTitles_.reset([titles copy]);
201 hungFavicons_.reset([favicons copy]);
202 [tableView_ reloadData];
204 [[self window] center];
205 [self showWindow:self];
208 - (void)endForWebContents:(WebContents*)contents {
210 DCHECK(hungContents_);
211 if (hungContents_ && hungContents_->GetRenderProcessHost() ==
212 contents->GetRenderProcessHost()) {
213 // Cannot call performClose:, because the close button is disabled.
218 - (void)renderProcessGone {
219 // Cannot call performClose:, because the close button is disabled.
225 @implementation HungRendererController (JustForTesting)
226 - (NSButton*)killButton {
230 - (MultiKeyEquivalentButton*)waitButton {