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 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 bool 192 TimeZoneView::GetToolTipAt(BPoint point, BToolTip** _tip) 193 { 194 TimeZoneListItem* item = static_cast<TimeZoneListItem*>( 195 fZoneList->ItemAt(fZoneList->IndexOf(point))); 196 if (item == NULL || !item->HasTimeZone()) 197 return false; 198 199 BString nowInTimeZone; 200 time_t now = time(NULL); 201 BLocale::Default()->FormatTime(&nowInTimeZone, now, B_SHORT_TIME_FORMAT, 202 &item->TimeZone()); 203 204 BString dateInTimeZone; 205 BLocale::Default()->FormatDate(&dateInTimeZone, now, B_SHORT_DATE_FORMAT, 206 &item->TimeZone()); 207 208 BString toolTip = item->Text(); 209 toolTip << '\n' << item->TimeZone().ShortName() << " / " 210 << item->TimeZone().ShortDaylightSavingName() 211 << B_TRANSLATE("\nNow: ") << nowInTimeZone 212 << " (" << dateInTimeZone << ')'; 213 214 if (fToolTip != NULL) 215 fToolTip->ReleaseReference(); 216 fToolTip = new (std::nothrow) BTextToolTip(toolTip.String()); 217 if (fToolTip == NULL) 218 return false; 219 220 *_tip = fToolTip; 221 222 return true; 223 } 224 225 226 void 227 TimeZoneView::_UpdateDateTime(BMessage* message) 228 { 229 // only need to update once every minute 230 int32 minute; 231 if (message->FindInt32("minute", &minute) == B_OK) { 232 if (fLastUpdateMinute != minute) { 233 _UpdateCurrent(); 234 _UpdatePreview(); 235 236 fLastUpdateMinute = minute; 237 } 238 } 239 } 240 241 242 void 243 TimeZoneView::_InitView() 244 { 245 fZoneList = new BOutlineListView("cityList", B_SINGLE_SELECTION_LIST); 246 fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED)); 247 fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE)); 248 _BuildZoneMenu(); 249 BScrollView* scrollList = new BScrollView("scrollList", fZoneList, 250 B_FRAME_EVENTS | B_WILL_DRAW, false, true); 251 scrollList->SetExplicitMinSize(BSize(200, 0)); 252 253 fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:")); 254 fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:")); 255 256 fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"), 257 new BMessage(H_SET_TIME_ZONE)); 258 fSetZone->SetEnabled(false); 259 fSetZone->SetExplicitAlignment( 260 BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM)); 261 262 BStringView* text = new BStringView("clockSetTo", 263 B_TRANSLATE("Hardware clock set to:")); 264 fLocalTime = new BRadioButton("localTime", 265 B_TRANSLATE("Local time (Windows compatible)"), new BMessage(kRTCUpdate)); 266 fGmtTime = new BRadioButton("greenwichMeanTime", 267 B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate)); 268 269 if (fUseGmtTime) 270 fGmtTime->SetValue(B_CONTROL_ON); 271 else 272 fLocalTime->SetValue(B_CONTROL_ON); 273 _ShowOrHidePreview(); 274 fOldUseGmtTime = fUseGmtTime; 275 276 277 const float kInset = be_control_look->DefaultItemSpacing(); 278 BLayoutBuilder::Group<>(this) 279 .Add(scrollList) 280 .AddGroup(B_VERTICAL, kInset) 281 .Add(text) 282 .AddGroup(B_VERTICAL, kInset) 283 .Add(fLocalTime) 284 .Add(fGmtTime) 285 .End() 286 .AddGlue() 287 .Add(fCurrent) 288 .Add(fPreview) 289 .Add(fSetZone) 290 .End() 291 .SetInsets(kInset, kInset, kInset, kInset); 292 } 293 294 295 void 296 TimeZoneView::_BuildZoneMenu() 297 { 298 BTimeZone defaultTimeZone; 299 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); 300 301 BLanguage language; 302 BLocale::Default()->GetLanguage(&language); 303 304 BMessage countryList; 305 BLocaleRoster::Default()->GetAvailableCountries(&countryList); 306 countryList.AddString("country", ""); 307 308 /* 309 * Group timezones by regions, but filter out unwanted (duplicate) regions 310 * and add an additional region with generic GMT-offset timezones at the end 311 */ 312 typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap; 313 ZoneItemMap zoneItemMap; 314 const char* kOtherRegion = B_TRANSLATE_MARK("<Other>"); 315 const char* kSupportedRegions[] = { 316 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"), 317 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"), 318 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"), 319 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"), 320 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"), 321 kOtherRegion, 322 NULL 323 }; 324 // Since the zone-map contains translated country-names (we get those from 325 // ICU), we need to use translated region names in the zone-map, too: 326 typedef std::map<BString, BString> TranslatedRegionMap; 327 TranslatedRegionMap regionMap; 328 for (const char** region = kSupportedRegions; *region != NULL; ++region) { 329 BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region); 330 regionMap[*region] = translatedRegion; 331 332 TimeZoneListItem* regionItem 333 = new TimeZoneListItem(translatedRegion, NULL, NULL); 334 regionItem->SetOutlineLevel(0); 335 zoneItemMap[translatedRegion] = regionItem; 336 } 337 338 BString countryCode; 339 for (int c = 0; countryList.FindString("country", c, &countryCode) 340 == B_OK; c++) { 341 BCountry country(countryCode); 342 BString countryName; 343 country.GetName(countryName); 344 345 // Now list the timezones for this country 346 BMessage zoneList; 347 BLocaleRoster::Default()->GetAvailableTimeZonesForCountry(&zoneList, 348 countryCode.Length() == 0 ? NULL : countryCode.String()); 349 350 int32 count = 0; 351 type_code dummy; 352 zoneList.GetInfo("timeZone", &dummy, &count); 353 354 BString zoneID; 355 for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK; 356 tz++) { 357 int32 slashPos = zoneID.FindFirst('/'); 358 359 // ignore any "global" timezones, as those are just aliases of 360 // regional ones 361 if (slashPos <= 0) 362 continue; 363 364 BString region(zoneID, slashPos); 365 366 if (region == "Etc") 367 region = kOtherRegion; 368 else if (countryName.Length() == 0) { 369 // skip global timezones from other regions, we are just 370 // interested in the generic GMT-based ones under "Etc/" 371 continue; 372 } 373 374 // just accept timezones from our supported regions, others are 375 // aliases and would just make the list even longer 376 TranslatedRegionMap::iterator regionIter = regionMap.find(region); 377 if (regionIter == regionMap.end()) 378 continue; 379 const BString& regionName = regionIter->second; 380 381 BString fullCountryID = regionName; 382 bool countryIsRegion = countryName == regionName; 383 if (!countryIsRegion) 384 fullCountryID << "/" << countryName; 385 386 BTimeZone* timeZone = new BTimeZone(zoneID, &language); 387 BString tzName = timeZone->Name(); 388 if (tzName == "GMT+00:00") 389 tzName = "GMT"; 390 391 int32 openParenthesisPos = tzName.FindFirst('('); 392 if (openParenthesisPos >= 0) { 393 tzName.Remove(0, openParenthesisPos + 1); 394 int32 closeParenthesisPos = tzName.FindLast(')'); 395 if (closeParenthesisPos >= 0) 396 tzName.Truncate(closeParenthesisPos); 397 } 398 BString fullZoneID = fullCountryID; 399 fullZoneID << "/" << tzName; 400 401 // skip duplicates 402 ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); 403 if (zoneIter != zoneItemMap.end()) { 404 delete timeZone; 405 continue; 406 } 407 408 TimeZoneListItem* countryItem = NULL; 409 TimeZoneListItem* zoneItem = NULL; 410 if (count > 1 && countryName.Length() > 0) { 411 ZoneItemMap::iterator countryIter 412 = zoneItemMap.find(fullCountryID); 413 if (countryIter == zoneItemMap.end()) { 414 countryItem = new TimeZoneListItem(countryName, NULL, NULL); 415 countryItem->SetOutlineLevel(1); 416 zoneItemMap[fullCountryID] = countryItem; 417 } else 418 countryItem = countryIter->second; 419 420 zoneItem = new TimeZoneListItem(tzName, NULL, timeZone); 421 zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); 422 } else { 423 BString& name = countryName.Length() > 0 ? countryName : tzName; 424 zoneItem = new TimeZoneListItem(name, NULL, timeZone); 425 zoneItem->SetOutlineLevel(1); 426 } 427 zoneItemMap[fullZoneID] = zoneItem; 428 429 if (timeZone->ID() == defaultTimeZone.ID()) { 430 fCurrentZoneItem = zoneItem; 431 if (countryItem != NULL) 432 countryItem->SetExpanded(true); 433 ZoneItemMap::iterator regionItemIter 434 = zoneItemMap.find(regionName); 435 if (regionItemIter != zoneItemMap.end()) 436 regionItemIter->second->SetExpanded(true); 437 } 438 } 439 } 440 441 fOldZoneItem = fCurrentZoneItem; 442 443 ZoneItemMap::iterator zoneIter; 444 bool lastWasCountryItem = false; 445 TimeZoneListItem* currentCountryItem = NULL; 446 for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); 447 ++zoneIter) { 448 if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { 449 /* Some countries (e.g. Spain and Chile) have their timezones 450 * spread across different regions. As a result, there might still 451 * be country items with only one timezone below them. We manually 452 * filter those country items here. 453 */ 454 ZoneItemMap::iterator next = zoneIter; 455 ++next; 456 if (next != zoneItemMap.end() 457 && next->second->OutlineLevel() != 2) { 458 fZoneList->RemoveItem(currentCountryItem); 459 zoneIter->second->SetText(currentCountryItem->Text()); 460 zoneIter->second->SetOutlineLevel(1); 461 delete currentCountryItem; 462 } 463 } 464 465 fZoneList->AddItem(zoneIter->second); 466 if (zoneIter->second->OutlineLevel() == 1) { 467 lastWasCountryItem = true; 468 currentCountryItem = zoneIter->second; 469 } else 470 lastWasCountryItem = false; 471 } 472 } 473 474 475 void 476 TimeZoneView::_Revert() 477 { 478 fCurrentZoneItem = fOldZoneItem; 479 480 if (fCurrentZoneItem != NULL) { 481 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 482 fZoneList->Select(currentZoneIndex); 483 } else 484 fZoneList->DeselectAll(); 485 fZoneList->ScrollToSelection(); 486 487 fUseGmtTime = fOldUseGmtTime; 488 if (fUseGmtTime) 489 fGmtTime->SetValue(B_CONTROL_ON); 490 else 491 fLocalTime->SetValue(B_CONTROL_ON); 492 _ShowOrHidePreview(); 493 494 _UpdateGmtSettings(); 495 _SetSystemTimeZone(); 496 _UpdatePreview(); 497 _UpdateCurrent(); 498 } 499 500 501 void 502 TimeZoneView::_UpdatePreview() 503 { 504 int32 selection = fZoneList->CurrentSelection(); 505 TimeZoneListItem* item 506 = selection < 0 507 ? NULL 508 : (TimeZoneListItem*)fZoneList->ItemAt(selection); 509 510 if (item == NULL || !item->HasTimeZone()) { 511 fPreview->SetText(""); 512 fPreview->SetTime(""); 513 return; 514 } 515 516 BString timeString = _FormatTime(item->TimeZone()); 517 fPreview->SetText(item->Text()); 518 fPreview->SetTime(timeString.String()); 519 520 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 521 } 522 523 524 void 525 TimeZoneView::_UpdateCurrent() 526 { 527 if (fCurrentZoneItem == NULL) 528 return; 529 530 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 531 fCurrent->SetText(fCurrentZoneItem->Text()); 532 fCurrent->SetTime(timeString.String()); 533 } 534 535 536 void 537 TimeZoneView::_SetSystemTimeZone() 538 { 539 /* Set sytem timezone for all different API levels. How to do this? 540 * 1) tell locale-roster about new default timezone 541 * 2) tell kernel about new timezone offset 542 */ 543 544 int32 selection = fZoneList->CurrentSelection(); 545 if (selection < 0) 546 return; 547 548 TimeZoneListItem* item 549 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 550 if (item == NULL || !item->HasTimeZone()) 551 return; 552 553 fCurrentZoneItem = item; 554 const BTimeZone& timeZone = item->TimeZone(); 555 556 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); 557 558 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 559 timeZone.ID().Length()); 560 561 fSetZone->SetEnabled(false); 562 fLastUpdateMinute = -1; 563 // just to trigger updating immediately 564 } 565 566 567 BString 568 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 569 { 570 BString result; 571 572 time_t now = time(NULL); 573 bool rtcIsGMT; 574 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 575 if (!rtcIsGMT) { 576 int32 currentOffset 577 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 578 ? fCurrentZoneItem->OffsetFromGMT() 579 : 0; 580 now -= timeZone.OffsetFromGMT() - currentOffset; 581 } 582 BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT, 583 &timeZone); 584 585 return result; 586 } 587 588 589 void 590 TimeZoneView::_ReadRTCSettings() 591 { 592 BPath path; 593 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) 594 return; 595 596 path.Append("RTC_time_settings"); 597 598 BEntry entry(path.Path()); 599 if (entry.Exists()) { 600 BFile file(&entry, B_READ_ONLY); 601 if (file.InitCheck() == B_OK) { 602 char buffer[6]; 603 file.Read(buffer, 6); 604 if (strncmp(buffer, "gmt", 3) == 0) 605 fUseGmtTime = true; 606 } 607 } 608 } 609 610 611 void 612 TimeZoneView::_WriteRTCSettings() 613 { 614 BPath path; 615 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK) 616 return; 617 618 path.Append("RTC_time_settings"); 619 620 BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 621 if (file.InitCheck() == B_OK) { 622 if (fUseGmtTime) 623 file.Write("gmt", 3); 624 else 625 file.Write("local", 5); 626 } 627 } 628 629 630 void 631 TimeZoneView::_UpdateGmtSettings() 632 { 633 _WriteRTCSettings(); 634 635 _ShowOrHidePreview(); 636 637 _kern_set_real_time_clock_is_gmt(fUseGmtTime); 638 } 639 640 641 void 642 TimeZoneView::_ShowOrHidePreview() 643 { 644 if (fUseGmtTime) { 645 // Hardware clock uses GMT time, changing timezone will adjust the 646 // offset and we need to display a preview 647 fCurrent->Show(); 648 fPreview->Show(); 649 } else { 650 // Hardware clock uses local time, changing timezone will adjust the 651 // clock and there is no offset to manage, thus, no preview. 652 fCurrent->Hide(); 653 fPreview->Hide(); 654 } 655 } 656 657