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