/* * Copyright 2004-2010, Haiku, Inc. All Rights Reserved. * Distributed under the terms of the MIT License. * * Authors: * Mike Berg * Julun * Philippe Saint-Pierre * Adrien Destugues * Oliver Tappe * Hamish Morrison */ #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 "TZDisplay.h" #undef B_TRANSLATE_CONTEXT #define B_TRANSLATE_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), fToolTip(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() { if (fToolTip != NULL) fToolTip->ReleaseReference(); _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; } } bool TimeZoneView::GetToolTipAt(BPoint point, BToolTip** _tip) { TimeZoneListItem* item = static_cast( fZoneList->ItemAt(fZoneList->IndexOf(point))); if (item == NULL || !item->HasTimeZone()) return false; BString nowInTimeZone; time_t now = time(NULL); BLocale::Default()->FormatTime(&nowInTimeZone, now, B_SHORT_TIME_FORMAT, &item->TimeZone()); BString dateInTimeZone; BLocale::Default()->FormatDate(&dateInTimeZone, now, B_SHORT_DATE_FORMAT, &item->TimeZone()); BString toolTip = item->Text(); toolTip << '\n' << item->TimeZone().ShortName() << " / " << item->TimeZone().ShortDaylightSavingName() << B_TRANSLATE("\nNow: ") << nowInTimeZone << " (" << dateInTimeZone << ')'; if (fToolTip != NULL) fToolTip->ReleaseReference(); fToolTip = new (std::nothrow) BTextToolTip(toolTip.String()); if (fToolTip == NULL) return false; *_tip = fToolTip; return true; } 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 BOutlineListView("cityList", B_SINGLE_SELECTION_LIST); 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)); BStringView* text = new BStringView("clockSetTo", B_TRANSLATE("Hardware clock set to:")); 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 kInset = be_control_look->DefaultItemSpacing(); BLayoutBuilder::Group<>(this) .Add(scrollList) .AddGroup(B_VERTICAL, kInset) .Add(text) .AddGroup(B_VERTICAL, kInset) .Add(fLocalTime) .Add(fGmtTime) .End() .AddGlue() .Add(fCurrent) .Add(fPreview) .Add(fSetZone) .End() .SetInsets(kInset, kInset, kInset, kInset); } void TimeZoneView::_BuildZoneMenu() { BTimeZone defaultTimeZone; BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); BLanguage language; BLocale::Default()->GetLanguage(&language); BMessage countryList; BLocaleRoster::Default()->GetAvailableCountries(&countryList); countryList.AddString("country", ""); /* * 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; } BString countryCode; for (int c = 0; countryList.FindString("country", c, &countryCode) == B_OK; c++) { BCountry country(countryCode); BString countryName; country.GetName(countryName); // Now list the timezones for this country BMessage zoneList; BLocaleRoster::Default()->GetAvailableTimeZonesForCountry(&zoneList, countryCode.Length() == 0 ? NULL : countryCode.String()); int32 count = 0; type_code dummy; zoneList.GetInfo("timeZone", &dummy, &count); BString zoneID; for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK; tz++) { int32 slashPos = zoneID.FindFirst('/'); // ignore any "global" timezones, as those are just aliases of // regional ones if (slashPos <= 0) continue; BString region(zoneID, slashPos); if (region == "Etc") region = kOtherRegion; else if (countryName.Length() == 0) { // skip global timezones from other regions, we are just // interested in the generic GMT-based ones under "Etc/" continue; } // 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; const BString& regionName = regionIter->second; BString fullCountryID = regionName; bool countryIsRegion = countryName == regionName; if (!countryIsRegion) fullCountryID << "/" << countryName; BTimeZone* timeZone = new BTimeZone(zoneID, &language); BString tzName = timeZone->Name(); if (tzName == "GMT+00:00") tzName = "GMT"; int32 openParenthesisPos = tzName.FindFirst('('); if (openParenthesisPos >= 0) { tzName.Remove(0, openParenthesisPos + 1); int32 closeParenthesisPos = tzName.FindLast(')'); if (closeParenthesisPos >= 0) tzName.Truncate(closeParenthesisPos); } BString fullZoneID = fullCountryID; fullZoneID << "/" << tzName; // skip duplicates ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); if (zoneIter != zoneItemMap.end()) { delete timeZone; continue; } TimeZoneListItem* countryItem = NULL; TimeZoneListItem* zoneItem = NULL; if (count > 1 && countryName.Length() > 0) { ZoneItemMap::iterator countryIter = zoneItemMap.find(fullCountryID); if (countryIter == zoneItemMap.end()) { countryItem = new TimeZoneListItem(countryName, NULL, NULL); countryItem->SetOutlineLevel(1); zoneItemMap[fullCountryID] = countryItem; } else countryItem = countryIter->second; zoneItem = new TimeZoneListItem(tzName, NULL, timeZone); zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); } else { BString& name = countryName.Length() > 0 ? countryName : tzName; zoneItem = new TimeZoneListItem(name, NULL, timeZone); zoneItem->SetOutlineLevel(1); } zoneItemMap[fullZoneID] = zoneItem; if (timeZone->ID() == defaultTimeZone.ID()) { fCurrentZoneItem = zoneItem; if (countryItem != NULL) countryItem->SetExpanded(true); ZoneItemMap::iterator regionItemIter = zoneItemMap.find(regionName); if (regionItemIter != zoneItemMap.end()) regionItemIter->second->SetExpanded(true); } } } fOldZoneItem = fCurrentZoneItem; ZoneItemMap::iterator zoneIter; bool lastWasCountryItem = false; TimeZoneListItem* currentCountryItem = 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(currentCountryItem); zoneIter->second->SetText(currentCountryItem->Text()); zoneIter->second->SetOutlineLevel(1); delete currentCountryItem; } } fZoneList->AddItem(zoneIter->second); if (zoneIter->second->OutlineLevel() == 1) { lastWasCountryItem = true; currentCountryItem = 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 : (TimeZoneListItem*)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 sytem 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(); } }