xref: /haiku/src/preferences/time/ZoneView.cpp (revision 3c6e2dd68577c34d93e17f19711f6245bf6d0915)
1 /*
2  * Copyright 2004-2008, 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  */
10 
11 /*
12 	Exceptions:
13 		doesn't calc "Time in" time.
14 */
15 
16 #include "ZoneView.h"
17 
18 #include <stdio.h>
19 #include <stdlib.h>
20 
21 #include <Button.h>
22 #include <Directory.h>
23 #include <Entry.h>
24 #include <FindDirectory.h>
25 #include <ListItem.h>
26 #include <ListView.h>
27 #include <MenuField.h>
28 #include <MenuItem.h>
29 #include <MenuBar.h>
30 #include <PopUpMenu.h>
31 #include <ScrollView.h>
32 #include <StorageDefs.h>
33 #include <String.h>
34 #include <View.h>
35 #include <Window.h>
36 
37 #include <syscalls.h>
38 
39 #include "TimeMessages.h"
40 #include "TZDisplay.h"
41 #include "TimeWindow.h"
42 
43 
44 class TZoneItem : public BStringItem {
45 	public:
46 					TZoneItem(const char *text, const char *zone)
47 						: BStringItem(text), fZone(new BPath(zone)) { }
48 
49 					~TZoneItem() {	delete fZone;	}
50 
51 		const char 	*Zone() const	{	return fZone->Leaf();	}
52 		const char 	*Path() const	{	return fZone->Path();	}
53 
54 	private:
55 		BPath 		*fZone;
56 };
57 
58 
59 TimeZoneView::TimeZoneView(BRect frame)
60 	: BView(frame, "timeZoneView", B_FOLLOW_NONE, B_WILL_DRAW | B_NAVIGABLE_JUMP),
61 	  fInitialized(false)
62 {
63 	ReadTimeZoneLink();
64 	InitView();
65 }
66 
67 
68 bool
69 TimeZoneView::CheckCanRevert()
70 {
71 	return fCurrentZone != fOldZone;
72 }
73 
74 
75 void
76 TimeZoneView::_Revert()
77 {
78 	BPath parent;
79 
80 	fCurrentZone = fOldZone;
81 	int32 czone = 0;
82 
83 	if (fCurrentZone.Leaf() != NULL
84 		&& strcmp(fCurrentZone.Leaf(), "Greenwich") == 0) {
85 		if (BMenuItem* item = fRegionPopUp->FindItem("Others"))
86 			item->SetMarked(true);
87 		czone = FillCityList("Others");
88 	} else if (fCurrentZone.GetParent(&parent) == B_OK) {
89 		if (BMenuItem* item = fRegionPopUp->FindItem(parent.Leaf()))
90 			item->SetMarked(true);
91 		czone = FillCityList(parent.Path());
92 	}
93 
94 	if (czone > -1) {
95 		fCityList->Select(czone);
96 		fCityList->ScrollToSelection();
97 		fCurrent->SetText(((TZoneItem *)fCityList->ItemAt(czone))->Text());
98 		SetPreview();
99 	}
100 	SetTimeZone();
101 }
102 
103 
104 TimeZoneView::~TimeZoneView()
105 {
106 }
107 
108 
109 void
110 TimeZoneView::AttachedToWindow()
111 {
112 	if (Parent())
113 		SetViewColor(Parent()->ViewColor());
114 
115 	if (!fInitialized) {
116 		fInitialized = true;
117 
118 		fSetZone->SetTarget(this);
119 		fCityList->SetTarget(this);
120 		fRegionPopUp->SetTargetForItems(this);
121 
122 		// update displays
123 		BPath parent;
124 		if (fCurrentZone.Leaf() != NULL
125 			&& strcmp(fCurrentZone.Leaf(), "Greenwich") != 0) {
126 			fCurrentZone.GetParent(&parent);
127 			int32 czone = FillCityList(parent.Path());
128 			if (czone > -1) {
129 				fCityList->Select(czone);
130 				fCurrent->SetText(((TZoneItem *)fCityList->ItemAt(czone))->Text());
131 			}
132 		} else {
133 			int32 czone = FillCityList("Others");
134 			if (czone > -1) {
135 				fCityList->Select(czone);
136 				fCurrent->SetText(((TZoneItem *)fCityList->ItemAt(czone))->Text());
137 			}
138 		}
139 	}
140 	fCityList->ScrollToSelection();
141 }
142 
143 
144 void
145 TimeZoneView::MessageReceived(BMessage *message)
146 {
147 	switch (message->what) {
148 		case B_OBSERVER_NOTICE_CHANGE:
149 		{
150 			int32 change;
151 			message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
152 			switch(change) {
153 				case H_TM_CHANGED:
154 					UpdateDateTime(message);
155 					break;
156 
157 				default:
158 					BView::MessageReceived(message);
159 					break;
160 			}
161 			break;
162 		}
163 
164 		case H_REGION_CHANGED:
165 			ChangeRegion(message);
166 			break;
167 
168 		case H_SET_TIME_ZONE:
169 		{
170 			SetTimeZone();
171 			((TTimeWindow*)Window())->SetRevertStatus();
172 			break;
173 		}
174 
175 		case kMsgRevert:
176 			_Revert();
177 			break;
178 
179 		case H_CITY_CHANGED:
180 			SetPreview();
181 			break;
182 
183 		default:
184 			BView::MessageReceived(message);
185 			break;
186 	}
187 }
188 
189 
190 void
191 TimeZoneView::UpdateDateTime(BMessage *message)
192 {
193 	int32 hour;
194 	int32 minute;
195 
196 	// only need hour and minute
197 	if (message->FindInt32("hour", &hour) == B_OK
198 		&& message->FindInt32("minute", &minute) == B_OK) {
199 		if (fHour != hour || fMinute != minute) {
200 			fHour = hour;
201 			fMinute = minute;
202 			fCurrent->SetTime(hour, minute);
203 
204 			// do calc to get other zone time
205 			if (fCityList->CurrentSelection() > -1)
206 				SetPreview();
207 		}
208 	}
209 }
210 
211 
212 void
213 TimeZoneView::InitView()
214 {
215 	// Zone menu
216 	fRegionPopUp = new BPopUpMenu("", true, true, B_ITEMS_IN_COLUMN);
217 
218 	BuildRegionMenu();
219 
220 	// left side
221 	BRect frameLeft(Bounds());
222 	frameLeft.right = frameLeft.Width() / 2.0;
223 	frameLeft.InsetBy(10.0f, 10.0f);
224 
225 	BMenuField *menuField = new BMenuField(frameLeft, "regions", NULL, fRegionPopUp, false);
226 	AddChild(menuField);
227 	menuField->ResizeToPreferred();
228 
229 	frameLeft.top = menuField->Frame().bottom + 10.0;
230 	frameLeft.right -= B_V_SCROLL_BAR_WIDTH;
231 
232 	// City Listing
233 	fCityList = new BListView(frameLeft, "cityList", B_SINGLE_SELECTION_LIST);
234 	fCityList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
235 	fCityList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
236 
237 	BScrollView *scrollList = new BScrollView("scrollList", fCityList,
238 		B_FOLLOW_ALL, 0, false, true);
239 	AddChild(scrollList);
240 
241 	// right side
242 	BRect frameRight(Bounds());
243 	frameRight.left = frameRight.Width() / 2.0;
244 	frameRight.InsetBy(10.0f, 10.0f);
245 	frameRight.top = frameLeft.top;
246 
247 	// Time Displays
248 	fCurrent = new TTZDisplay(frameRight, "currentTime", "Current time:");
249 	AddChild(fCurrent);
250 	fCurrent->ResizeToPreferred();
251 
252 	frameRight.top = fCurrent->Frame().bottom + 10.0;
253 	fPreview = new TTZDisplay(frameRight, "previewTime", "Preview time:");
254 	AddChild(fPreview);
255 	fPreview->ResizeToPreferred();
256 
257 	// set button
258 	fSetZone = new BButton(frameRight, "setTimeZone", "Set time zone",
259 		new BMessage(H_SET_TIME_ZONE));
260 	AddChild(fSetZone);
261 	fSetZone->SetEnabled(false);
262 	fSetZone->ResizeToPreferred();
263 
264 	fSetZone->MoveTo(frameRight.right - fSetZone->Bounds().Width(),
265 		scrollList->Frame().bottom - fSetZone->Bounds().Height());
266 }
267 
268 
269 void
270 TimeZoneView::BuildRegionMenu()
271 {
272 	BPath path;
273 	if (_GetTimezonesPath(path) != B_OK)
274 		return;
275 
276 	// get current region
277 	BPath region;
278 	fCurrentZone.GetParent(&region);
279 
280 	bool markit;
281 	BEntry entry;
282 	BMenuItem *item;
283 	BDirectory dir(path.Path());
284 
285 	dir.Rewind();
286 
287 	// walk timezones dir and add entries to menu
288 	BString itemtext;
289 	while (dir.GetNextEntry(&entry) == B_NO_ERROR) {
290 		if (entry.IsDirectory()) {
291 			path.SetTo(&entry);
292 			itemtext = path.Leaf();
293 
294 			// skip Etc directory
295 			if (itemtext.Compare("Etc", 3) == 0)
296 				continue;
297 
298 			markit = itemtext.Compare(region.Leaf()) == 0;
299 
300 			// add Ocean to oceans :)
301 			if (itemtext.Compare("Atlantic", 8) == 0
302 				 ||itemtext.Compare("Pacific", 7) == 0
303 				 ||itemtext.Compare("Indian", 6) == 0)
304 				itemtext.Append(" Ocean");
305 
306 			// underscores are spaces
307 			itemtext = itemtext.ReplaceAll('_', ' ');
308 
309 			BMessage *msg = new BMessage(H_REGION_CHANGED);
310 			msg->AddString("region", path.Path());
311 
312 			item = new BMenuItem(itemtext.String(), msg);
313 			item->SetMarked(markit);
314 			fRegionPopUp->AddItem(item);
315 		}
316 	}
317 	BMessage *msg = new BMessage(H_REGION_CHANGED);
318 	msg->AddString("region", "Others");
319 
320 	item = new BMenuItem("Others", msg);
321 	if (fCurrentZone.Leaf() != NULL)
322 		item->SetMarked(strcmp(fCurrentZone.Leaf(), "Greenwich") == 0);
323 	fRegionPopUp->AddItem(item);
324 }
325 
326 
327 int32
328 TimeZoneView::FillCityList(const char *area)
329 {
330 	// clear list
331 	int32 count = fCityList->CountItems();
332 	if (count > 0) {
333 		for (int32 idx = count; idx >= 0; idx--)
334 			delete fCityList->RemoveItem(idx);
335 		fCityList->MakeEmpty();
336 	}
337 
338  	BStringItem *city;
339 	int32 index = -1;
340 	if (strcmp(area, "Others") != 0) {
341 		// Enter time zones directory. Find subdir with name that matches string
342 		// stored in area. Enter subdirectory and count the items. For each item,
343 		// add a StringItem to fCityList Time zones directory
344 
345 		BPath path;
346 		_GetTimezonesPath(path);
347 
348 		BPath areaPath(area);
349 	 	BDirectory zoneDir(path.Path());
350 	 	BDirectory cityDir;
351 	 	BString city_name;
352 	 	BEntry entry;
353 
354 		// locate subdirectory:
355 		if (zoneDir.Contains(areaPath.Leaf(), B_DIRECTORY_NODE)) {
356 			cityDir.SetTo(&zoneDir, areaPath.Leaf());
357 
358 			// There is a subdir with a name that matches 'area'. That's the one!!
359 			// Iterate over the items in the subdir, fill listview with TZoneItems
360 			while(cityDir.GetNextEntry(&entry) == B_NO_ERROR) {
361 				if (!entry.IsDirectory()) {
362 					BPath zone(&entry);
363 					city_name = zone.Leaf();
364 					city_name.ReplaceAll("_IN", ", Indiana");
365 					city_name.ReplaceAll("__Calif", ", Calif");
366 					city_name.ReplaceAll("__", ", ");
367 					city_name.ReplaceAll("_", " ");
368 					city = new TZoneItem(city_name.String(), zone.Path());
369 					fCityList->AddItem(city);
370 					if (fCurrentZone.Leaf() != NULL
371 						&& strcmp(fCurrentZone.Leaf(), zone.Leaf()) == 0)
372 						index = fCityList->IndexOf(city);
373 				}
374 			}
375 		}
376 	} else {
377 		BPath path;
378 		_GetTimezonesPath(path);
379 		path.Append("Greenwich");
380 
381 		city = new TZoneItem("Greenwich", path.Path());
382 		fCityList->AddItem(city);
383 		if (fCurrentZone.Leaf() != NULL
384 			&& strcmp(fCurrentZone.Leaf(), "Greenwich") == 0) {
385 			index = fCityList->IndexOf(city);
386 		}
387 	}
388 	return index;
389 }
390 
391 
392 void
393 TimeZoneView::ChangeRegion(BMessage *message)
394 {
395 	BString area;
396 	message->FindString("region", &area);
397 
398 	FillCityList(area.String());
399 }
400 
401 
402 void
403 TimeZoneView::ReadTimeZoneLink()
404 {
405 	char tzFileName[B_PATH_NAME_LENGTH];
406 	bool isGMT;
407 	_kern_get_tzfilename(tzFileName, B_PATH_NAME_LENGTH, &isGMT);
408 
409 	BEntry tzLink;
410 	tzLink.SetTo(tzFileName);
411 
412 	if (!tzLink.Exists()) {
413 		BPath path;
414 		_GetTimezonesPath(path);
415 		path.Append("Greenwich");
416 
417 		tzLink.SetTo(path.Path());
418 	}
419 
420 	// we need something in the current zone
421 	fCurrentZone.SetTo(&tzLink);
422 	fOldZone.SetTo(&tzLink);
423 }
424 
425 
426 void
427 TimeZoneView::SetPreview()
428 {
429 	int32 selection = fCityList->CurrentSelection();
430 	if (selection >= 0) {
431 		TZoneItem *item = (TZoneItem *)fCityList->ItemAt(selection);
432 
433 		// set timezone to selection
434 		SetTimeZone(item->Path());
435 
436 		// calc preview time
437 		time_t current = time(NULL);
438 		struct tm localTime;
439 		localtime_r(&current, &localTime);
440 
441 		// update prview
442 		fPreview->SetText(item->Text());
443 		fPreview->SetTime(localTime.tm_hour, localTime.tm_min);
444 
445 		// set timezone back to current
446 		SetTimeZone(fCurrentZone.Path());
447 
448 		fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
449 	}
450 }
451 
452 
453 void
454 TimeZoneView::SetCurrent(const char *text)
455 {
456 	SetTimeZone(fCurrentZone.Path());
457 
458 	time_t current = time(NULL);
459 	struct tm localTime;
460 	localtime_r(&current, &localTime);
461 
462 	fCurrent->SetText(text);
463 	fCurrent->SetTime(localTime.tm_hour, localTime.tm_min);
464 }
465 
466 
467 void
468 TimeZoneView::SetTimeZone()
469 {
470 	/*	set time based on supplied timezone. How to do this?
471 		1) replace symlink "timezone" in B_USER_SETTINGS_DIR with a link to the new timezone
472 		2) set TZ environment var
473 		3) call settz()
474 		4) call set_timezone from OS.h passing path to timezone file
475 	*/
476 
477 	// get path to current link
478 	// update/create timezone symlink in B_USER_SETTINGS_DIRECTORY
479 	BPath path;
480 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
481 		return;
482 
483 	path.Append("timezone");
484 
485 	// build target for new link
486 	int32 selection = fCityList->CurrentSelection();
487 	if (selection < 0)
488 		return;
489 
490 	BPath target(((TZoneItem *)fCityList->ItemAt(selection))->Path());
491 
492 	// remove old
493 	BEntry entry(path.Path());
494 	if (entry.Exists())
495 		entry.Remove();
496 
497 	// create new
498 	BDirectory dir(target.Path());
499 	if (dir.CreateSymLink(path.Path(), target.Path(), NULL) != B_OK)
500 		fprintf(stderr, "timezone not linked\n");
501 
502 	// update environment
503 	SetTimeZone(target.Path());
504 
505 	// update display
506 	time_t current = time(NULL);
507 	struct tm localTime;
508 	localtime_r(&current, &localTime);
509 
510 	set_timezone(target.Path());
511 
512 	// disable button
513 	fSetZone->SetEnabled(false);
514 
515 	time_t newTime = mktime(&localTime);
516 	localtime_r(&newTime, &localTime);
517 	stime(&newTime);
518 
519 	fHour = localTime.tm_hour;
520 	fMinute = localTime.tm_min;
521 	fCurrentZone.SetTo(target.Path());
522 	SetCurrent(((TZoneItem *)fCityList->ItemAt(selection))->Text());
523 }
524 
525 
526 void
527 TimeZoneView::SetTimeZone(const char *zone)
528 {
529 	putenv(BString("TZ=").Append(zone).String());
530 	tzset();
531 }
532 
533 
534 status_t
535 TimeZoneView::_GetTimezonesPath(BPath& path)
536 {
537 	status_t status = find_directory(B_SYSTEM_DATA_DIRECTORY, &path);
538 	if (status != B_OK)
539 		return status;
540 
541 	return path.Append("timezones");
542 }
543 
544