xref: /haiku/src/preferences/time/ZoneView.cpp (revision 56187df6ebd9d1fb16bf954fce8df0999e9e3048)
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, false, &item->TimeZone());
188 
189 	BString dateInTimeZone;
190 	be_locale->FormatDate(&dateInTimeZone, now, false, &item->TimeZone());
191 
192 	BString toolTip = item->Text();
193 	toolTip << '\n' << item->TimeZone().ShortName() << " / "
194 			<< item->TimeZone().ShortDaylightSavingName()
195 			<< "\nNow: " << nowInTimeZone << " (" << dateInTimeZone << ')';
196 
197 	if (fToolTip != NULL)
198 		fToolTip->ReleaseReference();
199 	fToolTip = new (std::nothrow) BTextToolTip(toolTip.String());
200 	if (fToolTip == NULL)
201 		return false;
202 
203 	*_tip = fToolTip;
204 
205 	return true;
206 }
207 
208 
209 void
210 TimeZoneView::_UpdateDateTime(BMessage* message)
211 {
212 	// only need to update once every minute
213 	int32 minute;
214 	if (message->FindInt32("minute", &minute) == B_OK) {
215 		if (fLastUpdateMinute != minute) {
216 			_UpdateCurrent();
217 			_UpdatePreview();
218 
219 			fLastUpdateMinute = minute;
220 		}
221 	}
222 }
223 
224 
225 void
226 TimeZoneView::_InitView()
227 {
228 	// left side
229 	BRect frameLeft(Bounds());
230 	frameLeft.right = frameLeft.Width() / 2.0;
231 	frameLeft.InsetBy(10.0f, 10.0f);
232 
233 	// City Listing
234 	fZoneList = new BOutlineListView(frameLeft, "cityList",
235 		B_SINGLE_SELECTION_LIST);
236 	fZoneList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
237 	fZoneList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
238 
239 	_BuildZoneMenu();
240 
241 	BScrollView* scrollList = new BScrollView("scrollList", fZoneList,
242 		B_FOLLOW_ALL, 0, false, true);
243 	AddChild(scrollList);
244 
245 	// right side
246 	BRect frameRight(Bounds());
247 	frameRight.left = frameRight.Width() / 2.0;
248 	frameRight.InsetBy(10.0f, 10.0f);
249 	frameRight.top = frameLeft.top;
250 
251 	// Time Displays
252 	fCurrent = new TTZDisplay(frameRight, "currentTime", "Current time:");
253 	AddChild(fCurrent);
254 	fCurrent->ResizeToPreferred();
255 
256 	frameRight.top = fCurrent->Frame().bottom + 10.0;
257 	fPreview = new TTZDisplay(frameRight, "previewTime", "Preview time:");
258 	AddChild(fPreview);
259 	fPreview->ResizeToPreferred();
260 
261 	// set button
262 	fSetZone = new BButton(frameRight, "setTimeZone", "Set time zone",
263 		new BMessage(H_SET_TIME_ZONE));
264 	AddChild(fSetZone);
265 	fSetZone->SetEnabled(false);
266 	fSetZone->ResizeToPreferred();
267 
268 	fSetZone->MoveTo(frameRight.right - fSetZone->Bounds().Width(),
269 		scrollList->Frame().bottom - fSetZone->Bounds().Height());
270 }
271 
272 
273 void
274 TimeZoneView::_BuildZoneMenu()
275 {
276 	BTimeZone defaultTimeZone;
277 	be_locale_roster->GetDefaultTimeZone(&defaultTimeZone);
278 
279 	BLanguage language;
280 	be_locale->GetLanguage(&language);
281 
282 	BMessage countryList;
283 	be_locale_roster->GetAvailableCountries(&countryList);
284 	countryList.AddString("country", "");
285 
286 	/*
287 	 * Group timezones by regions, but filter out unwanted (duplicate) regions
288 	 * and add an additional region with generic GMT-offset timezones at the end
289 	 */
290 	typedef	std::map<BString, TimeZoneListItem*, TimeZoneItemLess> ZoneItemMap;
291 	ZoneItemMap zoneMap;
292 	const char* kOtherRegion = "<Other>";
293 	const char* kSupportedRegions[] = {
294 		"Africa", "America", "Antarctica", "Arctic", "Asia", "Atlantic",
295 		"Australia", "Europe", "Indian", "Pacific", kOtherRegion, NULL
296 	};
297 	for (const char** region = kSupportedRegions; *region != NULL; ++region)
298 		zoneMap[*region] = NULL;
299 
300 	BString countryCode;
301 	for (int c = 0; countryList.FindString("country", c, &countryCode)
302 			== B_OK; c++) {
303 		BCountry country("", countryCode);
304 		BString countryName;
305 		country.GetName(countryName);
306 
307 		// Now list the timezones for this country
308 		BMessage zoneList;
309 		be_locale_roster->GetAvailableTimeZonesForCountry(&zoneList,
310 			countryCode.Length() == 0 ? NULL : countryCode.String());
311 
312 		int32 count = 0;
313 		type_code dummy;
314 		zoneList.GetInfo("timeZone", &dummy, &count);
315 
316 		BString zoneID;
317 		for (int tz = 0; zoneList.FindString("timeZone", tz, &zoneID) == B_OK;
318 			tz++) {
319 			int32 slashPos = zoneID.FindFirst('/');
320 
321 			// ignore any "global" timezones, as those are just aliases of
322 			// regional ones
323 			if (slashPos <= 0)
324 				continue;
325 
326 			BString region(zoneID, slashPos);
327 
328 			if (region == "Etc")
329 				region = kOtherRegion;
330 			else if (countryName.Length() == 0) {
331 				// skip global timezones from other regions, we are just
332 				// interested in the generic GMT-based ones under "Etc/"
333 				continue;
334 			}
335 
336 
337 			// just accept timezones from "proper" regions, others are aliases
338 			ZoneItemMap::iterator regionIter = zoneMap.find(region);
339 			if (regionIter == zoneMap.end())
340 				continue;
341 
342 			BString fullCountryID = region;
343 			if (countryName != region)
344 				fullCountryID << "/" << countryName;
345 
346 			TimeZoneListItem* regionItem = regionIter->second;
347 			if (regionItem == NULL) {
348 				regionItem = new TimeZoneListItem(region, NULL, NULL);
349 				regionItem->SetOutlineLevel(0);
350 				zoneMap[region] = regionItem;
351 			}
352 
353 			BTimeZone* timeZone = new BTimeZone(zoneID, &language);
354 			BString tzName = timeZone->Name();
355 			if (tzName == "GMT+00:00")
356 				tzName = "GMT";
357 
358 			int32 openParenthesisPos = tzName.FindFirst('(');
359 			if (openParenthesisPos >= 0) {
360 				tzName.Remove(0, openParenthesisPos + 1);
361 				int32 closeParenthesisPos = tzName.FindLast(')');
362 				if (closeParenthesisPos >= 0)
363 					tzName.Truncate(closeParenthesisPos);
364 			}
365 			BString fullZoneID = fullCountryID;
366 			fullZoneID << "/" << tzName;
367 
368 			// skip duplicates
369 			ZoneItemMap::iterator zoneIter = zoneMap.find(fullZoneID);
370 			if (zoneIter != zoneMap.end()) {
371 				delete timeZone;
372 				continue;
373 			}
374 
375 			TimeZoneListItem* countryItem = NULL;
376 			TimeZoneListItem* zoneItem = NULL;
377 			if (count > 1 && countryName.Length() > 0) {
378 				ZoneItemMap::iterator countryIter = zoneMap.find(fullCountryID);
379 				if (countryIter == zoneMap.end()) {
380 					countryItem = new TimeZoneListItem(countryName, NULL, NULL);
381 					countryItem->SetOutlineLevel(1);
382 					zoneMap[fullCountryID] = countryItem;
383 				} else
384 					countryItem = countryIter->second;
385 
386 				zoneItem = new TimeZoneListItem(tzName, NULL, timeZone);
387 				zoneItem->SetOutlineLevel(2);
388 			} else {
389 				BString& name = countryName.Length() > 0 ? countryName : tzName;
390 				zoneItem = new TimeZoneListItem(name, NULL, timeZone);
391 				zoneItem->SetOutlineLevel(1);
392 			}
393 			zoneMap[fullZoneID] = zoneItem;
394 
395 			if (timeZone->ID() == defaultTimeZone.ID()) {
396 				fCurrentZoneItem = zoneItem;
397 				if (countryItem != NULL)
398 					countryItem->SetExpanded(true);
399 				regionItem->SetExpanded(true);
400 			}
401 		}
402 	}
403 
404 	fOldZoneItem = fCurrentZoneItem;
405 
406 	ZoneItemMap::iterator zoneIter;
407 	bool lastWasCountryItem = false;
408 	TimeZoneListItem* lastCountryItem = NULL;
409 	for (zoneIter = zoneMap.begin(); zoneIter != zoneMap.end(); ++zoneIter) {
410 		if (zoneIter->second->OutlineLevel() == 2 && lastWasCountryItem) {
411 			/* Some countries (e.g. Spain and Chile) have their timezones
412 			 * spread across different regions. As a result, there might still
413 			 * be country items with only one timezone below them. We manually
414 			 * filter those country items here.
415 			 */
416 			ZoneItemMap::iterator next = zoneIter;
417 			++next;
418 			if (next != zoneMap.end() && next->second->OutlineLevel() != 2) {
419 				fZoneList->RemoveItem(lastCountryItem);
420 				zoneIter->second->SetText(lastCountryItem->Text());
421 				zoneIter->second->SetOutlineLevel(1);
422 				delete lastCountryItem;
423 			}
424 		}
425 
426 		fZoneList->AddItem(zoneIter->second);
427 		if (zoneIter->second->OutlineLevel() == 1) {
428 			lastWasCountryItem = true;
429 			lastCountryItem = zoneIter->second;
430 		} else
431 			lastWasCountryItem = false;
432 	}
433 }
434 
435 
436 void
437 TimeZoneView::_Revert()
438 {
439 	fCurrentZoneItem = fOldZoneItem;
440 
441 	if (fCurrentZoneItem != NULL) {
442 		int32 currentZoneIndex = fZoneList->IndexOf(fCurrentZoneItem);
443 		fZoneList->Select(currentZoneIndex);
444 	} else
445 		fZoneList->DeselectAll();
446 	fZoneList->ScrollToSelection();
447 
448 	_SetSystemTimeZone();
449 	_UpdatePreview();
450 	_UpdateCurrent();
451 }
452 
453 
454 void
455 TimeZoneView::_UpdatePreview()
456 {
457 	int32 selection = fZoneList->CurrentSelection();
458 	TimeZoneListItem* item
459 		= selection < 0
460 			? NULL
461 			: (TimeZoneListItem*)fZoneList->ItemAt(selection);
462 
463 	if (item == NULL || !item->HasTimeZone()) {
464 		fPreview->SetText("");
465 		fPreview->SetTime("");
466 		return;
467 	}
468 
469 	BString timeString = _FormatTime(item->TimeZone());
470 	fPreview->SetText(item->Text());
471 	fPreview->SetTime(timeString.String());
472 
473 	fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
474 }
475 
476 
477 void
478 TimeZoneView::_UpdateCurrent()
479 {
480 	if (fCurrentZoneItem == NULL)
481 		return;
482 
483 	BString timeString = _FormatTime(fCurrentZoneItem->TimeZone());
484 	fCurrent->SetText(fCurrentZoneItem->Text());
485 	fCurrent->SetTime(timeString.String());
486 }
487 
488 
489 void
490 TimeZoneView::_SetSystemTimeZone()
491 {
492 	/*	Set sytem timezone for all different API levels. How to do this?
493 	 *	1) tell locale-roster about new default timezone
494 	 *	2) tell kernel about new timezone offset
495 	 */
496 
497 	int32 selection = fZoneList->CurrentSelection();
498 	if (selection < 0)
499 		return;
500 
501 	TimeZoneListItem* item
502 		= static_cast<TimeZoneListItem*>(fZoneList->ItemAt(selection));
503 	if (item == NULL || !item->HasTimeZone())
504 		return;
505 
506 	fCurrentZoneItem = item;
507 	const BTimeZone& timeZone = item->TimeZone();
508 
509 	gMutableLocaleRoster->SetDefaultTimeZone(timeZone);
510 
511 	_kern_set_timezone(timeZone.OffsetFromGMT(), timeZone.ID().String(),
512 		timeZone.ID().Length());
513 
514 	fSetZone->SetEnabled(false);
515 	fLastUpdateMinute = -1;
516 		// just to trigger updating immediately
517 }
518 
519 
520 BString
521 TimeZoneView::_FormatTime(const BTimeZone& timeZone)
522 {
523 	BString result;
524 
525 	time_t now = time(NULL);
526 	bool rtcIsGMT;
527 	_kern_get_real_time_clock_is_gmt(&rtcIsGMT);
528 	if (!rtcIsGMT) {
529 		int32 currentOffset
530 			= fCurrentZoneItem != NULL && fCurrentZoneItem->HasTimeZone()
531 				? fCurrentZoneItem->OffsetFromGMT()
532 				: 0;
533 		now -= timeZone.OffsetFromGMT() - currentOffset;
534 	}
535 	be_locale->FormatTime(&result, now, false, &timeZone);
536 
537 	return result;
538 }
539