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