OLD | NEW |
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #import "chrome/browser/cocoa/clear_browsing_data_controller.h" | 5 #import "chrome/browser/cocoa/clear_browsing_data_controller.h" |
6 | 6 |
7 #include "app/l10n_util.h" | 7 #include "app/l10n_util.h" |
8 #include "base/command_line.h" | |
9 #include "base/mac_util.h" | 8 #include "base/mac_util.h" |
10 #include "base/scoped_nsobject.h" | 9 #include "base/scoped_nsobject.h" |
11 #include "base/singleton.h" | 10 #include "base/singleton.h" |
12 #include "chrome/browser/browser.h" | 11 #include "chrome/browser/browser.h" |
13 #include "chrome/browser/browser_window.h" | 12 #include "chrome/browser/browser_window.h" |
14 #include "chrome/browser/browsing_data_remover.h" | 13 #include "chrome/browser/browsing_data_remover.h" |
15 #include "chrome/browser/platform_util.h" | |
16 #include "chrome/browser/prefs/pref_service.h" | 14 #include "chrome/browser/prefs/pref_service.h" |
17 #include "chrome/browser/profile.h" | 15 #include "chrome/browser/profile.h" |
18 #include "chrome/browser/sync/profile_sync_service.h" | |
19 #include "chrome/common/chrome_switches.h" | |
20 #include "chrome/common/pref_names.h" | 16 #include "chrome/common/pref_names.h" |
21 #include "grit/generated_resources.h" | |
22 #include "grit/locale_settings.h" | 17 #include "grit/locale_settings.h" |
23 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" | 18 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
24 | 19 |
25 NSString* const kClearBrowsingDataControllerDidDelete = | 20 NSString* const kClearBrowsingDataControllerDidDelete = |
26 @"kClearBrowsingDataControllerDidDelete"; | 21 @"kClearBrowsingDataControllerDidDelete"; |
27 NSString* const kClearBrowsingDataControllerRemoveMask = | 22 NSString* const kClearBrowsingDataControllerRemoveMask = |
28 @"kClearBrowsingDataControllerRemoveMask"; | 23 @"kClearBrowsingDataControllerRemoveMask"; |
29 | 24 |
30 namespace { | |
31 | |
32 // Compare function for -[NSArray sortedArrayUsingFunction:context:] that | |
33 // sorts the views in Y order top down. | |
34 NSInteger CompareFrameY(id view1, id view2, void* context) { | |
35 CGFloat y1 = NSMinY([view1 frame]); | |
36 CGFloat y2 = NSMinY([view2 frame]); | |
37 if (y1 < y2) | |
38 return NSOrderedDescending; | |
39 else if (y1 > y2) | |
40 return NSOrderedAscending; | |
41 else | |
42 return NSOrderedSame; | |
43 } | |
44 | |
45 typedef std::map<Profile*, ClearBrowsingDataController*> ProfileControllerMap; | |
46 | |
47 } // namespace | |
48 | |
49 @interface ClearBrowsingDataController(Private) | 25 @interface ClearBrowsingDataController(Private) |
50 - (void)initFromPrefs; | 26 - (void)initFromPrefs; |
51 - (void)persistToPrefs; | 27 - (void)persistToPrefs; |
52 - (void)dataRemoverDidFinish; | 28 - (void)dataRemoverDidFinish; |
53 - (void)syncStateChanged; | |
54 @end | 29 @end |
55 | 30 |
56 class ClearBrowsingObserver : public BrowsingDataRemover::Observer, | 31 class ClearBrowsingObserver : public BrowsingDataRemover::Observer { |
57 public ProfileSyncServiceObserver { | |
58 public: | 32 public: |
59 ClearBrowsingObserver(ClearBrowsingDataController* controller) | 33 ClearBrowsingObserver(ClearBrowsingDataController* controller) |
60 : controller_(controller) { } | 34 : controller_(controller) { } |
61 void OnBrowsingDataRemoverDone() { [controller_ dataRemoverDidFinish]; } | 35 void OnBrowsingDataRemoverDone() { [controller_ dataRemoverDidFinish]; } |
62 void OnStateChanged() { [controller_ syncStateChanged]; } | |
63 private: | 36 private: |
64 ClearBrowsingDataController* controller_; | 37 ClearBrowsingDataController* controller_; |
65 }; | 38 }; |
66 | 39 |
| 40 namespace { |
| 41 |
| 42 typedef std::map<Profile*, ClearBrowsingDataController*> ProfileControllerMap; |
| 43 |
| 44 } // namespace |
| 45 |
67 @implementation ClearBrowsingDataController | 46 @implementation ClearBrowsingDataController |
68 | 47 |
69 @synthesize clearBrowsingHistory = clearBrowsingHistory_; | 48 @synthesize clearBrowsingHistory = clearBrowsingHistory_; |
70 @synthesize clearDownloadHistory = clearDownloadHistory_; | 49 @synthesize clearDownloadHistory = clearDownloadHistory_; |
71 @synthesize emptyCache = emptyCache_; | 50 @synthesize emptyCache = emptyCache_; |
72 @synthesize deleteCookies = deleteCookies_; | 51 @synthesize deleteCookies = deleteCookies_; |
73 @synthesize clearSavedPasswords = clearSavedPasswords_; | 52 @synthesize clearSavedPasswords = clearSavedPasswords_; |
74 @synthesize clearFormData = clearFormData_; | 53 @synthesize clearFormData = clearFormData_; |
75 @synthesize timePeriod = timePeriod_; | 54 @synthesize timePeriod = timePeriod_; |
76 @synthesize isClearing = isClearing_; | 55 @synthesize isClearing = isClearing_; |
77 @synthesize clearingStatus = clearingStatus_; | |
78 | 56 |
79 + (void)showClearBrowsingDialogForProfile:(Profile*)profile { | 57 + (void)showClearBrowsingDialogForProfile:(Profile*)profile { |
80 ClearBrowsingDataController* controller = | 58 ClearBrowsingDataController* controller = |
81 [ClearBrowsingDataController controllerForProfile:profile]; | 59 [ClearBrowsingDataController controllerForProfile:profile]; |
82 if (![controller isWindowLoaded]) { | 60 if (![controller isWindowLoaded]) { |
83 // This function needs to return instead of blocking, to match the Windows | 61 // This function needs to return instead of blocking, to match the windows |
84 // version. It caused problems when launching the dialog from the | 62 // api call. It caused problems when launching the dialog from the |
85 // DomUI history page. See bug and code review for more details. | 63 // DomUI history page. See bug and code review for more details. |
86 // http://crbug.com/37976 | 64 // http://crbug.com/37976 |
87 [controller performSelector:@selector(runModalDialog) | 65 [controller performSelector:@selector(runModalDialog) |
88 withObject:nil | 66 withObject:nil |
89 afterDelay:0]; | 67 afterDelay:0]; |
90 } | 68 } |
91 } | 69 } |
92 | 70 |
93 + (ClearBrowsingDataController *)controllerForProfile:(Profile*)profile { | 71 + (ClearBrowsingDataController *)controllerForProfile:(Profile*)profile { |
94 // Get the original profile in case we get here from an incognito window | 72 // Get the original profile in case we get here from an incognito window |
95 // |GetOriginalProfile()| will return the same profile if it is the original | 73 // |GetOriginalProfile()| will return the same profile if it is the original |
(...skipping 20 matching lines...) Expand all Loading... |
116 - (id)initWithProfile:(Profile*)profile { | 94 - (id)initWithProfile:(Profile*)profile { |
117 DCHECK(profile); | 95 DCHECK(profile); |
118 // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we | 96 // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we |
119 // can override it in a unit test. | 97 // can override it in a unit test. |
120 NSString *nibpath = [mac_util::MainAppBundle() | 98 NSString *nibpath = [mac_util::MainAppBundle() |
121 pathForResource:@"ClearBrowsingData" | 99 pathForResource:@"ClearBrowsingData" |
122 ofType:@"nib"]; | 100 ofType:@"nib"]; |
123 if ((self = [super initWithWindowNibPath:nibpath owner:self])) { | 101 if ((self = [super initWithWindowNibPath:nibpath owner:self])) { |
124 profile_ = profile; | 102 profile_ = profile; |
125 observer_.reset(new ClearBrowsingObserver(self)); | 103 observer_.reset(new ClearBrowsingObserver(self)); |
126 profile_->GetProfileSyncService()->ResetClearServerDataState(); | |
127 profile_->GetProfileSyncService()->AddObserver(observer_.get()); | |
128 [self initFromPrefs]; | 104 [self initFromPrefs]; |
129 } | 105 } |
130 return self; | 106 return self; |
131 } | 107 } |
132 | 108 |
133 - (void)dealloc { | 109 - (void)dealloc { |
134 if (remover_) { | 110 if (remover_) { |
135 // We were destroyed while clearing history was in progress. This can only | 111 // We were destroyed while clearing history was in progress. This can only |
136 // occur during automated tests (normally the user can't close the dialog | 112 // occur during automated tests (normally the user can't close the dialog |
137 // while clearing is in progress as the dialog is modal and not closeable). | 113 // while clearing is in progress as the dialog is modal and not closeable). |
138 remover_->RemoveObserver(observer_.get()); | 114 remover_->RemoveObserver(observer_.get()); |
139 } | 115 } |
140 profile_->GetProfileSyncService()->RemoveObserver(observer_.get()); | |
141 [self setClearingStatus:nil]; | |
142 | 116 |
143 [super dealloc]; | 117 [super dealloc]; |
144 } | 118 } |
145 | 119 |
146 // Run application modal. | 120 // Run application modal. |
147 - (void)runModalDialog { | 121 - (void)runModalDialog { |
148 // Check again to make sure there is only one window. Since we use | 122 // Check again to make sure there is only one window. Since we use |
149 // |performSelector:afterDelay:| it is possible for this to somehow be | 123 // |performSelector:afterDelay:| it is possible for this to somehow be |
150 // triggered twice. | 124 // triggered twice. |
151 DCHECK([NSThread isMainThread]); | 125 DCHECK([NSThread isMainThread]); |
152 if (![self isWindowLoaded]) { | 126 if (![self isWindowLoaded]) { |
153 // It takes two passes to adjust the window size. The first pass is to | 127 // The Window size in the nib is a min size, loop over the views collecting |
154 // determine the width of all the non-wrappable items. The window is then | 128 // the max they grew by, that is how much the window needs to be widened by. |
155 // resized to that width. Once the width is fixed, the heights of the | 129 CGFloat maxWidthGrowth = 0.0; |
156 // variable-height items can then be calculated, the items can be spaced, | |
157 // and the window resized (again) if necessary. | |
158 | |
159 NSWindow* window = [self window]; | 130 NSWindow* window = [self window]; |
160 | 131 NSView* contentView = [window contentView]; |
161 // Adjust the widths of non-wrappable items. | |
162 CGFloat maxWidthGrowth = 0.0; | |
163 Class widthBasedTweakerClass = [GTMWidthBasedTweaker class]; | 132 Class widthBasedTweakerClass = [GTMWidthBasedTweaker class]; |
164 for (NSTabViewItem* tabViewItem in [tabView_ tabViewItems]) | 133 for (id subView in [contentView subviews]) { |
165 for (NSView* subView in [[tabViewItem view] subviews]) | 134 if ([subView isKindOfClass:widthBasedTweakerClass]) { |
166 if ([subView isKindOfClass:widthBasedTweakerClass]) { | 135 GTMWidthBasedTweaker* tweaker = subView; |
167 GTMWidthBasedTweaker* tweaker = (GTMWidthBasedTweaker*)subView; | 136 CGFloat delta = [tweaker changedWidth]; |
168 CGFloat delta = [tweaker changedWidth]; | 137 maxWidthGrowth = std::max(maxWidthGrowth, delta); |
169 maxWidthGrowth = std::max(maxWidthGrowth, delta); | 138 } |
170 } | 139 } |
171 | |
172 // Adjust the width of the window. | |
173 if (maxWidthGrowth > 0.0) { | 140 if (maxWidthGrowth > 0.0) { |
174 NSSize adjustSize = NSMakeSize(maxWidthGrowth, 0); | 141 NSRect rect = [contentView convertRect:[window frame] fromView:nil]; |
175 adjustSize = [[window contentView] convertSize:adjustSize toView:nil]; | 142 rect.size.width += maxWidthGrowth; |
176 NSRect windowFrame = [window frame]; | 143 rect = [contentView convertRect:rect toView:nil]; |
177 windowFrame.size.width += adjustSize.width; | 144 [window setFrame:rect display:NO]; |
178 [window setFrame:windowFrame display:NO]; | 145 // For some reason the content view is resizing, but some times not |
| 146 // adjusting its origin, so correct it manually. |
| 147 [contentView setFrameOrigin:NSZeroPoint]; |
179 } | 148 } |
180 | |
181 // Adjust the heights and locations of the items on the "Other data" tab. | |
182 CGFloat cumulativeHeightGrowth = 0.0; | |
183 NSArray* subViews = | |
184 [[otherDataTab_ subviews] sortedArrayUsingFunction:CompareFrameY | |
185 context:NULL]; | |
186 for (NSView* view in subViews) { | |
187 if ([view isHidden]) | |
188 continue; | |
189 | |
190 if ([objectsToVerticallySize_ containsObject:view]) { | |
191 DCHECK([view isKindOfClass:[NSTextField class]]); | |
192 CGFloat viewHeightGrowth = [GTMUILocalizerAndLayoutTweaker | |
193 sizeToFitFixedWidthTextField:(NSTextField*)view]; | |
194 if (viewHeightGrowth > 0.0) | |
195 cumulativeHeightGrowth += viewHeightGrowth; | |
196 } | |
197 | |
198 NSRect viewFrame = [view frame]; | |
199 viewFrame.origin.y -= cumulativeHeightGrowth; | |
200 [view setFrame:viewFrame]; | |
201 } | |
202 | |
203 // Adjust the height of the window. | |
204 if (cumulativeHeightGrowth > 0.0) { | |
205 NSSize adjustSize = NSMakeSize(0, cumulativeHeightGrowth); | |
206 adjustSize = [[window contentView] convertSize:adjustSize toView:nil]; | |
207 NSRect windowFrame = [window frame]; | |
208 windowFrame.size.height += adjustSize.height; | |
209 [window setFrame:windowFrame display:NO]; | |
210 } | |
211 | |
212 // Now start the modal loop. | 149 // Now start the modal loop. |
213 [NSApp runModalForWindow:window]; | 150 [NSApp runModalForWindow:window]; |
214 } | 151 } |
215 } | 152 } |
216 | 153 |
217 - (int)removeMask { | 154 - (int)removeMask { |
218 int removeMask = 0L; | 155 int removeMask = 0L; |
219 if (clearBrowsingHistory_) | 156 if (clearBrowsingHistory_) |
220 removeMask |= BrowsingDataRemover::REMOVE_HISTORY; | 157 removeMask |= BrowsingDataRemover::REMOVE_HISTORY; |
221 if (clearDownloadHistory_) | 158 if (clearDownloadHistory_) |
222 removeMask |= BrowsingDataRemover::REMOVE_DOWNLOADS; | 159 removeMask |= BrowsingDataRemover::REMOVE_DOWNLOADS; |
223 if (emptyCache_) | 160 if (emptyCache_) |
224 removeMask |= BrowsingDataRemover::REMOVE_CACHE; | 161 removeMask |= BrowsingDataRemover::REMOVE_CACHE; |
225 if (deleteCookies_) | 162 if (deleteCookies_) |
226 removeMask |= BrowsingDataRemover::REMOVE_COOKIES; | 163 removeMask |= BrowsingDataRemover::REMOVE_COOKIES; |
227 if (clearSavedPasswords_) | 164 if (clearSavedPasswords_) |
228 removeMask |= BrowsingDataRemover::REMOVE_PASSWORDS; | 165 removeMask |= BrowsingDataRemover::REMOVE_PASSWORDS; |
229 if (clearFormData_) | 166 if (clearFormData_) |
230 removeMask |= BrowsingDataRemover::REMOVE_FORM_DATA; | 167 removeMask |= BrowsingDataRemover::REMOVE_FORM_DATA; |
231 return removeMask; | 168 return removeMask; |
232 } | 169 } |
233 | 170 |
234 // Called when the user clicks the "clear" button. Do the work and persist | 171 // Called when the user clicks the "clear" button. Do the work and persist |
235 // the prefs for next time. We don't stop the modal session until we get | 172 // the prefs for next time. We don't stop the modal session until we get |
236 // the callback from the BrowsingDataRemover so the window stays on the screen. | 173 // the callback from the BrowsingDataRemover so the window stays on the screen. |
237 // While we're working, dim the buttons so the user can't click them. | 174 // While we're working, dim the buttons so the user can't click them. |
238 - (IBAction)clearData:(id)sender { | 175 - (IBAction)clearData:(id)sender { |
239 // Set that we're working so that the buttons disable. | 176 // Set that we're working so that the buttons disable. |
240 [self setClearingStatus:l10n_util::GetNSString(IDS_CLEAR_DATA_DELETING)]; | |
241 [self setIsClearing:YES]; | 177 [self setIsClearing:YES]; |
242 | 178 |
243 [self persistToPrefs]; | 179 [self persistToPrefs]; |
244 | 180 |
245 // BrowsingDataRemover deletes itself when done. | 181 // BrowsingDataRemover deletes itself when done. |
246 remover_ = new BrowsingDataRemover(profile_, | 182 remover_ = new BrowsingDataRemover(profile_, |
247 static_cast<BrowsingDataRemover::TimePeriod>(timePeriod_), | 183 static_cast<BrowsingDataRemover::TimePeriod>(timePeriod_), |
248 base::Time()); | 184 base::Time()); |
249 remover_->AddObserver(observer_.get()); | 185 remover_->AddObserver(observer_.get()); |
250 remover_->Remove([self removeMask]); | 186 remover_->Remove([self removeMask]); |
(...skipping 10 matching lines...) Expand all Loading... |
261 // The "Clear Data" dialog is app-modal on OS X. Hence, close it before | 197 // The "Clear Data" dialog is app-modal on OS X. Hence, close it before |
262 // opening a tab with flash settings. | 198 // opening a tab with flash settings. |
263 [self closeDialog]; | 199 [self closeDialog]; |
264 | 200 |
265 Browser* browser = Browser::Create(profile_); | 201 Browser* browser = Browser::Create(profile_); |
266 browser->OpenURL(GURL(l10n_util::GetStringUTF8(IDS_FLASH_STORAGE_URL)), | 202 browser->OpenURL(GURL(l10n_util::GetStringUTF8(IDS_FLASH_STORAGE_URL)), |
267 GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK); | 203 GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK); |
268 browser->window()->Show(); | 204 browser->window()->Show(); |
269 } | 205 } |
270 | 206 |
271 - (IBAction)openGoogleDashboard:(id)sender { | |
272 // The "Clear Data" dialog is app-modal on OS X. Hence, close it before | |
273 // opening a tab with the dashboard. | |
274 [self closeDialog]; | |
275 | |
276 Browser* browser = Browser::Create(profile_); | |
277 browser->OpenURL(GURL(l10n_util::GetStringUTF8(IDS_PRIVACY_DASHBOARD_URL)), | |
278 GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK); | |
279 browser->window()->Show(); | |
280 } | |
281 | |
282 - (void)closeDialog { | 207 - (void)closeDialog { |
283 ProfileControllerMap* map = Singleton<ProfileControllerMap>::get(); | 208 ProfileControllerMap* map = Singleton<ProfileControllerMap>::get(); |
284 ProfileControllerMap::iterator it = map->find(profile_); | 209 ProfileControllerMap::iterator it = map->find(profile_); |
285 if (it != map->end()) { | 210 if (it != map->end()) { |
286 map->erase(it); | 211 map->erase(it); |
287 } | 212 } |
288 [self autorelease]; | 213 [self autorelease]; |
289 [[self window] orderOut:self]; | 214 [[self window] orderOut:self]; |
290 [NSApp stopModal]; | 215 [NSApp stopModal]; |
291 } | 216 } |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
324 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; | 249 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
325 int removeMask = [self removeMask]; | 250 int removeMask = [self removeMask]; |
326 NSDictionary* userInfo = | 251 NSDictionary* userInfo = |
327 [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:removeMask] | 252 [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:removeMask] |
328 forKey:kClearBrowsingDataControllerRemoveMask]; | 253 forKey:kClearBrowsingDataControllerRemoveMask]; |
329 [center postNotificationName:kClearBrowsingDataControllerDidDelete | 254 [center postNotificationName:kClearBrowsingDataControllerDidDelete |
330 object:self | 255 object:self |
331 userInfo:userInfo]; | 256 userInfo:userInfo]; |
332 | 257 |
333 [self closeDialog]; | 258 [self closeDialog]; |
334 [self setClearingStatus:nil]; | 259 [[self window] orderOut:self]; |
335 [self setIsClearing:NO]; | 260 [self setIsClearing:NO]; |
336 remover_ = NULL; | 261 remover_ = NULL; |
337 } | 262 } |
338 | 263 |
339 - (IBAction)stopSyncAndDeleteData:(id)sender { | |
340 // Protect against the unlikely case where the server received a message, and | |
341 // the syncer syncs and resets itself before the user tries pressing the Clear | |
342 // button in this dialog again. TODO(raz) Confirm whether we have an issue | |
343 // here | |
344 if (profile_->GetProfileSyncService()->HasSyncSetupCompleted()) { | |
345 bool clear = platform_util::SimpleYesNoBox( | |
346 nil, | |
347 l10n_util::GetStringUTF16(IDS_CONFIRM_CLEAR_TITLE), | |
348 l10n_util::GetStringUTF16(IDS_CONFIRM_CLEAR_DESCRIPTION)); | |
349 if (clear) { | |
350 profile_->GetProfileSyncService()->ClearServerData(); | |
351 [self syncStateChanged]; | |
352 } | |
353 } | |
354 } | |
355 | |
356 - (void)syncStateChanged { | |
357 bool deleteInProgress = false; | |
358 | |
359 ProfileSyncService::ClearServerDataState clearState = | |
360 profile_->GetProfileSyncService()->GetClearServerDataState(); | |
361 profile_->GetProfileSyncService()->ResetClearServerDataState(); | |
362 | |
363 switch (clearState) { | |
364 case ProfileSyncService::CLEAR_NOT_STARTED: | |
365 // This can occur on a first start and after a failed clear (which does | |
366 // not close the tab). Do nothing. | |
367 break; | |
368 case ProfileSyncService::CLEAR_CLEARING: | |
369 // Clearing buttons on all tabs are disabled at this point, throbber is | |
370 // going. | |
371 [self setClearingStatus:l10n_util::GetNSString(IDS_CLEAR_DATA_SENDING)]; | |
372 deleteInProgress = true; | |
373 break; | |
374 case ProfileSyncService::CLEAR_FAILED: | |
375 // Show an error and reallow clearing. | |
376 [self setClearingStatus:l10n_util::GetNSString(IDS_CLEAR_DATA_ERROR)]; | |
377 deleteInProgress = false; | |
378 break; | |
379 case ProfileSyncService::CLEAR_SUCCEEDED: | |
380 // Close the dialog box, success! | |
381 [self setClearingStatus:nil]; | |
382 deleteInProgress = false; | |
383 [self closeDialog]; | |
384 break; | |
385 } | |
386 | |
387 [self setIsClearing:deleteInProgress]; | |
388 } | |
389 | |
390 - (BOOL)isSyncEnabled { | |
391 BOOL allowClearServerDataUI = | |
392 CommandLine::ForCurrentProcess()->HasSwitch( | |
393 switches::kEnableClearServerData); | |
394 | |
395 return allowClearServerDataUI && | |
396 profile_->GetProfileSyncService()->HasSyncSetupCompleted(); | |
397 } | |
398 | |
399 - (NSFont*)labelFont { | |
400 return [NSFont boldSystemFontOfSize:13]; | |
401 } | |
402 | |
403 @end | 264 @end |
OLD | NEW |