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 <ScrollView.h> 39 #include <StorageDefs.h> 40 #include <String.h> 41 #include <TimeZone.h> 42 #include <ToolTip.h> 43 #include <View.h> 44 #include <Window.h> 45 46 #include <unicode/datefmt.h> 47 #include <unicode/utmscale.h> 48 #include <ICUWrapper.h> 49 50 #include "TimeMessages.h" 51 #include "TimeZoneListItem.h" 52 #include "TZDisplay.h" 53 54 55 #undef B_TRANSLATE_CONTEXT 56 #define B_TRANSLATE_CONTEXT "Time" 57 58 59 using BPrivate::MutableLocaleRoster; 60 using BPrivate::ObjectDeleter; 61 62 63 struct TimeZoneItemLess { 64 bool operator()(const BString& first, const BString& second) 65 { 66 // sort anything starting with '<' behind anything else 67 if (first.ByteAt(0) == '<') { 68 if (second.ByteAt(0) != '<') 69 return false; 70 } else if (second.ByteAt(0) == '<') 71 return true; 72 return fCollator.Compare(first.String(), second.String()) < 0; 73 } 74 private: 75 BCollator fCollator; 76 }; 77 78 79 80 TimeZoneView::TimeZoneView(const char* name) 81 : 82 BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING), 83 fToolTip(NULL), 84 fCurrentZoneItem(NULL), 85 fOldZoneItem(NULL), 86 fInitialized(false) 87 { 88 _InitView(); 89 } 90 91 92 bool 93 TimeZoneView::CheckCanRevert() 94 { 95 return fCurrentZoneItem != fOldZoneItem; 96 } 97 98 99 TimeZoneView::~TimeZoneView() 100 { 101 if (fToolTip != NULL) 102 fToolTip->ReleaseReference(); 103 } 104 105 106 void 107 TimeZoneView::AttachedToWindow() 108 { 109 BView::AttachedToWindow(); 110 if (Parent()) 111 SetViewColor(Parent()->ViewColor()); 112 113 if (!fInitialized) { 114 fInitialized = true; 115 116 fSetZone->SetTarget(this); 117 fZoneList->SetTarget(this); 118 } 119 } 120 121 122 void 123 TimeZoneView::DoLayout() 124 { 125 BView::DoLayout(); 126 if (fCurrentZoneItem != NULL) { 127 fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem)); 128 fCurrent->SetText(fCurrentZoneItem->Text()); 129 fZoneList->ScrollToSelection(); 130 } 131 } 132 133 134 void 135 TimeZoneView::MessageReceived(BMessage* message) 136 { 137 switch (message->what) { 138 case B_OBSERVER_NOTICE_CHANGE: 139 { 140 int32 change; 141 message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change); 142 switch(change) { 143 case H_TM_CHANGED: 144 _UpdateDateTime(message); 145 break; 146 147 default: 148 BView::MessageReceived(message); 149 break; 150 } 151 break; 152 } 153 154 case H_CITY_CHANGED: 155 _UpdatePreview(); 156 break; 157 158 case H_SET_TIME_ZONE: 159 { 160 _SetSystemTimeZone(); 161 Looper()->PostMessage(new BMessage(kMsgChange)); 162 break; 163 } 164 165 case kMsgRevert: 166 _Revert(); 167 break; 168 169 case kRTCUpdate: 170 _UpdateCurrent(); 171 _UpdatePreview(); 172 break; 173 174 default: 175 BGroupView::MessageReceived(message); 176 break; 177 } 178 } 179 180 181 bool 182 TimeZoneView::GetToolTipAt(BPoint point, BToolTip** _tip) 183 { 184 TimeZoneListItem* item = static_cast<TimeZoneListItem*>( 185 fZoneList->ItemAt(fZoneList->IndexOf(point))); 186 if (item == NULL || !item->HasTimeZone()) 187 return false; 188 189 BString nowInTimeZone; 190 time_t now = time(NULL); 191 BLocale::Default()->FormatTime(&nowInTimeZone, now, B_SHORT_TIME_FORMAT, 192 &item->TimeZone()); 193 194 BString dateInTimeZone; 195 BLocale::Default()->FormatDate(&dateInTimeZone, now, B_SHORT_DATE_FORMAT, 196 &item->TimeZone()); 197 198 BString toolTip = item->Text(); 199 toolTip << '\n' << item->TimeZone().ShortName() << " / " 200 << item->TimeZone().ShortDaylightSavingName() 201 << B_TRANSLATE("\nNow: ") << nowInTimeZone 202 << " (" << dateInTimeZone << ')'; 203 204 if (fToolTip != NULL) 205 fToolTip->ReleaseReference(); 206 fToolTip = new (std::nothrow) BTextToolTip(toolTip.String()); 207 if (fToolTip == NULL) 208 return false; 209 210 *_tip = fToolTip; 211 212 return true; 213 } 214 215 216 void 217 TimeZoneView::_UpdateDateTime(BMessage* message) 218 { 219 // only need to update once every minute 220 int32 minute; 221 if (message->FindInt32("minute", &minute) == B_OK) { 222 if (fLastUpdateMinute != minute) { 223 _UpdateCurrent(); 224 _UpdatePreview(); 225 226 fLastUpdateMinute = minute; 227 } 228 } 229 } 230 231 232 void 233 TimeZoneView::_InitView() 234 { 235 fZoneList = new BOutlineListView("cityList", B_SINGLE_SELECTION_LIST); 236 fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED)); 237 fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE)); 238 _BuildZoneMenu(); 239 BScrollView* scrollList = new BScrollView("scrollList", fZoneList, 240 B_FRAME_EVENTS | B_WILL_DRAW, false, true); 241 242 fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:")); 243 fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:")); 244 245 fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"), 246 new BMessage(H_SET_TIME_ZONE)); 247 fSetZone->SetEnabled(false); 248 fSetZone->SetExplicitAlignment( 249 BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM)); 250 251 const float kInset = be_control_look->DefaultItemSpacing(); 252 BLayoutBuilder::Group<>(this) 253 .Add(scrollList) 254 .AddGroup(B_VERTICAL, kInset) 255 .Add(fCurrent) 256 .Add(fPreview) 257 .AddGlue() 258 .Add(fSetZone) 259 .End() 260 .SetInsets(kInset, kInset, kInset, kInset); 261 } 262 263 264 void 265 TimeZoneView::_BuildZoneMenu() 266 { 267 BTimeZone defaultTimeZone; 268 BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone); 269 270 BLanguage language; 271 BLocale::Default()->GetLanguage(&language); 272 273 BMessage countryList; 274 BLocaleRoster::Default()->GetAvailableCountries(&countryList); 275 countryList.AddString("country", ""); 276 277 /* 278 * Group timezones by regions, but filter out unwanted (duplicate) regions 279 * and add an additional region with generic GMT-offset timezones at the end 280 */ 281 typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap; 282 ZoneItemMap zoneItemMap; 283 const char* kOtherRegion = B_TRANSLATE_MARK("<Other>"); 284 const char* kSupportedRegions[] = { 285 B_TRANSLATE_MARK("Africa"), B_TRANSLATE_MARK("America"), 286 B_TRANSLATE_MARK("Antarctica"), B_TRANSLATE_MARK("Arctic"), 287 B_TRANSLATE_MARK("Asia"), B_TRANSLATE_MARK("Atlantic"), 288 B_TRANSLATE_MARK("Australia"), B_TRANSLATE_MARK("Europe"), 289 B_TRANSLATE_MARK("Indian"), B_TRANSLATE_MARK("Pacific"), 290 kOtherRegion, 291 NULL 292 }; 293 // Since the zone-map contains translated country-names (we get those from 294 // ICU), we need to use translated region names in the zone-map, too: 295 typedef std::map<BString, BString> TranslatedRegionMap; 296 TranslatedRegionMap regionMap; 297 for (const char** region = kSupportedRegions; *region != NULL; ++region) { 298 BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region); 299 regionMap[*region] = translatedRegion; 300 301 TimeZoneListItem* regionItem 302 = new TimeZoneListItem(translatedRegion, NULL, NULL); 303 regionItem->SetOutlineLevel(0); 304 zoneItemMap[translatedRegion] = regionItem; 305 } 306 307 BString countryCode; 308 for (int c = 0; countryList.FindString("country", c, &countryCode) 309 == B_OK; c++) { 310 BCountry country(countryCode); 311 BString countryName; 312 country.GetName(countryName); 313 314 // Now list the timezones for this country 315 BMessage zoneList; 316 BLocaleRoster::Default()->GetAvailableTimeZonesForCountry(&zoneList, 317 countryCode.Length() == 0 ? NULL : countryCode.String()); 318 319 int32 count = 0; 320 type_code dummy; 321 zoneList.GetInfo("timeZone", &dummy, &count); 322 323 BString zoneID; 324 for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK; 325 tz++) { 326 int32 slashPos = zoneID.FindFirst('/'); 327 328 // ignore any "global" timezones, as those are just aliases of 329 // regional ones 330 if (slashPos <= 0) 331 continue; 332 333 BString region(zoneID, slashPos); 334 335 if (region == "Etc") 336 region = kOtherRegion; 337 else if (countryName.Length() == 0) { 338 // skip global timezones from other regions, we are just 339 // interested in the generic GMT-based ones under "Etc/" 340 continue; 341 } 342 343 // just accept timezones from our supported regions, others are 344 // aliases and would just make the list even longer 345 TranslatedRegionMap::iterator regionIter = regionMap.find(region); 346 if (regionIter == regionMap.end()) 347 continue; 348 const BString& regionName = regionIter->second; 349 350 BString fullCountryID = regionName; 351 bool countryIsRegion = countryName == regionName; 352 if (!countryIsRegion) 353 fullCountryID << "/" << countryName; 354 355 BTimeZone* timeZone = new BTimeZone(zoneID, &language); 356 BString tzName = timeZone->Name(); 357 if (tzName == "GMT+00:00") 358 tzName = "GMT"; 359 360 int32 openParenthesisPos = tzName.FindFirst('('); 361 if (openParenthesisPos >= 0) { 362 tzName.Remove(0, openParenthesisPos + 1); 363 int32 closeParenthesisPos = tzName.FindLast(')'); 364 if (closeParenthesisPos >= 0) 365 tzName.Truncate(closeParenthesisPos); 366 } 367 BString fullZoneID = fullCountryID; 368 fullZoneID << "/" << tzName; 369 370 // skip duplicates 371 ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID); 372 if (zoneIter != zoneItemMap.end()) { 373 delete timeZone; 374 continue; 375 } 376 377 TimeZoneListItem* countryItem = NULL; 378 TimeZoneListItem* zoneItem = NULL; 379 if (count > 1 && countryName.Length() > 0) { 380 ZoneItemMap::iterator countryIter 381 = zoneItemMap.find(fullCountryID); 382 if (countryIter == zoneItemMap.end()) { 383 countryItem = new TimeZoneListItem(countryName, NULL, NULL); 384 countryItem->SetOutlineLevel(1); 385 zoneItemMap[fullCountryID] = countryItem; 386 } else 387 countryItem = countryIter->second; 388 389 zoneItem = new TimeZoneListItem(tzName, NULL, timeZone); 390 zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2); 391 } else { 392 BString& name = countryName.Length() > 0 ? countryName : tzName; 393 zoneItem = new TimeZoneListItem(name, NULL, timeZone); 394 zoneItem->SetOutlineLevel(1); 395 } 396 zoneItemMap[fullZoneID] = zoneItem; 397 398 if (timeZone->ID() == defaultTimeZone.ID()) { 399 fCurrentZoneItem = zoneItem; 400 if (countryItem != NULL) 401 countryItem->SetExpanded(true); 402 ZoneItemMap::iterator regionItemIter 403 = zoneItemMap.find(regionName); 404 if (regionItemIter != zoneItemMap.end()) 405 regionItemIter->second->SetExpanded(true); 406 } 407 } 408 } 409 410 fOldZoneItem = fCurrentZoneItem; 411 412 ZoneItemMap::iterator zoneIter; 413 bool lastWasCountryItem = false; 414 TimeZoneListItem* currentCountryItem = NULL; 415 for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end(); 416 ++zoneIter) { 417 if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { 418 /* Some countries (e.g. Spain and Chile) have their timezones 419 * spread across different regions. As a result, there might still 420 * be country items with only one timezone below them. We manually 421 * filter those country items here. 422 */ 423 ZoneItemMap::iterator next = zoneIter; 424 ++next; 425 if (next != zoneItemMap.end() 426 && next->second->OutlineLevel() != 2) { 427 fZoneList->RemoveItem(currentCountryItem); 428 zoneIter->second->SetText(currentCountryItem->Text()); 429 zoneIter->second->SetOutlineLevel(1); 430 delete currentCountryItem; 431 } 432 } 433 434 fZoneList->AddItem(zoneIter->second); 435 if (zoneIter->second->OutlineLevel() == 1) { 436 lastWasCountryItem = true; 437 currentCountryItem = zoneIter->second; 438 } else 439 lastWasCountryItem = false; 440 } 441 } 442 443 444 void 445 TimeZoneView::_Revert() 446 { 447 fCurrentZoneItem = fOldZoneItem; 448 449 if (fCurrentZoneItem != NULL) { 450 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 451 fZoneList->Select(currentZoneIndex); 452 } else 453 fZoneList->DeselectAll(); 454 fZoneList->ScrollToSelection(); 455 456 _SetSystemTimeZone(); 457 _UpdatePreview(); 458 _UpdateCurrent(); 459 } 460 461 462 void 463 TimeZoneView::_UpdatePreview() 464 { 465 int32 selection = fZoneList->CurrentSelection(); 466 TimeZoneListItem* item 467 = selection < 0 468 ? NULL 469 : (TimeZoneListItem*)fZoneList->ItemAt(selection); 470 471 if (item == NULL || !item->HasTimeZone()) { 472 fPreview->SetText(""); 473 fPreview->SetTime(""); 474 return; 475 } 476 477 BString timeString = _FormatTime(item->TimeZone()); 478 fPreview->SetText(item->Text()); 479 fPreview->SetTime(timeString.String()); 480 481 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 482 } 483 484 485 void 486 TimeZoneView::_UpdateCurrent() 487 { 488 if (fCurrentZoneItem == NULL) 489 return; 490 491 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 492 fCurrent->SetText(fCurrentZoneItem->Text()); 493 fCurrent->SetTime(timeString.String()); 494 } 495 496 497 void 498 TimeZoneView::_SetSystemTimeZone() 499 { 500 /* Set sytem timezone for all different API levels. How to do this? 501 * 1) tell locale-roster about new default timezone 502 * 2) tell kernel about new timezone offset 503 */ 504 505 int32 selection = fZoneList->CurrentSelection(); 506 if (selection < 0) 507 return; 508 509 TimeZoneListItem* item 510 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 511 if (item == NULL || !item->HasTimeZone()) 512 return; 513 514 fCurrentZoneItem = item; 515 const BTimeZone& timeZone = item->TimeZone(); 516 517 MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone); 518 519 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 520 timeZone.ID().Length()); 521 522 fSetZone->SetEnabled(false); 523 fLastUpdateMinute = -1; 524 // just to trigger updating immediately 525 } 526 527 528 BString 529 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 530 { 531 BString result; 532 533 time_t now = time(NULL); 534 bool rtcIsGMT; 535 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 536 if (!rtcIsGMT) { 537 int32 currentOffset 538 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 539 ? fCurrentZoneItem->OffsetFromGMT() 540 : 0; 541 now -= timeZone.OffsetFromGMT() - currentOffset; 542 } 543 BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT, 544 &timeZone); 545 546 return result; 547 } 548