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 {
operator ()TimeZoneItemLess69 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
TimeZoneView(const char * name)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
CheckCanRevert()100 TimeZoneView::CheckCanRevert()
101 {
102 // check GMT vs Local setting
103 bool enable = fUseGmtTime != fOldUseGmtTime;
104
105 return enable || fCurrentZoneItem != fOldZoneItem;
106 }
107
108
~TimeZoneView()109 TimeZoneView::~TimeZoneView()
110 {
111 _WriteRTCSettings();
112 }
113
114
115 void
AttachedToWindow()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
DoLayout()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
MessageReceived(BMessage * message)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
_UpdateDateTime(BMessage * message)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
_InitView()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
_BuildZoneMenu()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
_Revert()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
_UpdatePreview()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
_UpdateCurrent()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
_SetSystemTimeZone()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
_FormatTime(const BTimeZone & timeZone)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
_ReadRTCSettings()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
_WriteRTCSettings()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
_UpdateGmtSettings()618 TimeZoneView::_UpdateGmtSettings()
619 {
620 _WriteRTCSettings();
621
622 _ShowOrHidePreview();
623
624 _kern_set_real_time_clock_is_gmt(fUseGmtTime);
625 }
626
627
628 void
_ShowOrHidePreview()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