xref: /haiku/src/preferences/locale/LocaleWindow.cpp (revision e36a1b58e6daf3efeec46621114691ef499faafc)
1 /*
2  * Copyright 2005, Axel Dörfler, axeld@pinc-software.de. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  */
5 
6 
7 #include "Locale.h"
8 #include "LocaleWindow.h"
9 
10 #include <Alert.h>
11 #include <Application.h>
12 #include <Bitmap.h>
13 #include <Button.h>
14 #include <Catalog.h>
15 #include <GroupLayout.h>
16 #include <GroupLayoutBuilder.h>
17 #include <ListView.h>
18 #include <Locale.h>
19 #include <LocaleRoster.h>
20 #include <Screen.h>
21 #include <ScrollView.h>
22 #include <StringView.h>
23 #include <TabView.h>
24 
25 #include <ICUWrapper.h>
26 
27 #include <unicode/locid.h>
28 #include <unicode/datefmt.h>
29 
30 #include "TimeFormatSettingsView.h"
31 
32 
33 #define TR_CONTEXT "Locale Preflet Window"
34 #define MAX_DRAG_HEIGHT		200.0
35 #define ALPHA				170
36 #define TEXT_OFFSET			5.0
37 
38 // Language lists management
39 
40 
41 // This is a language item. It's a BStringItem but we also want to keep the
42 // language code and not only the displayName
43 class LanguageListItem: public BStringItem
44 {
45 	public:
46 		LanguageListItem(const char* display, const char* code)
47 			: BStringItem(display)
48 			, LanguageCode(code)
49 		{}
50 
51 		~LanguageListItem() {};
52 
53 		const inline BString getLanguageCode() { return LanguageCode; }
54 		void Draw(BView *owner, BRect frame);
55 	private:
56 		const BString LanguageCode;
57 };
58 
59 
60 void
61 LanguageListItem::Draw(BView *owner, BRect frame)
62 {
63 	owner->SetLowColor(255, 255, 255, 255);
64 	owner->FillRect(frame, B_SOLID_LOW);
65 	// label
66 	owner->SetHighColor(0, 0, 0, 255);
67 	font_height fh;
68 	owner->GetFontHeight(&fh);
69 	const char* text = Text();
70 	BString truncatedString(text);
71 	owner->TruncateString(&truncatedString, B_TRUNCATE_MIDDLE,
72 		frame.Width() - TEXT_OFFSET - 4.0);
73 	float height = frame.Height();
74 	float textHeight = fh.ascent + fh.descent;
75 	BPoint textPoint;
76 	textPoint.x = frame.left + TEXT_OFFSET;
77 	textPoint.y = frame.top
78 		+ ceilf(height / 2.0 - textHeight / 2.0 + fh.ascent);
79 	owner->DrawString(truncatedString.String(), textPoint);
80 }
81 
82 
83 // This is a language list. Basically, a drag-n-drop enabled list.
84 class LanguageListView: public BListView
85 {
86 	public:
87 		LanguageListView(const char* name, list_view_type type)
88 			: BListView(name, type)
89 		{}
90 
91 		bool InitiateDrag(BPoint point, int32 index, bool wasSelected);
92 		void MouseMoved(BPoint where, uint32 transit, const BMessage *msg);
93 		void MoveItems(BList& items, int32 index);
94 		void AttachedToWindow()
95 		{
96 			BListView::AttachedToWindow();
97 			ScrollToSelection();
98 		}
99 
100 		void MessageReceived (BMessage* message)
101 		{
102 			if (message->what == 'DRAG') {
103 				LanguageListView *list = NULL;
104 				if (message->FindPointer("list", (void **)&list) == B_OK) {
105 					if (list == this) {
106 						int32 count = CountItems();
107 						if (fDropIndex < 0 || fDropIndex > count)
108 							fDropIndex = count;
109 
110 						// gather all the items from the BMessage
111 						BList items;
112 						int32 index;
113 						for (int32 i = 0; message->FindInt32("index", i, &index)
114 							 	== B_OK; i++)
115 							if (BListItem* item = ItemAt(index))
116 								items.AddItem((void*)item);
117 
118 						// handle them
119 						if (items.CountItems() > 0) {
120 							MoveItems(items, fDropIndex);
121 						}
122 						fDropIndex = -1;
123 					} else {
124 						int32 count = CountItems();
125 						if (fDropIndex < 0 || fDropIndex > count)
126 							fDropIndex = count;
127 
128 						// gather all the items from the BMessage
129 						int32 index;
130 						for (int32 i = 0; message->FindInt32("index", i, &index)
131 								== B_OK; i++)
132 							if (BListItem* item = list->RemoveItem(index)) {
133 								AddItem(item, fDropIndex);
134 								fDropIndex++;
135 							}
136 
137 						fDropIndex = -1;
138 					}
139 				}
140 			} else BListView::MessageReceived(message);
141 		}
142 	private:
143 		int32 fDropIndex;
144 };
145 
146 
147 void
148 LanguageListView::MoveItems(BList& items, int32 index)
149 {
150 	DeselectAll();
151 	// we remove the items while we look at them, the insertion index is
152 	// decreaded when the items index is lower, so that we insert at the right
153 	// spot after removal
154 	BList removedItems;
155 	int32 count = items.CountItems();
156 	for (int32 i = 0; i < count; i++) {
157 		BListItem* item = (BListItem*)items.ItemAt(i);
158 		int32 removeIndex = IndexOf(item);
159 		if (RemoveItem(item) && removedItems.AddItem((void*)item)) {
160 			if (removeIndex < index)
161 				index--;
162 		}
163 		// else ??? -> blow up
164 	}
165 	for (int32 i = 0;
166 		 BListItem* item = (BListItem*)removedItems.ItemAt(i); i++) {
167 		if (AddItem(item, index)) {
168 			// after we're done, the newly inserted items will be selected
169 			Select(index, true);
170 			// next items will be inserted after this one
171 			index++;
172 		} else
173 			delete item;
174 	}
175 }
176 
177 
178 bool
179 LanguageListView::InitiateDrag(BPoint point, int32 index, bool)
180 {
181 	bool success = false;
182 	BListItem* item = ItemAt(CurrentSelection(0));
183 	if (!item) {
184 		// workarround a timing problem
185 		Select(index);
186 		item = ItemAt(index);
187 	}
188 	if (item) {
189 		// create drag message
190 		BMessage msg('DRAG');
191 		msg.AddPointer("list",(void*)(this));
192 		int32 index;
193 		for (int32 i = 0; (index = CurrentSelection(i)) >= 0; i++)
194 			msg.AddInt32("index", index);
195 		// figure out drag rect
196 		float width = Bounds().Width();
197 		BRect dragRect(0.0, 0.0, width, -1.0);
198 		// figure out, how many items fit into our bitmap
199 		int32 numItems;
200 		bool fade = false;
201 		for (numItems = 0; BListItem* item = ItemAt(CurrentSelection(numItems));
202 				numItems++) {
203 			dragRect.bottom += ceilf(item->Height()) + 1.0;
204 			if (dragRect.Height() > MAX_DRAG_HEIGHT) {
205 				fade = true;
206 				dragRect.bottom = MAX_DRAG_HEIGHT;
207 				numItems++;
208 				break;
209 			}
210 		}
211 		BBitmap* dragBitmap = new BBitmap(dragRect, B_RGB32, true);
212 		if (dragBitmap && dragBitmap->IsValid()) {
213 			if (BView *v = new BView(dragBitmap->Bounds(), "helper",
214 									 B_FOLLOW_NONE, B_WILL_DRAW)) {
215 				dragBitmap->AddChild(v);
216 				dragBitmap->Lock();
217 				BRect itemBounds(dragRect) ;
218 				itemBounds.bottom = 0.0;
219 				// let all selected items, that fit into our drag_bitmap, draw
220 				for (int32 i = 0; i < numItems; i++) {
221 					int32 index = CurrentSelection(i);
222 					LanguageListItem* item
223 						= static_cast<LanguageListItem*>(ItemAt(index));
224 					itemBounds.bottom = itemBounds.top + ceilf(item->Height());
225 					if (itemBounds.bottom > dragRect.bottom)
226 						itemBounds.bottom = dragRect.bottom;
227 					item->Draw(v, itemBounds);
228 					itemBounds.top = itemBounds.bottom + 1.0;
229 				}
230 				// make a black frame arround the edge
231 				v->SetHighColor(0, 0, 0, 255);
232 				v->StrokeRect(v->Bounds());
233 				v->Sync();
234 
235 				uint8 *bits = (uint8 *)dragBitmap->Bits();
236 				int32 height = (int32)dragBitmap->Bounds().Height() + 1;
237 				int32 width = (int32)dragBitmap->Bounds().Width() + 1;
238 				int32 bpr = dragBitmap->BytesPerRow();
239 
240 				if (fade) {
241 					for (int32 y = 0; y < height - ALPHA / 2;
242 							y++, bits += bpr) {
243 						uint8 *line = bits + 3;
244 						for (uint8 *end = line + 4 * width; line < end;
245 								line += 4)
246 							*line = ALPHA;
247 					}
248 					for (int32 y = height - ALPHA / 2; y < height;
249 							y++, bits += bpr) {
250 						uint8 *line = bits + 3;
251 						for (uint8 *end = line + 4 * width; line < end;
252 								line += 4)
253 							*line = (height - y) << 1;
254 					}
255 				} else {
256 					for (int32 y = 0; y < height; y++, bits += bpr) {
257 						uint8 *line = bits + 3;
258 						for (uint8 *end = line + 4 * width; line < end;
259 								line += 4)
260 							*line = ALPHA;
261 					}
262 				}
263 				dragBitmap->Unlock();
264 			}
265 		} else {
266 			delete dragBitmap;
267 			dragBitmap = NULL;
268 		}
269 		if (dragBitmap)
270 			DragMessage(&msg, dragBitmap, B_OP_ALPHA, BPoint(0.0, 0.0));
271 		else
272 			DragMessage(&msg, dragRect.OffsetToCopy(point), this);
273 
274 		success = true;
275 	}
276 	return success;
277 }
278 
279 
280 void
281 LanguageListView::MouseMoved(BPoint where, uint32 transit, const BMessage *msg)
282 {
283 	if (msg && (msg->what == 'DRAG')) {
284 		switch (transit) {
285 			case B_ENTERED_VIEW:
286 			case B_INSIDE_VIEW: {
287 				// set drop target through virtual function
288 				// offset where by half of item height
289 				BRect r = ItemFrame(0);
290 				where.y += r.Height() / 2.0;
291 
292 				int32 index = IndexOf(where);
293 				if (index < 0)
294 					index = CountItems();
295 				if (fDropIndex != index) {
296 					fDropIndex = index;
297 					if (fDropIndex >= 0) {
298 						int32 count = CountItems();
299 						if (fDropIndex == count) {
300 							BRect r;
301 							if (ItemAt(count - 1)) {
302 								r = ItemFrame(count - 1);
303 								r.top = r.bottom;
304 								r.bottom = r.top + 1.0;
305 							} else {
306 								r = Bounds();
307 								r.bottom--;
308 									// compensate for scrollbars moved slightly
309 									// out of window
310 							}
311 						} else {
312 							BRect r = ItemFrame(fDropIndex);
313 							r.top--;
314 							r.bottom = r.top + 1.0;
315 						}
316 					}
317 				}
318 				break;
319 			}
320 		}
321 	} else {
322 		BListView::MouseMoved(where, transit, msg);
323 	}
324 }
325 
326 
327 const static uint32 kMsgSelectLanguage = 'slng';
328 const static uint32 kMsgDefaults = 'dflt';
329 const static uint32 kMsgRevert = 'revt';
330 
331 
332 LocaleWindow::LocaleWindow(BRect rect)
333 	: BWindow(rect, "Locale", B_TITLED_WINDOW,
334 		B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_ASYNCHRONOUS_CONTROLS
335 			| B_AUTO_UPDATE_SIZE_LIMITS)
336 {
337 	SetLayout(new BGroupLayout(B_HORIZONTAL));
338 
339 	// Buttons at the bottom
340 
341 	BButton *button = new BButton(TR("Defaults"), new BMessage(kMsgDefaults));
342 
343 	fRevertButton = new BButton(TR("Revert"), new BMessage(kMsgRevert));
344 	fRevertButton->SetEnabled(false);
345 
346 	// Tabs
347 	BTabView *tabView = new BTabView("tabview");
348 
349 	// Language tab
350 	BView *tab = new BView(TR("Language"), B_WILL_DRAW);
351 	//tab->SetViewColor(tabView->ViewColor());
352 	tab->SetLayout(new BGroupLayout(B_VERTICAL, 0));
353 	tabView->AddTab(tab);
354 
355 	{
356 		// first list: available languages
357 		LanguageListView *listView = new LanguageListView("available",
358 			B_MULTIPLE_SELECTION_LIST);
359 		BScrollView *scrollView = new BScrollView("scroller", listView,
360 			0, false, true, B_FANCY_BORDER);
361 
362 		// Fill the language list from the LocaleRoster data
363 		BMessage installedLanguages;
364 		if (be_locale_roster->GetInstalledLanguages(&installedLanguages)
365 				== B_OK) {
366 
367 			BString currentLanguage;
368 			for (int i = 0; installedLanguages.FindString("langs",
369 					i, &currentLanguage) == B_OK; i++) {
370 
371 				// Now get an human-readable, loacalized name for each language
372 				// TODO: sort them using collators.
373 				Locale currentLocale
374 					= Locale::createFromName(currentLanguage.String());
375 				UnicodeString languageFullName;
376 				BString str;
377 				BStringByteSink bbs(&str);
378 				currentLocale.getDisplayName(languageFullName);
379 				languageFullName.toUTF8(bbs);
380 				LanguageListItem* si
381 					= new LanguageListItem(str, currentLanguage.String());
382 				listView->AddItem(si);
383 			}
384 
385 		} else {
386 			BAlert* myAlert = new BAlert("Error",
387 				TR("Unable to find the available languages! You can't use this "
388 					"preflet!"),
389 				TR("Ok"), NULL, NULL,
390 				B_WIDTH_AS_USUAL, B_OFFSET_SPACING, B_STOP_ALERT);
391 			myAlert->Go();
392 		}
393 
394 		// Second list: active languages
395 		fPreferredListView = new LanguageListView("preferred",
396 			B_MULTIPLE_SELECTION_LIST);
397 		BScrollView *scrollViewEnabled = new BScrollView("scroller",
398 			fPreferredListView, 0, false, true, B_FANCY_BORDER);
399 
400 		// get the preferred languages from the Settings. Move them here from
401 		// the other list.
402 		BMessage msg;
403 		be_locale_roster->GetPreferredLanguages(&msg);
404 		BString langCode;
405 		for (int index = 0; msg.FindString("language", index, &langCode)
406 					== B_OK;
407 				index++) {
408 			for (int listPos = 0; LanguageListItem* lli
409 					= static_cast<LanguageListItem*>(listView->ItemAt(listPos));
410 					listPos++) {
411 				if (langCode == lli->getLanguageCode()) {
412 					fPreferredListView->AddItem(lli);
413 					listView->RemoveItem(lli);
414 				}
415 			}
416 		}
417 
418 		tab->AddChild(BGroupLayoutBuilder(B_HORIZONTAL, 10)
419 			.Add(BGroupLayoutBuilder(B_VERTICAL, 10)
420 				.Add(new BStringView("", TR("Available languages")))
421 				.Add(scrollView)
422 			)
423 			.Add(BGroupLayoutBuilder(B_VERTICAL, 10)
424 				.Add(new BStringView("", TR("Preferred languages")))
425 				.Add(scrollViewEnabled)
426 			)
427 		);
428 
429 	}
430 
431 	// Country tab
432 	tab = new BView(TR("Country"), B_WILL_DRAW);
433 	//tab->SetViewColor(tabView->ViewColor());
434 	tab->SetLayout(new BGroupLayout(B_VERTICAL, 0));
435 	tabView->AddTab(tab);
436 
437 	{
438 		BListView* listView = new BListView("country", B_SINGLE_SELECTION_LIST);
439 		BScrollView *scrollView = new BScrollView("scroller",
440 			listView, 0, false, true, B_FANCY_BORDER);
441 
442 		BMessage* msg = new BMessage('csel');
443 		listView->SetSelectionMessage(msg);
444 
445 
446 		// get all available countries from ICU
447 		// Use DateFormat::getAvailableLocale so we get only the one we can
448 		// use. Maybe check the NumberFormat one and see if there is more.
449 		int32_t localeCount;
450 		const Locale* currentLocale
451 			= Locale::getAvailableLocales(localeCount);
452 		BCountry* defaultCountry;
453 		be_locale_roster->GetDefaultCountry(&defaultCountry);
454 
455 		for (int index = 0; index < localeCount; index++)
456 		{
457 			UnicodeString countryFullName;
458 			BString str;
459 			BStringByteSink bbs(&str);
460 			currentLocale[index].getDisplayName(countryFullName);
461 			countryFullName.toUTF8(bbs);
462 			LanguageListItem* si
463 				= new LanguageListItem(str, currentLocale[index].getName());
464 			listView->AddItem(si);
465 			if (strcmp(currentLocale[index].getName(),
466 					defaultCountry->Code()) == 0)
467 				listView->Select(listView->CountItems() - 1);
468 		}
469 
470 		fTimeFormatSettings
471 			= new TimeFormatSettingsView(defaultCountry);
472 
473 		tab->AddChild(BGroupLayoutBuilder(B_HORIZONTAL, 5)
474 			.Add(scrollView)
475 			.Add(new BScrollView("advanced", fTimeFormatSettings, 0,
476 				false, true, B_NO_BORDER))
477 		);
478 
479 		listView->ScrollToSelection();
480 	}
481 
482 	// check if the window is on screen
483 	rect = BScreen().Frame();
484 	rect.right -= 20;
485 	rect.bottom -= 20;
486 
487 	BPoint position = Frame().LeftTop();
488 	if (!rect.Contains(position)) {
489 		// center window on screen as it doesn't fit on the saved position
490 		position.x = (rect.Width() - Bounds().Width()) / 2;
491 		position.y = (rect.Height() - Bounds().Height()) / 2;
492 	}
493 	MoveTo(position);
494 
495 	// Layout management
496 	AddChild(BGroupLayoutBuilder(B_VERTICAL, 3)
497 		.Add(tabView)
498 		.Add(BGroupLayoutBuilder(B_HORIZONTAL, 3)
499 			.Add(button)
500 			.Add(fRevertButton)
501 			.AddGlue()
502 		)
503 		.SetInsets(5, 5, 5, 5)
504 	);
505 }
506 
507 
508 bool
509 LocaleWindow::QuitRequested()
510 {
511 	BMessage update(kMsgSettingsChanged);
512 	update.AddRect("window_frame", Frame());
513 	int index = 0;
514 	while (index < fPreferredListView->CountItems()) {
515 		update.AddString("language", static_cast<LanguageListItem*>
516 			(fPreferredListView->ItemAt(index))->getLanguageCode());
517 		index++;
518 	}
519 	// TODO also save Country tab settings
520 	be_app_messenger.SendMessage(&update);
521 
522 	be_app_messenger.SendMessage(B_QUIT_REQUESTED);
523 	return true;
524 }
525 
526 
527 void
528 LocaleWindow::MessageReceived(BMessage *message)
529 {
530 	switch (message->what) {
531 		case kMsgDefaults:
532 			// reset default settings
533 			break;
534 
535 		case kMsgRevert:
536 			// revert to last settings
537 			break;
538 
539 		case 'csel':
540 		{
541 			// Country selection changed.
542 			// Get the new selected country from the ListView and send it to the
543 			// main app event handler.
544 			void* ptr;
545 			message->FindPointer("source", &ptr);
546 			BListView* countryList = static_cast<BListView*>(ptr);
547 			LanguageListItem* lli = static_cast<LanguageListItem*>
548 				(countryList->ItemAt(countryList->CurrentSelection()));
549 			BMessage* newMessage = new BMessage(kMsgSettingsChanged);
550 			newMessage->AddString("country",lli->getLanguageCode());
551 			be_app_messenger.SendMessage(newMessage);
552 
553 			BCountry* country = new BCountry(lli->getLanguageCode());
554 			fTimeFormatSettings->SetCountry(country);
555 			break;
556 		}
557 
558 		default:
559 			BWindow::MessageReceived(message);
560 			break;
561 	}
562 }
563 
564