xref: /haiku/src/preferences/time/ZoneView.cpp (revision adb0d19d561947362090081e81d90dde59142026)
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 			BMessage msg(*message);
172 			msg.what = kRTCUpdate;
173 			Window()->PostMessage(&msg);
174 			((TTimeWindow*)Window())->SetRevertStatus();
175 			break;
176 		}
177 
178 		case kMsgRevert:
179 			_Revert();
180 			break;
181 
182 		case H_CITY_CHANGED:
183 			SetPreview();
184 			break;
185 
186 		default:
187 			BView::MessageReceived(message);
188 			break;
189 	}
190 }
191 
192 
193 void
194 TimeZoneView::UpdateDateTime(BMessage *message)
195 {
196 	int32 hour;
197 	int32 minute;
198 
199 	// only need hour and minute
200 	if (message->FindInt32("hour", &hour) == B_OK
201 		&& message->FindInt32("minute", &minute) == B_OK) {
202 		if (fHour != hour || fMinute != minute) {
203 			fHour = hour;
204 			fMinute = minute;
205 			fCurrent->SetTime(hour, minute);
206 
207 			// do calc to get other zone time
208 			if (fCityList->CurrentSelection() > -1)
209 				SetPreview();
210 		}
211 	}
212 }
213 
214 
215 void
216 TimeZoneView::InitView()
217 {
218 	// Zone menu
219 	fRegionPopUp = new BPopUpMenu("", true, true, B_ITEMS_IN_COLUMN);
220 
221 	BuildRegionMenu();
222 
223 	// left side
224 	BRect frameLeft(Bounds());
225 	frameLeft.right = frameLeft.Width() / 2.0;
226 	frameLeft.InsetBy(10.0f, 10.0f);
227 
228 	BMenuField *menuField = new BMenuField(frameLeft, "regions", NULL, fRegionPopUp, false);
229 	AddChild(menuField);
230 	menuField->ResizeToPreferred();
231 
232 	frameLeft.top = menuField->Frame().bottom + 10.0;
233 	frameLeft.right -= B_V_SCROLL_BAR_WIDTH;
234 
235 	// City Listing
236 	fCityList = new BListView(frameLeft, "cityList", B_SINGLE_SELECTION_LIST);
237 	fCityList->SetSelectionMessage(new BMessage(H_CITY_CHANGED));
238 	fCityList->SetInvocationMessage(new BMessage(H_SET_TIME_ZONE));
239 
240 	BScrollView *scrollList = new BScrollView("scrollList", fCityList,
241 		B_FOLLOW_ALL, 0, false, true);
242 	AddChild(scrollList);
243 
244 	// right side
245 	BRect frameRight(Bounds());
246 	frameRight.left = frameRight.Width() / 2.0;
247 	frameRight.InsetBy(10.0f, 10.0f);
248 	frameRight.top = frameLeft.top;
249 
250 	// Time Displays
251 	fCurrent = new TTZDisplay(frameRight, "currentTime", "Current time:");
252 	AddChild(fCurrent);
253 	fCurrent->ResizeToPreferred();
254 
255 	frameRight.top = fCurrent->Frame().bottom + 10.0;
256 	fPreview = new TTZDisplay(frameRight, "previewTime", "Preview time:");
257 	AddChild(fPreview);
258 	fPreview->ResizeToPreferred();
259 
260 	// set button
261 	fSetZone = new BButton(frameRight, "setTimeZone", "Set Time Zone",
262 		new BMessage(H_SET_TIME_ZONE));
263 	AddChild(fSetZone);
264 	fSetZone->SetEnabled(false);
265 	fSetZone->ResizeToPreferred();
266 
267 	fSetZone->MoveTo(frameRight.right - fSetZone->Bounds().Width(),
268 		scrollList->Frame().bottom - fSetZone->Bounds().Height());
269 }
270 
271 
272 void
273 TimeZoneView::BuildRegionMenu()
274 {
275 	BPath path;
276 	if (_GetTimezonesPath(path) != B_OK)
277 		return;
278 
279 	// get current region
280 	BPath region;
281 	fCurrentZone.GetParent(&region);
282 
283 	bool markit;
284 	BEntry entry;
285 	BMenuItem *item;
286 	BDirectory dir(path.Path());
287 
288 	dir.Rewind();
289 
290 	// walk timezones dir and add entries to menu
291 	BString itemtext;
292 	while (dir.GetNextEntry(&entry) == B_NO_ERROR) {
293 		if (entry.IsDirectory()) {
294 			path.SetTo(&entry);
295 			itemtext = path.Leaf();
296 
297 			// skip Etc directory
298 			if (itemtext.Compare("Etc", 3) == 0)
299 				continue;
300 
301 			markit = itemtext.Compare(region.Leaf()) == 0;
302 
303 			// add Ocean to oceans :)
304 			if (itemtext.Compare("Atlantic", 8) == 0
305 				 ||itemtext.Compare("Pacific", 7) == 0
306 				 ||itemtext.Compare("Indian", 6) == 0)
307 				itemtext.Append(" Ocean");
308 
309 			// underscores are spaces
310 			itemtext = itemtext.ReplaceAll('_', ' ');
311 
312 			BMessage *msg = new BMessage(H_REGION_CHANGED);
313 			msg->AddString("region", path.Path());
314 
315 			item = new BMenuItem(itemtext.String(), msg);
316 			item->SetMarked(markit);
317 			fRegionPopUp->AddItem(item);
318 		}
319 	}
320 	BMessage *msg = new BMessage(H_REGION_CHANGED);
321 	msg->AddString("region", "Others");
322 
323 	item = new BMenuItem("Others", msg);
324 	if (fCurrentZone.Leaf() != NULL)
325 		item->SetMarked(strcmp(fCurrentZone.Leaf(), "Greenwich") == 0);
326 	fRegionPopUp->AddItem(item);
327 }
328 
329 
330 int32
331 TimeZoneView::FillCityList(const char *area)
332 {
333 	// clear list
334 	int32 count = fCityList->CountItems();
335 	if (count > 0) {
336 		for (int32 idx = count; idx >= 0; idx--)
337 			delete fCityList->RemoveItem(idx);
338 		fCityList->MakeEmpty();
339 	}
340 
341  	BStringItem *city;
342 	int32 index = -1;
343 	if (strcmp(area, "Others") != 0) {
344 		// Enter time zones directory. Find subdir with name that matches string
345 		// stored in area. Enter subdirectory and count the items. For each item,
346 		// add a StringItem to fCityList Time zones directory
347 
348 		BPath path;
349 		_GetTimezonesPath(path);
350 
351 		BPath areaPath(area);
352 	 	BDirectory zoneDir(path.Path());
353 	 	BDirectory cityDir;
354 	 	BString city_name;
355 	 	BEntry entry;
356 
357 		// locate subdirectory:
358 		if (zoneDir.Contains(areaPath.Leaf(), B_DIRECTORY_NODE)) {
359 			cityDir.SetTo(&zoneDir, areaPath.Leaf());
360 
361 			// There is a subdir with a name that matches 'area'. That's the one!!
362 			// Iterate over the items in the subdir, fill listview with TZoneItems
363 			while(cityDir.GetNextEntry(&entry) == B_NO_ERROR) {
364 				if (!entry.IsDirectory()) {
365 					BPath zone(&entry);
366 					city_name = zone.Leaf();
367 					city_name.ReplaceAll("_IN", ", Indiana");
368 					city_name.ReplaceAll("__Calif", ", Calif");
369 					city_name.ReplaceAll("__", ", ");
370 					city_name.ReplaceAll("_", " ");
371 					city = new TZoneItem(city_name.String(), zone.Path());
372 					fCityList->AddItem(city);
373 					if (fCurrentZone.Leaf() != NULL
374 						&& strcmp(fCurrentZone.Leaf(), zone.Leaf()) == 0)
375 						index = fCityList->IndexOf(city);
376 				}
377 			}
378 		}
379 	} else {
380 		BPath path;
381 		_GetTimezonesPath(path);
382 		path.Append("Greenwich");
383 
384 		city = new TZoneItem("Greenwich", path.Path());
385 		fCityList->AddItem(city);
386 		if (fCurrentZone.Leaf() != NULL
387 			&& strcmp(fCurrentZone.Leaf(), "Greenwich") == 0) {
388 			index = fCityList->IndexOf(city);
389 		}
390 	}
391 	return index;
392 }
393 
394 
395 void
396 TimeZoneView::ChangeRegion(BMessage *message)
397 {
398 	BString area;
399 	message->FindString("region", &area);
400 
401 	FillCityList(area.String());
402 }
403 
404 
405 void
406 TimeZoneView::ReadTimeZoneLink()
407 {
408 	char tzFileName[B_PATH_NAME_LENGTH];
409 	bool isGMT;
410 	_kern_get_tzfilename(tzFileName, B_PATH_NAME_LENGTH, &isGMT);
411 
412 	BEntry tzLink;
413 	tzLink.SetTo(tzFileName);
414 
415 	if (!tzLink.Exists()) {
416 		BPath path;
417 		_GetTimezonesPath(path);
418 		path.Append("Greenwich");
419 
420 		tzLink.SetTo(path.Path());
421 	}
422 
423 	// we need something in the current zone
424 	fCurrentZone.SetTo(&tzLink);
425 	fOldZone.SetTo(&tzLink);
426 }
427 
428 
429 void
430 TimeZoneView::SetPreview()
431 {
432 	int32 selection = fCityList->CurrentSelection();
433 	if (selection >= 0) {
434 		TZoneItem *item = (TZoneItem *)fCityList->ItemAt(selection);
435 
436 		// set timezone to selection
437 		SetTimeZone(item->Path());
438 
439 		// calc preview time
440 		time_t current = time(0);
441 		struct tm *ltime = localtime(&current);
442 
443 		// update prview
444 		fPreview->SetText(item->Text());
445 		fPreview->SetTime(ltime->tm_hour, ltime->tm_min);
446 
447 		// set timezone back to current
448 		SetTimeZone(fCurrentZone.Path());
449 
450 		fSetZone->SetEnabled((strcmp(fCurrent->Text(), item->Text()) != 0));
451 	}
452 }
453 
454 
455 void
456 TimeZoneView::SetCurrent(const char *text)
457 {
458 	SetTimeZone(fCurrentZone.Path());
459 
460 	time_t current = time(0);
461 	struct tm *ltime = localtime(&current);
462 
463 	fCurrent->SetText(text);
464 	fCurrent->SetTime(ltime->tm_hour, ltime->tm_min);
465 }
466 
467 
468 void
469 TimeZoneView::SetTimeZone()
470 {
471 	/*	set time based on supplied timezone. How to do this?
472 		1) replace symlink "timezone" in B_USER_SETTINGS_DIR with a link to the new timezone
473 		2) set TZ environment var
474 		3) call settz()
475 		4) call set_timezone from OS.h passing path to timezone file
476 	*/
477 
478 	// get path to current link
479 	// update/create timezone symlink in B_USER_SETTINGS_DIRECTORY
480 	BPath path;
481 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
482 		return;
483 
484 	path.Append("timezone");
485 
486 	// build target for new link
487 	int32 selection = fCityList->CurrentSelection();
488 	if (selection < 0)
489 		return;
490 
491 	BPath target(((TZoneItem *)fCityList->ItemAt(selection))->Path());
492 
493 	// remove old
494 	BEntry entry(path.Path());
495 	if (entry.Exists())
496 		entry.Remove();
497 
498 	// create new
499 	BDirectory dir(target.Path());
500 	if (dir.CreateSymLink(path.Path(), target.Path(), NULL) != B_OK)
501 		fprintf(stderr, "timezone not linked\n");
502 
503 	// update environment
504 	SetTimeZone(target.Path());
505 
506 	// update display
507 	time_t current = time(0);
508 	struct tm *ltime = localtime(&current);
509 
510 	char tza[B_PATH_NAME_LENGTH];
511 	sprintf(tza, "%s", target.Path());
512 	set_timezone(tza);
513 
514 	// disable button
515 	fSetZone->SetEnabled(false);
516 
517 	time_t newtime = mktime(ltime);
518 	ltime = localtime(&newtime);
519 	stime(&newtime);
520 
521 	fHour = ltime->tm_hour;
522 	fMinute = ltime->tm_min;
523 	fCurrentZone.SetTo(target.Path());
524 	SetCurrent(((TZoneItem *)fCityList->ItemAt(selection))->Text());
525 }
526 
527 
528 void
529 TimeZoneView::SetTimeZone(const char *zone)
530 {
531 	putenv(BString("TZ=").Append(zone).String());
532 	tzset();
533 }
534 
535 
536 status_t
537 TimeZoneView::_GetTimezonesPath(BPath& path)
538 {
539 	status_t status = find_directory(B_SYSTEM_DATA_DIRECTORY, &path);
540 	if (status != B_OK)
541 		return status;
542 
543 	return path.Append("timezones");
544 }
545 
546