xref: /haiku/src/preferences/time/ZoneView.cpp (revision 220d04022750f40f8bac8f01fa551211e28d04f2)
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 "ZoneView.h"
17 
18 #include <stdlib.h>
19 #include <syscalls.h>
20 
21 #include <map>
22 #include <new>
23 #include <vector>
24 
25 #include <AutoDeleter.h>
26 #include <Button.h>
27 #include <Catalog.h>
28 #include <Collator.h>
29 #include <ControlLook.h>
30 #include <Country.h>
31 #include <Directory.h>
32 #include <Entry.h>
33 #include <File.h>
34 #include <FindDirectory.h>
35 #include <ListItem.h>
36 #include <Locale.h>
37 #include <MutableLocaleRoster.h>
38 #include <OutlineListView.h>
39 #include <Path.h>
40 #include <RadioButton.h>
41 #include <ScrollView.h>
42 #include <StorageDefs.h>
43 #include <String.h>
44 #include <StringView.h>
45 #include <TimeZone.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 "TimeZoneListView.h"
56 #include "TZDisplay.h"
57 
58 
59 #undef B_TRANSLATION_CONTEXT
60 #define B_TRANSLATION_CONTEXT "Time"
61 
62 
63 using BPrivate::MutableLocaleRoster;
64 using BPrivate::ObjectDeleter;
65 
66 
67 struct TimeZoneItemLess {
68 	bool operator()(const BString& first, const BString& second)
69 	{
70 		// sort anything starting with '<' behind anything else
71 		if (first.ByteAt(0) == '<') {
72 			if (second.ByteAt(0) != '<')
73 				return false;
74 		} else if (second.ByteAt(0) == '<')
75 			return true;
76 		return fCollator.Compare(first.String(), second.String()) < 0;
77 	}
78 private:
79 	BCollator fCollator;
80 };
81 
82 
83 
84 TimeZoneView::TimeZoneView(const char* name)
85 	:
86 	BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING),
87 	fGmtTime(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 	_WriteRTCSettings();
111 }
112 
113 
114 void
115 TimeZoneView::AttachedToWindow()
116 {
117 	BView::AttachedToWindow();
118 	if (Parent())
119 		SetViewColor(Parent()->ViewColor());
120 
121 	if (!fInitialized) {
122 		fInitialized = true;
123 
124 		fSetZone->SetTarget(this);
125 		fZoneList->SetTarget(this);
126 	}
127 }
128 
129 
130 void
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
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
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
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(BSize(200, 0));
216 
217 	fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:"));
218 	fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:"));
219 
220 	fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"),
221 		new BMessage(H_SET_TIME_ZONE));
222 	fSetZone->SetEnabled(false);
223 	fSetZone->SetExplicitAlignment(
224 		BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM));
225 
226 	fLocalTime = new BRadioButton("localTime",
227 		B_TRANSLATE("Local time (Windows compatible)"),
228 			new BMessage(kRTCUpdate));
229 	fGmtTime = new BRadioButton("greenwichMeanTime",
230 		B_TRANSLATE("GMT (UNIX compatible)"), new BMessage(kRTCUpdate));
231 
232 	if (fUseGmtTime)
233 		fGmtTime->SetValue(B_CONTROL_ON);
234 	else
235 		fLocalTime->SetValue(B_CONTROL_ON);
236 	_ShowOrHidePreview();
237 	fOldUseGmtTime = fUseGmtTime;
238 
239 	const float kIndentSpacing
240 		= be_control_look->DefaultItemSpacing() * 2;
241 	BLayoutBuilder::Group<>(this)
242 		.Add(scrollList)
243 		.AddGroup(B_VERTICAL, 0)
244 			.Add(new BStringView("clockSetTo",
245 				B_TRANSLATE("Hardware clock set to:")))
246 			.AddGroup(B_VERTICAL, 0)
247 				.Add(fLocalTime)
248 				.Add(fGmtTime)
249 				.SetInsets(kIndentSpacing, 0, 0, 0)
250 			.End()
251 			.AddGlue()
252 			.AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING)
253 				.Add(fCurrent)
254 				.Add(fPreview)
255 			.End()
256 			.Add(fSetZone)
257 		.End()
258 		.SetInsets(B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING,
259 			B_USE_DEFAULT_SPACING, B_USE_DEFAULT_SPACING);
260 }
261 
262 
263 void
264 TimeZoneView::_BuildZoneMenu()
265 {
266 	BTimeZone defaultTimeZone;
267 	BLocaleRoster::Default()->GetDefaultTimeZone(&defaultTimeZone);
268 
269 	BLanguage language;
270 	BLocale::Default()->GetLanguage(&language);
271 
272 	// Group timezones by regions, but filter out unwanted (duplicate) regions
273 	// and add an additional region with generic GMT-offset timezones at the end
274 	typedef std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
275 	ZoneItemMap zoneItemMap;
276 	const char* kOtherRegion = B_TRANSLATE_MARK("<Other>");
277 	const char* kSupportedRegions[] = {
278 		B_TRANSLATE_MARK("Africa"),		B_TRANSLATE_MARK("America"),
279 		B_TRANSLATE_MARK("Antarctica"),	B_TRANSLATE_MARK("Arctic"),
280 		B_TRANSLATE_MARK("Asia"),		B_TRANSLATE_MARK("Atlantic"),
281 		B_TRANSLATE_MARK("Australia"),	B_TRANSLATE_MARK("Europe"),
282 		B_TRANSLATE_MARK("Indian"),		B_TRANSLATE_MARK("Pacific"),
283 		kOtherRegion,
284 		NULL
285 	};
286 
287 	// Since the zone-map contains translated country-names (we get those from
288 	// ICU), we need to use translated region names in the zone-map, too:
289 	typedef std::map<BString, BString> TranslatedRegionMap;
290 	TranslatedRegionMap regionMap;
291 	for (const char** region = kSupportedRegions; *region != NULL; ++region) {
292 		BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region);
293 		regionMap[*region] = translatedRegion;
294 
295 		TimeZoneListItem* regionItem
296 			= new TimeZoneListItem(translatedRegion, NULL, NULL);
297 		regionItem->SetOutlineLevel(0);
298 		zoneItemMap[translatedRegion] = regionItem;
299 	}
300 
301 	// Get all time zones
302 	BMessage zoneList;
303 	BLocaleRoster::Default()->GetAvailableTimeZonesWithRegionInfo(&zoneList);
304 
305 	typedef std::map<BString, std::vector<const char*> > ZonesByCountyMap;
306 	ZonesByCountyMap zonesByCountryMap;
307 	const char* zoneID;
308 	BString timeZoneCode;
309 	for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK
310 			&& zoneList.FindString("region", tz, &timeZoneCode) == B_OK; tz++) {
311 		// From the global ("001") timezones, we only accept the generic GMT
312 		// timezones, as all the other world-zones are duplicates of others.
313 		if (timeZoneCode == "001" && strncmp(zoneID, "Etc/GMT", 7) != 0)
314 			continue;
315 		zonesByCountryMap[timeZoneCode].push_back(zoneID);
316 	}
317 
318 	ZonesByCountyMap::const_iterator countryIter = zonesByCountryMap.begin();
319 	for (; countryIter != zonesByCountryMap.end(); ++countryIter) {
320 		const char* countryCode = countryIter->first.String();
321 		if (countryCode == NULL)
322 			continue;
323 
324 		size_t zoneCountInCountry = countryIter->second.size();
325 		for (size_t tz = 0; tz < zoneCountInCountry; tz++) {
326 			BString zoneID(countryIter->second[tz]);
327 			BTimeZone* timeZone
328 				= new(std::nothrow) BTimeZone(zoneID, &language);
329 			if (timeZone == NULL)
330 				continue;
331 
332 			int32 slashPos = zoneID.FindFirst('/');
333 			BString region(zoneID, slashPos);
334 			if (region == "Etc")
335 				region = kOtherRegion;
336 
337 			// just accept timezones from our supported regions, others are
338 			// aliases and would just make the list even longer
339 			TranslatedRegionMap::iterator regionIter = regionMap.find(region);
340 			if (regionIter == regionMap.end())
341 				continue;
342 
343 			BString fullCountryID = regionIter->second;
344 			BCountry* country = new(std::nothrow) BCountry(countryCode);
345 			if (country == NULL)
346 				continue;
347 
348 			BString countryName;
349 			country->GetName(countryName);
350 			bool hasUsedCountry = false;
351 			bool countryIsRegion = countryName == regionIter->second
352 				|| region == kOtherRegion;
353 			if (!countryIsRegion)
354 				fullCountryID << "/" << countryName;
355 
356 			BString timeZoneName;
357 			BString fullZoneID = fullCountryID;
358 			if (zoneCountInCountry > 1) {
359 				// we can't use the country name as timezone name, since there
360 				// are more than one timezones in this country - fetch the
361 				// localized name of the timezone and use that
362 				timeZoneName = timeZone->Name();
363 				int32 openParenthesisPos = timeZoneName.FindFirst('(');
364 				if (openParenthesisPos >= 0) {
365 					timeZoneName.Remove(0, openParenthesisPos + 1);
366 					int32 closeParenthesisPos = timeZoneName.FindLast(')');
367 					if (closeParenthesisPos >= 0)
368 						timeZoneName.Truncate(closeParenthesisPos);
369 				}
370 				fullZoneID << "/" << timeZoneName;
371 			} else {
372 				timeZoneName = countryName;
373 				fullZoneID << "/" << zoneID;
374 			}
375 
376 			// skip duplicates
377 			ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID);
378 			if (zoneIter != zoneItemMap.end()) {
379 				delete timeZone;
380 				continue;
381 			}
382 
383 			TimeZoneListItem* countryItem = NULL;
384 			TimeZoneListItem* zoneItem = NULL;
385 			if (zoneCountInCountry > 1) {
386 				ZoneItemMap::iterator countryIter
387 					= zoneItemMap.find(fullCountryID);
388 				if (countryIter == zoneItemMap.end()) {
389 					countryItem = new TimeZoneListItem(countryName.String(),
390 						country, NULL);
391 					countryItem->SetOutlineLevel(1);
392 					zoneItemMap[fullCountryID] = countryItem;
393 					hasUsedCountry = true;
394 				} else
395 					countryItem = countryIter->second;
396 
397 				zoneItem = new TimeZoneListItem(timeZoneName.String(),
398 					NULL, timeZone);
399 				zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2);
400 			} else {
401 				zoneItem = new TimeZoneListItem(timeZoneName.String(),
402 					country, timeZone);
403 				zoneItem->SetOutlineLevel(1);
404 				hasUsedCountry = true;
405 			}
406 			zoneItemMap[fullZoneID] = zoneItem;
407 
408 			if (timeZone->ID() == defaultTimeZone.ID()) {
409 				fCurrentZoneItem = zoneItem;
410 				if (countryItem != NULL)
411 					countryItem->SetExpanded(true);
412 
413 				ZoneItemMap::iterator regionItemIter
414 					= zoneItemMap.find(regionIter->second);
415 				if (regionItemIter != zoneItemMap.end())
416 					regionItemIter->second->SetExpanded(true);
417 			}
418 
419 			if (!hasUsedCountry)
420 				delete country;
421 		}
422 	}
423 
424 	fOldZoneItem = fCurrentZoneItem;
425 
426 	ZoneItemMap::iterator zoneIter;
427 	bool lastWasCountryItem = false;
428 	TimeZoneListItem* currentItem = NULL;
429 	for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end();
430 			++zoneIter) {
431 		if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
432 			// Some countries (e.g. Spain and Chile) have their timezones
433 			// spread across different regions. As a result, there might still
434 			// be country items with only one timezone below them. We manually
435 			// filter those country items here.
436 			ZoneItemMap::iterator next = zoneIter;
437 			++next;
438 			if (next != zoneItemMap.end()
439 				&& next->second->OutlineLevel() != 2) {
440 				fZoneList->RemoveItem(currentItem);
441 				zoneIter->second->SetText(currentItem->Text());
442 				zoneIter->second->SetCountry(currentItem->HasCountry()
443 					? new(std::nothrow) BCountry(currentItem->Country())
444 					: NULL);
445 				zoneIter->second->SetTimeZone(currentItem->HasTimeZone()
446 					? new(std::nothrow) BTimeZone(currentItem->TimeZone())
447 					: NULL);
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
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
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
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
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
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 	BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT,
571 		&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