1 /* 2 * Copyright 2004-2013 Haiku, Inc. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 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 13 */ 14 15 16 #include <unicode/uversion.h> 17 #include "ZoneView.h" 18 19 #include <stdlib.h> 20 #include <syscalls.h> 21 22 #include <map> 23 #include <new> 24 #include <vector> 25 26 #include <AutoDeleter.h> 27 #include <Button.h> 28 #include <Catalog.h> 29 #include <Collator.h> 30 #include <ControlLook.h> 31 #include <Country.h> 32 #include <Directory.h> 33 #include <Entry.h> 34 #include <File.h> 35 #include <FindDirectory.h> 36 #include <ListItem.h> 37 #include <Locale.h> 38 #include <MutableLocaleRoster.h> 39 #include <OutlineListView.h> 40 #include <Path.h> 41 #include <RadioButton.h> 42 #include <ScrollView.h> 43 #include <StorageDefs.h> 44 #include <String.h> 45 #include <StringView.h> 46 #include <TimeZone.h> 47 #include <View.h> 48 #include <Window.h> 49 50 #include <unicode/datefmt.h> 51 #include <unicode/utmscale.h> 52 #include <ICUWrapper.h> 53 54 #include "TimeMessages.h" 55 #include "TimeZoneListItem.h" 56 #include "TimeZoneListView.h" 57 #include "TZDisplay.h" 58 59 60 #undef B_TRANSLATION_CONTEXT 61 #define B_TRANSLATION_CONTEXT "Time" 62 63 64 using BPrivate::MutableLocaleRoster; 65 using BPrivate::ObjectDeleter; 66 67 68 struct TimeZoneItemLess { 69 bool operator()(const BString& first, const BString& second) 70 { 71 // sort anything starting with '<' behind anything else 72 if (first.ByteAt(0) == '<') { 73 if (second.ByteAt(0) != '<') 74 return false; 75 } else if (second.ByteAt(0) == '<') 76 return true; 77 return fCollator.Compare(first.String(), second.String()) < 0; 78 } 79 private: 80 BCollator fCollator; 81 }; 82 83 84 85 TimeZoneView::TimeZoneView(const char* name) 86 : 87 BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING), 88 fGmtTime(NULL), 89 fUseGmtTime(false), 90 fCurrentZoneItem(NULL), 91 fOldZoneItem(NULL), 92 fInitialized(false) 93 { 94 _ReadRTCSettings(); 95 _InitView(); 96 } 97 98 99 bool 100 TimeZoneView::CheckCanRevert() 101 { 102 // check GMT vs Local setting 103 bool enable = fUseGmtTime != fOldUseGmtTime; 104 105 return enable || fCurrentZoneItem != fOldZoneItem; 106 } 107 108 109 TimeZoneView::~TimeZoneView() 110 { 111 _WriteRTCSettings(); 112 } 113 114 115 void 116 TimeZoneView::AttachedToWindow() 117 { 118 BView::AttachedToWindow(); 119 if (Parent()) 120 SetViewColor(Parent()->ViewColor()); 121 122 if (!fInitialized) { 123 fInitialized = true; 124 125 fSetZone->SetTarget(this); 126 fZoneList->SetTarget(this); 127 } 128 } 129 130 131 void 132 TimeZoneView::DoLayout() 133 { 134 BView::DoLayout(); 135 if (fCurrentZoneItem != NULL) { 136 fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem)); 137 fCurrent->SetText(fCurrentZoneItem->Text()); 138 fZoneList->ScrollToSelection(); 139 } 140 } 141 142 143 void 144 TimeZoneView::MessageReceived(BMessage* message) 145 { 146 switch (message->what) { 147 case B_OBSERVER_NOTICE_CHANGE: 148 { 149 int32 change; 150 message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change); 151 switch(change) { 152 case H_TM_CHANGED: 153 _UpdateDateTime(message); 154 break; 155 156 default: 157 BView::MessageReceived(message); 158 break; 159 } 160 break; 161 } 162 163 case H_CITY_CHANGED: 164 _UpdatePreview(); 165 break; 166 167 case H_SET_TIME_ZONE: 168 { 169 _SetSystemTimeZone(); 170 break; 171 } 172 173 case kMsgRevert: 174 _Revert(); 175 break; 176 177 case kRTCUpdate: 178 fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON; 179 _UpdateGmtSettings(); 180 _UpdateCurrent(); 181 _UpdatePreview(); 182 break; 183 184 default: 185 BGroupView::MessageReceived(message); 186 break; 187 } 188 } 189 190 191 void 192 TimeZoneView::_UpdateDateTime(BMessage* message) 193 { 194 // only need to update once every minute 195 int32 minute; 196 if (message->FindInt32("minute", &minute) == B_OK) { 197 if (fLastUpdateMinute != minute) { 198 _UpdateCurrent(); 199 _UpdatePreview(); 200 201 fLastUpdateMinute = minute; 202 } 203 } 204 } 205 206 207 void 208 TimeZoneView::_InitView() 209 { 210 fZoneList = new TimeZoneListView(); 211 fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED)); 212 fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE)); 213 _BuildZoneMenu(); 214 BScrollView* scrollList = new BScrollView("scrollList", fZoneList, 215 B_FRAME_EVENTS | B_WILL_DRAW, false, true); 216 scrollList->SetExplicitMinSize(BSize(200, 0)); 217 218 fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:")); 219 fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:")); 220 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)); 226 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)); 232 233 if (fUseGmtTime) 234 fGmtTime->SetValue(B_CONTROL_ON); 235 else 236 fLocalTime->SetValue(B_CONTROL_ON); 237 _ShowOrHidePreview(); 238 fOldUseGmtTime = fUseGmtTime; 239 240 const float kIndentSpacing 241 = be_control_look->DefaultItemSpacing() * 2; 242 BLayoutBuilder::Group<>(this) 243 .Add(scrollList) 244 .AddGroup(B_VERTICAL, 0) 245 .Add(new BStringView("clockSetTo", 246 B_TRANSLATE("Hardware clock set to:"))) 247 .AddGroup(B_VERTICAL, 0) 248 .Add(fLocalTime) 249 .Add(fGmtTime) 250 .SetInsets(kIndentSpacing, 0, 0, 0) 251 .End() 252 .AddGlue() 253 .AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING) 254 .Add(fCurrent) 255 .Add(fPreview) 256 .End() 257 .Add(fSetZone) 258 .End() 259 .SetInsets(B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING, 260 B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING); 261 } 262 263 264 void 265 TimeZoneView::_BuildZoneMenu() 266 { 267 BTimeZone defaultTimeZone; 268 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); 269 270 BLanguage language; 271 BLocale::Default()->GetLanguage(&language); 272 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"), 284 kOtherRegion, 285 NULL 286 }; 287 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; 295 296 TimeZoneListItem* regionItem 297 = new TimeZoneListItem(translatedRegion, NULL, NULL); 298 regionItem->SetOutlineLevel(0); 299 zoneItemMap[translatedRegion] = regionItem; 300 } 301 302 // Get all time zones 303 BMessage zoneList; 304 BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList); 305 306 typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap; 307 ZonesByCountyMap zonesByCountryMap; 308 const char* zoneID; 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) 315 continue; 316 zonesByCountryMap[timeZoneCode].push_back(zoneID); 317 } 318 319 ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin(); 320 for (; countryIter != zonesByCountryMap.end(); ++countryIter) { 321 const char* countryCode = countryIter->first.String(); 322 if (countryCode == NULL) 323 continue; 324 325 size_t zoneCountInCountry = countryIter->second.size(); 326 for (size_t tz = 0; tz < zoneCountInCountry; tz++) { 327 BString zoneID(countryIter->second[tz]); 328 BTimeZone* timeZone 329 = new(std::nothrow) BTimeZone(zoneID, &language); 330 if (timeZone == NULL) 331 continue; 332 333 int32 slashPos = zoneID.FindFirst('/'); 334 BString region(zoneID, slashPos); 335 if (region == "Etc") 336 region = kOtherRegion; 337 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()) 342 continue; 343 344 BString fullCountryID = regionIter->second; 345 BCountry* country = new(std::nothrow) BCountry(countryCode); 346 if (country == NULL) 347 continue; 348 349 BString countryName; 350 country->GetName(countryName); 351 bool hasUsedCountry = false; 352 bool countryIsRegion = countryName == regionIter->second 353 || region == kOtherRegion; 354 if (!countryIsRegion) 355 fullCountryID << "/" << countryName; 356 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); 370 } 371 fullZoneID << "/" << timeZoneName; 372 } else { 373 timeZoneName = countryName; 374 fullZoneID << "/" << zoneID; 375 } 376 377 // skip duplicates 378 ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); 379 if (zoneIter != zoneItemMap.end()) { 380 delete timeZone; 381 continue; 382 } 383 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(), 391 country, NULL); 392 countryItem->SetOutlineLevel(1); 393 zoneItemMap[fullCountryID] = countryItem; 394 hasUsedCountry = true; 395 } else 396 countryItem = countryIter->second; 397 398 zoneItem = new TimeZoneListItem(timeZoneName.String(), 399 NULL, timeZone); 400 zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); 401 } else { 402 zoneItem = new TimeZoneListItem(timeZoneName.String(), 403 country, timeZone); 404 zoneItem->SetOutlineLevel(1); 405 hasUsedCountry = true; 406 } 407 zoneItemMap[fullZoneID] = zoneItem; 408 409 if (timeZone->ID() == defaultTimeZone.ID()) { 410 fCurrentZoneItem = zoneItem; 411 if (countryItem != NULL) 412 countryItem->SetExpanded(true); 413 414 ZoneItemMap::iterator regionItemIter 415 = zoneItemMap.find(regionIter->second); 416 if (regionItemIter != zoneItemMap.end()) 417 regionItemIter->second->SetExpanded(true); 418 } 419 420 if (!hasUsedCountry) 421 delete country; 422 } 423 } 424 425 fOldZoneItem = fCurrentZoneItem; 426 427 ZoneItemMap::iterator zoneIter; 428 bool lastWasCountryItem = false; 429 TimeZoneListItem* currentItem = NULL; 430 for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); 431 ++zoneIter) { 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; 438 ++next; 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()) 445 : NULL); 446 zoneIter->second->SetTimeZone(currentItem->HasTimeZone() 447 ? new(std::nothrow) BTimeZone(currentItem->TimeZone()) 448 : NULL); 449 zoneIter->second->SetOutlineLevel(1); 450 delete currentItem; 451 } 452 } 453 454 fZoneList->AddItem(zoneIter->second); 455 if (zoneIter->second->OutlineLevel() == 1) { 456 lastWasCountryItem = true; 457 currentItem = zoneIter->second; 458 } else 459 lastWasCountryItem = false; 460 } 461 } 462 463 464 void 465 TimeZoneView::_Revert() 466 { 467 fCurrentZoneItem = fOldZoneItem; 468 469 if (fCurrentZoneItem != NULL) { 470 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 471 fZoneList->Select(currentZoneIndex); 472 } else 473 fZoneList->DeselectAll(); 474 fZoneList->ScrollToSelection(); 475 476 fUseGmtTime = fOldUseGmtTime; 477 if (fUseGmtTime) 478 fGmtTime->SetValue(B_CONTROL_ON); 479 else 480 fLocalTime->SetValue(B_CONTROL_ON); 481 _ShowOrHidePreview(); 482 483 _UpdateGmtSettings(); 484 _SetSystemTimeZone(); 485 _UpdatePreview(); 486 _UpdateCurrent(); 487 } 488 489 490 void 491 TimeZoneView::_UpdatePreview() 492 { 493 int32 selection = fZoneList->CurrentSelection(); 494 TimeZoneListItem* item 495 = selection < 0 496 ? NULL 497 : static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 498 499 if (item == NULL || !item->HasTimeZone()) { 500 fPreview->SetText(""); 501 fPreview->SetTime(""); 502 return; 503 } 504 505 BString timeString = _FormatTime(item->TimeZone()); 506 fPreview->SetText(item->Text()); 507 fPreview->SetTime(timeString.String()); 508 509 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 510 } 511 512 513 void 514 TimeZoneView::_UpdateCurrent() 515 { 516 if (fCurrentZoneItem == NULL) 517 return; 518 519 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 520 fCurrent->SetText(fCurrentZoneItem->Text()); 521 fCurrent->SetTime(timeString.String()); 522 } 523 524 525 void 526 TimeZoneView::_SetSystemTimeZone() 527 { 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 531 */ 532 533 int32 selection = fZoneList->CurrentSelection(); 534 if (selection < 0) 535 return; 536 537 TimeZoneListItem* item 538 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 539 if (item == NULL || !item->HasTimeZone()) 540 return; 541 542 fCurrentZoneItem = item; 543 const BTimeZone& timeZone = item->TimeZone(); 544 545 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); 546 547 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 548 timeZone.ID().Length()); 549 550 fSetZone->SetEnabled(false); 551 fLastUpdateMinute = -1; 552 // just to trigger updating immediately 553 } 554 555 556 BString 557 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 558 { 559 BString result; 560 561 time_t now = time(NULL); 562 bool rtcIsGMT; 563 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 564 if (!rtcIsGMT) { 565 int32 currentOffset 566 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 567 ? fCurrentZoneItem->OffsetFromGMT() 568 : 0; 569 now -= timeZone.OffsetFromGMT() - currentOffset; 570 } 571 fTimeFormat.Format(result, now, B_SHORT_TIME_FORMAT, &timeZone); 572 573 return result; 574 } 575 576 577 void 578 TimeZoneView::_ReadRTCSettings() 579 { 580 BPath path; 581 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) 582 return; 583 584 path.Append("RTC_time_settings"); 585 586 BEntry entry(path.Path()); 587 if (entry.Exists()) { 588 BFile file(&entry, B_READ_ONLY); 589 if (file.InitCheck() == B_OK) { 590 char buffer[6]; 591 file.Read(buffer, 6); 592 if (strncmp(buffer, "gmt", 3) == 0) 593 fUseGmtTime = true; 594 } 595 } 596 } 597 598 599 void 600 TimeZoneView::_WriteRTCSettings() 601 { 602 BPath path; 603 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK) 604 return; 605 606 path.Append("RTC_time_settings"); 607 608 BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 609 if (file.InitCheck() == B_OK) { 610 if (fUseGmtTime) 611 file.Write("gmt", 3); 612 else 613 file.Write("local", 5); 614 } 615 } 616 617 618 void 619 TimeZoneView::_UpdateGmtSettings() 620 { 621 _WriteRTCSettings(); 622 623 _ShowOrHidePreview(); 624 625 _kern_set_real_time_clock_is_gmt(fUseGmtTime); 626 } 627 628 629 void 630 TimeZoneView::_ShowOrHidePreview() 631 { 632 if (fUseGmtTime) { 633 // Hardware clock uses GMT time, changing timezone will adjust the 634 // offset and we need to display a preview 635 fCurrent->Show(); 636 fPreview->Show(); 637 } else { 638 // Hardware clock uses local time, changing timezone will adjust the 639 // clock and there is no offset to manage, thus, no preview. 640 fCurrent->Hide(); 641 fPreview->Hide(); 642 } 643 } 644