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) 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 zoneIter->second->SetTimeZone(currentItem->HasTimeZone() 445 ? new(std::nothrow) BTimeZone(currentItem->TimeZone()) 446 : NULL); 447 zoneIter->second->SetOutlineLevel(1); 448 delete currentItem; 449 } 450 } 451 452 fZoneList->AddItem(zoneIter->second); 453 if (zoneIter->second->OutlineLevel() == 1) { 454 lastWasCountryItem = true; 455 currentItem = zoneIter->second; 456 } else 457 lastWasCountryItem = false; 458 } 459 } 460 461 462 void 463 TimeZoneView::_Revert() 464 { 465 fCurrentZoneItem = fOldZoneItem; 466 467 if (fCurrentZoneItem != NULL) { 468 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 469 fZoneList->Select(currentZoneIndex); 470 } else 471 fZoneList->DeselectAll(); 472 fZoneList->ScrollToSelection(); 473 474 fUseGmtTime = fOldUseGmtTime; 475 if (fUseGmtTime) 476 fGmtTime->SetValue(B_CONTROL_ON); 477 else 478 fLocalTime->SetValue(B_CONTROL_ON); 479 _ShowOrHidePreview(); 480 481 _UpdateGmtSettings(); 482 _SetSystemTimeZone(); 483 _UpdatePreview(); 484 _UpdateCurrent(); 485 } 486 487 488 void 489 TimeZoneView::_UpdatePreview() 490 { 491 int32 selection = fZoneList->CurrentSelection(); 492 TimeZoneListItem* item 493 = selection < 0 494 ? NULL 495 : static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 496 497 if (item == NULL || !item->HasTimeZone()) { 498 fPreview->SetText(""); 499 fPreview->SetTime(""); 500 return; 501 } 502 503 BString timeString = _FormatTime(item->TimeZone()); 504 fPreview->SetText(item->Text()); 505 fPreview->SetTime(timeString.String()); 506 507 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 508 } 509 510 511 void 512 TimeZoneView::_UpdateCurrent() 513 { 514 if (fCurrentZoneItem == NULL) 515 return; 516 517 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 518 fCurrent->SetText(fCurrentZoneItem->Text()); 519 fCurrent->SetTime(timeString.String()); 520 } 521 522 523 void 524 TimeZoneView::_SetSystemTimeZone() 525 { 526 /* Set system timezone for all different API levels. How to do this? 527 * 1) tell locale-roster about new default timezone 528 * 2) tell kernel about new timezone offset 529 */ 530 531 int32 selection = fZoneList->CurrentSelection(); 532 if (selection < 0) 533 return; 534 535 TimeZoneListItem* item 536 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 537 if (item == NULL || !item->HasTimeZone()) 538 return; 539 540 fCurrentZoneItem = item; 541 const BTimeZone& timeZone = item->TimeZone(); 542 543 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); 544 545 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 546 timeZone.ID().Length()); 547 548 fSetZone->SetEnabled(false); 549 fLastUpdateMinute = -1; 550 // just to trigger updating immediately 551 } 552 553 554 BString 555 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 556 { 557 BString result; 558 559 time_t now = time(NULL); 560 bool rtcIsGMT; 561 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 562 if (!rtcIsGMT) { 563 int32 currentOffset 564 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 565 ? fCurrentZoneItem->OffsetFromGMT() 566 : 0; 567 now -= timeZone.OffsetFromGMT() - currentOffset; 568 } 569 fTimeFormat.Format(result, now, B_SHORT_TIME_FORMAT, &timeZone); 570 571 return result; 572 } 573 574 575 void 576 TimeZoneView::_ReadRTCSettings() 577 { 578 BPath path; 579 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) 580 return; 581 582 path.Append("RTC_time_settings"); 583 584 BEntry entry(path.Path()); 585 if (entry.Exists()) { 586 BFile file(&entry, B_READ_ONLY); 587 if (file.InitCheck() == B_OK) { 588 char buffer[6]; 589 file.Read(buffer, 6); 590 if (strncmp(buffer, "gmt", 3) == 0) 591 fUseGmtTime = true; 592 } 593 } 594 } 595 596 597 void 598 TimeZoneView::_WriteRTCSettings() 599 { 600 BPath path; 601 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK) 602 return; 603 604 path.Append("RTC_time_settings"); 605 606 BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 607 if (file.InitCheck() == B_OK) { 608 if (fUseGmtTime) 609 file.Write("gmt", 3); 610 else 611 file.Write("local", 5); 612 } 613 } 614 615 616 void 617 TimeZoneView::_UpdateGmtSettings() 618 { 619 _WriteRTCSettings(); 620 621 _ShowOrHidePreview(); 622 623 _kern_set_real_time_clock_is_gmt(fUseGmtTime); 624 } 625 626 627 void 628 TimeZoneView::_ShowOrHidePreview() 629 { 630 if (fUseGmtTime) { 631 // Hardware clock uses GMT time, changing timezone will adjust the 632 // offset and we need to display a preview 633 fCurrent->Show(); 634 fPreview->Show(); 635 } else { 636 // Hardware clock uses local time, changing timezone will adjust the 637 // clock and there is no offset to manage, thus, no preview. 638 fCurrent->Hide(); 639 fPreview->Hide(); 640 } 641 } 642