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();
124 fSetZone
->SetTarget(this);
125 fZoneList
->SetTarget(this);
131 TimeZoneView::DoLayout()
134 if (fCurrentZoneItem
!= NULL
) {
135 fZoneList
->Select(fZoneList
->IndexOf(fCurrentZoneItem
));
136 fCurrent
->SetText(fCurrentZoneItem
->Text());
137 fZoneList
->ScrollToSelection();
143 TimeZoneView::MessageReceived(BMessage
* message
)
145 switch (message
->what
) {
146 case B_OBSERVER_NOTICE_CHANGE
:
149 message
->FindInt32(B_OBSERVE_WHAT_CHANGE
, &change
);
152 _UpdateDateTime(message
);
156 BView::MessageReceived(message
);
166 case H_SET_TIME_ZONE
:
168 _SetSystemTimeZone();
177 fUseGmtTime
= fGmtTime
->Value() == B_CONTROL_ON
;
178 _UpdateGmtSettings();
184 BGroupView::MessageReceived(message
);
191 TimeZoneView::_UpdateDateTime(BMessage
* message
)
193 // only need to update once every minute
195 if (message
->FindInt32("minute", &minute
) == B_OK
) {
196 if (fLastUpdateMinute
!= minute
) {
200 fLastUpdateMinute
= minute
;
207 TimeZoneView::_InitView()
209 fZoneList
= new TimeZoneListView();
210 fZoneList
->SetSelectionMessage(new BMessage(H_CITY_CHANGED
));
211 fZoneList
->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE
));
213 BScrollView
* scrollList
= new BScrollView("scrollList", fZoneList
,
214 B_FRAME_EVENTS
| B_WILL_DRAW
, false, true);
215 scrollList
->SetExplicitMinSize(
216 BSize(200 * be_plain_font
->Size() / 12.0f
, 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 BLayoutBuilder::Group
<>(this)
242 .AddGroup(B_VERTICAL
, 0)
243 .Add(new BStringView("clockSetTo",
244 B_TRANSLATE("Hardware clock set to:")))
245 .AddGroup(B_VERTICAL
, 0)
248 .SetInsets(B_USE_WINDOW_SPACING
, 0, 0, 0)
251 .AddGroup(B_VERTICAL
, B_USE_DEFAULT_SPACING
)
257 .SetInsets(B_USE_WINDOW_SPACING
, B_USE_WINDOW_SPACING
,
258 B_USE_WINDOW_SPACING
, B_USE_DEFAULT_SPACING
);
263 TimeZoneView::_BuildZoneMenu()
265 BTimeZone defaultTimeZone
;
266 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone
);
269 BLocale::Default()->GetLanguage(&language
);
271 // Group timezones by regions, but filter out unwanted (duplicate) regions
272 // and add an additional region with generic GMT-offset timezones at the end
273 typedef std::map
<BString
, TimeZoneListItem
*, TimeZoneItemLess
> ZoneItemMap
;
274 ZoneItemMap zoneItemMap
;
275 const char* kOtherRegion
= B_TRANSLATE_MARK("<Other>");
276 const char* kSupportedRegions
[] = {
277 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"),
278 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"),
279 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"),
280 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"),
281 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"),
286 // Since the zone-map contains translated country-names (we get those from
287 // ICU), we need to use translated region names in the zone-map, too:
288 typedef std::map
<BString
, BString
> TranslatedRegionMap
;
289 TranslatedRegionMap regionMap
;
290 for (const char** region
= kSupportedRegions
; *region
!= NULL
; ++region
) {
291 BString translatedRegion
= B_TRANSLATE_NOCOLLECT(*region
);
292 regionMap
[*region
] = translatedRegion
;
294 TimeZoneListItem
* regionItem
295 = new TimeZoneListItem(translatedRegion
, NULL
, NULL
);
296 regionItem
->SetOutlineLevel(0);
297 zoneItemMap
[translatedRegion
] = regionItem
;
300 // Get all time zones
302 BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList
);
304 typedef std::map
<BString
, std::vector
<const char*> > ZonesByCountyMap
;
305 ZonesByCountyMap zonesByCountryMap
;
307 BString timeZoneCode
;
308 for (int tz
= 0; zoneList
.FindString("timeZone", tz
, &zoneID
) == B_OK
309 && zoneList
.FindString("region", tz
, &timeZoneCode
) == B_OK
; tz
++) {
310 // From the global ("001") timezones, we only accept the generic GMT
311 // timezones, as all the other world-zones are duplicates of others.
312 if (timeZoneCode
== "001" && strncmp(zoneID
, "Etc/GMT", 7) != 0)
314 zonesByCountryMap
[timeZoneCode
].push_back(zoneID
);
317 ZonesByCountyMap::const_iterator countryIter
= zonesByCountryMap
.begin();
318 for (; countryIter
!= zonesByCountryMap
.end(); ++countryIter
) {
319 const char* countryCode
= countryIter
->first
.String();
320 if (countryCode
== NULL
)
323 size_t zoneCountInCountry
= countryIter
->second
.size();
324 for (size_t tz
= 0; tz
< zoneCountInCountry
; tz
++) {
325 BString
zoneID(countryIter
->second
[tz
]);
327 = new(std::nothrow
) BTimeZone(zoneID
, &language
);
328 if (timeZone
== NULL
)
331 int32 slashPos
= zoneID
.FindFirst('/');
332 BString
region(zoneID
, slashPos
);
334 region
= kOtherRegion
;
336 // just accept timezones from our supported regions, others are
337 // aliases and would just make the list even longer
338 TranslatedRegionMap::iterator regionIter
= regionMap
.find(region
);
339 if (regionIter
== regionMap
.end())
342 BString fullCountryID
= regionIter
->second
;
343 BCountry
* country
= new(std::nothrow
) BCountry(countryCode
);
348 country
->GetName(countryName
);
349 bool hasUsedCountry
= false;
350 bool countryIsRegion
= countryName
== regionIter
->second
351 || region
== kOtherRegion
;
352 if (!countryIsRegion
)
353 fullCountryID
<< "/" << countryName
;
355 BString timeZoneName
;
356 BString fullZoneID
= fullCountryID
;
357 if (zoneCountInCountry
> 1) {
358 // we can't use the country name as timezone name, since there
359 // are more than one timezones in this country - fetch the
360 // localized name of the timezone and use that
361 timeZoneName
= timeZone
->Name();
362 int32 openParenthesisPos
= timeZoneName
.FindFirst('(');
363 if (openParenthesisPos
>= 0) {
364 timeZoneName
.Remove(0, openParenthesisPos
+ 1);
365 int32 closeParenthesisPos
= timeZoneName
.FindLast(')');
366 if (closeParenthesisPos
>= 0)
367 timeZoneName
.Truncate(closeParenthesisPos
);
369 fullZoneID
<< "/" << timeZoneName
;
371 timeZoneName
= countryName
;
372 fullZoneID
<< "/" << zoneID
;
376 ZoneItemMap::iterator zoneIter
= zoneItemMap
.find(fullZoneID
);
377 if (zoneIter
!= zoneItemMap
.end()) {
382 TimeZoneListItem
* countryItem
= NULL
;
383 TimeZoneListItem
* zoneItem
= NULL
;
384 if (zoneCountInCountry
> 1) {
385 ZoneItemMap::iterator countryIter
386 = zoneItemMap
.find(fullCountryID
);
387 if (countryIter
== zoneItemMap
.end()) {
388 countryItem
= new TimeZoneListItem(countryName
.String(),
390 countryItem
->SetOutlineLevel(1);
391 zoneItemMap
[fullCountryID
] = countryItem
;
392 hasUsedCountry
= true;
394 countryItem
= countryIter
->second
;
396 zoneItem
= new TimeZoneListItem(timeZoneName
.String(),
398 zoneItem
->SetOutlineLevel(countryIsRegion
? 1 : 2);
400 zoneItem
= new TimeZoneListItem(timeZoneName
.String(),
402 zoneItem
->SetOutlineLevel(1);
403 hasUsedCountry
= true;
405 zoneItemMap
[fullZoneID
] = zoneItem
;
407 if (timeZone
->ID() == defaultTimeZone
.ID()) {
408 fCurrentZoneItem
= zoneItem
;
409 if (countryItem
!= NULL
)
410 countryItem
->SetExpanded(true);
412 ZoneItemMap::iterator regionItemIter
413 = zoneItemMap
.find(regionIter
->second
);
414 if (regionItemIter
!= zoneItemMap
.end())
415 regionItemIter
->second
->SetExpanded(true);
423 fOldZoneItem
= fCurrentZoneItem
;
425 ZoneItemMap::iterator zoneIter
;
426 bool lastWasCountryItem
= false;
427 TimeZoneListItem
* currentItem
= NULL
;
428 for (zoneIter
= zoneItemMap
.begin(); zoneIter
!= zoneItemMap
.end();
430 if (zoneIter
->second
->OutlineLevel() == 2 && lastWasCountryItem
) {
431 // Some countries (e.g. Spain and Chile) have their timezones
432 // spread across different regions. As a result, there might still
433 // be country items with only one timezone below them. We manually
434 // filter those country items here.
435 ZoneItemMap::iterator next
= zoneIter
;
437 if (next
!= zoneItemMap
.end()
438 && next
->second
->OutlineLevel() != 2) {
439 fZoneList
->RemoveItem(currentItem
);
440 zoneIter
->second
->SetText(currentItem
->Text());
441 zoneIter
->second
->SetCountry(currentItem
->HasCountry()
442 ? new(std::nothrow
) BCountry(currentItem
->Country())
444 zoneIter
->second
->SetTimeZone(currentItem
->HasTimeZone()
445 ? new(std::nothrow
) BTimeZone(currentItem
->TimeZone())
447 zoneIter
->second
->SetOutlineLevel(1);
452 fZoneList
->AddItem(zoneIter
->second
);
453 if (zoneIter
->second
->OutlineLevel() == 1) {
454 lastWasCountryItem
= true;
455 currentItem
= zoneIter
->second
;
457 lastWasCountryItem
= false;
463 TimeZoneView::_Revert()
465 fCurrentZoneItem
= fOldZoneItem
;
467 if (fCurrentZoneItem
!= NULL
) {
468 int32 currentZoneIndex
= fZoneList
->IndexOf(fCurrentZoneItem
);
469 fZoneList
->Select(currentZoneIndex
);
471 fZoneList
->DeselectAll();
472 fZoneList
->ScrollToSelection();
474 fUseGmtTime
= fOldUseGmtTime
;
476 fGmtTime
->SetValue(B_CONTROL_ON
);
478 fLocalTime
->SetValue(B_CONTROL_ON
);
479 _ShowOrHidePreview();
481 _UpdateGmtSettings();
482 _SetSystemTimeZone();
489 TimeZoneView::_UpdatePreview()
491 int32 selection
= fZoneList
->CurrentSelection();
492 TimeZoneListItem
* item
495 : static_cast<TimeZoneListItem
*>(fZoneList
->ItemAt(selection
));
497 if (item
== NULL
|| !item
->HasTimeZone()) {
498 fPreview
->SetText("");
499 fPreview
->SetTime("");
503 BString timeString
= _FormatTime(item
->TimeZone());
504 fPreview
->SetText(item
->Text());
505 fPreview
->SetTime(timeString
.String());
507 fSetZone
->SetEnabled((strcmp(fCurrent
->Text(), item
->Text()) != 0));
512 TimeZoneView::_UpdateCurrent()
514 if (fCurrentZoneItem
== NULL
)
517 BString timeString
= _FormatTime(fCurrentZoneItem
->TimeZone());
518 fCurrent
->SetText(fCurrentZoneItem
->Text());
519 fCurrent
->SetTime(timeString
.String());
524 TimeZoneView::_SetSystemTimeZone()
526 /* Set system timezone for all different API levels. How to do this?
527 * 1) tell locale-roster about new default timezone
528 * 2) tell kernel about new timezone offset
531 int32 selection
= fZoneList
->CurrentSelection();
535 TimeZoneListItem
* item
536 = static_cast<TimeZoneListItem
*>(fZoneList
->ItemAt(selection
));
537 if (item
== NULL
|| !item
->HasTimeZone())
540 fCurrentZoneItem
= item
;
541 const BTimeZone
& timeZone
= item
->TimeZone();
543 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone
);
545 _kern_set_timezone(timeZone
.OffsetFromGMT(), timeZone
.ID().String(),
546 timeZone
.ID().Length());
548 fSetZone
->SetEnabled(false);
549 fLastUpdateMinute
= -1;
550 // just to trigger updating immediately
555 TimeZoneView::_FormatTime(const BTimeZone
& timeZone
)
559 time_t now
= time(NULL
);
561 _kern_get_real_time_clock_is_gmt(&rtcIsGMT
);
564 = fCurrentZoneItem
!= NULL
&& fCurrentZoneItem
->HasTimeZone()
565 ? fCurrentZoneItem
->OffsetFromGMT()
567 now
-= timeZone
.OffsetFromGMT() - currentOffset
;
569 fTimeFormat
.Format(result
, now
, B_SHORT_TIME_FORMAT
, &timeZone
);
576 TimeZoneView::_ReadRTCSettings()
579 if (find_directory(B_USER_SETTINGS_DIRECTORY
, &path
) != B_OK
)
582 path
.Append("RTC_time_settings");
584 BEntry
entry(path
.Path());
585 if (entry
.Exists()) {
586 BFile
file(&entry
, B_READ_ONLY
);
587 if (file
.InitCheck() == B_OK
) {
589 file
.Read(buffer
, 6);
590 if (strncmp(buffer
, "gmt", 3) == 0)
598 TimeZoneView::_WriteRTCSettings()
601 if (find_directory(B_USER_SETTINGS_DIRECTORY
, &path
, true) != B_OK
)
604 path
.Append("RTC_time_settings");
606 BFile
file(path
.Path(), B_CREATE_FILE
| B_ERASE_FILE
| B_WRITE_ONLY
);
607 if (file
.InitCheck() == B_OK
) {
609 file
.Write("gmt", 3);
611 file
.Write("local", 5);
617 TimeZoneView::_UpdateGmtSettings()
621 _ShowOrHidePreview();
623 _kern_set_real_time_clock_is_gmt(fUseGmtTime
);
628 TimeZoneView::_ShowOrHidePreview()
631 // Hardware clock uses GMT time, changing timezone will adjust the
632 // offset and we need to display a preview
636 // Hardware clock uses local time, changing timezone will adjust the
637 // clock and there is no offset to manage, thus, no preview.