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/system/date/date_view.h"
6
7#include "ash/shell.h"
8#include "ash/system/tray/system_tray_delegate.h"
9#include "ash/system/tray/tray_constants.h"
10#include "ash/system/tray/tray_utils.h"
11#include "base/i18n/rtl.h"
12#include "base/i18n/time_formatting.h"
13#include "base/strings/utf_string_conversions.h"
14#include "base/time/time.h"
15#include "grit/ash_strings.h"
16#include "third_party/icu/source/i18n/unicode/datefmt.h"
17#include "third_party/icu/source/i18n/unicode/dtptngen.h"
18#include "third_party/icu/source/i18n/unicode/smpdtfmt.h"
19#include "ui/base/l10n/l10n_util.h"
20#include "ui/views/border.h"
21#include "ui/views/controls/label.h"
22#include "ui/views/layout/box_layout.h"
23#include "ui/views/layout/grid_layout.h"
24#include "ui/views/widget/widget.h"
25
26namespace ash {
27namespace tray {
28namespace {
29
30// Amount of slop to add into the timer to make sure we're into the next minute
31// when the timer goes off.
32const int kTimerSlopSeconds = 1;
33
34// Text color of the vertical clock minutes.
35const SkColor kVerticalClockMinuteColor = SkColorSetRGB(0xBA, 0xBA, 0xBA);
36
37// Padding between the left edge of the shelf and the left edge of the vertical
38// clock.
39const int kVerticalClockLeftPadding = 9;
40
41// Offset used to bring the minutes line closer to the hours line in the
42// vertical clock.
43const int kVerticalClockMinutesTopOffset = -4;
44
45base::string16 FormatDate(const base::Time& time) {
46  icu::UnicodeString date_string;
47  scoped_ptr<icu::DateFormat> formatter(
48      icu::DateFormat::createDateInstance(icu::DateFormat::kMedium));
49  formatter->format(static_cast<UDate>(time.ToDoubleT() * 1000), date_string);
50  return base::string16(date_string.getBuffer(),
51                  static_cast<size_t>(date_string.length()));
52}
53
54base::string16 FormatDayOfWeek(const base::Time& time) {
55  UErrorCode status = U_ZERO_ERROR;
56  scoped_ptr<icu::DateTimePatternGenerator> generator(
57      icu::DateTimePatternGenerator::createInstance(status));
58  DCHECK(U_SUCCESS(status));
59  const char kBasePattern[] = "EEE";
60  icu::UnicodeString generated_pattern =
61      generator->getBestPattern(icu::UnicodeString(kBasePattern), status);
62  DCHECK(U_SUCCESS(status));
63  icu::SimpleDateFormat simple_formatter(generated_pattern, status);
64  DCHECK(U_SUCCESS(status));
65  icu::UnicodeString date_string;
66  simple_formatter.format(
67      static_cast<UDate>(time.ToDoubleT() * 1000), date_string, status);
68  DCHECK(U_SUCCESS(status));
69  return base::string16(
70      date_string.getBuffer(), static_cast<size_t>(date_string.length()));
71}
72
73views::Label* CreateLabel() {
74  views::Label* label = new views::Label;
75  label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
76  label->SetBackgroundColor(SkColorSetARGB(0, 255, 255, 255));
77  return label;
78}
79
80}  // namespace
81
82BaseDateTimeView::~BaseDateTimeView() {
83  timer_.Stop();
84}
85
86void BaseDateTimeView::UpdateText() {
87  base::Time now = base::Time::Now();
88  UpdateTextInternal(now);
89  SchedulePaint();
90  SetTimer(now);
91}
92
93BaseDateTimeView::BaseDateTimeView() {
94  SetTimer(base::Time::Now());
95}
96
97void BaseDateTimeView::SetTimer(const base::Time& now) {
98  // Try to set the timer to go off at the next change of the minute. We don't
99  // want to have the timer go off more than necessary since that will cause
100  // the CPU to wake up and consume power.
101  base::Time::Exploded exploded;
102  now.LocalExplode(&exploded);
103
104  // Often this will be called at minute boundaries, and we'll actually want
105  // 60 seconds from now.
106  int seconds_left = 60 - exploded.second;
107  if (seconds_left == 0)
108    seconds_left = 60;
109
110  // Make sure that the timer fires on the next minute. Without this, if it is
111  // called just a teeny bit early, then it will skip the next minute.
112  seconds_left += kTimerSlopSeconds;
113
114  timer_.Stop();
115  timer_.Start(
116      FROM_HERE, base::TimeDelta::FromSeconds(seconds_left),
117      this, &BaseDateTimeView::UpdateText);
118}
119
120void BaseDateTimeView::ChildPreferredSizeChanged(views::View* child) {
121  PreferredSizeChanged();
122}
123
124void BaseDateTimeView::OnLocaleChanged() {
125  UpdateText();
126}
127
128DateView::DateView()
129    : hour_type_(ash::Shell::GetInstance()->system_tray_delegate()->
130                 GetHourClockType()),
131      action_(TrayDate::NONE) {
132  SetLayoutManager(
133      new views::BoxLayout(
134          views::BoxLayout::kVertical, 0, 0, 0));
135  date_label_ = CreateLabel();
136  date_label_->SetEnabledColor(kHeaderTextColorNormal);
137  UpdateTextInternal(base::Time::Now());
138  AddChildView(date_label_);
139  SetFocusable(false);
140}
141
142DateView::~DateView() {
143}
144
145void DateView::SetAction(TrayDate::DateAction action) {
146  if (action == action_)
147    return;
148  if (IsMouseHovered()) {
149    date_label_->SetEnabledColor(
150        action == TrayDate::NONE ? kHeaderTextColorNormal :
151                                   kHeaderTextColorHover);
152    SchedulePaint();
153  }
154  action_ = action;
155  SetFocusable(action_ != TrayDate::NONE);
156}
157
158void DateView::UpdateTimeFormat() {
159  hour_type_ =
160      ash::Shell::GetInstance()->system_tray_delegate()->GetHourClockType();
161  UpdateText();
162}
163
164base::HourClockType DateView::GetHourTypeForTesting() const {
165  return hour_type_;
166}
167
168void DateView::UpdateTextInternal(const base::Time& now) {
169  SetAccessibleName(
170      base::TimeFormatFriendlyDate(now) +
171      base::ASCIIToUTF16(", ") +
172      base::TimeFormatTimeOfDayWithHourClockType(
173          now, hour_type_, base::kKeepAmPm));
174  date_label_->SetText(
175      l10n_util::GetStringFUTF16(
176          IDS_ASH_STATUS_TRAY_DATE, FormatDayOfWeek(now), FormatDate(now)));
177}
178
179bool DateView::PerformAction(const ui::Event& event) {
180  if (action_ == TrayDate::NONE)
181    return false;
182  if (action_ == TrayDate::SHOW_DATE_SETTINGS)
183    ash::Shell::GetInstance()->system_tray_delegate()->ShowDateSettings();
184  else if (action_ == TrayDate::SET_SYSTEM_TIME)
185    ash::Shell::GetInstance()->system_tray_delegate()->ShowSetTimeDialog();
186  return true;
187}
188
189void DateView::OnMouseEntered(const ui::MouseEvent& event) {
190  if (action_ == TrayDate::NONE)
191    return;
192  date_label_->SetEnabledColor(kHeaderTextColorHover);
193  SchedulePaint();
194}
195
196void DateView::OnMouseExited(const ui::MouseEvent& event) {
197  if (action_ == TrayDate::NONE)
198    return;
199  date_label_->SetEnabledColor(kHeaderTextColorNormal);
200  SchedulePaint();
201}
202
203///////////////////////////////////////////////////////////////////////////////
204
205TimeView::TimeView(TrayDate::ClockLayout clock_layout)
206    : hour_type_(ash::Shell::GetInstance()->system_tray_delegate()->
207                 GetHourClockType()) {
208  SetupLabels();
209  UpdateTextInternal(base::Time::Now());
210  UpdateClockLayout(clock_layout);
211  SetFocusable(false);
212}
213
214TimeView::~TimeView() {
215}
216
217void TimeView::UpdateTimeFormat() {
218  hour_type_ =
219      ash::Shell::GetInstance()->system_tray_delegate()->GetHourClockType();
220  UpdateText();
221}
222
223base::HourClockType TimeView::GetHourTypeForTesting() const {
224  return hour_type_;
225}
226
227void TimeView::UpdateTextInternal(const base::Time& now) {
228  // Just in case |now| is null, do NOT update time; otherwise, it will
229  // crash icu code by calling into base::TimeFormatTimeOfDayWithHourClockType,
230  // see details in crbug.com/147570.
231  if (now.is_null()) {
232    LOG(ERROR) << "Received null value from base::Time |now| in argument";
233    return;
234  }
235
236  base::string16 current_time = base::TimeFormatTimeOfDayWithHourClockType(
237      now, hour_type_, base::kDropAmPm);
238  horizontal_label_->SetText(current_time);
239  horizontal_label_->SetTooltipText(base::TimeFormatFriendlyDate(now));
240
241  // Calculate vertical clock layout labels.
242  size_t colon_pos = current_time.find(base::ASCIIToUTF16(":"));
243  base::string16 hour = current_time.substr(0, colon_pos);
244  base::string16 minute = current_time.substr(colon_pos + 1);
245
246  // Sometimes pad single-digit hours with a zero for aesthetic reasons.
247  if (hour.length() == 1 &&
248      hour_type_ == base::k24HourClock &&
249      !base::i18n::IsRTL())
250    hour = base::ASCIIToUTF16("0") + hour;
251
252  vertical_label_hours_->SetText(hour);
253  vertical_label_minutes_->SetText(minute);
254  Layout();
255}
256
257bool TimeView::PerformAction(const ui::Event& event) {
258  return false;
259}
260
261bool TimeView::OnMousePressed(const ui::MouseEvent& event) {
262  // Let the event fall through.
263  return false;
264}
265
266void TimeView::UpdateClockLayout(TrayDate::ClockLayout clock_layout){
267  SetBorderFromLayout(clock_layout);
268  if (clock_layout == TrayDate::HORIZONTAL_CLOCK) {
269    RemoveChildView(vertical_label_hours_.get());
270    RemoveChildView(vertical_label_minutes_.get());
271    SetLayoutManager(
272        new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 0));
273    AddChildView(horizontal_label_.get());
274  } else {
275    RemoveChildView(horizontal_label_.get());
276    views::GridLayout* layout = new views::GridLayout(this);
277    SetLayoutManager(layout);
278    const int kColumnId = 0;
279    views::ColumnSet* columns = layout->AddColumnSet(kColumnId);
280    columns->AddPaddingColumn(0, kVerticalClockLeftPadding);
281    columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::CENTER,
282                       0, views::GridLayout::USE_PREF, 0, 0);
283    layout->AddPaddingRow(0, kTrayLabelItemVerticalPaddingVerticalAlignment);
284    layout->StartRow(0, kColumnId);
285    layout->AddView(vertical_label_hours_.get());
286    layout->StartRow(0, kColumnId);
287    layout->AddView(vertical_label_minutes_.get());
288    layout->AddPaddingRow(0, kTrayLabelItemVerticalPaddingVerticalAlignment);
289  }
290  Layout();
291}
292
293void TimeView::SetBorderFromLayout(TrayDate::ClockLayout clock_layout) {
294  if (clock_layout == TrayDate::HORIZONTAL_CLOCK)
295    SetBorder(views::Border::CreateEmptyBorder(
296        0,
297        kTrayLabelItemHorizontalPaddingBottomAlignment,
298        0,
299        kTrayLabelItemHorizontalPaddingBottomAlignment));
300  else
301    SetBorder(views::Border::NullBorder());
302}
303
304void TimeView::SetupLabels() {
305  horizontal_label_.reset(CreateLabel());
306  SetupLabel(horizontal_label_.get());
307  vertical_label_hours_.reset(CreateLabel());
308  SetupLabel(vertical_label_hours_.get());
309  vertical_label_minutes_.reset(CreateLabel());
310  SetupLabel(vertical_label_minutes_.get());
311  vertical_label_minutes_->SetEnabledColor(kVerticalClockMinuteColor);
312  // Pull the minutes up closer to the hours by using a negative top border.
313  vertical_label_minutes_->SetBorder(views::Border::CreateEmptyBorder(
314      kVerticalClockMinutesTopOffset, 0, 0, 0));
315}
316
317void TimeView::SetupLabel(views::Label* label) {
318  label->set_owned_by_client();
319  SetupLabelForTray(label);
320  label->SetFontList(label->font_list().DeriveWithStyle(
321      label->font_list().GetFontStyle() & ~gfx::Font::BOLD));
322}
323
324}  // namespace tray
325}  // namespace ash
326