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/toolbar/reload_button.h" 6 7 #include "chrome/app/chrome_command_ids.h" 8 #import "chrome/browser/ui/cocoa/view_id_util.h" 9 #include "grit/generated_resources.h" 10 #include "grit/theme_resources.h" 11 #include "ui/base/l10n/l10n_util.h" 12 #include "ui/base/l10n/l10n_util_mac.h" 13 14 namespace { 15 16 // Constant matches Windows. 17 NSTimeInterval kPendingReloadTimeout = 1.35; 18 19 } // namespace 20 21 @interface ReloadButton () 22 - (void)invalidatePendingReloadTimer; 23 - (void)forceReloadState:(NSTimer *)timer; 24 @end 25 26 @implementation ReloadButton 27 28 + (Class)cellClass { 29 return [ImageButtonCell class]; 30 } 31 32 - (id)initWithFrame:(NSRect)frameRect { 33 if ((self = [super initWithFrame:frameRect])) { 34 // Since this is not a custom view, -awakeFromNib won't be called twice. 35 [self awakeFromNib]; 36 } 37 return self; 38 } 39 40 - (void)viewWillMoveToWindow:(NSWindow *)newWindow { 41 // If this view is moved to a new window, reset its state. 42 [self setIsLoading:NO force:YES]; 43 [super viewWillMoveToWindow:newWindow]; 44 } 45 46 - (void)awakeFromNib { 47 // Don't allow multi-clicks, because the user probably wouldn't ever 48 // want to stop+reload or reload+stop. 49 [self setIgnoresMultiClick:YES]; 50 } 51 52 - (void)invalidatePendingReloadTimer { 53 [pendingReloadTimer_ invalidate]; 54 pendingReloadTimer_ = nil; 55 } 56 57 - (void)updateTag:(NSInteger)anInt { 58 if ([self tag] == anInt) 59 return; 60 61 // Forcibly remove any stale tooltip which is being displayed. 62 [self removeAllToolTips]; 63 id cell = [self cell]; 64 [self setTag:anInt]; 65 if (anInt == IDC_RELOAD) { 66 [cell setImageID:IDR_RELOAD 67 forButtonState:image_button_cell::kDefaultState]; 68 [cell setImageID:IDR_RELOAD_H 69 forButtonState:image_button_cell::kHoverState]; 70 [cell setImageID:IDR_RELOAD_P 71 forButtonState:image_button_cell::kPressedState]; 72 // The stop button has a disabled image but the reload button doesn't. To 73 // unset it we have to explicilty change the image ID to 0. 74 [cell setImageID:0 75 forButtonState:image_button_cell::kDisabledState]; 76 [self setToolTip:l10n_util::GetNSStringWithFixup(IDS_TOOLTIP_RELOAD)]; 77 } else if (anInt == IDC_STOP) { 78 [cell setImageID:IDR_STOP 79 forButtonState:image_button_cell::kDefaultState]; 80 [cell setImageID:IDR_STOP_H 81 forButtonState:image_button_cell::kHoverState]; 82 [cell setImageID:IDR_STOP_P 83 forButtonState:image_button_cell::kPressedState]; 84 [cell setImageID:IDR_STOP_D 85 forButtonState:image_button_cell::kDisabledState]; 86 [self setToolTip:l10n_util::GetNSStringWithFixup(IDS_TOOLTIP_STOP)]; 87 } else { 88 NOTREACHED(); 89 } 90 } 91 92 - (id)accessibilityAttributeValue:(NSString *)attribute { 93 if ([attribute isEqualToString:NSAccessibilityEnabledAttribute] && 94 pendingReloadTimer_) { 95 return [NSNumber numberWithBool:NO]; 96 } else { 97 return [super accessibilityAttributeValue:attribute]; 98 } 99 } 100 101 - (void)setIsLoading:(BOOL)isLoading force:(BOOL)force { 102 // Can always transition to stop mode. Only transition to reload 103 // mode if forced or if the mouse isn't hovering. Otherwise, note 104 // that reload mode is desired and disable the button. 105 if (isLoading) { 106 [self invalidatePendingReloadTimer]; 107 [self updateTag:IDC_STOP]; 108 } else if (force) { 109 [self invalidatePendingReloadTimer]; 110 [self updateTag:IDC_RELOAD]; 111 } else if ([self tag] == IDC_STOP && 112 !pendingReloadTimer_ && 113 [[self cell] isMouseInside]) { 114 id cell = [self cell]; 115 [cell setImageID:IDR_STOP_D 116 forButtonState:image_button_cell::kDefaultState]; 117 [cell setImageID:IDR_STOP_D 118 forButtonState:image_button_cell::kDisabledState]; 119 [cell setImageID:IDR_STOP_D 120 forButtonState:image_button_cell::kHoverState]; 121 [cell setImageID:IDR_STOP_D 122 forButtonState:image_button_cell::kPressedState]; 123 pendingReloadTimer_ = 124 [NSTimer timerWithTimeInterval:kPendingReloadTimeout 125 target:self 126 selector:@selector(forceReloadState:) 127 userInfo:nil 128 repeats:NO]; 129 // Must add the timer to |NSRunLoopCommonModes| because 130 // it should run in |NSEventTrackingRunLoopMode| as well as 131 // |NSDefaultRunLoopMode|. 132 [[NSRunLoop currentRunLoop] addTimer:pendingReloadTimer_ 133 forMode:NSRunLoopCommonModes]; 134 } else { 135 [self invalidatePendingReloadTimer]; 136 [self updateTag:IDC_RELOAD]; 137 } 138 [self setEnabled:pendingReloadTimer_ == nil]; 139 } 140 141 - (void)forceReloadState:(NSTimer *)timer { 142 DCHECK_EQ(timer, pendingReloadTimer_); 143 [self setIsLoading:NO force:YES]; 144 // Verify that |pendingReloadTimer_| is nil so it is not left dangling. 145 DCHECK(!pendingReloadTimer_); 146 } 147 148 - (BOOL)sendAction:(SEL)theAction to:(id)theTarget { 149 if ([self tag] == IDC_STOP) { 150 if (pendingReloadTimer_) { 151 // If |pendingReloadTimer_| then the control is currently being 152 // drawn in a disabled state, so just return. The control is NOT actually 153 // disabled, otherwise mousetracking (courtesy of the NSButtonCell) 154 // would not work. 155 return YES; 156 } else { 157 // When the stop is processed, immediately change to reload mode, 158 // even though the IPC still has to bounce off the renderer and 159 // back before the regular |-setIsLoaded:force:| will be called. 160 // [This is how views and gtk do it.] 161 BOOL ret = [super sendAction:theAction to:theTarget]; 162 if (ret) 163 [self forceReloadState:pendingReloadTimer_]; 164 return ret; 165 } 166 } 167 168 return [super sendAction:theAction to:theTarget]; 169 } 170 171 - (ViewID)viewID { 172 return VIEW_ID_RELOAD_BUTTON; 173 } 174 175 - (void)mouseInsideStateDidChange:(BOOL)isInside { 176 [pendingReloadTimer_ fire]; 177 } 178 179 @end // ReloadButton 180 181 @implementation ReloadButton (Testing) 182 183 + (void)setPendingReloadTimeout:(NSTimeInterval)seconds { 184 kPendingReloadTimeout = seconds; 185 } 186 187 @end 188