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