xref: /haiku/src/preferences/time/ZoneView.cpp (revision e711e6e42fd7ec3111ba9dc2324fa8efedd6674b)
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 {
69 	bool operator()(const BString& first, const BString& second)
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 
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
100 TimeZoneView::CheckCanRevert()
101 {
102 	// check GMT vs Local setting
103 	bool enable = fUseGmtTime != fOldUseGmtTime;
104 
105 	return enable || fCurrentZoneItem != fOldZoneItem;
106 }
107 
108 
109 TimeZoneView::~TimeZoneView()
110 {
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 			break;
171 		}
172 
173 		case kMsgRevert:
174 			_Revert();
175 			break;
176 
177 		case kRTCUpdate:
178 			fUseGmtTime = fGmtTime->Value() == B_CONTROL_ON;
179 			_UpdateGmtSettings();
180 			_UpdateCurrent();
181 			_UpdatePreview();
182 			break;
183 
184 		default:
185 			BGroupView::MessageReceived(message);
186 			break;
187 	}
188 }
189 
190 
191 void
192 TimeZoneView::_UpdateDateTime(BMessage* message)
193 {
194 	// only need to update once every minute
195 	int32 minute;
196 	if (message->FindInt32("minute", &minute) == B_OK) {
197 		if (fLastUpdateMinute != minute) {
198 			_UpdateCurrent();
199 			_UpdatePreview();
200 
201 			fLastUpdateMinute = minute;
202 		}
203 	}
204 }
205 
206 
207 void
208 TimeZoneView::_InitView()
209 {
210 	fZoneList = new TimeZoneListView();
211 	fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
212 	fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
213 	_BuildZoneMenu();
214 	BScrollView* scrollList = new BScrollView("scrollList", fZoneList,
215 		B_FRAME_EVENTS | B_WILL_DRAW, false, true);
216 	scrollList->SetExplicitMinSize(BSize(200, 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 	const float kIndentSpacing
241 		= be_control_look->DefaultItemSpacing() * 2;
242 	BLayoutBuilder::Group<>(this)
243 		.Add(scrollList)
244 		.AddGroup(B_VERTICAL, 0)
245 			.Add(new BStringView("clockSetTo",
246 				B_TRANSLATE("Hardware clock set to:")))
247 			.AddGroup(B_VERTICAL, 0)
248 				.Add(fLocalTime)
249 				.Add(fGmtTime)
250 				.SetInsets(kIndentSpacing, 0, 0, 0)
251 			.End()
252 			.AddGlue()
253 			.AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING)
254 				.Add(fCurrent)
255 				.Add(fPreview)
256 			.End()
257 			.Add(fSetZone)
258 		.End()
259 		.SetInsets(B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING,
260 			B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING);
261 }
262 
263 
264 void
265 TimeZoneView::_BuildZoneMenu()
266 {
267 	BTimeZone defaultTimeZone;
268 	BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone);
269 
270 	BLanguage language;
271 	BLocale::Default()->GetLanguage(&language);
272 
273 	// Group timezones by regions, but filter out unwanted (duplicate) regions
274 	// and add an additional region with generic GMT-offset timezones at the end
275 	typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
276 	ZoneItemMap zoneItemMap;
277 	const char* kOtherRegion = B_TRANSLATE_MARK("<Other>");
278 	const char* kSupportedRegions[] = {
279 		B_TRANSLATE_MARK("Africa"),		B_TRANSLATE_MARK("America"),
280 		B_TRANSLATE_MARK("Antarctica"),	B_TRANSLATE_MARK("Arctic"),
281 		B_TRANSLATE_MARK("Asia"),		B_TRANSLATE_MARK("Atlantic"),
282 		B_TRANSLATE_MARK("Australia"),	B_TRANSLATE_MARK("Europe"),
283 		B_TRANSLATE_MARK("Indian"),		B_TRANSLATE_MARK("Pacific"),
284 		kOtherRegion,
285 		NULL
286 	};
287 
288 	// Since the zone-map contains translated country-names (we get those from
289 	// ICU), we need to use translated region names in the zone-map, too:
290 	typedef std::map<BString, BString> TranslatedRegionMap;
291 	TranslatedRegionMap regionMap;
292 	for (const char** region = kSupportedRegions; *region != NULL; ++region) {
293 		BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region);
294 		regionMap[*region] = translatedRegion;
295 
296 		TimeZoneListItem* regionItem
297 			= new TimeZoneListItem(translatedRegion, NULL, NULL);
298 		regionItem->SetOutlineLevel(0);
299 		zoneItemMap[translatedRegion] = regionItem;
300 	}
301 
302 	// Get all time zones
303 	BMessage zoneList;
304 	BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList);
305 
306 	typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap;
307 	ZonesByCountyMap zonesByCountryMap;
308 	const char* zoneID;
309 	BString timeZoneCode;
310 	for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK
311 			&& zoneList.FindString("region", tz, &timeZoneCode) == B_OK; tz++) {
312 		// From the global ("001") timezones, we only accept the generic GMT
313 		// timezones, as all the other world-zones are duplicates of others.
314 		if (timeZoneCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0)
315 			continue;
316 		zonesByCountryMap[timeZoneCode].push_back(zoneID);
317 	}
318 
319 	ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin();
320 	for (; countryIter != zonesByCountryMap.end(); ++countryIter) {
321 		const char* countryCode = countryIter->first.String();
322 		if (countryCode == NULL)
323 			continue;
324 
325 		size_t zoneCountInCountry = countryIter->second.size();
326 		for (size_t tz = 0; tz < zoneCountInCountry; tz++) {
327 			BString zoneID(countryIter->second[tz]);
328 			BTimeZone* timeZone
329 				= new(std::nothrow) BTimeZone(zoneID, &language);
330 			if (timeZone == NULL)
331 				continue;
332 
333 			int32 slashPos = zoneID.FindFirst('/');
334 			BString region(zoneID, slashPos);
335 			if (region == "Etc")
336 				region = kOtherRegion;
337 
338 			// just accept timezones from our supported regions, others are
339 			// aliases and would just make the list even longer
340 			TranslatedRegionMap::iterator regionIter = regionMap.find(region);
341 			if (regionIter == regionMap.end())
342 				continue;
343 
344 			BString fullCountryID = regionIter->second;
345 			BCountry* country = new(std::nothrow) BCountry(countryCode);
346 			if (country == NULL)
347 				continue;
348 
349 			BString countryName;
350 			country->GetName(countryName);
351 			bool hasUsedCountry = false;
352 			bool countryIsRegion = countryName == regionIter->second
353 				|| region == kOtherRegion;
354 			if (!countryIsRegion)
355 				fullCountryID << "/" << countryName;
356 
357 			BString timeZoneName;
358 			BString fullZoneID = fullCountryID;
359 			if (zoneCountInCountry > 1) {
360 				// we can't use the country name as timezone name, since there
361 				// are more than one timezones in this country - fetch the
362 				// localized name of the timezone and use that
363 				timeZoneName = timeZone->Name();
364 				int32 openParenthesisPos = timeZoneName.FindFirst('(');
365 				if (openParenthesisPos >= 0) {
366 					timeZoneName.Remove(0, openParenthesisPos + 1);
367 					int32 closeParenthesisPos = timeZoneName.FindLast(')');
368 					if (closeParenthesisPos >= 0)
369 						timeZoneName.Truncate(closeParenthesisPos);
370 				}
371 				fullZoneID << "/" << timeZoneName;
372 			} else {
373 				timeZoneName = countryName;
374 				fullZoneID << "/" << zoneID;
375 			}
376 
377 			// skip duplicates
378 			ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID);
379 			if (zoneIter != zoneItemMap.end()) {
380 				delete timeZone;
381 				continue;
382 			}
383 
384 			TimeZoneListItem* countryItem = NULL;
385 			TimeZoneListItem* zoneItem = NULL;
386 			if (zoneCountInCountry > 1) {
387 				ZoneItemMap::iterator countryIter
388 					= zoneItemMap.find(fullCountryID);
389 				if (countryIter == zoneItemMap.end()) {
390 					countryItem = new TimeZoneListItem(countryName.String(),
391 						country, NULL);
392 					countryItem->SetOutlineLevel(1);
393 					zoneItemMap[fullCountryID] = countryItem;
394 					hasUsedCountry = true;
395 				} else
396 					countryItem = countryIter->second;
397 
398 				zoneItem = new TimeZoneListItem(timeZoneName.String(),
399 					NULL, timeZone);
400 				zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2);
401 			} else {
402 				zoneItem = new TimeZoneListItem(timeZoneName.String(),
403 					country, timeZone);
404 				zoneItem->SetOutlineLevel(1);
405 				hasUsedCountry = true;
406 			}
407 			zoneItemMap[fullZoneID] = zoneItem;
408 
409 			if (timeZone->ID() == defaultTimeZone.ID()) {
410 				fCurrentZoneItem = zoneItem;
411 				if (countryItem != NULL)
412 					countryItem->SetExpanded(true);
413 
414 				ZoneItemMap::iterator regionItemIter
415 					= zoneItemMap.find(regionIter->second);
416 				if (regionItemIter != zoneItemMap.end())
417 					regionItemIter->second->SetExpanded(true);
418 			}
419 
420 			if (!hasUsedCountry)
421 				delete country;
422 		}
423 	}
424 
425 	fOldZoneItem = fCurrentZoneItem;
426 
427 	ZoneItemMap::iterator zoneIter;
428 	bool lastWasCountryItem = false;
429 	TimeZoneListItem* currentItem = NULL;
430 	for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end();
431 			++zoneIter) {
432 		if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
433 			// Some countries (e.g. Spain and Chile) have their timezones
434 			// spread across different regions. As a result, there might still
435 			// be country items with only one timezone below them. We manually
436 			// filter those country items here.
437 			ZoneItemMap::iterator next = zoneIter;
438 			++next;
439 			if (next != zoneItemMap.end()
440 				&& next->second->OutlineLevel() != 2) {
441 				fZoneList->RemoveItem(currentItem);
442 				zoneIter->second->SetText(currentItem->Text());
443 				zoneIter->second->SetCountry(currentItem->HasCountry()
444 					? new(std::nothrow) BCountry(currentItem->Country())
445 					: NULL);
446 				zoneIter->second->SetTimeZone(currentItem->HasTimeZone()
447 					? new(std::nothrow) BTimeZone(currentItem->TimeZone())
448 					: NULL);
449 				zoneIter->second->SetOutlineLevel(1);
450 				delete currentItem;
451 			}
452 		}
453 
454 		fZoneList->AddItem(zoneIter->second);
455 		if (zoneIter->second->OutlineLevel() == 1) {
456 			lastWasCountryItem = true;
457 			currentItem = zoneIter->second;
458 		} else
459 			lastWasCountryItem = false;
460 	}
461 }
462 
463 
464 void
465 TimeZoneView::_Revert()
466 {
467 	fCurrentZoneItem = fOldZoneItem;
468 
469 	if (fCurrentZoneItem != NULL) {
470 		int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem);
471 		fZoneList->Select(currentZoneIndex);
472 	} else
473 		fZoneList->DeselectAll();
474 	fZoneList->ScrollToSelection();
475 
476 	fUseGmtTime = fOldUseGmtTime;
477 	if (fUseGmtTime)
478 		fGmtTime->SetValue(B_CONTROL_ON);
479 	else
480 		fLocalTime->SetValue(B_CONTROL_ON);
481 	_ShowOrHidePreview();
482 
483 	_UpdateGmtSettings();
484 	_SetSystemTimeZone();
485 	_UpdatePreview();
486 	_UpdateCurrent();
487 }
488 
489 
490 void
491 TimeZoneView::_UpdatePreview()
492 {
493 	int32 selection = fZoneList->CurrentSelection();
494 	TimeZoneListItem* item
495 		= selection < 0
496 			? NULL
497 			: static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
498 
499 	if (item == NULL || !item->HasTimeZone()) {
500 		fPreview->SetText("");
501 		fPreview->SetTime("");
502 		return;
503 	}
504 
505 	BString timeString = _FormatTime(item->TimeZone());
506 	fPreview->SetText(item->Text());
507 	fPreview->SetTime(timeString.String());
508 
509 	fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
510 }
511 
512 
513 void
514 TimeZoneView::_UpdateCurrent()
515 {
516 	if (fCurrentZoneItem == NULL)
517 		return;
518 
519 	BString timeString = _FormatTime(fCurrentZoneItem->TimeZone());
520 	fCurrent->SetText(fCurrentZoneItem->Text());
521 	fCurrent->SetTime(timeString.String());
522 }
523 
524 
525 void
526 TimeZoneView::_SetSystemTimeZone()
527 {
528 	/*	Set system timezone for all different API levels. How to do this?
529 	 *	1) tell locale-roster about new default timezone
530 	 *	2) tell kernel about new timezone offset
531 	 */
532 
533 	int32 selection = fZoneList->CurrentSelection();
534 	if (selection < 0)
535 		return;
536 
537 	TimeZoneListItem* item
538 		= static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
539 	if (item == NULL || !item->HasTimeZone())
540 		return;
541 
542 	fCurrentZoneItem = item;
543 	const BTimeZone& timeZone = item->TimeZone();
544 
545 	MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone);
546 
547 	_kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(),
548 		timeZone.ID().Length());
549 
550 	fSetZone->SetEnabled(false);
551 	fLastUpdateMinute = -1;
552 		// just to trigger updating immediately
553 }
554 
555 
556 BString
557 TimeZoneView::_FormatTime(const BTimeZone& timeZone)
558 {
559 	BString result;
560 
561 	time_t now = time(NULL);
562 	bool rtcIsGMT;
563 	_kern_get_real_time_clock_is_gmt(&rtcIsGMT);
564 	if (!rtcIsGMT) {
565 		int32 currentOffset
566 			= fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone()
567 				? fCurrentZoneItem->OffsetFromGMT()
568 				: 0;
569 		now -= timeZone.OffsetFromGMT() - currentOffset;
570 	}
571 	fTimeFormat.Format(result, now, B_SHORT_TIME_FORMAT, &timeZone);
572 
573 	return result;
574 }
575 
576 
577 void
578 TimeZoneView::_ReadRTCSettings()
579 {
580 	BPath path;
581 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
582 		return;
583 
584 	path.Append("RTC_time_settings");
585 
586 	BEntry entry(path.Path());
587 	if (entry.Exists()) {
588 		BFile file(&entry, B_READ_ONLY);
589 		if (file.InitCheck() == B_OK) {
590 			char buffer[6];
591 			file.Read(buffer, 6);
592 			if (strncmp(buffer, "gmt", 3) == 0)
593 				fUseGmtTime = true;
594 		}
595 	}
596 }
597 
598 
599 void
600 TimeZoneView::_WriteRTCSettings()
601 {
602 	BPath path;
603 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, true) != B_OK)
604 		return;
605 
606 	path.Append("RTC_time_settings");
607 
608 	BFile file(path.Path(), B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
609 	if (file.InitCheck() == B_OK) {
610 		if (fUseGmtTime)
611 			file.Write("gmt", 3);
612 		else
613 			file.Write("local", 5);
614 	}
615 }
616 
617 
618 void
619 TimeZoneView::_UpdateGmtSettings()
620 {
621 	_WriteRTCSettings();
622 
623 	_ShowOrHidePreview();
624 
625 	_kern_set_real_time_clock_is_gmt(fUseGmtTime);
626 }
627 
628 
629 void
630 TimeZoneView::_ShowOrHidePreview()
631 {
632 	if (fUseGmtTime) {
633 		// Hardware clock uses GMT time, changing timezone will adjust the
634 		// offset and we need to display a preview
635 		fCurrent->Show();
636 		fPreview->Show();
637 	} else {
638 		// Hardware clock uses local time, changing timezone will adjust the
639 		// clock and there is no offset to manage, thus, no preview.
640 		fCurrent->Hide();
641 		fPreview->Hide();
642 	}
643 }
644