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.
4
5#import "chrome/browser/ui/cocoa/hung_renderer_controller.h"
6
7#import <Cocoa/Cocoa.h>
8
9#include "base/mac/bundle_locations.h"
10#include "base/mac/mac_util.h"
11#include "base/strings/sys_string_conversions.h"
12#include "chrome/browser/favicon/favicon_tab_helper.h"
13#include "chrome/browser/ui/browser_dialogs.h"
14#import "chrome/browser/ui/cocoa/multi_key_equivalent_button.h"
15#import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
16#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
17#include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
18#include "chrome/common/logging_chrome.h"
19#include "content/public/browser/render_process_host.h"
20#include "content/public/browser/render_view_host.h"
21#include "content/public/browser/web_contents.h"
22#include "content/public/common/result_codes.h"
23#include "grit/theme_resources.h"
24#include "skia/ext/skia_utils_mac.h"
25#include "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
26#include "ui/base/l10n/l10n_util_mac.h"
27#include "ui/base/resource/resource_bundle.h"
28#include "ui/gfx/image/image.h"
29
30using content::WebContents;
31
32namespace {
33// We only support showing one of these at a time per app.  The
34// controller owns itself and is released when its window is closed.
35HungRendererController* g_instance = NULL;
36}  // namespace
37
38class HungRendererWebContentsObserverBridge
39    : public content::WebContentsObserver {
40 public:
41  HungRendererWebContentsObserverBridge(WebContents* web_contents,
42                                        HungRendererController* controller)
43    : content::WebContentsObserver(web_contents),
44      controller_(controller) {
45  }
46
47 protected:
48  // WebContentsObserver overrides:
49  virtual void RenderProcessGone(base::TerminationStatus status) OVERRIDE {
50    [controller_ renderProcessGone];
51  }
52  virtual void WebContentsDestroyed() OVERRIDE {
53    [controller_ renderProcessGone];
54  }
55
56 private:
57  HungRendererController* controller_;  // weak
58
59  DISALLOW_COPY_AND_ASSIGN(HungRendererWebContentsObserverBridge);
60};
61
62@implementation HungRendererController
63
64- (id)initWithWindowNibName:(NSString*)nibName {
65  NSString* nibpath = [base::mac::FrameworkBundle() pathForResource:nibName
66                                                             ofType:@"nib"];
67  self = [super initWithWindowNibPath:nibpath owner:self];
68  if (self) {
69    [tableView_ setDataSource:self];
70  }
71  return self;
72}
73
74- (void)dealloc {
75  DCHECK(!g_instance);
76  [tableView_ setDataSource:nil];
77  [tableView_ setDelegate:nil];
78  [killButton_ setTarget:nil];
79  [waitButton_ setTarget:nil];
80  [super dealloc];
81}
82
83- (void)awakeFromNib {
84  // Load in the image
85  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
86  NSImage* backgroundImage =
87      rb.GetNativeImageNamed(IDR_FROZEN_TAB_ICON).ToNSImage();
88  [imageView_ setImage:backgroundImage];
89
90  // Make the message fit.
91  CGFloat messageShift =
92    [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:messageView_];
93
94  // Move the graphic up to be top even with the message.
95  NSRect graphicFrame = [imageView_ frame];
96  graphicFrame.origin.y += messageShift;
97  [imageView_ setFrame:graphicFrame];
98
99  // Make the window taller to fit everything.
100  NSSize windowDelta = NSMakeSize(0, messageShift);
101  [GTMUILocalizerAndLayoutTweaker
102      resizeWindowWithoutAutoResizingSubViews:[self window]
103                                        delta:windowDelta];
104
105  // Make the "wait" button respond to additional keys.  By setting this to
106  // @"\e", it will respond to both Esc and Command-. (period).
107  KeyEquivalentAndModifierMask key;
108  key.charCode = @"\e";
109  [waitButton_ addKeyEquivalent:key];
110}
111
112- (IBAction)kill:(id)sender {
113  if (hungContents_)
114    base::KillProcess(hungContents_->GetRenderProcessHost()->GetHandle(),
115                      content::RESULT_CODE_HUNG, false);
116  // Cannot call performClose:, because the close button is disabled.
117  [self close];
118}
119
120- (IBAction)wait:(id)sender {
121  if (hungContents_ && hungContents_->GetRenderViewHost())
122    hungContents_->GetRenderViewHost()->RestartHangMonitorTimeout();
123  // Cannot call performClose:, because the close button is disabled.
124  [self close];
125}
126
127- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView {
128  return [hungTitles_ count];
129}
130
131- (id)tableView:(NSTableView*)aTableView
132      objectValueForTableColumn:(NSTableColumn*)column
133            row:(NSInteger)rowIndex {
134  return [NSNumber numberWithInt:NSOffState];
135}
136
137- (NSCell*)tableView:(NSTableView*)tableView
138    dataCellForTableColumn:(NSTableColumn*)tableColumn
139                       row:(NSInteger)rowIndex {
140  NSCell* cell = [tableColumn dataCellForRow:rowIndex];
141
142  if ([[tableColumn identifier] isEqualToString:@"title"]) {
143    DCHECK([cell isKindOfClass:[NSButtonCell class]]);
144    NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
145    [buttonCell setTitle:[hungTitles_ objectAtIndex:rowIndex]];
146    [buttonCell setImage:[hungFavicons_ objectAtIndex:rowIndex]];
147    [buttonCell setRefusesFirstResponder:YES];  // Don't push in like a button.
148    [buttonCell setHighlightsBy:NSNoCellMask];
149  }
150  return cell;
151}
152
153- (void)windowWillClose:(NSNotification*)notification {
154  // We have to reset g_instance before autoreleasing the window,
155  // because we want to avoid reusing the same dialog if someone calls
156  // chrome::ShowHungRendererDialog() between the autorelease call and the
157  // actual dealloc.
158  g_instance = nil;
159
160  // Prevent kills from happening after close if the user had the
161  // button depressed just when new activity was detected.
162  hungContents_ = NULL;
163
164  [self autorelease];
165}
166
167// TODO(shess): This could observe all of the tabs referenced in the
168// loop, updating the dialog and keeping it up so long as any remain.
169// Tabs closed by their renderer will close the dialog (that's
170// activity!), so it would not add much value.  Also, the views
171// implementation only monitors the initiating tab.
172- (void)showForWebContents:(WebContents*)contents {
173  DCHECK(contents);
174  hungContents_ = contents;
175  hungContentsObserver_.reset(
176      new HungRendererWebContentsObserverBridge(contents, self));
177  base::scoped_nsobject<NSMutableArray> titles([[NSMutableArray alloc] init]);
178  base::scoped_nsobject<NSMutableArray> favicons([[NSMutableArray alloc] init]);
179  for (TabContentsIterator it; !it.done(); it.Next()) {
180    if (it->GetRenderProcessHost() == hungContents_->GetRenderProcessHost()) {
181      base::string16 title = it->GetTitle();
182      if (title.empty())
183        title = CoreTabHelper::GetDefaultTitle();
184      [titles addObject:base::SysUTF16ToNSString(title)];
185      [favicons addObject:mac::FaviconForWebContents(*it)];
186    }
187  }
188  hungTitles_.reset([titles copy]);
189  hungFavicons_.reset([favicons copy]);
190  [tableView_ reloadData];
191
192  [[self window] center];
193  [self showWindow:self];
194}
195
196- (void)endForWebContents:(WebContents*)contents {
197  DCHECK(contents);
198  DCHECK(hungContents_);
199  if (hungContents_ && hungContents_->GetRenderProcessHost() ==
200      contents->GetRenderProcessHost()) {
201    // Cannot call performClose:, because the close button is disabled.
202    [self close];
203  }
204}
205
206- (void)renderProcessGone {
207  // Cannot call performClose:, because the close button is disabled.
208  [self close];
209}
210
211@end
212
213@implementation HungRendererController (JustForTesting)
214- (NSButton*)killButton {
215  return killButton_;
216}
217
218- (MultiKeyEquivalentButton*)waitButton {
219  return waitButton_;
220}
221@end
222
223namespace chrome {
224
225void ShowHungRendererDialog(WebContents* contents) {
226  if (!logging::DialogsAreSuppressed()) {
227    if (!g_instance)
228      g_instance = [[HungRendererController alloc]
229                     initWithWindowNibName:@"HungRendererDialog"];
230    [g_instance showForWebContents:contents];
231  }
232}
233
234void HideHungRendererDialog(WebContents* contents) {
235  if (!logging::DialogsAreSuppressed() && g_instance)
236    [g_instance endForWebContents:contents];
237}
238
239}  // namespace chrome
240