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, false, &item->TimeZone()); 188 189 BString dateInTimeZone; 190 be_locale->FormatDate(&dateInTimeZone, now, false, &item->TimeZone()); 191 192 BString toolTip = item->Text(); 193 toolTip << '\n' << item->TimeZone().ShortName() << " / " 194 << item->TimeZone().ShortDaylightSavingName() 195 << "\nNow: " << nowInTimeZone << " (" << dateInTimeZone << ')'; 196 197 if (fToolTip != NULL) 198 fToolTip->ReleaseReference(); 199 fToolTip = new (std::nothrow) BTextToolTip(toolTip.String()); 200 if (fToolTip == NULL) 201 return false; 202 203 *_tip = fToolTip; 204 205 return true; 206 } 207 208 209 void 210 TimeZoneView::_UpdateDateTime(BMessage* message) 211 { 212 // only need to update once every minute 213 int32 minute; 214 if (message->FindInt32("minute", &minute) == B_OK) { 215 if (fLastUpdateMinute != minute) { 216 _UpdateCurrent(); 217 _UpdatePreview(); 218 219 fLastUpdateMinute = minute; 220 } 221 } 222 } 223 224 225 void 226 TimeZoneView::_InitView() 227 { 228 // left side 229 BRect frameLeft(Bounds()); 230 frameLeft.right = frameLeft.Width() / 2.0; 231 frameLeft.InsetBy(10.0f, 10.0f); 232 233 // City Listing 234 fZoneList = new BOutlineListView(frameLeft, "cityList", 235 B_SINGLE_SELECTION_LIST); 236 fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED)); 237 fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE)); 238 239 _BuildZoneMenu(); 240 241 BScrollView* scrollList = new BScrollView("scrollList", fZoneList, 242 B_FOLLOW_ALL, 0, false, true); 243 AddChild(scrollList); 244 245 // right side 246 BRect frameRight(Bounds()); 247 frameRight.left = frameRight.Width() / 2.0; 248 frameRight.InsetBy(10.0f, 10.0f); 249 frameRight.top = frameLeft.top; 250 251 // Time Displays 252 fCurrent = new TTZDisplay(frameRight, "currentTime", "Current time:"); 253 AddChild(fCurrent); 254 fCurrent->ResizeToPreferred(); 255 256 frameRight.top = fCurrent->Frame().bottom + 10.0; 257 fPreview = new TTZDisplay(frameRight, "previewTime", "Preview time:"); 258 AddChild(fPreview); 259 fPreview->ResizeToPreferred(); 260 261 // set button 262 fSetZone = new BButton(frameRight, "setTimeZone", "Set time zone", 263 new BMessage(H_SET_TIME_ZONE)); 264 AddChild(fSetZone); 265 fSetZone->SetEnabled(false); 266 fSetZone->ResizeToPreferred(); 267 268 fSetZone->MoveTo(frameRight.right - fSetZone->Bounds().Width(), 269 scrollList->Frame().bottom - fSetZone->Bounds().Height()); 270 } 271 272 273 void 274 TimeZoneView::_BuildZoneMenu() 275 { 276 BTimeZone defaultTimeZone; 277 be_locale_roster->GetDefaultTimeZone(&defaultTimeZone); 278 279 BLanguage language; 280 be_locale->GetLanguage(&language); 281 282 BMessage countryList; 283 be_locale_roster->GetAvailableCountries(&countryList); 284 countryList.AddString("country", ""); 285 286 /* 287 * Group timezones by regions, but filter out unwanted (duplicate) regions 288 * and add an additional region with generic GMT-offset timezones at the end 289 */ 290 typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap; 291 ZoneItemMap zoneMap; 292 const char* kOtherRegion = "<Other>"; 293 const char* kSupportedRegions[] = { 294 "Africa", "America", "Antarctica", "Arctic", "Asia", "Atlantic", 295 "Australia", "Europe", "Indian", "Pacific", kOtherRegion, NULL 296 }; 297 for (const char** region = kSupportedRegions; *region != NULL; ++region) 298 zoneMap[*region] = NULL; 299 300 BString countryCode; 301 for (int c = 0; countryList.FindString("country", c, &countryCode) 302 == B_OK; c++) { 303 BCountry country("", countryCode); 304 BString countryName; 305 country.GetName(countryName); 306 307 // Now list the timezones for this country 308 BMessage zoneList; 309 be_locale_roster->GetAvailableTimeZonesForCountry(&zoneList, 310 countryCode.Length() == 0 ? NULL : countryCode.String()); 311 312 int32 count = 0; 313 type_code dummy; 314 zoneList.GetInfo("timeZone", &dummy, &count); 315 316 BString zoneID; 317 for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK; 318 tz++) { 319 int32 slashPos = zoneID.FindFirst('/'); 320 321 // ignore any "global" timezones, as those are just aliases of 322 // regional ones 323 if (slashPos <= 0) 324 continue; 325 326 BString region(zoneID, slashPos); 327 328 if (region == "Etc") 329 region = kOtherRegion; 330 else if (countryName.Length() == 0) { 331 // skip global timezones from other regions, we are just 332 // interested in the generic GMT-based ones under "Etc/" 333 continue; 334 } 335 336 337 // just accept timezones from "proper" regions, others are aliases 338 ZoneItemMap::iterator regionIter = zoneMap.find(region); 339 if (regionIter == zoneMap.end()) 340 continue; 341 342 BString fullCountryID = region; 343 if (countryName != region) 344 fullCountryID << "/" << countryName; 345 346 TimeZoneListItem* regionItem = regionIter->second; 347 if (regionItem == NULL) { 348 regionItem = new TimeZoneListItem(region, NULL, NULL); 349 regionItem->SetOutlineLevel(0); 350 zoneMap[region] = regionItem; 351 } 352 353 BTimeZone* timeZone = new BTimeZone(zoneID, &language); 354 BString tzName = timeZone->Name(); 355 if (tzName == "GMT+00:00") 356 tzName = "GMT"; 357 358 int32 openParenthesisPos = tzName.FindFirst('('); 359 if (openParenthesisPos >= 0) { 360 tzName.Remove(0, openParenthesisPos + 1); 361 int32 closeParenthesisPos = tzName.FindLast(')'); 362 if (closeParenthesisPos >= 0) 363 tzName.Truncate(closeParenthesisPos); 364 } 365 BString fullZoneID = fullCountryID; 366 fullZoneID << "/" << tzName; 367 368 // skip duplicates 369 ZoneItemMap::iterator zoneIter = zoneMap.find(fullZoneID); 370 if (zoneIter != zoneMap.end()) { 371 delete timeZone; 372 continue; 373 } 374 375 TimeZoneListItem* countryItem = NULL; 376 TimeZoneListItem* zoneItem = NULL; 377 if (count > 1 && countryName.Length() > 0) { 378 ZoneItemMap::iterator countryIter = zoneMap.find(fullCountryID); 379 if (countryIter == zoneMap.end()) { 380 countryItem = new TimeZoneListItem(countryName, NULL, NULL); 381 countryItem->SetOutlineLevel(1); 382 zoneMap[fullCountryID] = countryItem; 383 } else 384 countryItem = countryIter->second; 385 386 zoneItem = new TimeZoneListItem(tzName, NULL, timeZone); 387 zoneItem->SetOutlineLevel(2); 388 } else { 389 BString& name = countryName.Length() > 0 ? countryName : tzName; 390 zoneItem = new TimeZoneListItem(name, NULL, timeZone); 391 zoneItem->SetOutlineLevel(1); 392 } 393 zoneMap[fullZoneID] = zoneItem; 394 395 if (timeZone->ID() == defaultTimeZone.ID()) { 396 fCurrentZoneItem = zoneItem; 397 if (countryItem != NULL) 398 countryItem->SetExpanded(true); 399 regionItem->SetExpanded(true); 400 } 401 } 402 } 403 404 fOldZoneItem = fCurrentZoneItem; 405 406 ZoneItemMap::iterator zoneIter; 407 bool lastWasCountryItem = false; 408 TimeZoneListItem* lastCountryItem = NULL; 409 for (zoneIter = zoneMap.begin(); zoneIter != zoneMap.end(); ++zoneIter) { 410 if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) { 411 /* Some countries (e.g. Spain and Chile) have their timezones 412 * spread across different regions. As a result, there might still 413 * be country items with only one timezone below them. We manually 414 * filter those country items here. 415 */ 416 ZoneItemMap::iterator next = zoneIter; 417 ++next; 418 if (next != zoneMap.end() && next->second->OutlineLevel() != 2) { 419 fZoneList->RemoveItem(lastCountryItem); 420 zoneIter->second->SetText(lastCountryItem->Text()); 421 zoneIter->second->SetOutlineLevel(1); 422 delete lastCountryItem; 423 } 424 } 425 426 fZoneList->AddItem(zoneIter->second); 427 if (zoneIter->second->OutlineLevel() == 1) { 428 lastWasCountryItem = true; 429 lastCountryItem = zoneIter->second; 430 } else 431 lastWasCountryItem = false; 432 } 433 } 434 435 436 void 437 TimeZoneView::_Revert() 438 { 439 fCurrentZoneItem = fOldZoneItem; 440 441 if (fCurrentZoneItem != NULL) { 442 int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem); 443 fZoneList->Select(currentZoneIndex); 444 } else 445 fZoneList->DeselectAll(); 446 fZoneList->ScrollToSelection(); 447 448 _SetSystemTimeZone(); 449 _UpdatePreview(); 450 _UpdateCurrent(); 451 } 452 453 454 void 455 TimeZoneView::_UpdatePreview() 456 { 457 int32 selection = fZoneList->CurrentSelection(); 458 TimeZoneListItem* item 459 = selection < 0 460 ? NULL 461 : (TimeZoneListItem*)fZoneList->ItemAt(selection); 462 463 if (item == NULL || !item->HasTimeZone()) { 464 fPreview->SetText(""); 465 fPreview->SetTime(""); 466 return; 467 } 468 469 BString timeString = _FormatTime(item->TimeZone()); 470 fPreview->SetText(item->Text()); 471 fPreview->SetTime(timeString.String()); 472 473 fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0)); 474 } 475 476 477 void 478 TimeZoneView::_UpdateCurrent() 479 { 480 if (fCurrentZoneItem == NULL) 481 return; 482 483 BString timeString = _FormatTime(fCurrentZoneItem->TimeZone()); 484 fCurrent->SetText(fCurrentZoneItem->Text()); 485 fCurrent->SetTime(timeString.String()); 486 } 487 488 489 void 490 TimeZoneView::_SetSystemTimeZone() 491 { 492 /* Set sytem timezone for all different API levels. How to do this? 493 * 1) tell locale-roster about new default timezone 494 * 2) tell kernel about new timezone offset 495 */ 496 497 int32 selection = fZoneList->CurrentSelection(); 498 if (selection < 0) 499 return; 500 501 TimeZoneListItem* item 502 = static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection)); 503 if (item == NULL || !item->HasTimeZone()) 504 return; 505 506 fCurrentZoneItem = item; 507 const BTimeZone& timeZone = item->TimeZone(); 508 509 gMutableLocaleRoster->SetDefaultTimeZone(timeZone); 510 511 _kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(), 512 timeZone.ID().Length()); 513 514 fSetZone->SetEnabled(false); 515 fLastUpdateMinute = -1; 516 // just to trigger updating immediately 517 } 518 519 520 BString 521 TimeZoneView::_FormatTime(const BTimeZone& timeZone) 522 { 523 BString result; 524 525 time_t now = time(NULL); 526 bool rtcIsGMT; 527 _kern_get_real_time_clock_is_gmt(&rtcIsGMT); 528 if (!rtcIsGMT) { 529 int32 currentOffset 530 = fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone() 531 ? fCurrentZoneItem->OffsetFromGMT() 532 : 0; 533 now -= timeZone.OffsetFromGMT() - currentOffset; 534 } 535 be_locale->FormatTime(&result, now, false, &timeZone); 536 537 return result; 538 } 539