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 "ZoneView.h" 17 18 #include <stdlib.h> 19 #include <syscalls.h> 20 21 #include <map> 22 #include <new> 23 #include <vector> 24 25 #include <AutoDeleter.h> 26 #include <Button.h> 27 #include <Catalog.h> 28 #include <Collator.h> 29 #include <ControlLook.h> 30 #include <Country.h> 31 #include <Directory.h> 32 #include <Entry.h> 33 #include <File.h> 34 #include <FindDirectory.h> 35 #include <ListItem.h> 36 #include <Locale.h> 37 #include <MutableLocaleRoster.h> 38 #include <OutlineListView.h> 39 #include <Path.h> 40 #include <RadioButton.h> 41 #include <ScrollView.h> 42 #include <StorageDefs.h> 43 #include <String.h> 44 #include <StringView.h> 45 #include <TimeZone.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 "TimeZoneListView.h" 56 #include "TZDisplay.h" 57 58 59 #undef B_TRANSLATION_CONTEXT 60 #define B_TRANSLATION_CONTEXT "Time" 61 62 63 using BPrivate::MutableLocaleRoster; 64 using BPrivate::ObjectDeleter; 65 66 67 struct TimeZoneItemLess { 68 bool operator()(const BString& first, const BString& second) 69 { 70 // sort anything starting with '<' behind anything else 71 if (first.ByteAt(0) == '<') { 72 if (second.ByteAt(0) != '<') 73 return false; 74 } else if (second.ByteAt(0) == '<') 75 return true; 76 return fCollator.Compare(first.String(), second.String()) < 0; 77 } 78 private: 79 BCollator fCollator; 80 }; 81 82 83 84 TimeZoneView::TimeZoneView(const char* name) 85 : 86 BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING), 87 fGmtTime(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 _WriteRTCSettings(); 111 } 112 113 114 void 115 TimeZoneView::AttachedToWindow() 116 { 117 BView::AttachedToWindow(); 118 if (Parent()) 119 SetViewColor(Parent()->ViewColor()); 120 121 if (!fInitialized) { 122 fInitialized = true; 123 124 fSetZone->SetTarget(this); 125 fZoneList->SetTarget(this); 126 } 127 } 128 129 130 void 131 TimeZoneView::DoLayout() 132 { 133 BView::DoLayout(); 134 if (fCurrentZoneItem != NULL) { 135 fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem)); 136 fCurrent->SetText(fCurrentZoneItem->Text()); 137 fZoneList->ScrollToSelection(); 138 } 139 } 140 141 142 void 143 TimeZoneView::MessageReceived(BMessage* message) 144 { 145 switch (message->what) { 146 case B_OBSERVER_NOTICE_CHANGE: 147 { 148 int32 change; 149 message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change); 150 switch(change) { 151 case H_TM_CHANGED: 152 _UpdateDateTime(message); 153 break; 154 155 default: 156 BView::MessageReceived(message); 157 break; 158 } 159 break; 160 } 161 162 case H_CITY_CHANGED: 163 _UpdatePreview(); 164 break; 165 166 case H_SET_TIME_ZONE: 167 { 168 _SetSystemTimeZone(); 169 break; 170 } 171 172 case kMsgRevert: 173 _Revert(); 174 break; 175 176 case kRTCUpdate: 177 fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON; 178 _UpdateGmtSettings(); 179 _UpdateCurrent(); 180 _UpdatePreview(); 181 break; 182 183 default: 184 BGroupView::MessageReceived(message); 185 break; 186 } 187 } 188 189 190 void 191 TimeZoneView::_UpdateDateTime(BMessage* message) 192 { 193 // only need to update once every minute 194 int32 minute; 195 if (message->FindInt32("minute", &minute) == B_OK) { 196 if (fLastUpdateMinute != minute) { 197 _UpdateCurrent(); 198 _UpdatePreview(); 199 200 fLastUpdateMinute = minute; 201 } 202 } 203 } 204 205 206 void 207 TimeZoneView::_InitView() 208 { 209 fZoneList = new TimeZoneListView(); 210 fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED)); 211 fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE)); 212 _BuildZoneMenu(); 213 BScrollView* scrollList = new BScrollView("scrollList", fZoneList, 214 B_FRAME_EVENTS | B_WILL_DRAW, false, true); 215 scrollList->SetExplicitMinSize(BSize(200, 0)); 216 217 fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:")); 218 fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:")); 219 220 fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"), 221 new BMessage(H_SET_TIME_ZONE)); 222 fSetZone->SetEnabled(false); 223 fSetZone->SetExplicitAlignment( 224 BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM)); 225 226 fLocalTime = new BRadioButton("localTime", 227 B_TRANSLATE("Local time (Windows compatible)"), 228 new BMessage(kRTCUpdate)); 229 fGmtTime = new BRadioButton("greenwichMeanTime", 230 B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate)); 231 232 if (fUseGmtTime) 233 fGmtTime->SetValue(B_CONTROL_ON); 234 else 235 fLocalTime->SetValue(B_CONTROL_ON); 236 _ShowOrHidePreview(); 237 fOldUseGmtTime = fUseGmtTime; 238 239 const float kIndentSpacing 240 = be_control_look->DefaultItemSpacing() * 2; 241 BLayoutBuilder::Group<>(this) 242 .Add(scrollList) 243 .AddGroup(B_VERTICAL, 0) 244 .Add(new BStringView("clockSetTo", 245 B_TRANSLATE("Hardware clock set to:"))) 246 .AddGroup(B_VERTICAL, 0) 247 .Add(fLocalTime) 248 .Add(fGmtTime) 249 .SetInsets(kIndentSpacing, 0, 0, 0) 250 .End() 251 .AddGlue() 252 .AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING) 253 .Add(fCurrent) 254 .Add(fPreview) 255 .End() 256 .Add(fSetZone) 257 .End() 258 .SetInsets(B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING, 259 B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING); 260 } 261 262 263 void 264 TimeZoneView::_BuildZoneMenu() 265 { 266 BTimeZone defaultTimeZone; 267 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); 268 269 BLanguage language; 270 BLocale::Default()->GetLanguage(&language); 271 272 // Group timezones by regions, but filter out unwanted (duplicate) regions 273 // and add an additional region with generic GMT-offset timezones at the end 274 typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap; 275 ZoneItemMap zoneItemMap; 276 const char* kOtherRegion = B_TRANSLATE_MARK("<Other>"); 277 const char* kSupportedRegions[] = { 278 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"), 279 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"), 280 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"), 281 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"), 282 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"), 283 kOtherRegion, 284 NULL 285 }; 286 287 // Since the zone-map contains translated country-names (we get those from 288 // ICU), we need to use translated region names in the zone-map, too: 289 typedef std::map<BString, BString> TranslatedRegionMap; 290 TranslatedRegionMap regionMap; 291 for (const char** region = kSupportedRegions; *region != NULL; ++region) { 292 BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region); 293 regionMap[*region] = translatedRegion; 294 295 TimeZoneListItem* regionItem 296 = new TimeZoneListItem(translatedRegion, NULL, NULL); 297 regionItem->SetOutlineLevel(0); 298 zoneItemMap[translatedRegion] = regionItem; 299 } 300 301 // Get all time zones 302 BMessage zoneList; 303 BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList); 304 305 typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap; 306 ZonesByCountyMap zonesByCountryMap; 307 const char* zoneID; 308 BString timeZoneCode; 309 for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK 310 && zoneList.FindString("region", tz, &timeZoneCode) == B_OK; tz++) { 311 // From the global ("001") timezones, we only accept the generic GMT 312 // timezones, as all the other world-zones are duplicates of others. 313 if (timeZoneCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0) 314 continue; 315 zonesByCountryMap[timeZoneCode].push_back(zoneID); 316 } 317 318 ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin(); 319 for (; countryIter != zonesByCountryMap.end(); ++countryIter) { 320 const char* countryCode = countryIter->first.String(); 321 if (countryCode == NULL) 322 continue; 323 324 size_t zoneCountInCountry = countryIter->second.size(); 325 for (size_t tz = 0; tz < zoneCountInCountry; tz++) { 326 BString zoneID(countryIter->second[tz]); 327 BTimeZone* timeZone 328 = new(std::nothrow) BTimeZone(zoneID, &language); 329 if (timeZone == NULL) 330 continue; 331 332 int32 slashPos = zoneID.FindFirst('/'); 333 BString region(zoneID, slashPos); 334 if (region == "Etc") 335 region = kOtherRegion; 336 337 // just accept timezones from our supported regions, others are 338 // aliases and would just make the list even longer 339 TranslatedRegionMap::iterator regionIter = regionMap.find(region); 340 if (regionIter == regionMap.end()) 341 continue; 342 343 BString fullCountryID = regionIter->second; 344 BCountry* country = new(std::nothrow) BCountry(countryCode); 345 if (country == NULL) 346 continue; 347 348 BString countryName; 349 country->GetName(countryName); 350 bool hasUsedCountry = false; 351 bool countryIsRegion = countryName == regionIter->second 352 || region == kOtherRegion; 353 if (!countryIsRegion) 354 fullCountryID << "/" << countryName; 355 356 BString timeZoneName; 357 BString fullZoneID = fullCountryID; 358 if (zoneCountInCountry > 1) { 359 // we can't use the country name as timezone name, since there 360 // are more than one timezones in this country - fetch the 361 // localized name of the timezone and use that 362 timeZoneName = timeZone->Name(); 363 int32 openParenthesisPos = timeZoneName.FindFirst('('); 364 if (openParenthesisPos >= 0) { 365 timeZoneName.Remove(0, openParenthesisPos + 1); 366 int32 closeParenthesisPos = timeZoneName.FindLast(')'); 367 if (closeParenthesisPos >= 0) 368 timeZoneName.Truncate(closeParenthesisPos); 369 } 370 fullZoneID << "/" << timeZoneName; 371 } else { 372 timeZoneName = countryName; 373 fullZoneID << "/" << zoneID; 374 } 375 376 // skip duplicates 377 ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); 378 if (zoneIter != zoneItemMap.end()) { 379 delete timeZone; 380 continue; 381 } 382 383 TimeZoneListItem* countryItem = NULL; 384 TimeZoneListItem* zoneItem = NULL; 385 if (zoneCountInCountry > 1) { 386 ZoneItemMap::iterator countryIter 387 = zoneItemMap.find(fullCountryID); 388 if (countryIter == zoneItemMap.end()) { 389 countryItem = new TimeZoneListItem(countryName.String(), 390 country, NULL); 391 countryItem->SetOutlineLevel(1); 392 zoneItemMap[fullCountryID] = countryItem; 393 hasUsedCountry = true; 394 } else 395 countryItem = countryIter->second; 396 397 zoneItem = new TimeZoneListItem(timeZoneName.String(), 398 NULL, timeZone); 399 zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); 400 } else { 401 zoneItem = new TimeZoneListItem(timeZoneName.String(), 402 country, timeZone); 403 zoneItem->SetOutlineLevel(1); 404 hasUsedCountry = true; 405 } 406 zoneItemMap[fullZoneID] = zoneItem; 407 408 if (timeZone->ID() == defaultTimeZone.ID()) { 409 fCurrentZoneItem = zoneItem; 410 if (countryItem != NULL) 411 countryItem->SetExpanded(true); 412 413 ZoneItemMap::iterator regionItemIter 414 = zoneItemMap.find(regionIter->second); 415 if (regionItemIter != zoneItemMap.end()) 416 regionItemIter->second->SetExpanded(true); 417 } 418 419 if (!hasUsedCountry) 420 delete country; 421 } 422 } 423 424 fOldZoneItem = fCurrentZoneItem; 425 426 ZoneItemMap::iterator zoneIter; 427 bool lastWasCountryItem = false; 428 TimeZoneListItem* currentItem = NULL; 429 for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); 430 ++zoneIter) { 431 if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { 432 // Some countries (e.g. Spain and Chile) have their timezones 433 // spread across different regions. As a result, there might still 434 // be country items with only one timezone below them. We manually 435 // filter those country items here. 436 ZoneItemMap::iterator next = zoneIter; 437 ++next; 438 if (next != zoneItemMap.end() 439 && next->second->OutlineLevel() != 2) { 440 fZoneList->RemoveItem(currentItem); 441 zoneIter->second->SetText(currentItem->Text()); 442 zoneIter->second->SetCountry(currentItem->HasCountry() 443 ? new(std::nothrow) BCountry(currentItem->Country()) 444 : NULL); 445 zoneIter->second->SetTimeZone(currentItem->HasTimeZone() 446 ? new(std::nothrow) BTimeZone(currentItem->TimeZone()) 447 : NULL); 448 zoneIter->second->SetOutlineLevel(1); 449 delete currentItem; 450 } 451 } 452 453 fZoneList->AddItem(zoneIter->second); 454 if (zoneIter->second->OutlineLevel() == 1) { 455 lastWasCountryItem = true; 456 currentItem = zoneIter->second; 457 } else 458 lastWasCountryItem = false; 459 } 460 } 461 462 463 void 464 TimeZoneView::_Revert() 465 { 466 fCurrentZoneItem = fOldZoneItem; 467 468 if (fCurrentZoneItem != NULL) { 469 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 470 fZoneList->Select(currentZoneIndex); 471 } else 472 fZoneList->DeselectAll(); 473 fZoneList->ScrollToSelection(); 474 475 fUseGmtTime = fOldUseGmtTime; 476 if (fUseGmtTime) 477 fGmtTime->SetValue(B_CONTROL_ON); 478 else 479 fLocalTime->SetValue(B_CONTROL_ON); 480 _ShowOrHidePreview(); 481 482 _UpdateGmtSettings(); 483 _SetSystemTimeZone(); 484 _UpdatePreview(); 485 _UpdateCurrent(); 486 } 487 488 489 void 490 TimeZoneView::_UpdatePreview() 491 { 492 int32 selection = fZoneList->CurrentSelection(); 493 TimeZoneListItem* item 494 = selection < 0 495 ? NULL 496 : static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 497 498 if (item == NULL || !item->HasTimeZone()) { 499 fPreview->SetText(""); 500 fPreview->SetTime(""); 501 return; 502 } 503 504 BString timeString = _FormatTime(item->TimeZone()); 505 fPreview->SetText(item->Text()); 506 fPreview->SetTime(timeString.String()); 507 508 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 509 } 510 511 512 void 513 TimeZoneView::_UpdateCurrent() 514 { 515 if (fCurrentZoneItem == NULL) 516 return; 517 518 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 519 fCurrent->SetText(fCurrentZoneItem->Text()); 520 fCurrent->SetTime(timeString.String()); 521 } 522 523 524 void 525 TimeZoneView::_SetSystemTimeZone() 526 { 527 /* Set system timezone for all different API levels. How to do this? 528 * 1) tell locale-roster about new default timezone 529 * 2) tell kernel about new timezone offset 530 */ 531 532 int32 selection = fZoneList->CurrentSelection(); 533 if (selection < 0) 534 return; 535 536 TimeZoneListItem* item 537 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 538 if (item == NULL || !item->HasTimeZone()) 539 return; 540 541 fCurrentZoneItem = item; 542 const BTimeZone& timeZone = item->TimeZone(); 543 544 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); 545 546 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 547 timeZone.ID().Length()); 548 549 fSetZone->SetEnabled(false); 550 fLastUpdateMinute = -1; 551 // just to trigger updating immediately 552 } 553 554 555 BString 556 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 557 { 558 BString result; 559 560 time_t now = time(NULL); 561 bool rtcIsGMT; 562 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 563 if (!rtcIsGMT) { 564 int32 currentOffset 565 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 566 ? fCurrentZoneItem->OffsetFromGMT() 567 : 0; 568 now -= timeZone.OffsetFromGMT() - currentOffset; 569 } 570 BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT, 571 &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