1 /* 2 * Copyright 2004-2010, 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 * Julun <host.haiku@gmx.de> 8 * Philippe Saint-Pierre <stpere@gmail.com> 9 * Adrien Destugues <pulkomandy@pulkomandy.ath.cx> 10 * Oliver Tappe <zooey@hirschkaefer.de> 11 * Hamish Morrison <hamish@lavabit.com> 12 */ 13 14 15 #include "ZoneView.h" 16 17 #include <stdlib.h> 18 #include <syscalls.h> 19 20 #include <map> 21 #include <new> 22 23 #include <AutoDeleter.h> 24 #include <Button.h> 25 #include <Catalog.h> 26 #include <Collator.h> 27 #include <ControlLook.h> 28 #include <Country.h> 29 #include <Directory.h> 30 #include <Entry.h> 31 #include <File.h> 32 #include <FindDirectory.h> 33 #include <ListItem.h> 34 #include <Locale.h> 35 #include <MutableLocaleRoster.h> 36 #include <OutlineListView.h> 37 #include <Path.h> 38 #include <RadioButton.h> 39 #include <ScrollView.h> 40 #include <StorageDefs.h> 41 #include <String.h> 42 #include <StringView.h> 43 #include <TimeZone.h> 44 #include <ToolTip.h> 45 #include <View.h> 46 #include <Window.h> 47 48 #include <unicode/datefmt.h> 49 #include <unicode/utmscale.h> 50 #include <ICUWrapper.h> 51 52 #include "TimeMessages.h" 53 #include "TimeZoneListItem.h" 54 #include "TZDisplay.h" 55 56 57 #undef B_TRANSLATE_CONTEXT 58 #define B_TRANSLATE_CONTEXT "Time" 59 60 61 using BPrivate::MutableLocaleRoster; 62 using BPrivate::ObjectDeleter; 63 64 65 struct TimeZoneItemLess { 66 bool operator()(const BString& first, const BString& second) 67 { 68 // sort anything starting with '<' behind anything else 69 if (first.ByteAt(0) == '<') { 70 if (second.ByteAt(0) != '<') 71 return false; 72 } else if (second.ByteAt(0) == '<') 73 return true; 74 return fCollator.Compare(first.String(), second.String()) < 0; 75 } 76 private: 77 BCollator fCollator; 78 }; 79 80 81 82 TimeZoneView::TimeZoneView(const char* name) 83 : 84 BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING), 85 fGmtTime(NULL), 86 fToolTip(NULL), 87 fUseGmtTime(false), 88 fCurrentZoneItem(NULL), 89 fOldZoneItem(NULL), 90 fInitialized(false) 91 { 92 _ReadRTCSettings(); 93 _InitView(); 94 } 95 96 97 bool 98 TimeZoneView::CheckCanRevert() 99 { 100 // check GMT vs Local setting 101 bool enable = fUseGmtTime != fOldUseGmtTime; 102 103 return enable || fCurrentZoneItem != fOldZoneItem; 104 } 105 106 107 TimeZoneView::~TimeZoneView() 108 { 109 if (fToolTip != NULL) 110 fToolTip->ReleaseReference(); 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 _NotifyClockSettingChanged(); 171 break; 172 } 173 174 case kMsgRevert: 175 _Revert(); 176 break; 177 178 case kRTCUpdate: 179 fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON; 180 _UpdateGmtSettings(); 181 _UpdateCurrent(); 182 _UpdatePreview(); 183 break; 184 185 default: 186 BGroupView::MessageReceived(message); 187 break; 188 } 189 } 190 191 192 bool 193 TimeZoneView::GetToolTipAt(BPoint point, BToolTip** _tip) 194 { 195 TimeZoneListItem* item = static_cast<TimeZoneListItem*>( 196 fZoneList->ItemAt(fZoneList->IndexOf(point))); 197 if (item == NULL || !item->HasTimeZone()) 198 return false; 199 200 BString nowInTimeZone; 201 time_t now = time(NULL); 202 BLocale::Default()->FormatTime(&nowInTimeZone, now, B_SHORT_TIME_FORMAT, 203 &item->TimeZone()); 204 205 BString dateInTimeZone; 206 BLocale::Default()->FormatDate(&dateInTimeZone, now, B_SHORT_DATE_FORMAT, 207 &item->TimeZone()); 208 209 BString toolTip = item->Text(); 210 toolTip << '\n' << item->TimeZone().ShortName() << " / " 211 << item->TimeZone().ShortDaylightSavingName() 212 << B_TRANSLATE("\nNow: ") << nowInTimeZone 213 << " (" << dateInTimeZone << ')'; 214 215 if (fToolTip != NULL) 216 fToolTip->ReleaseReference(); 217 fToolTip = new (std::nothrow) BTextToolTip(toolTip.String()); 218 if (fToolTip == NULL) 219 return false; 220 221 *_tip = fToolTip; 222 223 return true; 224 } 225 226 227 void 228 TimeZoneView::_UpdateDateTime(BMessage* message) 229 { 230 // only need to update once every minute 231 int32 minute; 232 if (message->FindInt32("minute", &minute) == B_OK) { 233 if (fLastUpdateMinute != minute) { 234 _UpdateCurrent(); 235 _UpdatePreview(); 236 237 fLastUpdateMinute = minute; 238 } 239 } 240 } 241 242 243 void 244 TimeZoneView::_InitView() 245 { 246 fZoneList = new BOutlineListView("cityList", B_SINGLE_SELECTION_LIST); 247 fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED)); 248 fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE)); 249 _BuildZoneMenu(); 250 BScrollView* scrollList = new BScrollView("scrollList", fZoneList, 251 B_FRAME_EVENTS | B_WILL_DRAW, false, true); 252 scrollList->SetExplicitMinSize(BSize(200, 0)); 253 254 fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:")); 255 fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:")); 256 257 fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"), 258 new BMessage(H_SET_TIME_ZONE)); 259 fSetZone->SetEnabled(false); 260 fSetZone->SetExplicitAlignment( 261 BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM)); 262 263 BStringView* text = new BStringView("clockSetTo", 264 B_TRANSLATE("Hardware clock set to:")); 265 fLocalTime = new BRadioButton("localTime", 266 B_TRANSLATE("Local time (Windows compatible)"), new BMessage(kRTCUpdate)); 267 fGmtTime = new BRadioButton("greenwichMeanTime", 268 B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate)); 269 270 if (fUseGmtTime) 271 fGmtTime->SetValue(B_CONTROL_ON); 272 else 273 fLocalTime->SetValue(B_CONTROL_ON); 274 _ShowOrHidePreview(); 275 fOldUseGmtTime = fUseGmtTime; 276 277 278 const float kInset = be_control_look->DefaultItemSpacing(); 279 BLayoutBuilder::Group<>(this) 280 .Add(scrollList) 281 .AddGroup(B_VERTICAL, kInset) 282 .Add(text) 283 .AddGroup(B_VERTICAL, kInset) 284 .Add(fLocalTime) 285 .Add(fGmtTime) 286 .End() 287 .AddGlue() 288 .Add(fCurrent) 289 .Add(fPreview) 290 .Add(fSetZone) 291 .End() 292 .SetInsets(kInset, kInset, kInset, kInset); 293 } 294 295 296 void 297 TimeZoneView::_BuildZoneMenu() 298 { 299 BTimeZone defaultTimeZone; 300 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); 301 302 BLanguage language; 303 BLocale::Default()->GetLanguage(&language); 304 305 BMessage countryList; 306 BLocaleRoster::Default()->GetAvailableCountries(&countryList); 307 countryList.AddString("country", ""); 308 309 /* 310 * Group timezones by regions, but filter out unwanted (duplicate) regions 311 * and add an additional region with generic GMT-offset timezones at the end 312 */ 313 typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap; 314 ZoneItemMap zoneItemMap; 315 const char* kOtherRegion = B_TRANSLATE_MARK("<Other>"); 316 const char* kSupportedRegions[] = { 317 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"), 318 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"), 319 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"), 320 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"), 321 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"), 322 kOtherRegion, 323 NULL 324 }; 325 // Since the zone-map contains translated country-names (we get those from 326 // ICU), we need to use translated region names in the zone-map, too: 327 typedef std::map<BString, BString> TranslatedRegionMap; 328 TranslatedRegionMap regionMap; 329 for (const char** region = kSupportedRegions; *region != NULL; ++region) { 330 BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region); 331 regionMap[*region] = translatedRegion; 332 333 TimeZoneListItem* regionItem 334 = new TimeZoneListItem(translatedRegion, NULL, NULL); 335 regionItem->SetOutlineLevel(0); 336 zoneItemMap[translatedRegion] = regionItem; 337 } 338 339 BString countryCode; 340 for (int c = 0; countryList.FindString("country", c, &countryCode) 341 == B_OK; c++) { 342 BCountry country(countryCode); 343 BString countryName; 344 country.GetName(countryName); 345 346 // Now list the timezones for this country 347 BMessage zoneList; 348 BLocaleRoster::Default()->GetAvailableTimeZonesForCountry(&zoneList, 349 countryCode.Length() == 0 ? NULL : countryCode.String()); 350 351 int32 count = 0; 352 type_code dummy; 353 zoneList.GetInfo("timeZone", &dummy, &count); 354 355 BString zoneID; 356 for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK; 357 tz++) { 358 int32 slashPos = zoneID.FindFirst('/'); 359 360 // ignore any "global" timezones, as those are just aliases of 361 // regional ones 362 if (slashPos <= 0) 363 continue; 364 365 BString region(zoneID, slashPos); 366 367 if (region == "Etc") 368 region = kOtherRegion; 369 else if (countryName.Length() == 0) { 370 // skip global timezones from other regions, we are just 371 // interested in the generic GMT-based ones under "Etc/" 372 continue; 373 } 374 375 // just accept timezones from our supported regions, others are 376 // aliases and would just make the list even longer 377 TranslatedRegionMap::iterator regionIter = regionMap.find(region); 378 if (regionIter == regionMap.end()) 379 continue; 380 const BString& regionName = regionIter->second; 381 382 BString fullCountryID = regionName; 383 bool countryIsRegion = countryName == regionName; 384 if (!countryIsRegion) 385 fullCountryID << "/" << countryName; 386 387 BTimeZone* timeZone = new BTimeZone(zoneID, &language); 388 BString tzName = timeZone->Name(); 389 if (tzName == "GMT+00:00") 390 tzName = "GMT"; 391 392 int32 openParenthesisPos = tzName.FindFirst('('); 393 if (openParenthesisPos >= 0) { 394 tzName.Remove(0, openParenthesisPos + 1); 395 int32 closeParenthesisPos = tzName.FindLast(')'); 396 if (closeParenthesisPos >= 0) 397 tzName.Truncate(closeParenthesisPos); 398 } 399 BString fullZoneID = fullCountryID; 400 fullZoneID << "/" << tzName; 401 402 // skip duplicates 403 ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); 404 if (zoneIter != zoneItemMap.end()) { 405 delete timeZone; 406 continue; 407 } 408 409 TimeZoneListItem* countryItem = NULL; 410 TimeZoneListItem* zoneItem = NULL; 411 if (count > 1 && countryName.Length() > 0) { 412 ZoneItemMap::iterator countryIter 413 = zoneItemMap.find(fullCountryID); 414 if (countryIter == zoneItemMap.end()) { 415 countryItem = new TimeZoneListItem(countryName, NULL, NULL); 416 countryItem->SetOutlineLevel(1); 417 zoneItemMap[fullCountryID] = countryItem; 418 } else 419 countryItem = countryIter->second; 420 421 zoneItem = new TimeZoneListItem(tzName, NULL, timeZone); 422 zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); 423 } else { 424 BString& name = countryName.Length() > 0 ? countryName : tzName; 425 zoneItem = new TimeZoneListItem(name, NULL, timeZone); 426 zoneItem->SetOutlineLevel(1); 427 } 428 zoneItemMap[fullZoneID] = zoneItem; 429 430 if (timeZone->ID() == defaultTimeZone.ID()) { 431 fCurrentZoneItem = zoneItem; 432 if (countryItem != NULL) 433 countryItem->SetExpanded(true); 434 ZoneItemMap::iterator regionItemIter 435 = zoneItemMap.find(regionName); 436 if (regionItemIter != zoneItemMap.end()) 437 regionItemIter->second->SetExpanded(true); 438 } 439 } 440 } 441 442 fOldZoneItem = fCurrentZoneItem; 443 444 ZoneItemMap::iterator zoneIter; 445 bool lastWasCountryItem = false; 446 TimeZoneListItem* currentCountryItem = NULL; 447 for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); 448 ++zoneIter) { 449 if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { 450 /* Some countries (e.g. Spain and Chile) have their timezones 451 * spread across different regions. As a result, there might still 452 * be country items with only one timezone below them. We manually 453 * filter those country items here. 454 */ 455 ZoneItemMap::iterator next = zoneIter; 456 ++next; 457 if (next != zoneItemMap.end() 458 && next->second->OutlineLevel() != 2) { 459 fZoneList->RemoveItem(currentCountryItem); 460 zoneIter->second->SetText(currentCountryItem->Text()); 461 zoneIter->second->SetOutlineLevel(1); 462 delete currentCountryItem; 463 } 464 } 465 466 fZoneList->AddItem(zoneIter->second); 467 if (zoneIter->second->OutlineLevel() == 1) { 468 lastWasCountryItem = true; 469 currentCountryItem = zoneIter->second; 470 } else 471 lastWasCountryItem = false; 472 } 473 } 474 475 476 void 477 TimeZoneView::_Revert() 478 { 479 fCurrentZoneItem = fOldZoneItem; 480 481 if (fCurrentZoneItem != NULL) { 482 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 483 fZoneList->Select(currentZoneIndex); 484 } else 485 fZoneList->DeselectAll(); 486 fZoneList->ScrollToSelection(); 487 488 fUseGmtTime = fOldUseGmtTime; 489 if (fUseGmtTime) 490 fGmtTime->SetValue(B_CONTROL_ON); 491 else 492 fLocalTime->SetValue(B_CONTROL_ON); 493 _ShowOrHidePreview(); 494 495 _UpdateGmtSettings(); 496 _SetSystemTimeZone(); 497 _UpdatePreview(); 498 _UpdateCurrent(); 499 } 500 501 502 void 503 TimeZoneView::_UpdatePreview() 504 { 505 int32 selection = fZoneList->CurrentSelection(); 506 TimeZoneListItem* item 507 = selection < 0 508 ? NULL 509 : (TimeZoneListItem*)fZoneList->ItemAt(selection); 510 511 if (item == NULL || !item->HasTimeZone()) { 512 fPreview->SetText(""); 513 fPreview->SetTime(""); 514 return; 515 } 516 517 BString timeString = _FormatTime(item->TimeZone()); 518 fPreview->SetText(item->Text()); 519 fPreview->SetTime(timeString.String()); 520 521 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 522 } 523 524 525 void 526 TimeZoneView::_UpdateCurrent() 527 { 528 if (fCurrentZoneItem == NULL) 529 return; 530 531 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 532 fCurrent->SetText(fCurrentZoneItem->Text()); 533 fCurrent->SetTime(timeString.String()); 534 } 535 536 537 void 538 TimeZoneView::_SetSystemTimeZone() 539 { 540 /* Set sytem timezone for all different API levels. How to do this? 541 * 1) tell locale-roster about new default timezone 542 * 2) tell kernel about new timezone offset 543 */ 544 545 int32 selection = fZoneList->CurrentSelection(); 546 if (selection < 0) 547 return; 548 549 TimeZoneListItem* item 550 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 551 if (item == NULL || !item->HasTimeZone()) 552 return; 553 554 fCurrentZoneItem = item; 555 const BTimeZone& timeZone = item->TimeZone(); 556 557 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); 558 559 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 560 timeZone.ID().Length()); 561 562 fSetZone->SetEnabled(false); 563 fLastUpdateMinute = -1; 564 // just to trigger updating immediately 565 } 566 567 568 BString 569 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 570 { 571 BString result; 572 573 time_t now = time(NULL); 574 bool rtcIsGMT; 575 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 576 if (!rtcIsGMT) { 577 int32 currentOffset 578 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 579 ? fCurrentZoneItem->OffsetFromGMT() 580 : 0; 581 now -= timeZone.OffsetFromGMT() - currentOffset; 582 } 583 BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT, 584 &timeZone); 585 586 return result; 587 } 588 589 590 void 591 TimeZoneView::_ReadRTCSettings() 592 { 593 BPath path; 594 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) 595 return; 596 597 path.Append("RTC_time_settings"); 598 599 BEntry entry(path.Path()); 600 if (entry.Exists()) { 601 BFile file(&entry, B_READ_ONLY); 602 if (file.InitCheck() == B_OK) { 603 char buffer[6]; 604 file.Read(buffer, 6); 605 if (strncmp(buffer, "gmt", 3) == 0) 606 fUseGmtTime = true; 607 } 608 } 609 } 610 611 612 void 613 TimeZoneView::_WriteRTCSettings() 614 { 615 BPath path; 616 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK) 617 return; 618 619 path.Append("RTC_time_settings"); 620 621 BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 622 if (file.InitCheck() == B_OK) { 623 if (fUseGmtTime) 624 file.Write("gmt", 3); 625 else 626 file.Write("local", 5); 627 } 628 } 629 630 631 void 632 TimeZoneView::_UpdateGmtSettings() 633 { 634 _WriteRTCSettings(); 635 636 _ShowOrHidePreview(); 637 _NotifyClockSettingChanged(); 638 639 _kern_set_real_time_clock_is_gmt(fUseGmtTime); 640 } 641 642 643 void 644 TimeZoneView::_ShowOrHidePreview() 645 { 646 if (fUseGmtTime) { 647 // Hardware clock uses GMT time, changing timezone will adjust the 648 // offset and we need to display a preview 649 fCurrent->Show(); 650 fPreview->Show(); 651 } else { 652 // Hardware clock uses local time, changing timezone will adjust the 653 // clock and there is no offset to manage, thus, no preview. 654 fCurrent->Hide(); 655 fPreview->Hide(); 656 } 657 } 658 659 660 void 661 TimeZoneView::_NotifyClockSettingChanged() 662 { 663 BMessage msg(kMsgClockSettingChanged); 664 Window()->PostMessage(&msg); 665 } 666 667