xref: /haiku/src/preferences/time/ZoneView.cpp (revision e2932f63b00ba76ab3769a9c217754a4d03868ca)
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 <ScrollView.h>
39 #include <StorageDefs.h>
40 #include <String.h>
41 #include <TimeZone.h>
42 #include <ToolTip.h>
43 #include <View.h>
44 #include <Window.h>
45 
46 #include <unicode/datefmt.h>
47 #include <unicode/utmscale.h>
48 #include <ICUWrapper.h>
49 
50 #include "TimeMessages.h"
51 #include "TimeZoneListItem.h"
52 #include "TZDisplay.h"
53 
54 
55 #undef B_TRANSLATE_CONTEXT
56 #define B_TRANSLATE_CONTEXT "Time"
57 
58 
59 using BPrivate::MutableLocaleRoster;
60 using BPrivate::ObjectDeleter;
61 
62 
63 struct TimeZoneItemLess {
64 	bool operator()(const BString& first, const BString& second)
65 	{
66 		// sort anything starting with '<' behind anything else
67 		if (first.ByteAt(0) == '<') {
68 			if (second.ByteAt(0) != '<')
69 				return false;
70 		} else if (second.ByteAt(0) == '<')
71 			return true;
72 		return fCollator.Compare(first.String(), second.String()) < 0;
73 	}
74 private:
75 	BCollator fCollator;
76 };
77 
78 
79 
80 TimeZoneView::TimeZoneView(const char* name)
81 	:
82 	BGroupView(name, B_HORIZONTAL, B_USE_DEFAULT_SPACING),
83 	fToolTip(NULL),
84 	fCurrentZoneItem(NULL),
85 	fOldZoneItem(NULL),
86 	fInitialized(false)
87 {
88 	_InitView();
89 }
90 
91 
92 bool
93 TimeZoneView::CheckCanRevert()
94 {
95 	return fCurrentZoneItem != fOldZoneItem;
96 }
97 
98 
99 TimeZoneView::~TimeZoneView()
100 {
101 	if (fToolTip != NULL)
102 		fToolTip->ReleaseReference();
103 }
104 
105 
106 void
107 TimeZoneView::AttachedToWindow()
108 {
109 	BView::AttachedToWindow();
110 	if (Parent())
111 		SetViewColor(Parent()->ViewColor());
112 
113 	if (!fInitialized) {
114 		fInitialized = true;
115 
116 		fSetZone->SetTarget(this);
117 		fZoneList->SetTarget(this);
118 	}
119 }
120 
121 
122 void
123 TimeZoneView::DoLayout()
124 {
125 	BView::DoLayout();
126 	if (fCurrentZoneItem != NULL) {
127 		fZoneList->Select(fZoneList->IndexOf(fCurrentZoneItem));
128 		fCurrent->SetText(fCurrentZoneItem->Text());
129 		fZoneList->ScrollToSelection();
130 	}
131 }
132 
133 
134 void
135 TimeZoneView::MessageReceived(BMessage* message)
136 {
137 	switch (message->what) {
138 		case B_OBSERVER_NOTICE_CHANGE:
139 		{
140 			int32 change;
141 			message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
142 			switch(change) {
143 				case H_TM_CHANGED:
144 					_UpdateDateTime(message);
145 					break;
146 
147 				default:
148 					BView::MessageReceived(message);
149 					break;
150 			}
151 			break;
152 		}
153 
154 		case H_CITY_CHANGED:
155 			_UpdatePreview();
156 			break;
157 
158 		case H_SET_TIME_ZONE:
159 		{
160 			_SetSystemTimeZone();
161 			Looper()->PostMessage(new BMessage(kMsgChange));
162 			break;
163 		}
164 
165 		case kMsgRevert:
166 			_Revert();
167 			break;
168 
169 		case kRTCUpdate:
170 			_UpdateCurrent();
171 			_UpdatePreview();
172 			break;
173 
174 		default:
175 			BGroupView::MessageReceived(message);
176 			break;
177 	}
178 }
179 
180 
181 bool
182 TimeZoneView::GetToolTipAt(BPoint point, BToolTip** _tip)
183 {
184 	TimeZoneListItem* item = static_cast<TimeZoneListItem*>(
185 		fZoneList->ItemAt(fZoneList->IndexOf(point)));
186 	if (item == NULL || !item->HasTimeZone())
187 		return false;
188 
189 	BString nowInTimeZone;
190 	time_t now = time(NULL);
191 	BLocale::Default()->FormatTime(&nowInTimeZone, now, B_SHORT_TIME_FORMAT,
192 		&item->TimeZone());
193 
194 	BString dateInTimeZone;
195 	BLocale::Default()->FormatDate(&dateInTimeZone, now, B_SHORT_DATE_FORMAT,
196 		&item->TimeZone());
197 
198 	BString toolTip = item->Text();
199 	toolTip << '\n' << item->TimeZone().ShortName() << " / "
200 			<< item->TimeZone().ShortDaylightSavingName()
201 			<< B_TRANSLATE("\nNow: ") << nowInTimeZone
202 			<< " (" << dateInTimeZone << ')';
203 
204 	if (fToolTip != NULL)
205 		fToolTip->ReleaseReference();
206 	fToolTip = new (std::nothrow) BTextToolTip(toolTip.String());
207 	if (fToolTip == NULL)
208 		return false;
209 
210 	*_tip = fToolTip;
211 
212 	return true;
213 }
214 
215 
216 void
217 TimeZoneView::_UpdateDateTime(BMessage* message)
218 {
219 	// only need to update once every minute
220 	int32 minute;
221 	if (message->FindInt32("minute", &minute) == B_OK) {
222 		if (fLastUpdateMinute != minute) {
223 			_UpdateCurrent();
224 			_UpdatePreview();
225 
226 			fLastUpdateMinute = minute;
227 		}
228 	}
229 }
230 
231 
232 void
233 TimeZoneView::_InitView()
234 {
235 	fZoneList = new BOutlineListView("cityList", B_SINGLE_SELECTION_LIST);
236 	fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
237 	fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
238 	_BuildZoneMenu();
239 	BScrollView* scrollList = new BScrollView("scrollList", fZoneList,
240 		B_FRAME_EVENTS | B_WILL_DRAW, false, true);
241 
242 	fCurrent = new TTZDisplay("currentTime", B_TRANSLATE("Current time:"));
243 	fPreview = new TTZDisplay("previewTime", B_TRANSLATE("Preview time:"));
244 
245 	fSetZone = new BButton("setTimeZone", B_TRANSLATE("Set time zone"),
246 		new BMessage(H_SET_TIME_ZONE));
247 	fSetZone->SetEnabled(false);
248 	fSetZone->SetExplicitAlignment(
249 		BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM));
250 
251 	const float kInset = be_control_look->DefaultItemSpacing();
252 	BLayoutBuilder::Group<>(this)
253 		.Add(scrollList)
254 		.AddGroup(B_VERTICAL, kInset)
255 			.Add(fCurrent)
256 			.Add(fPreview)
257 			.AddGlue()
258 			.Add(fSetZone)
259 		.End()
260 		.SetInsets(kInset, kInset, kInset, kInset);
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 	BMessage countryList;
274 	BLocaleRoster::Default()->GetAvailableCountries(&countryList);
275 	countryList.AddString("country", "");
276 
277 	/*
278 	 * Group timezones by regions, but filter out unwanted (duplicate) regions
279 	 * and add an additional region with generic GMT-offset timezones at the end
280 	 */
281 	typedef	std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
282 	ZoneItemMap zoneItemMap;
283 	const char* kOtherRegion = B_TRANSLATE_MARK("<Other>");
284 	const char* kSupportedRegions[] = {
285 		B_TRANSLATE_MARK("Africa"),		B_TRANSLATE_MARK("America"),
286 		B_TRANSLATE_MARK("Antarctica"),	B_TRANSLATE_MARK("Arctic"),
287 		B_TRANSLATE_MARK("Asia"),		B_TRANSLATE_MARK("Atlantic"),
288 		B_TRANSLATE_MARK("Australia"),	B_TRANSLATE_MARK("Europe"),
289 		B_TRANSLATE_MARK("Indian"),		B_TRANSLATE_MARK("Pacific"),
290 		kOtherRegion,
291 		NULL
292 	};
293 	// Since the zone-map contains translated country-names (we get those from
294 	// ICU), we need to use translated region names in the zone-map, too:
295 	typedef	std::map<BString, BString> TranslatedRegionMap;
296 	TranslatedRegionMap regionMap;
297 	for (const char** region = kSupportedRegions; *region != NULL; ++region) {
298 		BString translatedRegion = B_TRANSLATE_NOCOLLECT(*region);
299 		regionMap[*region] = translatedRegion;
300 
301 		TimeZoneListItem* regionItem
302 			= new TimeZoneListItem(translatedRegion, NULL, NULL);
303 		regionItem->SetOutlineLevel(0);
304 		zoneItemMap[translatedRegion] = regionItem;
305 	}
306 
307 	BString countryCode;
308 	for (int c = 0; countryList.FindString("country", c, &countryCode)
309 			== B_OK; c++) {
310 		BCountry country(countryCode);
311 		BString countryName;
312 		country.GetName(countryName);
313 
314 		// Now list the timezones for this country
315 		BMessage zoneList;
316 		BLocaleRoster::Default()->GetAvailableTimeZonesForCountry(&zoneList,
317 			countryCode.Length() == 0 ? NULL : countryCode.String());
318 
319 		int32 count = 0;
320 		type_code dummy;
321 		zoneList.GetInfo("timeZone", &dummy, &count);
322 
323 		BString zoneID;
324 		for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK;
325 			tz++) {
326 			int32 slashPos = zoneID.FindFirst('/');
327 
328 			// ignore any "global" timezones, as those are just aliases of
329 			// regional ones
330 			if (slashPos <= 0)
331 				continue;
332 
333 			BString region(zoneID, slashPos);
334 
335 			if (region == "Etc")
336 				region = kOtherRegion;
337 			else if (countryName.Length() == 0) {
338 				// skip global timezones from other regions, we are just
339 				// interested in the generic GMT-based ones under "Etc/"
340 				continue;
341 			}
342 
343 			// just accept timezones from our supported regions, others are
344 			// aliases and would just make the list even longer
345 			TranslatedRegionMap::iterator regionIter = regionMap.find(region);
346 			if (regionIter == regionMap.end())
347 				continue;
348 			const BString& regionName = regionIter->second;
349 
350 			BString fullCountryID = regionName;
351 			bool countryIsRegion = countryName == regionName;
352 			if (!countryIsRegion)
353 				fullCountryID << "/" << countryName;
354 
355 			BTimeZone* timeZone = new BTimeZone(zoneID, &language);
356 			BString tzName = timeZone->Name();
357 			if (tzName == "GMT+00:00")
358 				tzName = "GMT";
359 
360 			int32 openParenthesisPos = tzName.FindFirst('(');
361 			if (openParenthesisPos >= 0) {
362 				tzName.Remove(0, openParenthesisPos + 1);
363 				int32 closeParenthesisPos = tzName.FindLast(')');
364 				if (closeParenthesisPos >= 0)
365 					tzName.Truncate(closeParenthesisPos);
366 			}
367 			BString fullZoneID = fullCountryID;
368 			fullZoneID << "/" << tzName;
369 
370 			// skip duplicates
371 			ZoneItemMap::iterator zoneIter = zoneItemMap.find(fullZoneID);
372 			if (zoneIter != zoneItemMap.end()) {
373 				delete timeZone;
374 				continue;
375 			}
376 
377 			TimeZoneListItem* countryItem = NULL;
378 			TimeZoneListItem* zoneItem = NULL;
379 			if (count > 1 && countryName.Length() > 0) {
380 				ZoneItemMap::iterator countryIter
381 					= zoneItemMap.find(fullCountryID);
382 				if (countryIter == zoneItemMap.end()) {
383 					countryItem = new TimeZoneListItem(countryName, NULL, NULL);
384 					countryItem->SetOutlineLevel(1);
385 					zoneItemMap[fullCountryID] = countryItem;
386 				} else
387 					countryItem = countryIter->second;
388 
389 				zoneItem = new TimeZoneListItem(tzName, NULL, timeZone);
390 				zoneItem->SetOutlineLevel(countryIsRegion ? 1 : 2);
391 			} else {
392 				BString& name = countryName.Length() > 0 ? countryName : tzName;
393 				zoneItem = new TimeZoneListItem(name, NULL, timeZone);
394 				zoneItem->SetOutlineLevel(1);
395 			}
396 			zoneItemMap[fullZoneID] = zoneItem;
397 
398 			if (timeZone->ID() == defaultTimeZone.ID()) {
399 				fCurrentZoneItem = zoneItem;
400 				if (countryItem != NULL)
401 					countryItem->SetExpanded(true);
402 				ZoneItemMap::iterator regionItemIter
403 					= zoneItemMap.find(regionName);
404 				if (regionItemIter != zoneItemMap.end())
405 					regionItemIter->second->SetExpanded(true);
406 			}
407 		}
408 	}
409 
410 	fOldZoneItem = fCurrentZoneItem;
411 
412 	ZoneItemMap::iterator zoneIter;
413 	bool lastWasCountryItem = false;
414 	TimeZoneListItem* currentCountryItem = NULL;
415 	for (zoneIter = zoneItemMap.begin(); zoneIter != zoneItemMap.end();
416 		++zoneIter) {
417 		if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
418 			/* Some countries (e.g. Spain and Chile) have their timezones
419 			 * spread across different regions. As a result, there might still
420 			 * be country items with only one timezone below them. We manually
421 			 * filter those country items here.
422 			 */
423 			ZoneItemMap::iterator next = zoneIter;
424 			++next;
425 			if (next != zoneItemMap.end()
426 				&& next->second->OutlineLevel() != 2) {
427 				fZoneList->RemoveItem(currentCountryItem);
428 				zoneIter->second->SetText(currentCountryItem->Text());
429 				zoneIter->second->SetOutlineLevel(1);
430 				delete currentCountryItem;
431 			}
432 		}
433 
434 		fZoneList->AddItem(zoneIter->second);
435 		if (zoneIter->second->OutlineLevel() == 1) {
436 			lastWasCountryItem = true;
437 			currentCountryItem = zoneIter->second;
438 		} else
439 			lastWasCountryItem = false;
440 	}
441 }
442 
443 
444 void
445 TimeZoneView::_Revert()
446 {
447 	fCurrentZoneItem = fOldZoneItem;
448 
449 	if (fCurrentZoneItem != NULL) {
450 		int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem);
451 		fZoneList->Select(currentZoneIndex);
452 	} else
453 		fZoneList->DeselectAll();
454 	fZoneList->ScrollToSelection();
455 
456 	_SetSystemTimeZone();
457 	_UpdatePreview();
458 	_UpdateCurrent();
459 }
460 
461 
462 void
463 TimeZoneView::_UpdatePreview()
464 {
465 	int32 selection = fZoneList->CurrentSelection();
466 	TimeZoneListItem* item
467 		= selection < 0
468 			? NULL
469 			: (TimeZoneListItem*)fZoneList->ItemAt(selection);
470 
471 	if (item == NULL || !item->HasTimeZone()) {
472 		fPreview->SetText("");
473 		fPreview->SetTime("");
474 		return;
475 	}
476 
477 	BString timeString = _FormatTime(item->TimeZone());
478 	fPreview->SetText(item->Text());
479 	fPreview->SetTime(timeString.String());
480 
481 	fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
482 }
483 
484 
485 void
486 TimeZoneView::_UpdateCurrent()
487 {
488 	if (fCurrentZoneItem == NULL)
489 		return;
490 
491 	BString timeString = _FormatTime(fCurrentZoneItem->TimeZone());
492 	fCurrent->SetText(fCurrentZoneItem->Text());
493 	fCurrent->SetTime(timeString.String());
494 }
495 
496 
497 void
498 TimeZoneView::_SetSystemTimeZone()
499 {
500 	/*	Set sytem timezone for all different API levels. How to do this?
501 	 *	1) tell locale-roster about new default timezone
502 	 *	2) tell kernel about new timezone offset
503 	 */
504 
505 	int32 selection = fZoneList->CurrentSelection();
506 	if (selection < 0)
507 		return;
508 
509 	TimeZoneListItem* item
510 		= static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
511 	if (item == NULL || !item->HasTimeZone())
512 		return;
513 
514 	fCurrentZoneItem = item;
515 	const BTimeZone& timeZone = item->TimeZone();
516 
517 	MutableLocaleRoster::Default()->SetDefaultTimeZone(timeZone);
518 
519 	_kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(),
520 		timeZone.ID().Length());
521 
522 	fSetZone->SetEnabled(false);
523 	fLastUpdateMinute = -1;
524 		// just to trigger updating immediately
525 }
526 
527 
528 BString
529 TimeZoneView::_FormatTime(const BTimeZone& timeZone)
530 {
531 	BString result;
532 
533 	time_t now = time(NULL);
534 	bool rtcIsGMT;
535 	_kern_get_real_time_clock_is_gmt(&rtcIsGMT);
536 	if (!rtcIsGMT) {
537 		int32 currentOffset
538 			= fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone()
539 				? fCurrentZoneItem->OffsetFromGMT()
540 				: 0;
541 		now -= timeZone.OffsetFromGMT() - currentOffset;
542 	}
543 	BLocale::Default()->FormatTime(&result, now, B_SHORT_TIME_FORMAT,
544 		&timeZone);
545 
546 	return result;
547 }
548