xref: /haiku/src/preferences/time/ZoneView.cpp (revision 7a74a5df454197933bc6e80a542102362ee98703)
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 #include <vector>
23 
24 #include <AutoDeleter.h>
25 #include <Button.h>
26 #include <Catalog.h>
27 #include <Collator.h>
28 #include <ControlLook.h>
29 #include <Country.h>
30 #include <Directory.h>
31 #include <Entry.h>
32 #include <File.h>
33 #include <FindDirectory.h>
34 #include <ListItem.h>
35 #include <Locale.h>
36 #include <MutableLocaleRoster.h>
37 #include <OutlineListView.h>
38 #include <Path.h>
39 #include <RadioButton.h>
40 #include <ScrollView.h>
41 #include <StorageDefs.h>
42 #include <String.h>
43 #include <StringView.h>
44 #include <TimeZone.h>
45 #include <ToolTip.h>
46 #include <View.h>
47 #include <Window.h>
48 
49 #include <unicode/datefmt.h>
50 #include <unicode/utmscale.h>
51 #include <ICUWrapper.h>
52 
53 #include "TimeMessages.h"
54 #include "TimeZoneListItem.h"
55 #include "TZDisplay.h"
56 
57 
58 #undef B_TRANSLATION_CONTEXT
59 #define B_TRANSLATION_CONTEXT "Time"
60 
61 
62 using BPrivate::MutableLocaleRoster;
63 using BPrivate::ObjectDeleter;
64 
65 
66 struct TimeZoneItemLess {
67 	bool operator()(const BString& first, const BString& second)
68 	{
69 		// sort anything starting with '<' behind anything else
70 		if (first.ByteAt(0) == '<') {
71 			if (second.ByteAt(0) != '<')
72 				return false;
73 		} else if (second.ByteAt(0) == '<')
74 			return true;
75 		return fCollator.Compare(first.String(), second.String()) < 0;
76 	}
77 private:
78 	BCollator fCollator;
79 };
80 
81 
82 
83 TimeZoneView::TimeZoneView(const char* name)
84 	:
85 	BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING),
86 	fGmtTime(NULL),
87 	fToolTip(NULL),
88 	fUseGmtTime(false),
89 	fCurrentZoneItem(NULL),
90 	fOldZoneItem(NULL),
91 	fInitialized(false)
92 {
93 	_ReadRTCSettings();
94 	_InitView();
95 }
96 
97 
98 bool
99 TimeZoneView::CheckCanRevert()
100 {
101 	// check GMT vs Local setting
102 	bool enable = fUseGmtTime != fOldUseGmtTime;
103 
104 	return enable || fCurrentZoneItem != fOldZoneItem;
105 }
106 
107 
108 TimeZoneView::~TimeZoneView()
109 {
110 	if (fToolTip != NULL)
111 		fToolTip->ReleaseReference();
112 	_WriteRTCSettings();
113 }
114 
115 
116 void
117 TimeZoneView::AttachedToWindow()
118 {
119 	BView::AttachedToWindow();
120 	if (Parent())
121 		SetViewColor(Parent()->ViewColor());
122 
123 	if (!fInitialized) {
124 		fInitialized = true;
125 
126 		fSetZone->SetTarget(this);
127 		fZoneList->SetTarget(this);
128 	}
129 }
130 
131 
132 void
133 TimeZoneView::DoLayout()
134 {
135 	BView::DoLayout();
136 	if (fCurrentZoneItem != NULL) {
137 		fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem));
138 		fCurrent->SetText(fCurrentZoneItem->Text());
139 		fZoneList->ScrollToSelection();
140 	}
141 }
142 
143 
144 void
145 TimeZoneView::MessageReceived(BMessage* message)
146 {
147 	switch (message->what) {
148 		case B_OBSERVER_NOTICE_CHANGE:
149 		{
150 			int32 change;
151 			message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
152 			switch(change) {
153 				case H_TM_CHANGED:
154 					_UpdateDateTime(message);
155 					break;
156 
157 				default:
158 					BView::MessageReceived(message);
159 					break;
160 			}
161 			break;
162 		}
163 
164 		case H_CITY_CHANGED:
165 			_UpdatePreview();
166 			break;
167 
168 		case H_SET_TIME_ZONE:
169 		{
170 			_SetSystemTimeZone();
171 			break;
172 		}
173 
174 		case kMsgRevert:
175 			_Revert();
176 			break;
177 
178 		case kRTCUpdate:
179 			fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON;
180 			_UpdateGmtSettings();
181 			_UpdateCurrent();
182 			_UpdatePreview();
183 			break;
184 
185 		default:
186 			BGroupView::MessageReceived(message);
187 			break;
188 	}
189 }
190 
191 
192 bool
193 TimeZoneView::GetToolTipAt(BPoint point, BToolTip** _tip)
194 {
195 	TimeZoneListItem* item = static_cast<TimeZoneListItem*>(
196 		fZoneList->ItemAt(fZoneList->IndexOf(point)));
197 	if (item == NULL || !item->HasTimeZone())
198 		return false;
199 
200 	BString nowInTimeZone;
201 	time_t now = time(NULL);
202 	BLocale::Default()->FormatTime(&nowInTimeZone, now, B_SHORT_TIME_FORMAT,
203 		&item->TimeZone());
204 
205 	BString dateInTimeZone;
206 	BLocale::Default()->FormatDate(&dateInTimeZone, now, B_SHORT_DATE_FORMAT,
207 		&item->TimeZone());
208 
209 	BString toolTip = item->Text();
210 	toolTip << '\n' << item->TimeZone().ShortName() << " / "
211 			<< item->TimeZone().ShortDaylightSavingName()
212 			<< B_TRANSLATE("\nNow: ") << nowInTimeZone
213 			<< " (" << dateInTimeZone << ')';
214 
215 	if (fToolTip != NULL)
216 		fToolTip->ReleaseReference();
217 	fToolTip = new (std::nothrow) BTextToolTip(toolTip.String());
218 	if (fToolTip == NULL)
219 		return false;
220 
221 	*_tip = fToolTip;
222 
223 	return true;
224 }
225 
226 
227 void
228 TimeZoneView::_UpdateDateTime(BMessage* message)
229 {
230 	// only need to update once every minute
231 	int32 minute;
232 	if (message->FindInt32("minute", &minute) == B_OK) {
233 		if (fLastUpdateMinute != minute) {
234 			_UpdateCurrent();
235 			_UpdatePreview();
236 
237 			fLastUpdateMinute = minute;
238 		}
239 	}
240 }
241 
242 
243 void
244 TimeZoneView::_InitView()
245 {
246 	fZoneList = new BOutlineListView("cityList", B_SINGLE_SELECTION_LIST);
247 	fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
248 	fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
249 	_BuildZoneMenu();
250 	BScrollView* scrollList = new BScrollView("scrollList", fZoneList,
251 		B_FRAME_EVENTS | B_WILL_DRAW, false, true);
252 	scrollList->SetExplicitMinSize(BSize(200, 0));
253 
254 	fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:"));
255 	fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:"));
256 
257 	fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"),
258 		new BMessage(H_SET_TIME_ZONE));
259 	fSetZone->SetEnabled(false);
260 	fSetZone->SetExplicitAlignment(
261 		BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM));
262 
263 	BStringView* text = new BStringView("clockSetTo",
264 		B_TRANSLATE("Hardware clock set to:"));
265 	fLocalTime = new BRadioButton("localTime",
266 		B_TRANSLATE("Local time (Windows compatible)"), new BMessage(kRTCUpdate));
267 	fGmtTime = new BRadioButton("greenwichMeanTime",
268 		B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate));
269 
270 	if (fUseGmtTime)
271 		fGmtTime->SetValue(B_CONTROL_ON);
272 	else
273 		fLocalTime->SetValue(B_CONTROL_ON);
274 	_ShowOrHidePreview();
275 	fOldUseGmtTime = fUseGmtTime;
276 
277 
278 	const float kInset = be_control_look->DefaultItemSpacing();
279 	BLayoutBuilder::Group<>(this)
280 		.Add(scrollList)
281 		.AddGroup(B_VERTICAL, kInset)
282 			.Add(text)
283 			.AddGroup(B_VERTICAL, kInset)
284 				.Add(fLocalTime)
285 				.Add(fGmtTime)
286 			.End()
287 			.AddGlue()
288 			.Add(fCurrent)
289 			.Add(fPreview)
290 			.Add(fSetZone)
291 		.End()
292 		.SetInsets(kInset, kInset, kInset, kInset);
293 }
294 
295 
296 void
297 TimeZoneView::_BuildZoneMenu()
298 {
299 	BTimeZone defaultTimeZone;
300 	BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone);
301 
302 	BLanguage language;
303 	BLocale::Default()->GetLanguage(&language);
304 
305 	/*
306 	 * Group timezones by regions, but filter out unwanted (duplicate) regions
307 	 * and add an additional region with generic GMT-offset timezones at the end
308 	 */
309 	typedef	std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
310 	ZoneItemMap zoneItemMap;
311 	const char* kOtherRegion = B_TRANSLATE_MARK("<Other>");
312 	const char* kSupportedRegions[] = {
313 		B_TRANSLATE_MARK("Africa"),		B_TRANSLATE_MARK("America"),
314 		B_TRANSLATE_MARK("Antarctica"),	B_TRANSLATE_MARK("Arctic"),
315 		B_TRANSLATE_MARK("Asia"),		B_TRANSLATE_MARK("Atlantic"),
316 		B_TRANSLATE_MARK("Australia"),	B_TRANSLATE_MARK("Europe"),
317 		B_TRANSLATE_MARK("Indian"),		B_TRANSLATE_MARK("Pacific"),
318 		kOtherRegion,
319 		NULL
320 	};
321 	// Since the zone-map contains translated country-names (we get those from
322 	// ICU), we need to use translated region names in the zone-map, too:
323 	typedef	std::map<BString, BString> TranslatedRegionMap;
324 	TranslatedRegionMap regionMap;
325 	for (const char** region = kSupportedRegions; *region != NULL; ++region) {
326 		BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region);
327 		regionMap[*region] = translatedRegion;
328 
329 		TimeZoneListItem* regionItem
330 			= new TimeZoneListItem(translatedRegion, NULL, NULL);
331 		regionItem->SetOutlineLevel(0);
332 		zoneItemMap[translatedRegion] = regionItem;
333 	}
334 
335 	// Get all time zones
336 	BMessage zoneList;
337 	BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList);
338 
339 	typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap;
340 	ZonesByCountyMap zonesByCountryMap;
341 	const char *zoneID;
342 	BString countryCode;
343 	for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK
344 			&& zoneList.FindString("region", tz, &countryCode) == B_OK; tz++) {
345 		// From the global ("001") timezones, we only accept the generic GMT
346 		// timezones, as all the other world-zones are duplicates of others.
347 		if (countryCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0)
348 			continue;
349 		zonesByCountryMap[countryCode].push_back(zoneID);
350 	}
351 
352 	ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin();
353 	for (; countryIter != zonesByCountryMap.end(); ++countryIter) {
354 		BCountry country(countryIter->first.String());
355 		BString countryName;
356 		country.GetName(countryName);
357 
358 		size_t zoneCountInCountry = countryIter->second.size();
359 		for (size_t tz = 0; tz < zoneCountInCountry; tz++) {
360 			BString zoneID(countryIter->second[tz]);
361 			int32 slashPos = zoneID.FindFirst('/');
362 
363 			BString region(zoneID, slashPos);
364 			if (region == "Etc")
365 				region = kOtherRegion;
366 
367 			// just accept timezones from our supported regions, others are
368 			// aliases and would just make the list even longer
369 			TranslatedRegionMap::iterator regionIter = regionMap.find(region);
370 			if (regionIter == regionMap.end())
371 				continue;
372 
373 			BString fullCountryID = regionIter->second;
374 			bool countryIsRegion
375 				= countryName == regionIter->second || region == kOtherRegion;
376 			if (!countryIsRegion)
377 				fullCountryID << "/" << countryName;
378 
379 			BTimeZone* timeZone = new BTimeZone(zoneID, &language);
380 			BString tzName;
381 			BString fullZoneID = fullCountryID;
382 			if (zoneCountInCountry > 1)
383 			{
384 				// we can't use the country name as timezone name, since there
385 				// are more than one timezones in this country - fetch the
386 				// localized name of the timezone and use that
387 				tzName = timeZone->Name();
388 				int32 openParenthesisPos = tzName.FindFirst('(');
389 				if (openParenthesisPos >= 0) {
390 					tzName.Remove(0, openParenthesisPos + 1);
391 					int32 closeParenthesisPos = tzName.FindLast(')');
392 					if (closeParenthesisPos >= 0)
393 						tzName.Truncate(closeParenthesisPos);
394 				}
395 				fullZoneID << "/" << tzName;
396 			} else {
397 				tzName = countryName;
398 				fullZoneID << "/" << zoneID;
399 			}
400 
401 			// skip duplicates
402 			ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID);
403 			if (zoneIter != zoneItemMap.end()) {
404 				delete timeZone;
405 				continue;
406 			}
407 
408 			TimeZoneListItem* countryItem = NULL;
409 			TimeZoneListItem* zoneItem = NULL;
410 			if (zoneCountInCountry > 1) {
411 				ZoneItemMap::iterator countryIter
412 					= zoneItemMap.find(fullCountryID);
413 				if (countryIter == zoneItemMap.end()) {
414 					countryItem = new TimeZoneListItem(countryName, NULL, NULL);
415 					countryItem->SetOutlineLevel(1);
416 					zoneItemMap[fullCountryID] = countryItem;
417 				} else
418 					countryItem = countryIter->second;
419 
420 				zoneItem = new TimeZoneListItem(tzName, NULL, timeZone);
421 				zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2);
422 			} else {
423 				zoneItem = new TimeZoneListItem(tzName, NULL, timeZone);
424 				zoneItem->SetOutlineLevel(1);
425 			}
426 			zoneItemMap[fullZoneID] = zoneItem;
427 
428 			if (timeZone->ID() == defaultTimeZone.ID()) {
429 				fCurrentZoneItem = zoneItem;
430 				if (countryItem != NULL)
431 					countryItem->SetExpanded(true);
432 				ZoneItemMap::iterator regionItemIter
433 					= zoneItemMap.find(regionIter->second);
434 				if (regionItemIter != zoneItemMap.end())
435 					regionItemIter->second->SetExpanded(true);
436 			}
437 		}
438 	}
439 
440 	fOldZoneItem = fCurrentZoneItem;
441 
442 	ZoneItemMap::iterator zoneIter;
443 	bool lastWasCountryItem = false;
444 	TimeZoneListItem* currentCountryItem = NULL;
445 	for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end();
446 		++zoneIter) {
447 		if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
448 			/* Some countries (e.g. Spain and Chile) have their timezones
449 			 * spread across different regions. As a result, there might still
450 			 * be country items with only one timezone below them. We manually
451 			 * filter those country items here.
452 			 */
453 			ZoneItemMap::iterator next = zoneIter;
454 			++next;
455 			if (next != zoneItemMap.end()
456 				&& next->second->OutlineLevel() != 2) {
457 				fZoneList->RemoveItem(currentCountryItem);
458 				zoneIter->second->SetText(currentCountryItem->Text());
459 				zoneIter->second->SetOutlineLevel(1);
460 				delete currentCountryItem;
461 			}
462 		}
463 
464 		fZoneList->AddItem(zoneIter->second);
465 		if (zoneIter->second->OutlineLevel() == 1) {
466 			lastWasCountryItem = true;
467 			currentCountryItem = zoneIter->second;
468 		} else
469 			lastWasCountryItem = false;
470 	}
471 }
472 
473 
474 void
475 TimeZoneView::_Revert()
476 {
477 	fCurrentZoneItem = fOldZoneItem;
478 
479 	if (fCurrentZoneItem != NULL) {
480 		int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem);
481 		fZoneList->Select(currentZoneIndex);
482 	} else
483 		fZoneList->DeselectAll();
484 	fZoneList->ScrollToSelection();
485 
486 	fUseGmtTime = fOldUseGmtTime;
487 	if (fUseGmtTime)
488 		fGmtTime->SetValue(B_CONTROL_ON);
489 	else
490 		fLocalTime->SetValue(B_CONTROL_ON);
491 	_ShowOrHidePreview();
492 
493 	_UpdateGmtSettings();
494 	_SetSystemTimeZone();
495 	_UpdatePreview();
496 	_UpdateCurrent();
497 }
498 
499 
500 void
501 TimeZoneView::_UpdatePreview()
502 {
503 	int32 selection = fZoneList->CurrentSelection();
504 	TimeZoneListItem* item
505 		= selection < 0
506 			? NULL
507 			: (TimeZoneListItem*)fZoneList->ItemAt(selection);
508 
509 	if (item == NULL || !item->HasTimeZone()) {
510 		fPreview->SetText("");
511 		fPreview->SetTime("");
512 		return;
513 	}
514 
515 	BString timeString = _FormatTime(item->TimeZone());
516 	fPreview->SetText(item->Text());
517 	fPreview->SetTime(timeString.String());
518 
519 	fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
520 }
521 
522 
523 void
524 TimeZoneView::_UpdateCurrent()
525 {
526 	if (fCurrentZoneItem == NULL)
527 		return;
528 
529 	BString timeString = _FormatTime(fCurrentZoneItem->TimeZone());
530 	fCurrent->SetText(fCurrentZoneItem->Text());
531 	fCurrent->SetTime(timeString.String());
532 }
533 
534 
535 void
536 TimeZoneView::_SetSystemTimeZone()
537 {
538 	/*	Set system timezone for all different API levels. How to do this?
539 	 *	1) tell locale-roster about new default timezone
540 	 *	2) tell kernel about new timezone offset
541 	 */
542 
543 	int32 selection = fZoneList->CurrentSelection();
544 	if (selection < 0)
545 		return;
546 
547 	TimeZoneListItem* item
548 		= static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
549 	if (item == NULL || !item->HasTimeZone())
550 		return;
551 
552 	fCurrentZoneItem = item;
553 	const BTimeZone& timeZone = item->TimeZone();
554 
555 	MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone);
556 
557 	_kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(),
558 		timeZone.ID().Length());
559 
560 	fSetZone->SetEnabled(false);
561 	fLastUpdateMinute = -1;
562 		// just to trigger updating immediately
563 }
564 
565 
566 BString
567 TimeZoneView::_FormatTime(const BTimeZone& timeZone)
568 {
569 	BString result;
570 
571 	time_t now = time(NULL);
572 	bool rtcIsGMT;
573 	_kern_get_real_time_clock_is_gmt(&rtcIsGMT);
574 	if (!rtcIsGMT) {
575 		int32 currentOffset
576 			= fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone()
577 				? fCurrentZoneItem->OffsetFromGMT()
578 				: 0;
579 		now -= timeZone.OffsetFromGMT() - currentOffset;
580 	}
581 	BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT,
582 		&timeZone);
583 
584 	return result;
585 }
586 
587 
588 void
589 TimeZoneView::_ReadRTCSettings()
590 {
591 	BPath path;
592 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
593 		return;
594 
595 	path.Append("RTC_time_settings");
596 
597 	BEntry entry(path.Path());
598 	if (entry.Exists()) {
599 		BFile file(&entry, B_READ_ONLY);
600 		if (file.InitCheck() == B_OK) {
601 			char buffer[6];
602 			file.Read(buffer, 6);
603 			if (strncmp(buffer, "gmt", 3) == 0)
604 				fUseGmtTime = true;
605 		}
606 	}
607 }
608 
609 
610 void
611 TimeZoneView::_WriteRTCSettings()
612 {
613 	BPath path;
614 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK)
615 		return;
616 
617 	path.Append("RTC_time_settings");
618 
619 	BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
620 	if (file.InitCheck() == B_OK) {
621 		if (fUseGmtTime)
622 			file.Write("gmt", 3);
623 		else
624 			file.Write("local", 5);
625 	}
626 }
627 
628 
629 void
630 TimeZoneView::_UpdateGmtSettings()
631 {
632 	_WriteRTCSettings();
633 
634 	_ShowOrHidePreview();
635 
636 	_kern_set_real_time_clock_is_gmt(fUseGmtTime);
637 }
638 
639 
640 void
641 TimeZoneView::_ShowOrHidePreview()
642 {
643 	if (fUseGmtTime) {
644 		// Hardware clock uses GMT time, changing timezone will adjust the
645 		// offset and we need to display a preview
646 		fCurrent->Show();
647 		fPreview->Show();
648 	} else {
649 		// Hardware clock uses local time, changing timezone will adjust the
650 		// clock and there is no offset to manage, thus, no preview.
651 		fCurrent->Hide();
652 		fPreview->Hide();
653 	}
654 }
655 
656