xref: /haiku/src/preferences/time/ZoneView.cpp (revision fc75f2df0c666dcc61be83c4facdd3132340c2fb)
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 <RadioButton.h>
39 #include <ScrollView.h>
40 #include <StorageDefs.h>
41 #include <String.h>
42 #include <StringView.h>
43 #include <TimeZone.h>
44 #include <ToolTip.h>
45 #include <View.h>
46 #include <Window.h>
47 
48 #include <unicode/datefmt.h>
49 #include <unicode/utmscale.h>
50 #include <ICUWrapper.h>
51 
52 #include "TimeMessages.h"
53 #include "TimeZoneListItem.h"
54 #include "TZDisplay.h"
55 
56 
57 #undef B_TRANSLATE_CONTEXT
58 #define B_TRANSLATE_CONTEXT "Time"
59 
60 
61 using BPrivate::MutableLocaleRoster;
62 using BPrivate::ObjectDeleter;
63 
64 
65 struct TimeZoneItemLess {
66 	bool operator()(const BString& first, const BString& second)
67 	{
68 		// sort anything starting with '<' behind anything else
69 		if (first.ByteAt(0) == '<') {
70 			if (second.ByteAt(0) != '<')
71 				return false;
72 		} else if (second.ByteAt(0) == '<')
73 			return true;
74 		return fCollator.Compare(first.String(), second.String()) < 0;
75 	}
76 private:
77 	BCollator fCollator;
78 };
79 
80 
81 
82 TimeZoneView::TimeZoneView(const char* name)
83 	:
84 	BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING),
85 	fGmtTime(NULL),
86 	fToolTip(NULL),
87 	fUseGmtTime(false),
88 	fCurrentZoneItem(NULL),
89 	fOldZoneItem(NULL),
90 	fInitialized(false)
91 {
92 	_ReadRTCSettings();
93 	_InitView();
94 }
95 
96 
97 bool
98 TimeZoneView::CheckCanRevert()
99 {
100 	// check GMT vs Local setting
101 	bool enable = fUseGmtTime != fOldUseGmtTime;
102 
103 	return enable || fCurrentZoneItem != fOldZoneItem;
104 }
105 
106 
107 TimeZoneView::~TimeZoneView()
108 {
109 	if (fToolTip != NULL)
110 		fToolTip->ReleaseReference();
111 	_WriteRTCSettings();
112 }
113 
114 
115 void
116 TimeZoneView::AttachedToWindow()
117 {
118 	BView::AttachedToWindow();
119 	if (Parent())
120 		SetViewColor(Parent()->ViewColor());
121 
122 	if (!fInitialized) {
123 		fInitialized = true;
124 
125 		fSetZone->SetTarget(this);
126 		fZoneList->SetTarget(this);
127 	}
128 }
129 
130 
131 void
132 TimeZoneView::DoLayout()
133 {
134 	BView::DoLayout();
135 	if (fCurrentZoneItem != NULL) {
136 		fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem));
137 		fCurrent->SetText(fCurrentZoneItem->Text());
138 		fZoneList->ScrollToSelection();
139 	}
140 }
141 
142 
143 void
144 TimeZoneView::MessageReceived(BMessage* message)
145 {
146 	switch (message->what) {
147 		case B_OBSERVER_NOTICE_CHANGE:
148 		{
149 			int32 change;
150 			message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
151 			switch(change) {
152 				case H_TM_CHANGED:
153 					_UpdateDateTime(message);
154 					break;
155 
156 				default:
157 					BView::MessageReceived(message);
158 					break;
159 			}
160 			break;
161 		}
162 
163 		case H_CITY_CHANGED:
164 			_UpdatePreview();
165 			break;
166 
167 		case H_SET_TIME_ZONE:
168 		{
169 			_SetSystemTimeZone();
170 			_NotifyClockSettingChanged();
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 	BMessage countryList;
306 	BLocaleRoster::Default()->GetAvailableCountries(&countryList);
307 	countryList.AddString("country", "");
308 
309 	/*
310 	 * Group timezones by regions, but filter out unwanted (duplicate) regions
311 	 * and add an additional region with generic GMT-offset timezones at the end
312 	 */
313 	typedef	std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
314 	ZoneItemMap zoneItemMap;
315 	const char* kOtherRegion = B_TRANSLATE_MARK("<Other>");
316 	const char* kSupportedRegions[] = {
317 		B_TRANSLATE_MARK("Africa"),		B_TRANSLATE_MARK("America"),
318 		B_TRANSLATE_MARK("Antarctica"),	B_TRANSLATE_MARK("Arctic"),
319 		B_TRANSLATE_MARK("Asia"),		B_TRANSLATE_MARK("Atlantic"),
320 		B_TRANSLATE_MARK("Australia"),	B_TRANSLATE_MARK("Europe"),
321 		B_TRANSLATE_MARK("Indian"),		B_TRANSLATE_MARK("Pacific"),
322 		kOtherRegion,
323 		NULL
324 	};
325 	// Since the zone-map contains translated country-names (we get those from
326 	// ICU), we need to use translated region names in the zone-map, too:
327 	typedef	std::map<BString, BString> TranslatedRegionMap;
328 	TranslatedRegionMap regionMap;
329 	for (const char** region = kSupportedRegions; *region != NULL; ++region) {
330 		BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region);
331 		regionMap[*region] = translatedRegion;
332 
333 		TimeZoneListItem* regionItem
334 			= new TimeZoneListItem(translatedRegion, NULL, NULL);
335 		regionItem->SetOutlineLevel(0);
336 		zoneItemMap[translatedRegion] = regionItem;
337 	}
338 
339 	BString countryCode;
340 	for (int c = 0; countryList.FindString("country", c, &countryCode)
341 			== B_OK; c++) {
342 		BCountry country(countryCode);
343 		BString countryName;
344 		country.GetName(countryName);
345 
346 		// Now list the timezones for this country
347 		BMessage zoneList;
348 		BLocaleRoster::Default()->GetAvailableTimeZonesForCountry(&zoneList,
349 			countryCode.Length() == 0 ? NULL : countryCode.String());
350 
351 		int32 count = 0;
352 		type_code dummy;
353 		zoneList.GetInfo("timeZone", &dummy, &count);
354 
355 		BString zoneID;
356 		for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK;
357 			tz++) {
358 			int32 slashPos = zoneID.FindFirst('/');
359 
360 			// ignore any "global" timezones, as those are just aliases of
361 			// regional ones
362 			if (slashPos <= 0)
363 				continue;
364 
365 			BString region(zoneID, slashPos);
366 
367 			if (region == "Etc")
368 				region = kOtherRegion;
369 			else if (countryName.Length() == 0) {
370 				// skip global timezones from other regions, we are just
371 				// interested in the generic GMT-based ones under "Etc/"
372 				continue;
373 			}
374 
375 			// just accept timezones from our supported regions, others are
376 			// aliases and would just make the list even longer
377 			TranslatedRegionMap::iterator regionIter = regionMap.find(region);
378 			if (regionIter == regionMap.end())
379 				continue;
380 			const BString& regionName = regionIter->second;
381 
382 			BString fullCountryID = regionName;
383 			bool countryIsRegion = countryName == regionName;
384 			if (!countryIsRegion)
385 				fullCountryID << "/" << countryName;
386 
387 			BTimeZone* timeZone = new BTimeZone(zoneID, &language);
388 			BString tzName = timeZone->Name();
389 			if (tzName == "GMT+00:00")
390 				tzName = "GMT";
391 
392 			int32 openParenthesisPos = tzName.FindFirst('(');
393 			if (openParenthesisPos >= 0) {
394 				tzName.Remove(0, openParenthesisPos + 1);
395 				int32 closeParenthesisPos = tzName.FindLast(')');
396 				if (closeParenthesisPos >= 0)
397 					tzName.Truncate(closeParenthesisPos);
398 			}
399 			BString fullZoneID = fullCountryID;
400 			fullZoneID << "/" << tzName;
401 
402 			// skip duplicates
403 			ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID);
404 			if (zoneIter != zoneItemMap.end()) {
405 				delete timeZone;
406 				continue;
407 			}
408 
409 			TimeZoneListItem* countryItem = NULL;
410 			TimeZoneListItem* zoneItem = NULL;
411 			if (count > 1 && countryName.Length() > 0) {
412 				ZoneItemMap::iterator countryIter
413 					= zoneItemMap.find(fullCountryID);
414 				if (countryIter == zoneItemMap.end()) {
415 					countryItem = new TimeZoneListItem(countryName, NULL, NULL);
416 					countryItem->SetOutlineLevel(1);
417 					zoneItemMap[fullCountryID] = countryItem;
418 				} else
419 					countryItem = countryIter->second;
420 
421 				zoneItem = new TimeZoneListItem(tzName, NULL, timeZone);
422 				zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2);
423 			} else {
424 				BString& name = countryName.Length() > 0 ? countryName : tzName;
425 				zoneItem = new TimeZoneListItem(name, NULL, timeZone);
426 				zoneItem->SetOutlineLevel(1);
427 			}
428 			zoneItemMap[fullZoneID] = zoneItem;
429 
430 			if (timeZone->ID() == defaultTimeZone.ID()) {
431 				fCurrentZoneItem = zoneItem;
432 				if (countryItem != NULL)
433 					countryItem->SetExpanded(true);
434 				ZoneItemMap::iterator regionItemIter
435 					= zoneItemMap.find(regionName);
436 				if (regionItemIter != zoneItemMap.end())
437 					regionItemIter->second->SetExpanded(true);
438 			}
439 		}
440 	}
441 
442 	fOldZoneItem = fCurrentZoneItem;
443 
444 	ZoneItemMap::iterator zoneIter;
445 	bool lastWasCountryItem = false;
446 	TimeZoneListItem* currentCountryItem = NULL;
447 	for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end();
448 		++zoneIter) {
449 		if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
450 			/* Some countries (e.g. Spain and Chile) have their timezones
451 			 * spread across different regions. As a result, there might still
452 			 * be country items with only one timezone below them. We manually
453 			 * filter those country items here.
454 			 */
455 			ZoneItemMap::iterator next = zoneIter;
456 			++next;
457 			if (next != zoneItemMap.end()
458 				&& next->second->OutlineLevel() != 2) {
459 				fZoneList->RemoveItem(currentCountryItem);
460 				zoneIter->second->SetText(currentCountryItem->Text());
461 				zoneIter->second->SetOutlineLevel(1);
462 				delete currentCountryItem;
463 			}
464 		}
465 
466 		fZoneList->AddItem(zoneIter->second);
467 		if (zoneIter->second->OutlineLevel() == 1) {
468 			lastWasCountryItem = true;
469 			currentCountryItem = zoneIter->second;
470 		} else
471 			lastWasCountryItem = false;
472 	}
473 }
474 
475 
476 void
477 TimeZoneView::_Revert()
478 {
479 	fCurrentZoneItem = fOldZoneItem;
480 
481 	if (fCurrentZoneItem != NULL) {
482 		int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem);
483 		fZoneList->Select(currentZoneIndex);
484 	} else
485 		fZoneList->DeselectAll();
486 	fZoneList->ScrollToSelection();
487 
488 	fUseGmtTime = fOldUseGmtTime;
489 	if (fUseGmtTime)
490 		fGmtTime->SetValue(B_CONTROL_ON);
491 	else
492 		fLocalTime->SetValue(B_CONTROL_ON);
493 	_ShowOrHidePreview();
494 
495 	_UpdateGmtSettings();
496 	_SetSystemTimeZone();
497 	_UpdatePreview();
498 	_UpdateCurrent();
499 }
500 
501 
502 void
503 TimeZoneView::_UpdatePreview()
504 {
505 	int32 selection = fZoneList->CurrentSelection();
506 	TimeZoneListItem* item
507 		= selection < 0
508 			? NULL
509 			: (TimeZoneListItem*)fZoneList->ItemAt(selection);
510 
511 	if (item == NULL || !item->HasTimeZone()) {
512 		fPreview->SetText("");
513 		fPreview->SetTime("");
514 		return;
515 	}
516 
517 	BString timeString = _FormatTime(item->TimeZone());
518 	fPreview->SetText(item->Text());
519 	fPreview->SetTime(timeString.String());
520 
521 	fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
522 }
523 
524 
525 void
526 TimeZoneView::_UpdateCurrent()
527 {
528 	if (fCurrentZoneItem == NULL)
529 		return;
530 
531 	BString timeString = _FormatTime(fCurrentZoneItem->TimeZone());
532 	fCurrent->SetText(fCurrentZoneItem->Text());
533 	fCurrent->SetTime(timeString.String());
534 }
535 
536 
537 void
538 TimeZoneView::_SetSystemTimeZone()
539 {
540 	/*	Set sytem timezone for all different API levels. How to do this?
541 	 *	1) tell locale-roster about new default timezone
542 	 *	2) tell kernel about new timezone offset
543 	 */
544 
545 	int32 selection = fZoneList->CurrentSelection();
546 	if (selection < 0)
547 		return;
548 
549 	TimeZoneListItem* item
550 		= static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
551 	if (item == NULL || !item->HasTimeZone())
552 		return;
553 
554 	fCurrentZoneItem = item;
555 	const BTimeZone& timeZone = item->TimeZone();
556 
557 	MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone);
558 
559 	_kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(),
560 		timeZone.ID().Length());
561 
562 	fSetZone->SetEnabled(false);
563 	fLastUpdateMinute = -1;
564 		// just to trigger updating immediately
565 }
566 
567 
568 BString
569 TimeZoneView::_FormatTime(const BTimeZone& timeZone)
570 {
571 	BString result;
572 
573 	time_t now = time(NULL);
574 	bool rtcIsGMT;
575 	_kern_get_real_time_clock_is_gmt(&rtcIsGMT);
576 	if (!rtcIsGMT) {
577 		int32 currentOffset
578 			= fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone()
579 				? fCurrentZoneItem->OffsetFromGMT()
580 				: 0;
581 		now -= timeZone.OffsetFromGMT() - currentOffset;
582 	}
583 	BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT,
584 		&timeZone);
585 
586 	return result;
587 }
588 
589 
590 void
591 TimeZoneView::_ReadRTCSettings()
592 {
593 	BPath path;
594 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
595 		return;
596 
597 	path.Append("RTC_time_settings");
598 
599 	BEntry entry(path.Path());
600 	if (entry.Exists()) {
601 		BFile file(&entry, B_READ_ONLY);
602 		if (file.InitCheck() == B_OK) {
603 			char buffer[6];
604 			file.Read(buffer, 6);
605 			if (strncmp(buffer, "gmt", 3) == 0)
606 				fUseGmtTime = true;
607 		}
608 	}
609 }
610 
611 
612 void
613 TimeZoneView::_WriteRTCSettings()
614 {
615 	BPath path;
616 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK)
617 		return;
618 
619 	path.Append("RTC_time_settings");
620 
621 	BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
622 	if (file.InitCheck() == B_OK) {
623 		if (fUseGmtTime)
624 			file.Write("gmt", 3);
625 		else
626 			file.Write("local", 5);
627 	}
628 }
629 
630 
631 void
632 TimeZoneView::_UpdateGmtSettings()
633 {
634 	_WriteRTCSettings();
635 
636 	_ShowOrHidePreview();
637 	_NotifyClockSettingChanged();
638 
639 	_kern_set_real_time_clock_is_gmt(fUseGmtTime);
640 }
641 
642 
643 void
644 TimeZoneView::_ShowOrHidePreview()
645 {
646 	if (fUseGmtTime) {
647 		// Hardware clock uses GMT time, changing timezone will adjust the
648 		// offset and we need to display a preview
649 		fCurrent->Show();
650 		fPreview->Show();
651 	} else {
652 		// Hardware clock uses local time, changing timezone will adjust the
653 		// clock and there is no offset to manage, thus, no preview.
654 		fCurrent->Hide();
655 		fPreview->Hide();
656 	}
657 }
658 
659 
660 void
661 TimeZoneView::_NotifyClockSettingChanged()
662 {
663 	BMessage msg(kMsgClockSettingChanged);
664 	Window()->PostMessage(&msg);
665 }
666 
667