/* * Copyright 2004-2013 Haiku, Inc. All rights reserved. * Distributed under the terms of the MIT License. * * Authors: * Mike Berg, mike@berg-net.us * Adrien Destugues, pulkomandy@pulkomandy.ath.cx * Julun, host.haiku@gmx.de * Hamish Morrison, hamish@lavabit.com * Philippe Saint-Pierre, stpere@gmail.com * John Scipione, jscipione@gmail.com * Oliver Tappe, zooey@hirschkaefer.de */ #include "ZoneView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "TimeMessages.h" #include "TimeZoneListItem.h" #include "TimeZoneListView.h" #include "TZDisplay.h" #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "Time" using BPrivate::MutableLocaleRoster; using BPrivate::ObjectDeleter; struct TimeZoneItemLess { bool operator()(const BString& first, const BString& second) { // sort anything starting with '<' behind anything else if (first.ByteAt(0) == '<') { if (second.ByteAt(0) != '<') return false; } else if (second.ByteAt(0) == '<') return true; return fCollator.Compare(first.String(), second.String()) < 0; } private: BCollator fCollator; }; TimeZoneView::TimeZoneView(const char* name) : BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING), fGmtTime(NULL), fUseGmtTime(false), fCurrentZoneItem(NULL), fOldZoneItem(NULL), fInitialized(false) { _ReadRTCSettings(); _InitView(); } bool TimeZoneView::CheckCanRevert() { // check GMT vs Local setting bool enable = fUseGmtTime != fOldUseGmtTime; return enable || fCurrentZoneItem != fOldZoneItem; } TimeZoneView::~TimeZoneView() { _WriteRTCSettings(); } void TimeZoneView::AttachedToWindow() { BView::AttachedToWindow(); if (Parent()) SetViewColor(Parent()->ViewColor()); if (!fInitialized) { fInitialized = true; fSetZone->SetTarget(this); fZoneList->SetTarget(this); } } void TimeZoneView::DoLayout() { BView::DoLayout(); if (fCurrentZoneItem != NULL) { fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem)); fCurrent->SetText(fCurrentZoneItem->Text()); fZoneList->ScrollToSelection(); } } void TimeZoneView::MessageReceived(BMessage* message) { switch (message->what) { case B_OBSERVER_NOTICE_CHANGE: { int32 change; message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change); switch(change) { case H_TM_CHANGED: _UpdateDateTime(message); break; default: BView::MessageReceived(message); break; } break; } case H_CITY_CHANGED: _UpdatePreview(); break; case H_SET_TIME_ZONE: { _SetSystemTimeZone(); break; } case kMsgRevert: _Revert(); break; case kRTCUpdate: fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON; _UpdateGmtSettings(); _UpdateCurrent(); _UpdatePreview(); break; default: BGroupView::MessageReceived(message); break; } } void TimeZoneView::_UpdateDateTime(BMessage* message) { // only need to update once every minute int32 minute; if (message->FindInt32("minute", &minute) == B_OK) { if (fLastUpdateMinute != minute) { _UpdateCurrent(); _UpdatePreview(); fLastUpdateMinute = minute; } } } void TimeZoneView::_InitView() { fZoneList = new TimeZoneListView(); fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED)); fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE)); _BuildZoneMenu(); BScrollView* scrollList = new BScrollView("scrollList", fZoneList, B_FRAME_EVENTS | B_WILL_DRAW, false, true); scrollList->SetExplicitMinSize(BSize(200, 0)); fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:")); fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:")); fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"), new BMessage(H_SET_TIME_ZONE)); fSetZone->SetEnabled(false); fSetZone->SetExplicitAlignment( BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM)); fLocalTime = new BRadioButton("localTime", B_TRANSLATE("Local time (Windows compatible)"), new BMessage(kRTCUpdate)); fGmtTime = new BRadioButton("greenwichMeanTime", B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate)); if (fUseGmtTime) fGmtTime->SetValue(B_CONTROL_ON); else fLocalTime->SetValue(B_CONTROL_ON); _ShowOrHidePreview(); fOldUseGmtTime = fUseGmtTime; const float kIndentSpacing = be_control_look->DefaultItemSpacing() * 2; BLayoutBuilder::Group<>(this) .Add(scrollList) .AddGroup(B_VERTICAL, 0) .Add(new BStringView("clockSetTo", B_TRANSLATE("Hardware clock set to:"))) .AddGroup(B_VERTICAL, 0) .Add(fLocalTime) .Add(fGmtTime) .SetInsets(kIndentSpacing, 0, 0, 0) .End() .AddGlue() .AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING) .Add(fCurrent) .Add(fPreview) .End() .Add(fSetZone) .End() .SetInsets(B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING); } void TimeZoneView::_BuildZoneMenu() { BTimeZone defaultTimeZone; BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); BLanguage language; BLocale::Default()->GetLanguage(&language); // Group timezones by regions, but filter out unwanted (duplicate) regions // and add an additional region with generic GMT-offset timezones at the end typedef std::map ZoneItemMap; ZoneItemMap zoneItemMap; const char* kOtherRegion = B_TRANSLATE_MARK(""); const char* kSupportedRegions[] = { B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"), B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"), B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"), B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"), B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"), kOtherRegion, NULL }; // Since the zone-map contains translated country-names (we get those from // ICU), we need to use translated region names in the zone-map, too: typedef std::map TranslatedRegionMap; TranslatedRegionMap regionMap; for (const char** region = kSupportedRegions; *region != NULL; ++region) { BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region); regionMap[*region] = translatedRegion; TimeZoneListItem* regionItem = new TimeZoneListItem(translatedRegion, NULL, NULL); regionItem->SetOutlineLevel(0); zoneItemMap[translatedRegion] = regionItem; } // Get all time zones BMessage zoneList; BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList); typedef std::map > ZonesByCountyMap; ZonesByCountyMap zonesByCountryMap; const char* zoneID; BString timeZoneCode; for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK && zoneList.FindString("region", tz, &timeZoneCode) == B_OK; tz++) { // From the global ("001") timezones, we only accept the generic GMT // timezones, as all the other world-zones are duplicates of others. if (timeZoneCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0) continue; zonesByCountryMap[timeZoneCode].push_back(zoneID); } ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin(); for (; countryIter != zonesByCountryMap.end(); ++countryIter) { const char* countryCode = countryIter->first.String(); if (countryCode == NULL) continue; size_t zoneCountInCountry = countryIter->second.size(); for (size_t tz = 0; tz < zoneCountInCountry; tz++) { BString zoneID(countryIter->second[tz]); BTimeZone* timeZone = new(std::nothrow) BTimeZone(zoneID, &language); if (timeZone == NULL) continue; int32 slashPos = zoneID.FindFirst('/'); BString region(zoneID, slashPos); if (region == "Etc") region = kOtherRegion; // just accept timezones from our supported regions, others are // aliases and would just make the list even longer TranslatedRegionMap::iterator regionIter = regionMap.find(region); if (regionIter == regionMap.end()) continue; BString fullCountryID = regionIter->second; BCountry* country = new(std::nothrow) BCountry(countryCode); if (country == NULL) continue; BString countryName; country->GetName(countryName); bool hasUsedCountry = false; bool countryIsRegion = countryName == regionIter->second || region == kOtherRegion; if (!countryIsRegion) fullCountryID << "/" << countryName; BString timeZoneName; BString fullZoneID = fullCountryID; if (zoneCountInCountry > 1) { // we can't use the country name as timezone name, since there // are more than one timezones in this country - fetch the // localized name of the timezone and use that timeZoneName = timeZone->Name(); int32 openParenthesisPos = timeZoneName.FindFirst('('); if (openParenthesisPos >= 0) { timeZoneName.Remove(0, openParenthesisPos + 1); int32 closeParenthesisPos = timeZoneName.FindLast(')'); if (closeParenthesisPos >= 0) timeZoneName.Truncate(closeParenthesisPos); } fullZoneID << "/" << timeZoneName; } else { timeZoneName = countryName; fullZoneID << "/" << zoneID; } // skip duplicates ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); if (zoneIter != zoneItemMap.end()) { delete timeZone; continue; } TimeZoneListItem* countryItem = NULL; TimeZoneListItem* zoneItem = NULL; if (zoneCountInCountry > 1) { ZoneItemMap::iterator countryIter = zoneItemMap.find(fullCountryID); if (countryIter == zoneItemMap.end()) { countryItem = new TimeZoneListItem(countryName.String(), country, NULL); countryItem->SetOutlineLevel(1); zoneItemMap[fullCountryID] = countryItem; hasUsedCountry = true; } else countryItem = countryIter->second; zoneItem = new TimeZoneListItem(timeZoneName.String(), NULL, timeZone); zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); } else { zoneItem = new TimeZoneListItem(timeZoneName.String(), country, timeZone); zoneItem->SetOutlineLevel(1); hasUsedCountry = true; } zoneItemMap[fullZoneID] = zoneItem; if (timeZone->ID() == defaultTimeZone.ID()) { fCurrentZoneItem = zoneItem; if (countryItem != NULL) countryItem->SetExpanded(true); ZoneItemMap::iterator regionItemIter = zoneItemMap.find(regionIter->second); if (regionItemIter != zoneItemMap.end()) regionItemIter->second->SetExpanded(true); } if (!hasUsedCountry) delete country; } } fOldZoneItem = fCurrentZoneItem; ZoneItemMap::iterator zoneIter; bool lastWasCountryItem = false; TimeZoneListItem* currentItem = NULL; for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); ++zoneIter) { if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { // Some countries (e.g. Spain and Chile) have their timezones // spread across different regions. As a result, there might still // be country items with only one timezone below them. We manually // filter those country items here. ZoneItemMap::iterator next = zoneIter; ++next; if (next != zoneItemMap.end() && next->second->OutlineLevel() != 2) { fZoneList->RemoveItem(currentItem); zoneIter->second->SetText(currentItem->Text()); zoneIter->second->SetCountry(currentItem->HasCountry() ? new(std::nothrow) BCountry(currentItem->Country()) : NULL); zoneIter->second->SetTimeZone(currentItem->HasTimeZone() ? new(std::nothrow) BTimeZone(currentItem->TimeZone()) : NULL); zoneIter->second->SetOutlineLevel(1); delete currentItem; } } fZoneList->AddItem(zoneIter->second); if (zoneIter->second->OutlineLevel() == 1) { lastWasCountryItem = true; currentItem = zoneIter->second; } else lastWasCountryItem = false; } } void TimeZoneView::_Revert() { fCurrentZoneItem = fOldZoneItem; if (fCurrentZoneItem != NULL) { int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); fZoneList->Select(currentZoneIndex); } else fZoneList->DeselectAll(); fZoneList->ScrollToSelection(); fUseGmtTime = fOldUseGmtTime; if (fUseGmtTime) fGmtTime->SetValue(B_CONTROL_ON); else fLocalTime->SetValue(B_CONTROL_ON); _ShowOrHidePreview(); _UpdateGmtSettings(); _SetSystemTimeZone(); _UpdatePreview(); _UpdateCurrent(); } void TimeZoneView::_UpdatePreview() { int32 selection = fZoneList->CurrentSelection(); TimeZoneListItem* item = selection < 0 ? NULL : static_cast(fZoneList->ItemAt(selection)); if (item == NULL || !item->HasTimeZone()) { fPreview->SetText(""); fPreview->SetTime(""); return; } BString timeString = _FormatTime(item->TimeZone()); fPreview->SetText(item->Text()); fPreview->SetTime(timeString.String()); fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); } void TimeZoneView::_UpdateCurrent() { if (fCurrentZoneItem == NULL) return; BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); fCurrent->SetText(fCurrentZoneItem->Text()); fCurrent->SetTime(timeString.String()); } void TimeZoneView::_SetSystemTimeZone() { /* Set system timezone for all different API levels. How to do this? * 1) tell locale-roster about new default timezone * 2) tell kernel about new timezone offset */ int32 selection = fZoneList->CurrentSelection(); if (selection < 0) return; TimeZoneListItem* item = static_cast(fZoneList->ItemAt(selection)); if (item == NULL || !item->HasTimeZone()) return; fCurrentZoneItem = item; const BTimeZone& timeZone = item->TimeZone(); MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), timeZone.ID().Length()); fSetZone->SetEnabled(false); fLastUpdateMinute = -1; // just to trigger updating immediately } BString TimeZoneView::_FormatTime(const BTimeZone& timeZone) { BString result; time_t now = time(NULL); bool rtcIsGMT; _kern_get_real_time_clock_is_gmt(&rtcIsGMT); if (!rtcIsGMT) { int32 currentOffset = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() ? fCurrentZoneItem->OffsetFromGMT() : 0; now -= timeZone.OffsetFromGMT() - currentOffset; } BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT, &timeZone); return result; } void TimeZoneView::_ReadRTCSettings() { BPath path; if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) return; path.Append("RTC_time_settings"); BEntry entry(path.Path()); if (entry.Exists()) { BFile file(&entry, B_READ_ONLY); if (file.InitCheck() == B_OK) { char buffer[6]; file.Read(buffer, 6); if (strncmp(buffer, "gmt", 3) == 0) fUseGmtTime = true; } } } void TimeZoneView::_WriteRTCSettings() { BPath path; if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK) return; path.Append("RTC_time_settings"); BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); if (file.InitCheck() == B_OK) { if (fUseGmtTime) file.Write("gmt", 3); else file.Write("local", 5); } } void TimeZoneView::_UpdateGmtSettings() { _WriteRTCSettings(); _ShowOrHidePreview(); _kern_set_real_time_clock_is_gmt(fUseGmtTime); } void TimeZoneView::_ShowOrHidePreview() { if (fUseGmtTime) { // Hardware clock uses GMT time, changing timezone will adjust the // offset and we need to display a preview fCurrent->Show(); fPreview->Show(); } else { // Hardware clock uses local time, changing timezone will adjust the // clock and there is no offset to manage, thus, no preview. fCurrent->Hide(); fPreview->Hide(); } }