2 * Copyright 2004-2013 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
6 * Mike Berg, mike@berg-net.us
7 * Adrien Destugues, pulkomandy@pulkomandy.ath.cx
8 * Julun, host.haiku@gmx.de
9 * Hamish Morrison, hamish@lavabit.com
10 * Philippe Saint-Pierre, stpere@gmail.com
11 * John Scipione, jscipione@gmail.com
12 * Oliver Tappe, zooey@hirschkaefer.de
16 #include <unicode/uversion.h>
26 #include <AutoDeleter.h>
30 #include <ControlLook.h>
32 #include <Directory.h>
35 #include <FindDirectory.h>
38 #include <MutableLocaleRoster.h>
39 #include <OutlineListView.h>
41 #include <RadioButton.h>
42 #include <ScrollView.h>
43 #include <StorageDefs.h>
45 #include <StringView.h>
50 #include <unicode/datefmt.h>
51 #include <unicode/utmscale.h>
52 #include <ICUWrapper.h>
54 #include "TimeMessages.h"
55 #include "TimeZoneListItem.h"
56 #include "TimeZoneListView.h"
57 #include "TZDisplay.h"
60 #undef B_TRANSLATION_CONTEXT
61 #define B_TRANSLATION_CONTEXT "Time"
64 using BPrivate::MutableLocaleRoster
;
65 using BPrivate::ObjectDeleter
;
68 struct TimeZoneItemLess
{
69 bool operator()(const BString
& first
, const BString
& second
)
71 // sort anything starting with '<' behind anything else
72 if (first
.ByteAt(0) == '<') {
73 if (second
.ByteAt(0) != '<')
75 } else if (second
.ByteAt(0) == '<')
77 return fCollator
.Compare(first
.String(), second
.String()) < 0;
85 TimeZoneView::TimeZoneView(const char* name
)
87 BGroupView(name
, B_HORIZONTAL
, B_USE_DEFAULT_SPACING
),
90 fCurrentZoneItem(NULL
),
100 TimeZoneView::CheckCanRevert()
102 // check GMT vs Local setting
103 bool enable
= fUseGmtTime
!= fOldUseGmtTime
;
105 return enable
|| fCurrentZoneItem
!= fOldZoneItem
;
109 TimeZoneView::~TimeZoneView()
116 TimeZoneView::AttachedToWindow()
118 BView::AttachedToWindow();
120 SetViewColor(Parent()->ViewColor());
125 fSetZone
->SetTarget(this);
126 fZoneList
->SetTarget(this);
132 TimeZoneView::DoLayout()
135 if (fCurrentZoneItem
!= NULL
) {
136 fZoneList
->Select(fZoneList
->IndexOf(fCurrentZoneItem
));
137 fCurrent
->SetText(fCurrentZoneItem
->Text());
138 fZoneList
->ScrollToSelection();
144 TimeZoneView::MessageReceived(BMessage
* message
)
146 switch (message
->what
) {
147 case B_OBSERVER_NOTICE_CHANGE
:
150 message
->FindInt32(B_OBSERVE_WHAT_CHANGE
, &change
);
153 _UpdateDateTime(message
);
157 BView::MessageReceived(message
);
167 case H_SET_TIME_ZONE
:
169 _SetSystemTimeZone();
178 fUseGmtTime
= fGmtTime
->Value() == B_CONTROL_ON
;
179 _UpdateGmtSettings();
185 BGroupView::MessageReceived(message
);
192 TimeZoneView::_UpdateDateTime(BMessage
* message
)
194 // only need to update once every minute
196 if (message
->FindInt32("minute", &minute
) == B_OK
) {
197 if (fLastUpdateMinute
!= minute
) {
201 fLastUpdateMinute
= minute
;
208 TimeZoneView::_InitView()
210 fZoneList
= new TimeZoneListView();
211 fZoneList
->SetSelectionMessage(new BMessage(H_CITY_CHANGED
));
212 fZoneList
->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE
));
214 BScrollView
* scrollList
= new BScrollView("scrollList", fZoneList
,
215 B_FRAME_EVENTS
| B_WILL_DRAW
, false, true);
216 scrollList
->SetExplicitMinSize(BSize(200, 0));
218 fCurrent
= new TTZDisplay("currentTime", B_TRANSLATE("Current time:"));
219 fPreview
= new TTZDisplay("previewTime", B_TRANSLATE("Preview time:"));
221 fSetZone
= new BButton("setTimeZone", B_TRANSLATE("Set time zone"),
222 new BMessage(H_SET_TIME_ZONE
));
223 fSetZone
->SetEnabled(false);
224 fSetZone
->SetExplicitAlignment(
225 BAlignment(B_ALIGN_RIGHT
, B_ALIGN_BOTTOM
));
227 fLocalTime
= new BRadioButton("localTime",
228 B_TRANSLATE("Local time (Windows compatible)"),
229 new BMessage(kRTCUpdate
));
230 fGmtTime
= new BRadioButton("greenwichMeanTime",
231 B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate
));
234 fGmtTime
->SetValue(B_CONTROL_ON
);
236 fLocalTime
->SetValue(B_CONTROL_ON
);
237 _ShowOrHidePreview();
238 fOldUseGmtTime
= fUseGmtTime
;
240 const float kIndentSpacing
241 = be_control_look
->DefaultItemSpacing() * 2;
242 BLayoutBuilder::Group
<>(this)
244 .AddGroup(B_VERTICAL
, 0)
245 .Add(new BStringView("clockSetTo",
246 B_TRANSLATE("Hardware clock set to:")))
247 .AddGroup(B_VERTICAL
, 0)
250 .SetInsets(kIndentSpacing
, 0, 0, 0)
253 .AddGroup(B_VERTICAL
, B_USE_DEFAULT_SPACING
)
259 .SetInsets(B_USE_DEFAULT_SPACING
, B_USE_DEFAULT_SPACING
,
260 B_USE_DEFAULT_SPACING
, B_USE_DEFAULT_SPACING
);
265 TimeZoneView::_BuildZoneMenu()
267 BTimeZone defaultTimeZone
;
268 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone
);
271 BLocale::Default()->GetLanguage(&language
);
273 // Group timezones by regions, but filter out unwanted (duplicate) regions
274 // and add an additional region with generic GMT-offset timezones at the end
275 typedef std::map
<BString
, TimeZoneListItem
*, TimeZoneItemLess
> ZoneItemMap
;
276 ZoneItemMap zoneItemMap
;
277 const char* kOtherRegion
= B_TRANSLATE_MARK("<Other>");
278 const char* kSupportedRegions
[] = {
279 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"),
280 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"),
281 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"),
282 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"),
283 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"),
288 // Since the zone-map contains translated country-names (we get those from
289 // ICU), we need to use translated region names in the zone-map, too:
290 typedef std::map
<BString
, BString
> TranslatedRegionMap
;
291 TranslatedRegionMap regionMap
;
292 for (const char** region
= kSupportedRegions
; *region
!= NULL
; ++region
) {
293 BString translatedRegion
= B_TRANSLATE_NOCOLLECT(*region
);
294 regionMap
[*region
] = translatedRegion
;
296 TimeZoneListItem
* regionItem
297 = new TimeZoneListItem(translatedRegion
, NULL
, NULL
);
298 regionItem
->SetOutlineLevel(0);
299 zoneItemMap
[translatedRegion
] = regionItem
;
302 // Get all time zones
304 BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList
);
306 typedef std::map
<BString
, std::vector
<const char*> > ZonesByCountyMap
;
307 ZonesByCountyMap zonesByCountryMap
;
309 BString timeZoneCode
;
310 for (int tz
= 0; zoneList
.FindString("timeZone", tz
, &zoneID
) == B_OK
311 && zoneList
.FindString("region", tz
, &timeZoneCode
) == B_OK
; tz
++) {
312 // From the global ("001") timezones, we only accept the generic GMT
313 // timezones, as all the other world-zones are duplicates of others.
314 if (timeZoneCode
== "001" && strncmp(zoneID
, "Etc/GMT", 7) != 0)
316 zonesByCountryMap
[timeZoneCode
].push_back(zoneID
);
319 ZonesByCountyMap::const_iterator countryIter
= zonesByCountryMap
.begin();
320 for (; countryIter
!= zonesByCountryMap
.end(); ++countryIter
) {
321 const char* countryCode
= countryIter
->first
.String();
322 if (countryCode
== NULL
)
325 size_t zoneCountInCountry
= countryIter
->second
.size();
326 for (size_t tz
= 0; tz
< zoneCountInCountry
; tz
++) {
327 BString
zoneID(countryIter
->second
[tz
]);
329 = new(std::nothrow
) BTimeZone(zoneID
, &language
);
330 if (timeZone
== NULL
)
333 int32 slashPos
= zoneID
.FindFirst('/');
334 BString
region(zoneID
, slashPos
);
336 region
= kOtherRegion
;
338 // just accept timezones from our supported regions, others are
339 // aliases and would just make the list even longer
340 TranslatedRegionMap::iterator regionIter
= regionMap
.find(region
);
341 if (regionIter
== regionMap
.end())
344 BString fullCountryID
= regionIter
->second
;
345 BCountry
* country
= new(std::nothrow
) BCountry(countryCode
);
350 country
->GetName(countryName
);
351 bool hasUsedCountry
= false;
352 bool countryIsRegion
= countryName
== regionIter
->second
353 || region
== kOtherRegion
;
354 if (!countryIsRegion
)
355 fullCountryID
<< "/" << countryName
;
357 BString timeZoneName
;
358 BString fullZoneID
= fullCountryID
;
359 if (zoneCountInCountry
> 1) {
360 // we can't use the country name as timezone name, since there
361 // are more than one timezones in this country - fetch the
362 // localized name of the timezone and use that
363 timeZoneName
= timeZone
->Name();
364 int32 openParenthesisPos
= timeZoneName
.FindFirst('(');
365 if (openParenthesisPos
>= 0) {
366 timeZoneName
.Remove(0, openParenthesisPos
+ 1);
367 int32 closeParenthesisPos
= timeZoneName
.FindLast(')');
368 if (closeParenthesisPos
>= 0)
369 timeZoneName
.Truncate(closeParenthesisPos
);
371 fullZoneID
<< "/" << timeZoneName
;
373 timeZoneName
= countryName
;
374 fullZoneID
<< "/" << zoneID
;
378 ZoneItemMap::iterator zoneIter
= zoneItemMap
.find(fullZoneID
);
379 if (zoneIter
!= zoneItemMap
.end()) {
384 TimeZoneListItem
* countryItem
= NULL
;
385 TimeZoneListItem
* zoneItem
= NULL
;
386 if (zoneCountInCountry
> 1) {
387 ZoneItemMap::iterator countryIter
388 = zoneItemMap
.find(fullCountryID
);
389 if (countryIter
== zoneItemMap
.end()) {
390 countryItem
= new TimeZoneListItem(countryName
.String(),
392 countryItem
->SetOutlineLevel(1);
393 zoneItemMap
[fullCountryID
] = countryItem
;
394 hasUsedCountry
= true;
396 countryItem
= countryIter
->second
;
398 zoneItem
= new TimeZoneListItem(timeZoneName
.String(),
400 zoneItem
->SetOutlineLevel(countryIsRegion
? 1 : 2);
402 zoneItem
= new TimeZoneListItem(timeZoneName
.String(),
404 zoneItem
->SetOutlineLevel(1);
405 hasUsedCountry
= true;
407 zoneItemMap
[fullZoneID
] = zoneItem
;
409 if (timeZone
->ID() == defaultTimeZone
.ID()) {
410 fCurrentZoneItem
= zoneItem
;
411 if (countryItem
!= NULL
)
412 countryItem
->SetExpanded(true);
414 ZoneItemMap::iterator regionItemIter
415 = zoneItemMap
.find(regionIter
->second
);
416 if (regionItemIter
!= zoneItemMap
.end())
417 regionItemIter
->second
->SetExpanded(true);
425 fOldZoneItem
= fCurrentZoneItem
;
427 ZoneItemMap::iterator zoneIter
;
428 bool lastWasCountryItem
= false;
429 TimeZoneListItem
* currentItem
= NULL
;
430 for (zoneIter
= zoneItemMap
.begin(); zoneIter
!= zoneItemMap
.end();
432 if (zoneIter
->second
->OutlineLevel() == 2 && lastWasCountryItem
) {
433 // Some countries (e.g. Spain and Chile) have their timezones
434 // spread across different regions. As a result, there might still
435 // be country items with only one timezone below them. We manually
436 // filter those country items here.
437 ZoneItemMap::iterator next
= zoneIter
;
439 if (next
!= zoneItemMap
.end()
440 && next
->second
->OutlineLevel() != 2) {
441 fZoneList
->RemoveItem(currentItem
);
442 zoneIter
->second
->SetText(currentItem
->Text());
443 zoneIter
->second
->SetCountry(currentItem
->HasCountry()
444 ? new(std::nothrow
) BCountry(currentItem
->Country())
446 zoneIter
->second
->SetTimeZone(currentItem
->HasTimeZone()
447 ? new(std::nothrow
) BTimeZone(currentItem
->TimeZone())
449 zoneIter
->second
->SetOutlineLevel(1);
454 fZoneList
->AddItem(zoneIter
->second
);
455 if (zoneIter
->second
->OutlineLevel() == 1) {
456 lastWasCountryItem
= true;
457 currentItem
= zoneIter
->second
;
459 lastWasCountryItem
= false;
465 TimeZoneView::_Revert()
467 fCurrentZoneItem
= fOldZoneItem
;
469 if (fCurrentZoneItem
!= NULL
) {
470 int32 currentZoneIndex
= fZoneList
->IndexOf(fCurrentZoneItem
);
471 fZoneList
->Select(currentZoneIndex
);
473 fZoneList
->DeselectAll();
474 fZoneList
->ScrollToSelection();
476 fUseGmtTime
= fOldUseGmtTime
;
478 fGmtTime
->SetValue(B_CONTROL_ON
);
480 fLocalTime
->SetValue(B_CONTROL_ON
);
481 _ShowOrHidePreview();
483 _UpdateGmtSettings();
484 _SetSystemTimeZone();
491 TimeZoneView::_UpdatePreview()
493 int32 selection
= fZoneList
->CurrentSelection();
494 TimeZoneListItem
* item
497 : static_cast<TimeZoneListItem
*>(fZoneList
->ItemAt(selection
));
499 if (item
== NULL
|| !item
->HasTimeZone()) {
500 fPreview
->SetText("");
501 fPreview
->SetTime("");
505 BString timeString
= _FormatTime(item
->TimeZone());
506 fPreview
->SetText(item
->Text());
507 fPreview
->SetTime(timeString
.String());
509 fSetZone
->SetEnabled((strcmp(fCurrent
->Text(), item
->Text()) != 0));
514 TimeZoneView::_UpdateCurrent()
516 if (fCurrentZoneItem
== NULL
)
519 BString timeString
= _FormatTime(fCurrentZoneItem
->TimeZone());
520 fCurrent
->SetText(fCurrentZoneItem
->Text());
521 fCurrent
->SetTime(timeString
.String());
526 TimeZoneView::_SetSystemTimeZone()
528 /* Set system timezone for all different API levels. How to do this?
529 * 1) tell locale-roster about new default timezone
530 * 2) tell kernel about new timezone offset
533 int32 selection
= fZoneList
->CurrentSelection();
537 TimeZoneListItem
* item
538 = static_cast<TimeZoneListItem
*>(fZoneList
->ItemAt(selection
));
539 if (item
== NULL
|| !item
->HasTimeZone())
542 fCurrentZoneItem
= item
;
543 const BTimeZone
& timeZone
= item
->TimeZone();
545 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone
);
547 _kern_set_timezone(timeZone
.OffsetFromGMT(), timeZone
.ID().String(),
548 timeZone
.ID().Length());
550 fSetZone
->SetEnabled(false);
551 fLastUpdateMinute
= -1;
552 // just to trigger updating immediately
557 TimeZoneView::_FormatTime(const BTimeZone
& timeZone
)
561 time_t now
= time(NULL
);
563 _kern_get_real_time_clock_is_gmt(&rtcIsGMT
);
566 = fCurrentZoneItem
!= NULL
&& fCurrentZoneItem
->HasTimeZone()
567 ? fCurrentZoneItem
->OffsetFromGMT()
569 now
-= timeZone
.OffsetFromGMT() - currentOffset
;
571 fTimeFormat
.Format(result
, now
, B_SHORT_TIME_FORMAT
, &timeZone
);
578 TimeZoneView::_ReadRTCSettings()
581 if (find_directory(B_USER_SETTINGS_DIRECTORY
, &path
) != B_OK
)
584 path
.Append("RTC_time_settings");
586 BEntry
entry(path
.Path());
587 if (entry
.Exists()) {
588 BFile
file(&entry
, B_READ_ONLY
);
589 if (file
.InitCheck() == B_OK
) {
591 file
.Read(buffer
, 6);
592 if (strncmp(buffer
, "gmt", 3) == 0)
600 TimeZoneView::_WriteRTCSettings()
603 if (find_directory(B_USER_SETTINGS_DIRECTORY
, &path
, true) != B_OK
)
606 path
.Append("RTC_time_settings");
608 BFile
file(path
.Path(), B_CREATE_FILE
| B_ERASE_FILE
| B_WRITE_ONLY
);
609 if (file
.InitCheck() == B_OK
) {
611 file
.Write("gmt", 3);
613 file
.Write("local", 5);
619 TimeZoneView::_UpdateGmtSettings()
623 _ShowOrHidePreview();
625 _kern_set_real_time_clock_is_gmt(fUseGmtTime
);
630 TimeZoneView::_ShowOrHidePreview()
633 // Hardware clock uses GMT time, changing timezone will adjust the
634 // offset and we need to display a preview
638 // Hardware clock uses local time, changing timezone will adjust the
639 // clock and there is no offset to manage, thus, no preview.