xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 4a55cc230cf7566cadcbb23b1928eefff8aea9a2)
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 - floorf(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 + 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 + viewWidth;
171 				break;
172 
173 			case B_ALIGN_RIGHT:
174 				result.right = poseLoc.x + column->Width();
175 				result.left = result.right - 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 		} else {
194 			// mini icon mode
195 			result.left = poseLoc.x + view->IconSizeInt() + kMiniIconSeparator;
196 		}
197 		result.bottom = poseLoc.y + view->IconPoseHeight();
198 
199 		result.right = result.left + viewWidth;
200 	}
201 
202 	result.top = result.bottom - floorf(view->FontHeight());
203 
204 	return result;
205 }
206 
207 
208 BRect
209 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column,
210 	const BPoseView* view)
211 {
212 	return CalcRectCommon(poseLoc, column, view, fText->Width(view));
213 }
214 
215 
216 BRect
217 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column,
218 	const BPoseView* view)
219 {
220 	return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth());
221 }
222 
223 
224 BRect
225 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column,
226 	const BPoseView* view)
227 {
228 	BRect result = CalcRect(poseLoc, column, view);
229 	if (result.Width() < kWidthMargin) {
230 		// if resulting rect too narrow, make it a bit wider
231 		// for comfortable clicking
232 		if (column != NULL && column->Width() < kWidthMargin)
233 			result.right = result.left + column->Width();
234 		else
235 			result.right = result.left + kWidthMargin;
236 	}
237 
238 	return result;
239 }
240 
241 
242 void
243 BTextWidget::CheckExpiration()
244 {
245 	if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) {
246 		bigtime_t doubleClickSpeed;
247 		get_click_speed(&doubleClickSpeed);
248 
249 		bigtime_t delta = system_time() - fLastClickedTime;
250 
251 		if (delta > doubleClickSpeed) {
252 			// at least 'doubleClickSpeed' microseconds ellapsed and no click
253 			// was registered since.
254 			fLastClickedTime = 0;
255 			StartEdit(fParams.bounds, fParams.poseView, fParams.pose);
256 		}
257 	} else {
258 		fLastClickedTime = 0;
259 		fParams.poseView->SetTextWidgetToCheck(NULL);
260 	}
261 }
262 
263 
264 void
265 BTextWidget::CancelWait()
266 {
267 	fLastClickedTime = 0;
268 	fParams.poseView->SetTextWidgetToCheck(NULL);
269 }
270 
271 
272 void
273 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint)
274 {
275 	// Register the time of that click.  The PoseView, through its Pulse()
276 	// will allow us to StartEdit() if no other click have been registered since
277 	// then.
278 
279 	// TODO: re-enable modifiers, one should be enough
280 	view->SetTextWidgetToCheck(NULL);
281 	if (IsEditable() && pose->IsSelected()) {
282 		bigtime_t doubleClickSpeed;
283 		get_click_speed(&doubleClickSpeed);
284 
285 		if (fLastClickedTime == 0) {
286 			fLastClickedTime = system_time();
287 			if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime())
288 				fLastClickedTime = 0;
289 		} else
290 			fLastClickedTime = 0;
291 
292 		if (fLastClickedTime == 0)
293 			return;
294 
295 		view->SetTextWidgetToCheck(this);
296 
297 		fParams.pose = pose;
298 		fParams.bounds = bounds;
299 		fParams.poseView = view;
300 	} else
301 		fLastClickedTime = 0;
302 }
303 
304 
305 static filter_result
306 TextViewKeyDownFilter(BMessage* message, BHandler**, BMessageFilter* filter)
307 {
308 	uchar key;
309 	if (message->FindInt8("byte", (int8*)&key) != B_OK)
310 		return B_DISPATCH_MESSAGE;
311 
312 	ThrowOnAssert(filter != NULL);
313 
314 	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
315 		filter->Looper());
316 	ThrowOnAssert(window != NULL);
317 
318 	BPoseView* view = window->PoseView();
319 	ThrowOnAssert(view != NULL);
320 
321 	if (key == B_RETURN || key == B_ESCAPE) {
322 		view->CommitActivePose(key == B_RETURN);
323 		return B_SKIP_MESSAGE;
324 	}
325 
326 	if (key == B_TAB) {
327 		if (view->ActivePose()) {
328 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
329 				view->ActivePose()->EditPreviousWidget(view);
330 			else
331 				view->ActivePose()->EditNextWidget(view);
332 		}
333 
334 		return B_SKIP_MESSAGE;
335 	}
336 
337 	// the BTextView doesn't respect window borders when resizing itself;
338 	// we try to work-around this "bug" here.
339 
340 	// find the text editing view
341 	BView* scrollView = view->FindView("BorderView");
342 	if (scrollView != NULL) {
343 		BTextView* textView = dynamic_cast<BTextView*>(
344 			scrollView->FindView("WidgetTextView"));
345 		if (textView != NULL) {
346 			ASSERT(view->ActiveTextWidget() != NULL);
347 			float maxWidth = view->ActiveTextWidget()->MaxWidth();
348 			bool tooWide = textView->TextRect().Width() > maxWidth;
349 			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
350 		}
351 	}
352 
353 	return B_DISPATCH_MESSAGE;
354 }
355 
356 
357 static filter_result
358 TextViewPasteFilter(BMessage* message, BHandler**, BMessageFilter* filter)
359 {
360 	ThrowOnAssert(filter != NULL);
361 
362 	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
363 		filter->Looper());
364 	ThrowOnAssert(window != NULL);
365 
366 	BPoseView* view = window->PoseView();
367 	ThrowOnAssert(view != NULL);
368 
369 	// the BTextView doesn't respect window borders when resizing itself;
370 	// we try to work-around this "bug" here.
371 
372 	// find the text editing view
373 	BView* scrollView = view->FindView("BorderView");
374 	if (scrollView != NULL) {
375 		BTextView* textView = dynamic_cast<BTextView*>(
376 			scrollView->FindView("WidgetTextView"));
377 		if (textView != NULL) {
378 			float textWidth = textView->TextRect().Width();
379 
380 			// subtract out selected text region width
381 			int32 start, finish;
382 			textView->GetSelection(&start, &finish);
383 			if (start != finish) {
384 				BRegion selectedRegion;
385 				textView->GetTextRegion(start, finish, &selectedRegion);
386 				textWidth -= selectedRegion.Frame().Width();
387 			}
388 
389 			// add pasted text width
390 			if (be_clipboard->Lock()) {
391 				BMessage* clip = be_clipboard->Data();
392 				if (clip != NULL) {
393 					const char* text = NULL;
394 					ssize_t length = 0;
395 
396 					if (clip->FindData("text/plain", B_MIME_TYPE,
397 							(const void**)&text, &length) == B_OK) {
398 						textWidth += textView->StringWidth(text);
399 					}
400 				}
401 
402 				be_clipboard->Unlock();
403 			}
404 
405 			// check if pasted text is too wide
406 			ASSERT(view->ActiveTextWidget() != NULL);
407 			float maxWidth = view->ActiveTextWidget()->MaxWidth();
408 			bool tooWide = textWidth > maxWidth;
409 
410 			if (tooWide) {
411 				// resize text view to max width
412 
413 				// move scroll view if not left aligned
414 				float oldWidth = textView->Bounds().Width();
415 				float newWidth = maxWidth;
416 				float right = oldWidth - newWidth;
417 
418 				if (textView->Alignment() == B_ALIGN_CENTER)
419 					scrollView->MoveBy(roundf(right / 2), 0);
420 				else if (textView->Alignment() == B_ALIGN_RIGHT)
421 					scrollView->MoveBy(right, 0);
422 
423 				// resize scroll view
424 				float grow = newWidth - oldWidth;
425 				scrollView->ResizeBy(grow, 0);
426 			}
427 
428 			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
429 		}
430 	}
431 
432 	return B_DISPATCH_MESSAGE;
433 }
434 
435 
436 void
437 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose)
438 {
439 	view->SetTextWidgetToCheck(NULL, this);
440 	if (!IsEditable() || IsActive())
441 		return;
442 
443 	BEntry entry(pose->TargetModel()->EntryRef());
444 	if (entry.InitCheck() == B_OK
445 		&& !ConfirmChangeIfWellKnownDirectory(&entry, kRename)) {
446 		return;
447 	}
448 
449 	view->SetActiveTextWidget(this);
450 
451 	// TODO fix text rect being off by a pixel on some files
452 
453 	BRect rect(bounds);
454 	rect.OffsetBy(view->ViewMode() == kListMode ? -1 : 1, -4);
455 	BTextView* textView = new BTextView(rect, "WidgetTextView", rect,
456 		be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
457 
458 	textView->SetWordWrap(false);
459 	textView->SetInsets(2, 2, 2, 2);
460 	DisallowMetaKeys(textView);
461 	fText->SetUpEditing(textView);
462 
463 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter));
464 	textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter));
465 
466 	// get full text length
467 	rect.right = rect.left + textView->LineWidth();
468 	rect.bottom = rect.top + textView->LineHeight() - 1 + 4;
469 
470 	if (view->ViewMode() == kListMode) {
471 		// limit max width to column width in list mode
472 		BColumn* column = view->ColumnFor(fAttrHash);
473 		ASSERT(column != NULL);
474 		fMaxWidth = column->Width();
475 	} else {
476 		// limit max width to 30em in icon and mini icon mode
477 		fMaxWidth = textView->StringWidth("M") * 30;
478 
479 		if (textView->LineWidth() > fMaxWidth
480 			|| view->ViewMode() == kMiniIconMode) {
481 			// compensate for text going over right inset
482 			rect.OffsetBy(-2, 0);
483 		}
484 	}
485 
486 	// resize textView
487 	textView->MoveTo(rect.LeftTop());
488 	textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height());
489 	textView->SetTextRect(rect);
490 
491 	// set alignment before adding textView so it doesn't redraw
492 	switch (view->ViewMode()) {
493 		case kIconMode:
494 			textView->SetAlignment(B_ALIGN_CENTER);
495 			break;
496 
497 		case kMiniIconMode:
498 			textView->SetAlignment(B_ALIGN_LEFT);
499 			break;
500 
501 		case kListMode:
502 			textView->SetAlignment(fAlignment);
503 			break;
504 	}
505 
506 	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
507 		false, false, B_PLAIN_BORDER);
508 	view->AddChild(scrollView);
509 
510 	bool tooWide = textView->TextRect().Width() > fMaxWidth;
511 	textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
512 
513 	view->SetActivePose(pose);
514 		// tell view about pose
515 	SetActive(true);
516 		// for widget
517 
518 	textView->SelectAll();
519 	textView->ScrollToSelection();
520 		// scroll to beginning so that text is visible
521 	textView->MakeFocus();
522 
523 	// make this text widget invisible while we edit it
524 	SetVisible(false);
525 
526 	ASSERT(view->Window() != NULL);
527 		// how can I not have a Window here???
528 
529 	if (view->Window()) {
530 		// force immediate redraw so TextView appears instantly
531 		view->Window()->UpdateIfNeeded();
532 	}
533 }
534 
535 
536 void
537 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
538 	BPose* pose, int32 poseIndex)
539 {
540 	view->SetActiveTextWidget(NULL);
541 
542 	// find the text editing view
543 	BView* scrollView = view->FindView("BorderView");
544 	ASSERT(scrollView != NULL);
545 	if (scrollView == NULL)
546 		return;
547 
548 	BTextView* textView = dynamic_cast<BTextView*>(
549 		scrollView->FindView("WidgetTextView"));
550 	ASSERT(textView != NULL);
551 	if (textView == NULL)
552 		return;
553 
554 	BColumn* column = view->ColumnFor(fAttrHash);
555 	ASSERT(column != NULL);
556 	if (column == NULL)
557 		return;
558 
559 	if (saveChanges && fText->CommitEditedText(textView)) {
560 		// we have an actual change, re-sort
561 		view->CheckPoseSortOrder(pose, poseIndex);
562 	}
563 
564 	// make text widget visible again
565 	SetVisible(true);
566 	view->Invalidate(ColumnRect(poseLoc, column, view));
567 
568 	// force immediate redraw so TEView disappears
569 	scrollView->RemoveSelf();
570 	delete scrollView;
571 
572 	ASSERT(view->Window() != NULL);
573 	view->Window()->UpdateIfNeeded();
574 	view->MakeFocus();
575 
576 	SetActive(false);
577 }
578 
579 
580 void
581 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
582 	BPoseView* view, bool visible)
583 {
584 	BRect oldRect;
585 	if (view->ViewMode() != kListMode)
586 		oldRect = CalcOldRect(loc, column, view);
587 
588 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
589 		&& visible) {
590 		BRect invalRect(ColumnRect(loc, column, view));
591 		if (view->ViewMode() != kListMode)
592 			invalRect = invalRect | oldRect;
593 		view->Invalidate(invalRect);
594 	}
595 }
596 
597 
598 void
599 BTextWidget::SelectAll(BPoseView* view)
600 {
601 	BTextView* text = dynamic_cast<BTextView*>(
602 		view->FindView("WidgetTextView"));
603 	if (text != NULL)
604 		text->SelectAll();
605 }
606 
607 
608 void
609 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
610 	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
611 	bool direct)
612 {
613 	textRect.OffsetBy(offset);
614 
615 	if (direct) {
616 		// draw selection box if selected
617 		if (selected) {
618 			drawView->SetDrawingMode(B_OP_COPY);
619 //			eraseRect.OffsetBy(offset);
620 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
621 			drawView->FillRect(textRect, B_SOLID_LOW);
622 		} else
623 			drawView->SetDrawingMode(B_OP_OVER);
624 
625 		// set high color
626 		rgb_color highColor;
627 		// for active views, the selection is drawn as inverse text (background color for the text,
628 		// solid black for the background).
629 		// For inactive windows, the text is drawn normally, then the selection rect is
630 		// alpha-blended on top of it.
631 		// This all happens in BPose::Draw before and after calling this function, here we are
632 		// only concerned with setting the correct color for the text.
633 		if (selected && view->Window()->IsActive())
634 			highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
635 		else
636 			highColor = view->DeskTextColor();
637 
638 		if (clipboardMode == kMoveSelectionTo && !selected) {
639 			drawView->SetDrawingMode(B_OP_ALPHA);
640 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
641 			highColor.alpha = 64;
642 		}
643 		drawView->SetHighColor(highColor);
644 	}
645 
646 	BPoint loc;
647 	loc.y = textRect.bottom - view->FontInfo().descent;
648 	loc.x = textRect.left + 1;
649 
650 	const char* fittingText = fText->FittingText(view);
651 
652 	// TODO: Comparing view and drawView here to avoid rendering
653 	// the text outline when producing a drag bitmap. The check is
654 	// not fully correct, since an offscreen view is also used in some
655 	// other rare cases (something to do with columns). But for now, this
656 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
657 	if (!selected && view == drawView && view->WidgetTextOutline()) {
658 		// draw a halo around the text by using the "false bold"
659 		// feature for text rendering. Either black or white is used for
660 		// the glow (whatever acts as contrast) with a some alpha value,
661 		drawView->SetDrawingMode(B_OP_ALPHA);
662 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
663 
664 		BFont font;
665 		drawView->GetFont(&font);
666 
667 		rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
668 		if (view->IsDesktopWindow())
669 			textColor = view->DeskTextColor();
670 
671 		if (textColor.Brightness() < 100) {
672 			// dark text on light outline
673 			rgb_color glowColor = ui_color(B_SHINE_COLOR);
674 
675 			font.SetFalseBoldWidth(2.0);
676 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
677 			glowColor.alpha = 30;
678 			drawView->SetHighColor(glowColor);
679 
680 			drawView->DrawString(fittingText, loc);
681 
682 			font.SetFalseBoldWidth(1.0);
683 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
684 			glowColor.alpha = 65;
685 			drawView->SetHighColor(glowColor);
686 
687 			drawView->DrawString(fittingText, loc);
688 
689 			font.SetFalseBoldWidth(0.0);
690 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
691 		} else {
692 			// light text on dark outline
693 			rgb_color outlineColor = kBlack;
694 
695 			font.SetFalseBoldWidth(1.0);
696 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
697 			outlineColor.alpha = 30;
698 			drawView->SetHighColor(outlineColor);
699 
700 			drawView->DrawString(fittingText, loc);
701 
702 			font.SetFalseBoldWidth(0.0);
703 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
704 
705 			outlineColor.alpha = 200;
706 			drawView->SetHighColor(outlineColor);
707 
708 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
709 		}
710 
711 		drawView->SetDrawingMode(B_OP_OVER);
712 		drawView->SetHighColor(textColor);
713 	}
714 
715 	drawView->DrawString(fittingText, loc);
716 
717 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
718 		// TODO:
719 		// this should be exported to the WidgetAttribute class, probably
720 		// by having a per widget kind style
721 		if (direct) {
722 			rgb_color underlineColor = drawView->HighColor();
723 			underlineColor.alpha = 180;
724 			drawView->SetHighColor(underlineColor);
725 			drawView->SetDrawingMode(B_OP_ALPHA);
726 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
727 		}
728 
729 		textRect.right = textRect.left + fText->Width(view);
730 			// only underline text part
731 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
732 			B_MIXED_COLORS);
733 	}
734 }
735