xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 916c63e6d7f9ac2cb43eab07f195796c887b6016)
1 /*
2 Open Tracker License
3 
4 Terms and Conditions
5 
6 Copyright (c) 1991-2000, Be Incorporated. All rights reserved.
7 
8 Permission is hereby granted, free of charge, to any person obtaining a copy of
9 this software and associated documentation files (the "Software"), to deal in
10 the Software without restriction, including without limitation the rights to
11 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12 of the Software, and to permit persons to whom the Software is furnished to do
13 so, subject to the following conditions:
14 
15 The above copyright notice and this permission notice applies to all licensees
16 and shall be included in all copies or substantial portions of the Software.
17 
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
22 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
23 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 
25 Except as contained in this notice, the name of Be Incorporated shall not be
26 used in advertising or otherwise to promote the sale, use or other dealings in
27 this Software without prior written authorization from Be Incorporated.
28 
29 Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or registered trademarks
30 of Be Incorporated in the United States and other countries. Other brand product
31 names are registered trademarks or trademarks of their respective holders.
32 All rights reserved.
33 */
34 
35 
36 #include "TextWidget.h"
37 
38 #include <string.h>
39 #include <stdlib.h>
40 
41 #include <Alert.h>
42 #include <Catalog.h>
43 #include <Clipboard.h>
44 #include <Debug.h>
45 #include <Directory.h>
46 #include <MessageFilter.h>
47 #include <ScrollView.h>
48 #include <TextView.h>
49 #include <Volume.h>
50 #include <Window.h>
51 
52 #include "Attributes.h"
53 #include "ContainerWindow.h"
54 #include "Commands.h"
55 #include "FSUtils.h"
56 #include "PoseView.h"
57 #include "Utilities.h"
58 
59 
60 #undef B_TRANSLATION_CONTEXT
61 #define B_TRANSLATION_CONTEXT "TextWidget"
62 
63 
64 const float kWidthMargin = 20;
65 
66 
67 //	#pragma mark - BTextWidget
68 
69 
70 BTextWidget::BTextWidget(Model* model, BColumn* column, BPoseView* view)
71 	:
72 	fText(WidgetAttributeText::NewWidgetText(model, column, view)),
73 	fAttrHash(column->AttrHash()),
74 	fAlignment(column->Alignment()),
75 	fEditable(column->Editable()),
76 	fVisible(true),
77 	fActive(false),
78 	fSymLink(model->IsSymLink()),
79 	fMaxWidth(0),
80 	fLastClickedTime(0)
81 {
82 }
83 
84 
85 BTextWidget::~BTextWidget()
86 {
87 	if (fLastClickedTime != 0)
88 		fParams.poseView->SetTextWidgetToCheck(NULL, this);
89 
90 	delete fText;
91 }
92 
93 
94 int
95 BTextWidget::Compare(const BTextWidget& with, BPoseView* view) const
96 {
97 	return fText->Compare(*with.fText, view);
98 }
99 
100 
101 const char*
102 BTextWidget::Text(const BPoseView* view) const
103 {
104 	StringAttributeText* textAttribute
105 		= dynamic_cast<StringAttributeText*>(fText);
106 	if (textAttribute == NULL)
107 		return NULL;
108 
109 	return textAttribute->ValueAsText(view);
110 }
111 
112 
113 float
114 BTextWidget::TextWidth(const BPoseView* pose) const
115 {
116 	return fText->Width(pose);
117 }
118 
119 
120 float
121 BTextWidget::PreferredWidth(const BPoseView* pose) const
122 {
123 	return fText->PreferredWidth(pose) + 1;
124 }
125 
126 
127 BRect
128 BTextWidget::ColumnRect(BPoint poseLoc, const BColumn* column,
129 	const BPoseView* view)
130 {
131 	if (view->ViewMode() != kListMode) {
132 		// ColumnRect only makes sense in list view, return
133 		// CalcRect otherwise
134 		return CalcRect(poseLoc, column, view);
135 	}
136 	BRect result;
137 	result.left = column->Offset() + poseLoc.x;
138 	result.right = result.left + column->Width();
139 	result.bottom = poseLoc.y
140 		+ roundf((view->ListElemHeight() + view->FontHeight()) / 2);
141 	result.top = result.bottom - view->FontHeight();
142 	return result;
143 }
144 
145 
146 BRect
147 BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn* column,
148 	const BPoseView* view, float textWidth)
149 {
150 	BRect result;
151 	float viewWidth = textWidth;
152 
153 	if (view->ViewMode() == kListMode) {
154 		viewWidth = std::min(column->Width(), textWidth);
155 
156 		poseLoc.x += column->Offset();
157 
158 		switch (fAlignment) {
159 			case B_ALIGN_LEFT:
160 				result.left = poseLoc.x;
161 				result.right = result.left + 1 + viewWidth;
162 				break;
163 
164 			case B_ALIGN_CENTER:
165 				result.left = poseLoc.x
166 					+ roundf((column->Width() - viewWidth) / 2);
167 				if (result.left < 0)
168 					result.left = 0;
169 
170 				result.right = result.left + 1 + viewWidth;
171 				break;
172 
173 			case B_ALIGN_RIGHT:
174 				result.right = poseLoc.x + column->Width();
175 				result.left = result.right - 1 - viewWidth;
176 				if (result.left < 0)
177 					result.left = 0;
178 				break;
179 
180 			default:
181 				TRESPASS();
182 				break;
183 		}
184 
185 		result.bottom = poseLoc.y
186 			+ roundf((view->ListElemHeight() + view->FontHeight()) / 2);
187 	} else {
188 		viewWidth = std::min(view->StringWidth("M") * 30, textWidth);
189 		if (view->ViewMode() == kIconMode) {
190 			// icon mode
191 			result.left = poseLoc.x
192 				+ roundf((view->IconSizeInt() - viewWidth) / 2);
193 			result.bottom = poseLoc.y + view->IconPoseHeight();
194 		} else {
195 			// mini icon mode
196 			result.left = poseLoc.x + B_MINI_ICON + kMiniIconSeparator;
197 			result.bottom = poseLoc.y
198 				+ roundf((B_MINI_ICON + view->FontHeight()) / 2);
199 		}
200 
201 		result.right = result.left + viewWidth;
202 	}
203 
204 	result.top = result.bottom - view->FontHeight();
205 
206 	return result;
207 }
208 
209 
210 BRect
211 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column,
212 	const BPoseView* view)
213 {
214 	return CalcRectCommon(poseLoc, column, view, fText->Width(view));
215 }
216 
217 
218 BRect
219 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column,
220 	const BPoseView* view)
221 {
222 	return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth());
223 }
224 
225 
226 BRect
227 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column,
228 	const BPoseView* view)
229 {
230 	BRect result = CalcRect(poseLoc, column, view);
231 	if (result.Width() < kWidthMargin) {
232 		// if resulting rect too narrow, make it a bit wider
233 		// for comfortable clicking
234 		if (column != NULL && column->Width() < kWidthMargin)
235 			result.right = result.left + column->Width();
236 		else
237 			result.right = result.left + kWidthMargin;
238 	}
239 
240 	return result;
241 }
242 
243 
244 void
245 BTextWidget::CheckExpiration()
246 {
247 	if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) {
248 		bigtime_t doubleClickSpeed;
249 		get_click_speed(&doubleClickSpeed);
250 
251 		bigtime_t delta = system_time() - fLastClickedTime;
252 
253 		if (delta > doubleClickSpeed) {
254 			// at least 'doubleClickSpeed' microseconds ellapsed and no click
255 			// was registered since.
256 			fLastClickedTime = 0;
257 			StartEdit(fParams.bounds, fParams.poseView, fParams.pose);
258 		}
259 	} else {
260 		fLastClickedTime = 0;
261 		fParams.poseView->SetTextWidgetToCheck(NULL);
262 	}
263 }
264 
265 
266 void
267 BTextWidget::CancelWait()
268 {
269 	fLastClickedTime = 0;
270 	fParams.poseView->SetTextWidgetToCheck(NULL);
271 }
272 
273 
274 void
275 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint)
276 {
277 	// Register the time of that click.  The PoseView, through its Pulse()
278 	// will allow us to StartEdit() if no other click have been registered since
279 	// then.
280 
281 	// TODO: re-enable modifiers, one should be enough
282 	view->SetTextWidgetToCheck(NULL);
283 	if (IsEditable() && pose->IsSelected()) {
284 		bigtime_t doubleClickSpeed;
285 		get_click_speed(&doubleClickSpeed);
286 
287 		if (fLastClickedTime == 0) {
288 			fLastClickedTime = system_time();
289 			if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime())
290 				fLastClickedTime = 0;
291 		} else
292 			fLastClickedTime = 0;
293 
294 		if (fLastClickedTime == 0)
295 			return;
296 
297 		view->SetTextWidgetToCheck(this);
298 
299 		fParams.pose = pose;
300 		fParams.bounds = bounds;
301 		fParams.poseView = view;
302 	} else
303 		fLastClickedTime = 0;
304 }
305 
306 
307 static filter_result
308 TextViewKeyDownFilter(BMessage* message, BHandler**, BMessageFilter* filter)
309 {
310 	uchar key;
311 	if (message->FindInt8("byte", (int8*)&key) != B_OK)
312 		return B_DISPATCH_MESSAGE;
313 
314 	ThrowOnAssert(filter != NULL);
315 
316 	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
317 		filter->Looper());
318 	ThrowOnAssert(window != NULL);
319 
320 	BPoseView* view = window->PoseView();
321 	ThrowOnAssert(view != NULL);
322 
323 	if (key == B_RETURN || key == B_ESCAPE) {
324 		view->CommitActivePose(key == B_RETURN);
325 		return B_SKIP_MESSAGE;
326 	}
327 
328 	if (key == B_TAB) {
329 		if (view->ActivePose()) {
330 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
331 				view->ActivePose()->EditPreviousWidget(view);
332 			else
333 				view->ActivePose()->EditNextWidget(view);
334 		}
335 
336 		return B_SKIP_MESSAGE;
337 	}
338 
339 	// the BTextView doesn't respect window borders when resizing itself;
340 	// we try to work-around this "bug" here.
341 
342 	// find the text editing view
343 	BView* scrollView = view->FindView("BorderView");
344 	if (scrollView != NULL) {
345 		BTextView* textView = dynamic_cast<BTextView*>(
346 			scrollView->FindView("WidgetTextView"));
347 		if (textView != NULL) {
348 			ASSERT(view->ActiveTextWidget() != NULL);
349 			float maxWidth = view->ActiveTextWidget()->MaxWidth();
350 			bool tooWide = textView->TextRect().Width() > maxWidth;
351 			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
352 		}
353 	}
354 
355 	return B_DISPATCH_MESSAGE;
356 }
357 
358 
359 static filter_result
360 TextViewPasteFilter(BMessage* message, BHandler**, BMessageFilter* filter)
361 {
362 	ThrowOnAssert(filter != NULL);
363 
364 	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
365 		filter->Looper());
366 	ThrowOnAssert(window != NULL);
367 
368 	BPoseView* view = window->PoseView();
369 	ThrowOnAssert(view != NULL);
370 
371 	// the BTextView doesn't respect window borders when resizing itself;
372 	// we try to work-around this "bug" here.
373 
374 	// find the text editing view
375 	BView* scrollView = view->FindView("BorderView");
376 	if (scrollView != NULL) {
377 		BTextView* textView = dynamic_cast<BTextView*>(
378 			scrollView->FindView("WidgetTextView"));
379 		if (textView != NULL) {
380 			float textWidth = textView->TextRect().Width();
381 
382 			// subtract out selected text region width
383 			int32 start, finish;
384 			textView->GetSelection(&start, &finish);
385 			if (start != finish) {
386 				BRegion selectedRegion;
387 				textView->GetTextRegion(start, finish, &selectedRegion);
388 				textWidth -= selectedRegion.Frame().Width();
389 			}
390 
391 			// add pasted text width
392 			if (be_clipboard->Lock()) {
393 				BMessage* clip = be_clipboard->Data();
394 				if (clip != NULL) {
395 					const char* text = NULL;
396 					ssize_t length = 0;
397 
398 					if (clip->FindData("text/plain", B_MIME_TYPE,
399 							(const void**)&text, &length) == B_OK) {
400 						textWidth += textView->StringWidth(text);
401 					}
402 				}
403 
404 				be_clipboard->Unlock();
405 			}
406 
407 			// check if pasted text is too wide
408 			ASSERT(view->ActiveTextWidget() != NULL);
409 			float maxWidth = view->ActiveTextWidget()->MaxWidth();
410 			bool tooWide = textWidth > maxWidth;
411 
412 			if (tooWide) {
413 				// resize text view to max width
414 
415 				// move scroll view if not left aligned
416 				float oldWidth = textView->Bounds().Width();
417 				float newWidth = maxWidth;
418 				float right = oldWidth - newWidth;
419 
420 				if (textView->Alignment() == B_ALIGN_CENTER)
421 					scrollView->MoveBy(roundf(right / 2), 0);
422 				else if (textView->Alignment() == B_ALIGN_RIGHT)
423 					scrollView->MoveBy(right, 0);
424 
425 				// resize scroll view
426 				float grow = newWidth - oldWidth;
427 				scrollView->ResizeBy(grow, 0);
428 			}
429 
430 			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
431 		}
432 	}
433 
434 	return B_DISPATCH_MESSAGE;
435 }
436 
437 
438 void
439 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose)
440 {
441 	view->SetTextWidgetToCheck(NULL, this);
442 	if (!IsEditable() || IsActive())
443 		return;
444 
445 	BEntry entry(pose->TargetModel()->EntryRef());
446 	if (entry.InitCheck() == B_OK
447 		&& !ConfirmChangeIfWellKnownDirectory(&entry, kRename)) {
448 		return;
449 	}
450 
451 	view->SetActiveTextWidget(this);
452 
453 	// TODO fix text rect being off by a pixel on some files
454 
455 	BRect rect(bounds);
456 	rect.OffsetBy(view->ViewMode() == kListMode ? -1 : 1, -4);
457 	BTextView* textView = new BTextView(rect, "WidgetTextView", rect,
458 		be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
459 
460 	textView->SetWordWrap(false);
461 	textView->SetInsets(2, 2, 2, 2);
462 	DisallowMetaKeys(textView);
463 	fText->SetUpEditing(textView);
464 
465 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter));
466 	textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter));
467 
468 	// get full text length
469 	rect.right = rect.left + textView->LineWidth();
470 	rect.bottom = rect.top + textView->LineHeight() - 1 + 4;
471 
472 	if (view->ViewMode() == kListMode) {
473 		// limit max width to column width in list mode
474 		BColumn* column = view->ColumnFor(fAttrHash);
475 		ASSERT(column != NULL);
476 		fMaxWidth = column->Width();
477 	} else {
478 		// limit max width to 30em in icon and mini icon mode
479 		fMaxWidth = textView->StringWidth("M") * 30;
480 
481 		if (textView->LineWidth() > fMaxWidth) {
482 			// compensate for text going over right inset
483 			rect.OffsetBy(-2, 0);
484 		}
485 	}
486 
487 	// resize textView
488 	textView->MoveTo(rect.LeftTop());
489 	textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height());
490 	textView->SetTextRect(rect);
491 
492 	// set alignment before adding textView so it doesn't redraw
493 	switch (view->ViewMode()) {
494 		case kIconMode:
495 			textView->SetAlignment(B_ALIGN_CENTER);
496 			break;
497 
498 		case kMiniIconMode:
499 			textView->SetAlignment(B_ALIGN_LEFT);
500 			break;
501 
502 		case kListMode:
503 			textView->SetAlignment(fAlignment);
504 			break;
505 	}
506 
507 	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
508 		false, false, B_PLAIN_BORDER);
509 	view->AddChild(scrollView);
510 
511 	bool tooWide = textView->TextRect().Width() > fMaxWidth;
512 	textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
513 
514 	view->SetActivePose(pose);
515 		// tell view about pose
516 	SetActive(true);
517 		// for widget
518 
519 	textView->SelectAll();
520 	textView->ScrollToSelection();
521 		// scroll to beginning so that text is visible
522 	textView->MakeFocus();
523 
524 	// make this text widget invisible while we edit it
525 	SetVisible(false);
526 
527 	ASSERT(view->Window() != NULL);
528 		// how can I not have a Window here???
529 
530 	if (view->Window()) {
531 		// force immediate redraw so TextView appears instantly
532 		view->Window()->UpdateIfNeeded();
533 	}
534 }
535 
536 
537 void
538 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
539 	BPose* pose, int32 poseIndex)
540 {
541 	view->SetActiveTextWidget(NULL);
542 
543 	// find the text editing view
544 	BView* scrollView = view->FindView("BorderView");
545 	ASSERT(scrollView != NULL);
546 	if (scrollView == NULL)
547 		return;
548 
549 	BTextView* textView = dynamic_cast<BTextView*>(
550 		scrollView->FindView("WidgetTextView"));
551 	ASSERT(textView != NULL);
552 	if (textView == NULL)
553 		return;
554 
555 	BColumn* column = view->ColumnFor(fAttrHash);
556 	ASSERT(column != NULL);
557 	if (column == NULL)
558 		return;
559 
560 	if (saveChanges && fText->CommitEditedText(textView)) {
561 		// we have an actual change, re-sort
562 		view->CheckPoseSortOrder(pose, poseIndex);
563 	}
564 
565 	// make text widget visible again
566 	SetVisible(true);
567 	view->Invalidate(ColumnRect(poseLoc, column, view));
568 
569 	// force immediate redraw so TEView disappears
570 	scrollView->RemoveSelf();
571 	delete scrollView;
572 
573 	ASSERT(view->Window() != NULL);
574 	view->Window()->UpdateIfNeeded();
575 	view->MakeFocus();
576 
577 	SetActive(false);
578 }
579 
580 
581 void
582 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
583 	BPoseView* view, bool visible)
584 {
585 	BRect oldRect;
586 	if (view->ViewMode() != kListMode)
587 		oldRect = CalcOldRect(loc, column, view);
588 
589 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
590 		&& visible) {
591 		BRect invalRect(ColumnRect(loc, column, view));
592 		if (view->ViewMode() != kListMode)
593 			invalRect = invalRect | oldRect;
594 		view->Invalidate(invalRect);
595 	}
596 }
597 
598 
599 void
600 BTextWidget::SelectAll(BPoseView* view)
601 {
602 	BTextView* text = dynamic_cast<BTextView*>(
603 		view->FindView("WidgetTextView"));
604 	if (text != NULL)
605 		text->SelectAll();
606 }
607 
608 
609 void
610 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
611 	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
612 	bool direct)
613 {
614 	textRect.OffsetBy(offset);
615 
616 	if (direct) {
617 		// draw selection box if selected
618 		if (selected) {
619 			drawView->SetDrawingMode(B_OP_COPY);
620 //			eraseRect.OffsetBy(offset);
621 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
622 			drawView->FillRect(textRect, B_SOLID_LOW);
623 		} else
624 			drawView->SetDrawingMode(B_OP_OVER);
625 
626 		// set high color
627 		rgb_color highColor;
628 		// for active views, the selection is drawn as inverse text (background color for the text,
629 		// solid black for the background).
630 		// For inactive windows, the text is drawn normally, then the selection rect is
631 		// alpha-blended on top of it.
632 		// This all happens in BPose::Draw before and after calling this function, here we are
633 		// only concerned with setting the correct color for the text.
634 		if (selected && view->Window()->IsActive())
635 			highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
636 		else
637 			highColor = view->DeskTextColor();
638 
639 		if (clipboardMode == kMoveSelectionTo && !selected) {
640 			drawView->SetDrawingMode(B_OP_ALPHA);
641 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
642 			highColor.alpha = 64;
643 		}
644 		drawView->SetHighColor(highColor);
645 	}
646 
647 	BPoint loc;
648 	loc.y = textRect.bottom - view->FontInfo().descent;
649 	loc.x = textRect.left + 1;
650 
651 	const char* fittingText = fText->FittingText(view);
652 
653 	// TODO: Comparing view and drawView here to avoid rendering
654 	// the text outline when producing a drag bitmap. The check is
655 	// not fully correct, since an offscreen view is also used in some
656 	// other rare cases (something to do with columns). But for now, this
657 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
658 	if (!selected && view == drawView && view->WidgetTextOutline()) {
659 		// draw a halo around the text by using the "false bold"
660 		// feature for text rendering. Either black or white is used for
661 		// the glow (whatever acts as contrast) with a some alpha value,
662 		drawView->SetDrawingMode(B_OP_ALPHA);
663 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
664 
665 		BFont font;
666 		drawView->GetFont(&font);
667 
668 		rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
669 		if (view->IsDesktopWindow())
670 			textColor = view->DeskTextColor();
671 
672 		if (textColor.Brightness() < 100) {
673 			// dark text on light outline
674 			rgb_color glowColor = ui_color(B_SHINE_COLOR);
675 
676 			font.SetFalseBoldWidth(2.0);
677 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
678 			glowColor.alpha = 30;
679 			drawView->SetHighColor(glowColor);
680 
681 			drawView->DrawString(fittingText, loc);
682 
683 			font.SetFalseBoldWidth(1.0);
684 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
685 			glowColor.alpha = 65;
686 			drawView->SetHighColor(glowColor);
687 
688 			drawView->DrawString(fittingText, loc);
689 
690 			font.SetFalseBoldWidth(0.0);
691 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
692 		} else {
693 			// light text on dark outline
694 			rgb_color outlineColor = kBlack;
695 
696 			font.SetFalseBoldWidth(1.0);
697 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
698 			outlineColor.alpha = 30;
699 			drawView->SetHighColor(outlineColor);
700 
701 			drawView->DrawString(fittingText, loc);
702 
703 			font.SetFalseBoldWidth(0.0);
704 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
705 
706 			outlineColor.alpha = 200;
707 			drawView->SetHighColor(outlineColor);
708 
709 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
710 		}
711 
712 		drawView->SetDrawingMode(B_OP_OVER);
713 		drawView->SetHighColor(textColor);
714 	}
715 
716 	drawView->DrawString(fittingText, loc);
717 
718 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
719 		// TODO:
720 		// this should be exported to the WidgetAttribute class, probably
721 		// by having a per widget kind style
722 		if (direct) {
723 			rgb_color underlineColor = drawView->HighColor();
724 			underlineColor.alpha = 180;
725 			drawView->SetHighColor(underlineColor);
726 			drawView->SetDrawingMode(B_OP_ALPHA);
727 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
728 		}
729 
730 		textRect.right = textRect.left + fText->Width(view);
731 			// only underline text part
732 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
733 			B_MIXED_COLORS);
734 	}
735 }
736