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) const 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 AdoptParentColors(); 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( 216 BSize(200 * be_plain_font->Size() / 12.0f, 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 BLayoutBuilder::Group<>(this) 241 .Add(scrollList) 242 .AddGroup(B_VERTICAL, 0) 243 .Add(new BStringView("clockSetTo", 244 B_TRANSLATE("Hardware clock set to:"))) 245 .AddGroup(B_VERTICAL, 0) 246 .Add(fLocalTime) 247 .Add(fGmtTime) 248 .SetInsets(B_USE_WINDOW_SPACING, 0, 0, 0) 249 .End() 250 .AddGlue() 251 .AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING) 252 .Add(fCurrent) 253 .Add(fPreview) 254 .End() 255 .Add(fSetZone) 256 .End() 257 .SetInsets(B_USE_WINDOW_SPACING, B_USE_WINDOW_SPACING, 258 B_USE_WINDOW_SPACING, B_USE_DEFAULT_SPACING); 259 } 260 261 262 void 263 TimeZoneView::_BuildZoneMenu() 264 { 265 BTimeZone defaultTimeZone; 266 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); 267 268 BLanguage language; 269 BLocale::Default()->GetLanguage(&language); 270 271 // Group timezones by regions, but filter out unwanted (duplicate) regions 272 // and add an additional region with generic GMT-offset timezones at the end 273 typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap; 274 ZoneItemMap zoneItemMap; 275 const char* kOtherRegion = B_TRANSLATE_MARK("<Other>"); 276 const char* kSupportedRegions[] = { 277 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"), 278 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"), 279 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"), 280 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"), 281 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"), 282 kOtherRegion, 283 NULL 284 }; 285 286 // Since the zone-map contains translated country-names (we get those from 287 // ICU), we need to use translated region names in the zone-map, too: 288 typedef std::map<BString, BString> TranslatedRegionMap; 289 TranslatedRegionMap regionMap; 290 for (const char** region = kSupportedRegions; *region != NULL; ++region) { 291 BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region); 292 regionMap[*region] = translatedRegion; 293 294 TimeZoneListItem* regionItem 295 = new TimeZoneListItem(translatedRegion, NULL, NULL); 296 regionItem->SetOutlineLevel(0); 297 zoneItemMap[translatedRegion] = regionItem; 298 } 299 300 // Get all time zones 301 BMessage zoneList; 302 BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList); 303 304 typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap; 305 ZonesByCountyMap zonesByCountryMap; 306 const char* zoneID; 307 BString timeZoneCode; 308 for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK 309 && zoneList.FindString("region", tz, &timeZoneCode) == B_OK; tz++) { 310 // From the global ("001") timezones, we only accept the generic GMT 311 // timezones, as all the other world-zones are duplicates of others. 312 if (timeZoneCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0) 313 continue; 314 zonesByCountryMap[timeZoneCode].push_back(zoneID); 315 } 316 317 ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin(); 318 for (; countryIter != zonesByCountryMap.end(); ++countryIter) { 319 const char* countryCode = countryIter->first.String(); 320 if (countryCode == NULL) 321 continue; 322 323 size_t zoneCountInCountry = countryIter->second.size(); 324 for (size_t tz = 0; tz < zoneCountInCountry; tz++) { 325 BString zoneID(countryIter->second[tz]); 326 BTimeZone* timeZone 327 = new(std::nothrow) BTimeZone(zoneID, &language); 328 if (timeZone == NULL) 329 continue; 330 331 int32 slashPos = zoneID.FindFirst('/'); 332 BString region(zoneID, slashPos); 333 if (region == "Etc") 334 region = kOtherRegion; 335 336 // just accept timezones from our supported regions, others are 337 // aliases and would just make the list even longer 338 TranslatedRegionMap::iterator regionIter = regionMap.find(region); 339 if (regionIter == regionMap.end()) 340 continue; 341 342 BString fullCountryID = regionIter->second; 343 BCountry* country = new(std::nothrow) BCountry(countryCode); 344 if (country == NULL) 345 continue; 346 347 BString countryName; 348 country->GetName(countryName); 349 bool hasUsedCountry = false; 350 bool countryIsRegion = countryName == regionIter->second 351 || region == kOtherRegion; 352 if (!countryIsRegion) 353 fullCountryID << "/" << countryName; 354 355 BString timeZoneName; 356 BString fullZoneID = fullCountryID; 357 if (zoneCountInCountry > 1) { 358 // we can't use the country name as timezone name, since there 359 // are more than one timezones in this country - fetch the 360 // localized name of the timezone and use that 361 timeZoneName = timeZone->Name(); 362 int32 openParenthesisPos = timeZoneName.FindFirst('('); 363 if (openParenthesisPos >= 0) { 364 timeZoneName.Remove(0, openParenthesisPos + 1); 365 int32 closeParenthesisPos = timeZoneName.FindLast(')'); 366 if (closeParenthesisPos >= 0) 367 timeZoneName.Truncate(closeParenthesisPos); 368 } 369 fullZoneID << "/" << timeZoneName; 370 } else { 371 timeZoneName = countryName; 372 fullZoneID << "/" << zoneID; 373 } 374 375 // skip duplicates 376 ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); 377 if (zoneIter != zoneItemMap.end()) { 378 delete timeZone; 379 continue; 380 } 381 382 TimeZoneListItem* countryItem = NULL; 383 TimeZoneListItem* zoneItem = NULL; 384 if (zoneCountInCountry > 1) { 385 ZoneItemMap::iterator countryIter 386 = zoneItemMap.find(fullCountryID); 387 if (countryIter == zoneItemMap.end()) { 388 countryItem = new TimeZoneListItem(countryName.String(), 389 country, NULL); 390 countryItem->SetOutlineLevel(1); 391 zoneItemMap[fullCountryID] = countryItem; 392 hasUsedCountry = true; 393 } else 394 countryItem = countryIter->second; 395 396 zoneItem = new TimeZoneListItem(timeZoneName.String(), 397 NULL, timeZone); 398 zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); 399 } else { 400 zoneItem = new TimeZoneListItem(timeZoneName.String(), 401 country, timeZone); 402 zoneItem->SetOutlineLevel(1); 403 hasUsedCountry = true; 404 } 405 zoneItemMap[fullZoneID] = zoneItem; 406 407 if (timeZone->ID() == defaultTimeZone.ID()) { 408 fCurrentZoneItem = zoneItem; 409 if (countryItem != NULL) 410 countryItem->SetExpanded(true); 411 412 ZoneItemMap::iterator regionItemIter 413 = zoneItemMap.find(regionIter->second); 414 if (regionItemIter != zoneItemMap.end()) 415 regionItemIter->second->SetExpanded(true); 416 } 417 418 if (!hasUsedCountry) 419 delete country; 420 } 421 } 422 423 fOldZoneItem = fCurrentZoneItem; 424 425 ZoneItemMap::iterator zoneIter; 426 bool lastWasCountryItem = false; 427 TimeZoneListItem* currentItem = NULL; 428 for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); 429 ++zoneIter) { 430 if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { 431 // Some countries (e.g. Spain and Chile) have their timezones 432 // spread across different regions. As a result, there might still 433 // be country items with only one timezone below them. We manually 434 // filter those country items here. 435 ZoneItemMap::iterator next = zoneIter; 436 ++next; 437 if (next != zoneItemMap.end() 438 && next->second->OutlineLevel() != 2) { 439 fZoneList->RemoveItem(currentItem); 440 zoneIter->second->SetText(currentItem->Text()); 441 zoneIter->second->SetCountry(currentItem->HasCountry() 442 ? new(std::nothrow) BCountry(currentItem->Country()) 443 : NULL); 444 if (currentItem->HasTimeZone()) { 445 zoneIter->second->SetTimeZone(new(std::nothrow) 446 BTimeZone(currentItem->TimeZone())); 447 } 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 fTimeFormat.Format(result, now, B_SHORT_TIME_FORMAT, &timeZone); 571 572 return result; 573 } 574 575 576 void 577 TimeZoneView::_ReadRTCSettings() 578 { 579 BPath path; 580 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) 581 return; 582 583 path.Append("RTC_time_settings"); 584 585 BEntry entry(path.Path()); 586 if (entry.Exists()) { 587 BFile file(&entry, B_READ_ONLY); 588 if (file.InitCheck() == B_OK) { 589 char buffer[6]; 590 file.Read(buffer, 6); 591 if (strncmp(buffer, "gmt", 3) == 0) 592 fUseGmtTime = true; 593 } 594 } 595 } 596 597 598 void 599 TimeZoneView::_WriteRTCSettings() 600 { 601 BPath path; 602 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK) 603 return; 604 605 path.Append("RTC_time_settings"); 606 607 BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 608 if (file.InitCheck() == B_OK) { 609 if (fUseGmtTime) 610 file.Write("gmt", 3); 611 else 612 file.Write("local", 5); 613 } 614 } 615 616 617 void 618 TimeZoneView::_UpdateGmtSettings() 619 { 620 _WriteRTCSettings(); 621 622 _ShowOrHidePreview(); 623 624 _kern_set_real_time_clock_is_gmt(fUseGmtTime); 625 } 626 627 628 void 629 TimeZoneView::_ShowOrHidePreview() 630 { 631 if (fUseGmtTime) { 632 // Hardware clock uses GMT time, changing timezone will adjust the 633 // offset and we need to display a preview 634 fCurrent->Show(); 635 fPreview->Show(); 636 } else { 637 // Hardware clock uses local time, changing timezone will adjust the 638 // clock and there is no offset to manage, thus, no preview. 639 fCurrent->Hide(); 640 fPreview->Hide(); 641 } 642 } 643