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 #include <vector> 23 24 #include <AutoDeleter.h> 25 #include <Button.h> 26 #include <Catalog.h> 27 #include <Collator.h> 28 #include <ControlLook.h> 29 #include <Country.h> 30 #include <Directory.h> 31 #include <Entry.h> 32 #include <File.h> 33 #include <FindDirectory.h> 34 #include <ListItem.h> 35 #include <Locale.h> 36 #include <MutableLocaleRoster.h> 37 #include <OutlineListView.h> 38 #include <Path.h> 39 #include <RadioButton.h> 40 #include <ScrollView.h> 41 #include <StorageDefs.h> 42 #include <String.h> 43 #include <StringView.h> 44 #include <TimeZone.h> 45 #include <ToolTip.h> 46 #include <View.h> 47 #include <Window.h> 48 49 #include <unicode/datefmt.h> 50 #include <unicode/utmscale.h> 51 #include <ICUWrapper.h> 52 53 #include "TimeMessages.h" 54 #include "TimeZoneListItem.h" 55 #include "TZDisplay.h" 56 57 58 #undef B_TRANSLATION_CONTEXT 59 #define B_TRANSLATION_CONTEXT "Time" 60 61 62 using BPrivate::MutableLocaleRoster; 63 using BPrivate::ObjectDeleter; 64 65 66 struct TimeZoneItemLess { 67 bool operator()(const BString& first, const BString& second) 68 { 69 // sort anything starting with '<' behind anything else 70 if (first.ByteAt(0) == '<') { 71 if (second.ByteAt(0) != '<') 72 return false; 73 } else if (second.ByteAt(0) == '<') 74 return true; 75 return fCollator.Compare(first.String(), second.String()) < 0; 76 } 77 private: 78 BCollator fCollator; 79 }; 80 81 82 83 TimeZoneView::TimeZoneView(const char* name) 84 : 85 BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING), 86 fGmtTime(NULL), 87 fToolTip(NULL), 88 fUseGmtTime(false), 89 fCurrentZoneItem(NULL), 90 fOldZoneItem(NULL), 91 fInitialized(false) 92 { 93 _ReadRTCSettings(); 94 _InitView(); 95 } 96 97 98 bool 99 TimeZoneView::CheckCanRevert() 100 { 101 // check GMT vs Local setting 102 bool enable = fUseGmtTime != fOldUseGmtTime; 103 104 return enable || fCurrentZoneItem != fOldZoneItem; 105 } 106 107 108 TimeZoneView::~TimeZoneView() 109 { 110 if (fToolTip != NULL) 111 fToolTip->ReleaseReference(); 112 _WriteRTCSettings(); 113 } 114 115 116 void 117 TimeZoneView::AttachedToWindow() 118 { 119 BView::AttachedToWindow(); 120 if (Parent()) 121 SetViewColor(Parent()->ViewColor()); 122 123 if (!fInitialized) { 124 fInitialized = true; 125 126 fSetZone->SetTarget(this); 127 fZoneList->SetTarget(this); 128 } 129 } 130 131 132 void 133 TimeZoneView::DoLayout() 134 { 135 BView::DoLayout(); 136 if (fCurrentZoneItem != NULL) { 137 fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem)); 138 fCurrent->SetText(fCurrentZoneItem->Text()); 139 fZoneList->ScrollToSelection(); 140 } 141 } 142 143 144 void 145 TimeZoneView::MessageReceived(BMessage* message) 146 { 147 switch (message->what) { 148 case B_OBSERVER_NOTICE_CHANGE: 149 { 150 int32 change; 151 message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change); 152 switch(change) { 153 case H_TM_CHANGED: 154 _UpdateDateTime(message); 155 break; 156 157 default: 158 BView::MessageReceived(message); 159 break; 160 } 161 break; 162 } 163 164 case H_CITY_CHANGED: 165 _UpdatePreview(); 166 break; 167 168 case H_SET_TIME_ZONE: 169 { 170 _SetSystemTimeZone(); 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 /* 306 * Group timezones by regions, but filter out unwanted (duplicate) regions 307 * and add an additional region with generic GMT-offset timezones at the end 308 */ 309 typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap; 310 ZoneItemMap zoneItemMap; 311 const char* kOtherRegion = B_TRANSLATE_MARK("<Other>"); 312 const char* kSupportedRegions[] = { 313 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"), 314 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"), 315 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"), 316 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"), 317 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"), 318 kOtherRegion, 319 NULL 320 }; 321 // Since the zone-map contains translated country-names (we get those from 322 // ICU), we need to use translated region names in the zone-map, too: 323 typedef std::map<BString, BString> TranslatedRegionMap; 324 TranslatedRegionMap regionMap; 325 for (const char** region = kSupportedRegions; *region != NULL; ++region) { 326 BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region); 327 regionMap[*region] = translatedRegion; 328 329 TimeZoneListItem* regionItem 330 = new TimeZoneListItem(translatedRegion, NULL, NULL); 331 regionItem->SetOutlineLevel(0); 332 zoneItemMap[translatedRegion] = regionItem; 333 } 334 335 // Get all time zones 336 BMessage zoneList; 337 BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList); 338 339 typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap; 340 ZonesByCountyMap zonesByCountryMap; 341 const char *zoneID; 342 BString countryCode; 343 for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK 344 && zoneList.FindString("region", tz, &countryCode) == B_OK; tz++) { 345 // From the global ("001") timezones, we only accept the generic GMT 346 // timezones, as all the other world-zones are duplicates of others. 347 if (countryCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0) 348 continue; 349 zonesByCountryMap[countryCode].push_back(zoneID); 350 } 351 352 ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin(); 353 for (; countryIter != zonesByCountryMap.end(); ++countryIter) { 354 BCountry country(countryIter->first.String()); 355 BString countryName; 356 country.GetName(countryName); 357 358 size_t zoneCountInCountry = countryIter->second.size(); 359 for (size_t tz = 0; tz < zoneCountInCountry; tz++) { 360 BString zoneID(countryIter->second[tz]); 361 int32 slashPos = zoneID.FindFirst('/'); 362 363 BString region(zoneID, slashPos); 364 if (region == "Etc") 365 region = kOtherRegion; 366 367 // just accept timezones from our supported regions, others are 368 // aliases and would just make the list even longer 369 TranslatedRegionMap::iterator regionIter = regionMap.find(region); 370 if (regionIter == regionMap.end()) 371 continue; 372 373 BString fullCountryID = regionIter->second; 374 bool countryIsRegion 375 = countryName == regionIter->second || region == kOtherRegion; 376 if (!countryIsRegion) 377 fullCountryID << "/" << countryName; 378 379 BTimeZone* timeZone = new BTimeZone(zoneID, &language); 380 BString tzName; 381 BString fullZoneID = fullCountryID; 382 if (zoneCountInCountry > 1) 383 { 384 // we can't use the country name as timezone name, since there 385 // are more than one timezones in this country - fetch the 386 // localized name of the timezone and use that 387 tzName = timeZone->Name(); 388 int32 openParenthesisPos = tzName.FindFirst('('); 389 if (openParenthesisPos >= 0) { 390 tzName.Remove(0, openParenthesisPos + 1); 391 int32 closeParenthesisPos = tzName.FindLast(')'); 392 if (closeParenthesisPos >= 0) 393 tzName.Truncate(closeParenthesisPos); 394 } 395 fullZoneID << "/" << tzName; 396 } else { 397 tzName = countryName; 398 fullZoneID << "/" << zoneID; 399 } 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 (zoneCountInCountry > 1) { 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 zoneItem = new TimeZoneListItem(tzName, NULL, timeZone); 424 zoneItem->SetOutlineLevel(1); 425 } 426 zoneItemMap[fullZoneID] = zoneItem; 427 428 if (timeZone->ID() == defaultTimeZone.ID()) { 429 fCurrentZoneItem = zoneItem; 430 if (countryItem != NULL) 431 countryItem->SetExpanded(true); 432 ZoneItemMap::iterator regionItemIter 433 = zoneItemMap.find(regionIter->second); 434 if (regionItemIter != zoneItemMap.end()) 435 regionItemIter->second->SetExpanded(true); 436 } 437 } 438 } 439 440 fOldZoneItem = fCurrentZoneItem; 441 442 ZoneItemMap::iterator zoneIter; 443 bool lastWasCountryItem = false; 444 TimeZoneListItem* currentCountryItem = NULL; 445 for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); 446 ++zoneIter) { 447 if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { 448 /* Some countries (e.g. Spain and Chile) have their timezones 449 * spread across different regions. As a result, there might still 450 * be country items with only one timezone below them. We manually 451 * filter those country items here. 452 */ 453 ZoneItemMap::iterator next = zoneIter; 454 ++next; 455 if (next != zoneItemMap.end() 456 && next->second->OutlineLevel() != 2) { 457 fZoneList->RemoveItem(currentCountryItem); 458 zoneIter->second->SetText(currentCountryItem->Text()); 459 zoneIter->second->SetOutlineLevel(1); 460 delete currentCountryItem; 461 } 462 } 463 464 fZoneList->AddItem(zoneIter->second); 465 if (zoneIter->second->OutlineLevel() == 1) { 466 lastWasCountryItem = true; 467 currentCountryItem = zoneIter->second; 468 } else 469 lastWasCountryItem = false; 470 } 471 } 472 473 474 void 475 TimeZoneView::_Revert() 476 { 477 fCurrentZoneItem = fOldZoneItem; 478 479 if (fCurrentZoneItem != NULL) { 480 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 481 fZoneList->Select(currentZoneIndex); 482 } else 483 fZoneList->DeselectAll(); 484 fZoneList->ScrollToSelection(); 485 486 fUseGmtTime = fOldUseGmtTime; 487 if (fUseGmtTime) 488 fGmtTime->SetValue(B_CONTROL_ON); 489 else 490 fLocalTime->SetValue(B_CONTROL_ON); 491 _ShowOrHidePreview(); 492 493 _UpdateGmtSettings(); 494 _SetSystemTimeZone(); 495 _UpdatePreview(); 496 _UpdateCurrent(); 497 } 498 499 500 void 501 TimeZoneView::_UpdatePreview() 502 { 503 int32 selection = fZoneList->CurrentSelection(); 504 TimeZoneListItem* item 505 = selection < 0 506 ? NULL 507 : (TimeZoneListItem*)fZoneList->ItemAt(selection); 508 509 if (item == NULL || !item->HasTimeZone()) { 510 fPreview->SetText(""); 511 fPreview->SetTime(""); 512 return; 513 } 514 515 BString timeString = _FormatTime(item->TimeZone()); 516 fPreview->SetText(item->Text()); 517 fPreview->SetTime(timeString.String()); 518 519 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 520 } 521 522 523 void 524 TimeZoneView::_UpdateCurrent() 525 { 526 if (fCurrentZoneItem == NULL) 527 return; 528 529 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 530 fCurrent->SetText(fCurrentZoneItem->Text()); 531 fCurrent->SetTime(timeString.String()); 532 } 533 534 535 void 536 TimeZoneView::_SetSystemTimeZone() 537 { 538 /* Set system timezone for all different API levels. How to do this? 539 * 1) tell locale-roster about new default timezone 540 * 2) tell kernel about new timezone offset 541 */ 542 543 int32 selection = fZoneList->CurrentSelection(); 544 if (selection < 0) 545 return; 546 547 TimeZoneListItem* item 548 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 549 if (item == NULL || !item->HasTimeZone()) 550 return; 551 552 fCurrentZoneItem = item; 553 const BTimeZone& timeZone = item->TimeZone(); 554 555 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); 556 557 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 558 timeZone.ID().Length()); 559 560 fSetZone->SetEnabled(false); 561 fLastUpdateMinute = -1; 562 // just to trigger updating immediately 563 } 564 565 566 BString 567 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 568 { 569 BString result; 570 571 time_t now = time(NULL); 572 bool rtcIsGMT; 573 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 574 if (!rtcIsGMT) { 575 int32 currentOffset 576 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 577 ? fCurrentZoneItem->OffsetFromGMT() 578 : 0; 579 now -= timeZone.OffsetFromGMT() - currentOffset; 580 } 581 BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT, 582 &timeZone); 583 584 return result; 585 } 586 587 588 void 589 TimeZoneView::_ReadRTCSettings() 590 { 591 BPath path; 592 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) 593 return; 594 595 path.Append("RTC_time_settings"); 596 597 BEntry entry(path.Path()); 598 if (entry.Exists()) { 599 BFile file(&entry, B_READ_ONLY); 600 if (file.InitCheck() == B_OK) { 601 char buffer[6]; 602 file.Read(buffer, 6); 603 if (strncmp(buffer, "gmt", 3) == 0) 604 fUseGmtTime = true; 605 } 606 } 607 } 608 609 610 void 611 TimeZoneView::_WriteRTCSettings() 612 { 613 BPath path; 614 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK) 615 return; 616 617 path.Append("RTC_time_settings"); 618 619 BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 620 if (file.InitCheck() == B_OK) { 621 if (fUseGmtTime) 622 file.Write("gmt", 3); 623 else 624 file.Write("local", 5); 625 } 626 } 627 628 629 void 630 TimeZoneView::_UpdateGmtSettings() 631 { 632 _WriteRTCSettings(); 633 634 _ShowOrHidePreview(); 635 636 _kern_set_real_time_clock_is_gmt(fUseGmtTime); 637 } 638 639 640 void 641 TimeZoneView::_ShowOrHidePreview() 642 { 643 if (fUseGmtTime) { 644 // Hardware clock uses GMT time, changing timezone will adjust the 645 // offset and we need to display a preview 646 fCurrent->Show(); 647 fPreview->Show(); 648 } else { 649 // Hardware clock uses local time, changing timezone will adjust the 650 // clock and there is no offset to manage, thus, no preview. 651 fCurrent->Hide(); 652 fPreview->Hide(); 653 } 654 } 655 656