| OLD | NEW |
| (Empty) |
| 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 #include "ash/common/system/date/date_view.h" | |
| 6 | |
| 7 #include "ash/common/material_design/material_design_controller.h" | |
| 8 #include "ash/common/system/tray/system_tray_controller.h" | |
| 9 #include "ash/common/system/tray/tray_constants.h" | |
| 10 #include "ash/common/system/tray/tray_popup_item_style.h" | |
| 11 #include "ash/common/system/tray/tray_popup_utils.h" | |
| 12 #include "ash/common/system/tray/tray_utils.h" | |
| 13 #include "ash/common/wm_shell.h" | |
| 14 #include "ash/strings/grit/ash_strings.h" | |
| 15 #include "base/i18n/rtl.h" | |
| 16 #include "base/i18n/time_formatting.h" | |
| 17 #include "base/strings/utf_string_conversions.h" | |
| 18 #include "base/time/time.h" | |
| 19 #include "third_party/icu/source/i18n/unicode/datefmt.h" | |
| 20 #include "third_party/icu/source/i18n/unicode/dtptngen.h" | |
| 21 #include "third_party/icu/source/i18n/unicode/smpdtfmt.h" | |
| 22 #include "ui/accessibility/ax_node_data.h" | |
| 23 #include "ui/base/l10n/l10n_util.h" | |
| 24 #include "ui/views/border.h" | |
| 25 #include "ui/views/controls/button/button.h" | |
| 26 #include "ui/views/controls/label.h" | |
| 27 #include "ui/views/layout/box_layout.h" | |
| 28 #include "ui/views/layout/fill_layout.h" | |
| 29 #include "ui/views/layout/grid_layout.h" | |
| 30 #include "ui/views/widget/widget.h" | |
| 31 | |
| 32 namespace ash { | |
| 33 namespace tray { | |
| 34 namespace { | |
| 35 | |
| 36 // Amount of slop to add into the timer to make sure we're into the next minute | |
| 37 // when the timer goes off. | |
| 38 const int kTimerSlopSeconds = 1; | |
| 39 | |
| 40 // Text color of the vertical clock minutes. | |
| 41 const SkColor kVerticalClockMinuteColor = SkColorSetRGB(0xBA, 0xBA, 0xBA); | |
| 42 | |
| 43 // Padding between the left edge of the shelf and the left edge of the vertical | |
| 44 // clock. | |
| 45 const int kVerticalClockLeftPadding = 9; | |
| 46 | |
| 47 // Offset used to bring the minutes line closer to the hours line in the | |
| 48 // vertical clock. | |
| 49 const int kVerticalClockMinutesTopOffset = -4; | |
| 50 const int kVerticalClockMinutesTopOffsetMD = -2; | |
| 51 | |
| 52 // Leading padding used to draw the tray background to the left of the clock | |
| 53 // when the shelf is vertically aligned. | |
| 54 const int kClockLeadingPadding = 8; | |
| 55 | |
| 56 bool UseMd() { | |
| 57 return MaterialDesignController::IsSystemTrayMenuMaterial(); | |
| 58 } | |
| 59 | |
| 60 base::string16 FormatDateWithPattern(const base::Time& time, | |
| 61 const char* pattern) { | |
| 62 UErrorCode status = U_ZERO_ERROR; | |
| 63 std::unique_ptr<icu::DateTimePatternGenerator> generator( | |
| 64 icu::DateTimePatternGenerator::createInstance(status)); | |
| 65 DCHECK(U_SUCCESS(status)); | |
| 66 icu::UnicodeString generated_pattern = | |
| 67 generator->getBestPattern(icu::UnicodeString(pattern), status); | |
| 68 DCHECK(U_SUCCESS(status)); | |
| 69 icu::SimpleDateFormat simple_formatter(generated_pattern, status); | |
| 70 DCHECK(U_SUCCESS(status)); | |
| 71 icu::UnicodeString date_string; | |
| 72 simple_formatter.format(static_cast<UDate>(time.ToDoubleT() * 1000), | |
| 73 date_string, status); | |
| 74 DCHECK(U_SUCCESS(status)); | |
| 75 return base::string16(date_string.getBuffer(), | |
| 76 static_cast<size_t>(date_string.length())); | |
| 77 } | |
| 78 | |
| 79 base::string16 FormatDate(const base::Time& time) { | |
| 80 if (UseMd()) { | |
| 81 // Use 'short' month format (e.g., "Oct") followed by non-padded day of | |
| 82 // month (e.g., "2", "10"). | |
| 83 return FormatDateWithPattern(time, "LLLd"); | |
| 84 } else { | |
| 85 icu::UnicodeString date_string; | |
| 86 std::unique_ptr<icu::DateFormat> formatter( | |
| 87 icu::DateFormat::createDateInstance(icu::DateFormat::kMedium)); | |
| 88 formatter->format(static_cast<UDate>(time.ToDoubleT() * 1000), date_string); | |
| 89 return base::string16(date_string.getBuffer(), | |
| 90 static_cast<size_t>(date_string.length())); | |
| 91 } | |
| 92 } | |
| 93 | |
| 94 base::string16 FormatDayOfWeek(const base::Time& time) { | |
| 95 // Use 'short' day of week format (e.g., "Wed"). | |
| 96 return FormatDateWithPattern(time, "EEE"); | |
| 97 } | |
| 98 | |
| 99 } // namespace | |
| 100 | |
| 101 BaseDateTimeView::~BaseDateTimeView() { | |
| 102 timer_.Stop(); | |
| 103 } | |
| 104 | |
| 105 void BaseDateTimeView::UpdateText() { | |
| 106 base::Time now = base::Time::Now(); | |
| 107 UpdateTextInternal(now); | |
| 108 SchedulePaint(); | |
| 109 SetTimer(now); | |
| 110 } | |
| 111 | |
| 112 void BaseDateTimeView::GetAccessibleNodeData(ui::AXNodeData* node_data) { | |
| 113 ActionableView::GetAccessibleNodeData(node_data); | |
| 114 node_data->role = ui::AX_ROLE_TIME; | |
| 115 } | |
| 116 | |
| 117 BaseDateTimeView::BaseDateTimeView(SystemTrayItem* owner) | |
| 118 : ActionableView(owner, TrayPopupInkDropStyle::INSET_BOUNDS), | |
| 119 hour_type_(WmShell::Get()->system_tray_controller()->hour_clock_type()) { | |
| 120 SetTimer(base::Time::Now()); | |
| 121 SetFocusBehavior(FocusBehavior::NEVER); | |
| 122 } | |
| 123 | |
| 124 void BaseDateTimeView::SetTimer(const base::Time& now) { | |
| 125 // Try to set the timer to go off at the next change of the minute. We don't | |
| 126 // want to have the timer go off more than necessary since that will cause | |
| 127 // the CPU to wake up and consume power. | |
| 128 base::Time::Exploded exploded; | |
| 129 now.LocalExplode(&exploded); | |
| 130 | |
| 131 // Often this will be called at minute boundaries, and we'll actually want | |
| 132 // 60 seconds from now. | |
| 133 int seconds_left = 60 - exploded.second; | |
| 134 if (seconds_left == 0) | |
| 135 seconds_left = 60; | |
| 136 | |
| 137 // Make sure that the timer fires on the next minute. Without this, if it is | |
| 138 // called just a teeny bit early, then it will skip the next minute. | |
| 139 seconds_left += kTimerSlopSeconds; | |
| 140 | |
| 141 timer_.Stop(); | |
| 142 timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(seconds_left), this, | |
| 143 &BaseDateTimeView::UpdateText); | |
| 144 } | |
| 145 | |
| 146 void BaseDateTimeView::UpdateTextInternal(const base::Time& now) { | |
| 147 SetAccessibleName(base::TimeFormatTimeOfDayWithHourClockType( | |
| 148 now, hour_type_, base::kKeepAmPm) + | |
| 149 base::ASCIIToUTF16(", ") + | |
| 150 base::TimeFormatFriendlyDate(now)); | |
| 151 | |
| 152 NotifyAccessibilityEvent(ui::AX_EVENT_TEXT_CHANGED, true); | |
| 153 } | |
| 154 | |
| 155 void BaseDateTimeView::ChildPreferredSizeChanged(views::View* child) { | |
| 156 PreferredSizeChanged(); | |
| 157 } | |
| 158 | |
| 159 void BaseDateTimeView::OnLocaleChanged() { | |
| 160 UpdateText(); | |
| 161 } | |
| 162 | |
| 163 /////////////////////////////////////////////////////////////////////////////// | |
| 164 | |
| 165 DateView::DateView(SystemTrayItem* owner) | |
| 166 : BaseDateTimeView(owner), action_(DateAction::NONE) { | |
| 167 if (UseMd()) { | |
| 168 // TODO(tdanderson): Tweak spacing and layout for material design. | |
| 169 views::BoxLayout* box_layout = | |
| 170 new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 0); | |
| 171 box_layout->set_inside_border_insets(gfx::Insets(0, 12, 0, 0)); | |
| 172 box_layout->set_main_axis_alignment( | |
| 173 views::BoxLayout::MAIN_AXIS_ALIGNMENT_START); | |
| 174 box_layout->set_cross_axis_alignment( | |
| 175 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER); | |
| 176 SetLayoutManager(box_layout); | |
| 177 } else { | |
| 178 SetLayoutManager( | |
| 179 new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0)); | |
| 180 } | |
| 181 date_label_ = TrayPopupUtils::CreateDefaultLabel(); | |
| 182 date_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); | |
| 183 if (!UseMd()) | |
| 184 date_label_->SetEnabledColor(kHeaderTextColorNormal); | |
| 185 UpdateTextInternal(base::Time::Now()); | |
| 186 TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::SYSTEM_INFO); | |
| 187 style.SetupLabel(date_label_); | |
| 188 AddChildView(date_label_); | |
| 189 } | |
| 190 | |
| 191 DateView::~DateView() {} | |
| 192 | |
| 193 void DateView::SetAction(DateAction action) { | |
| 194 if (action == action_) | |
| 195 return; | |
| 196 if (IsMouseHovered() && !UseMd()) { | |
| 197 date_label_->SetEnabledColor(action == DateAction::NONE | |
| 198 ? kHeaderTextColorNormal | |
| 199 : kHeaderTextColorHover); | |
| 200 SchedulePaint(); | |
| 201 } | |
| 202 action_ = action; | |
| 203 SetFocusBehavior(action_ != DateAction::NONE ? FocusBehavior::ALWAYS | |
| 204 : FocusBehavior::NEVER); | |
| 205 | |
| 206 // Disable |this| when not clickable so that the material design ripple is | |
| 207 // not shown. | |
| 208 if (UseMd()) { | |
| 209 SetEnabled(action_ != DateAction::NONE); | |
| 210 if (action_ != DateAction::NONE) | |
| 211 SetInkDropMode(views::InkDropHostView::InkDropMode::ON); | |
| 212 } | |
| 213 } | |
| 214 | |
| 215 void DateView::UpdateTimeFormat() { | |
| 216 hour_type_ = WmShell::Get()->system_tray_controller()->hour_clock_type(); | |
| 217 UpdateText(); | |
| 218 } | |
| 219 | |
| 220 base::HourClockType DateView::GetHourTypeForTesting() const { | |
| 221 return hour_type_; | |
| 222 } | |
| 223 | |
| 224 void DateView::SetActive(bool active) { | |
| 225 if (UseMd()) | |
| 226 return; | |
| 227 | |
| 228 date_label_->SetEnabledColor(active ? kHeaderTextColorHover | |
| 229 : kHeaderTextColorNormal); | |
| 230 SchedulePaint(); | |
| 231 } | |
| 232 | |
| 233 void DateView::UpdateTextInternal(const base::Time& now) { | |
| 234 BaseDateTimeView::UpdateTextInternal(now); | |
| 235 date_label_->SetText(l10n_util::GetStringFUTF16( | |
| 236 IDS_ASH_STATUS_TRAY_DATE, FormatDayOfWeek(now), FormatDate(now))); | |
| 237 } | |
| 238 | |
| 239 bool DateView::PerformAction(const ui::Event& event) { | |
| 240 if (action_ == DateAction::NONE) | |
| 241 return false; | |
| 242 if (action_ == DateAction::SHOW_DATE_SETTINGS) | |
| 243 WmShell::Get()->system_tray_controller()->ShowDateSettings(); | |
| 244 else if (action_ == DateAction::SET_SYSTEM_TIME) | |
| 245 WmShell::Get()->system_tray_controller()->ShowSetTimeDialog(); | |
| 246 else | |
| 247 return false; | |
| 248 CloseSystemBubble(); | |
| 249 return true; | |
| 250 } | |
| 251 | |
| 252 void DateView::OnMouseEntered(const ui::MouseEvent& event) { | |
| 253 if (action_ == DateAction::NONE) | |
| 254 return; | |
| 255 SetActive(true); | |
| 256 } | |
| 257 | |
| 258 void DateView::OnMouseExited(const ui::MouseEvent& event) { | |
| 259 if (action_ == DateAction::NONE) | |
| 260 return; | |
| 261 SetActive(false); | |
| 262 } | |
| 263 | |
| 264 void DateView::OnGestureEvent(ui::GestureEvent* event) { | |
| 265 if (event->type() == ui::ET_GESTURE_TAP_DOWN) { | |
| 266 SetActive(true); | |
| 267 } else if (event->type() == ui::ET_GESTURE_TAP_CANCEL || | |
| 268 event->type() == ui::ET_GESTURE_END) { | |
| 269 SetActive(false); | |
| 270 } | |
| 271 BaseDateTimeView::OnGestureEvent(event); | |
| 272 } | |
| 273 | |
| 274 /////////////////////////////////////////////////////////////////////////////// | |
| 275 | |
| 276 TimeView::TimeView(ClockLayout clock_layout) : BaseDateTimeView(nullptr) { | |
| 277 SetupLabels(); | |
| 278 UpdateTextInternal(base::Time::Now()); | |
| 279 UpdateClockLayout(clock_layout); | |
| 280 } | |
| 281 | |
| 282 TimeView::~TimeView() {} | |
| 283 | |
| 284 void TimeView::UpdateTimeFormat() { | |
| 285 hour_type_ = WmShell::Get()->system_tray_controller()->hour_clock_type(); | |
| 286 UpdateText(); | |
| 287 } | |
| 288 | |
| 289 base::HourClockType TimeView::GetHourTypeForTesting() const { | |
| 290 return hour_type_; | |
| 291 } | |
| 292 | |
| 293 void TimeView::UpdateTextInternal(const base::Time& now) { | |
| 294 // Just in case |now| is null, do NOT update time; otherwise, it will | |
| 295 // crash icu code by calling into base::TimeFormatTimeOfDayWithHourClockType, | |
| 296 // see details in crbug.com/147570. | |
| 297 if (now.is_null()) { | |
| 298 LOG(ERROR) << "Received null value from base::Time |now| in argument"; | |
| 299 return; | |
| 300 } | |
| 301 | |
| 302 BaseDateTimeView::UpdateTextInternal(now); | |
| 303 base::string16 current_time = base::TimeFormatTimeOfDayWithHourClockType( | |
| 304 now, hour_type_, base::kDropAmPm); | |
| 305 horizontal_label_->SetText(current_time); | |
| 306 horizontal_label_->SetTooltipText(base::TimeFormatFriendlyDate(now)); | |
| 307 | |
| 308 // Calculate vertical clock layout labels. | |
| 309 size_t colon_pos = current_time.find(base::ASCIIToUTF16(":")); | |
| 310 base::string16 hour = current_time.substr(0, colon_pos); | |
| 311 base::string16 minute = current_time.substr(colon_pos + 1); | |
| 312 | |
| 313 // Sometimes pad single-digit hours with a zero for aesthetic reasons. | |
| 314 if (hour.length() == 1 && hour_type_ == base::k24HourClock && | |
| 315 !base::i18n::IsRTL()) | |
| 316 hour = base::ASCIIToUTF16("0") + hour; | |
| 317 | |
| 318 vertical_label_hours_->SetText(hour); | |
| 319 vertical_label_minutes_->SetText(minute); | |
| 320 Layout(); | |
| 321 } | |
| 322 | |
| 323 bool TimeView::PerformAction(const ui::Event& event) { | |
| 324 return false; | |
| 325 } | |
| 326 | |
| 327 bool TimeView::OnMousePressed(const ui::MouseEvent& event) { | |
| 328 // Let the event fall through. | |
| 329 return false; | |
| 330 } | |
| 331 | |
| 332 void TimeView::OnGestureEvent(ui::GestureEvent* event) { | |
| 333 // Skip gesture handling happening in CustomButton so that the container views | |
| 334 // receive and handle them properly. | |
| 335 // TODO(mohsen): Refactor TimeView/DateView classes so that they are not | |
| 336 // ActionableView anymore. Create an ActionableView as a container for when | |
| 337 // needed. | |
| 338 } | |
| 339 | |
| 340 void TimeView::UpdateClockLayout(ClockLayout clock_layout) { | |
| 341 SetBorderFromLayout(clock_layout); | |
| 342 if (clock_layout == ClockLayout::HORIZONTAL_CLOCK) { | |
| 343 RemoveChildView(vertical_label_hours_.get()); | |
| 344 RemoveChildView(vertical_label_minutes_.get()); | |
| 345 SetLayoutManager(new views::FillLayout()); | |
| 346 AddChildView(horizontal_label_.get()); | |
| 347 } else { | |
| 348 const bool is_material_design = MaterialDesignController::IsShelfMaterial(); | |
| 349 RemoveChildView(horizontal_label_.get()); | |
| 350 views::GridLayout* layout = new views::GridLayout(this); | |
| 351 SetLayoutManager(layout); | |
| 352 const int kColumnId = 0; | |
| 353 views::ColumnSet* columns = layout->AddColumnSet(kColumnId); | |
| 354 columns->AddPaddingColumn(0, kVerticalClockLeftPadding); | |
| 355 columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::CENTER, | |
| 356 0, views::GridLayout::USE_PREF, 0, 0); | |
| 357 layout->AddPaddingRow( | |
| 358 0, is_material_design ? kClockLeadingPadding | |
| 359 : kTrayLabelItemVerticalPaddingVerticalAlignment); | |
| 360 layout->StartRow(0, kColumnId); | |
| 361 layout->AddView(vertical_label_hours_.get()); | |
| 362 layout->StartRow(0, kColumnId); | |
| 363 layout->AddView(vertical_label_minutes_.get()); | |
| 364 layout->AddPaddingRow( | |
| 365 0, | |
| 366 is_material_design | |
| 367 ? kTrayImageItemPadding + kVerticalClockMinutesTopOffsetMD | |
| 368 : kTrayLabelItemVerticalPaddingVerticalAlignment); | |
| 369 } | |
| 370 Layout(); | |
| 371 } | |
| 372 | |
| 373 void TimeView::SetBorderFromLayout(ClockLayout clock_layout) { | |
| 374 if (clock_layout == ClockLayout::HORIZONTAL_CLOCK) { | |
| 375 SetBorder(views::CreateEmptyBorder( | |
| 376 gfx::Insets(0, | |
| 377 UseMd() ? kTrayImageItemPadding | |
| 378 : kTrayLabelItemHorizontalPaddingBottomAlignment))); | |
| 379 } else { | |
| 380 SetBorder(views::NullBorder()); | |
| 381 } | |
| 382 } | |
| 383 | |
| 384 void TimeView::SetupLabels() { | |
| 385 horizontal_label_.reset(new views::Label()); | |
| 386 SetupLabel(horizontal_label_.get()); | |
| 387 vertical_label_hours_.reset(new views::Label()); | |
| 388 SetupLabel(vertical_label_hours_.get()); | |
| 389 vertical_label_minutes_.reset(new views::Label()); | |
| 390 SetupLabel(vertical_label_minutes_.get()); | |
| 391 // TODO(estade): this should use the NativeTheme's secondary text color. | |
| 392 vertical_label_minutes_->SetEnabledColor(kVerticalClockMinuteColor); | |
| 393 // Pull the minutes up closer to the hours by using a negative top border. | |
| 394 vertical_label_minutes_->SetBorder( | |
| 395 views::CreateEmptyBorder(MaterialDesignController::IsShelfMaterial() | |
| 396 ? kVerticalClockMinutesTopOffsetMD | |
| 397 : kVerticalClockMinutesTopOffset, | |
| 398 0, 0, 0)); | |
| 399 } | |
| 400 | |
| 401 void TimeView::SetupLabel(views::Label* label) { | |
| 402 label->set_owned_by_client(); | |
| 403 SetupLabelForTray(label); | |
| 404 label->SetElideBehavior(gfx::NO_ELIDE); | |
| 405 } | |
| 406 | |
| 407 } // namespace tray | |
| 408 } // namespace ash | |
| OLD | NEW |