xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 445d4fd926c569e7b9ae28017da86280aaecbae2)
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 	view->SetActiveTextWidget(this);
444 
445 	// TODO fix text rect being off by a pixel on some files
446 
447 	BRect rect(bounds);
448 	rect.OffsetBy(view->ViewMode() == kListMode ? -1 : 1, -4);
449 	BTextView* textView = new BTextView(rect, "WidgetTextView", rect,
450 		be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
451 
452 	textView->SetWordWrap(false);
453 	textView->SetInsets(2, 2, 2, 2);
454 	DisallowMetaKeys(textView);
455 	fText->SetUpEditing(textView);
456 
457 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter));
458 
459 	if (view->TargetVolumeIsReadOnly()) {
460 		textView->MakeEditable(false);
461 		textView->MakeSelectable(true);
462 	} else
463 		textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter));
464 
465 	// get full text length
466 	rect.right = rect.left + textView->LineWidth();
467 	rect.bottom = rect.top + textView->LineHeight() - 1 + 4;
468 
469 	if (view->ViewMode() == kListMode) {
470 		// limit max width to column width in list mode
471 		BColumn* column = view->ColumnFor(fAttrHash);
472 		ASSERT(column != NULL);
473 		fMaxWidth = column->Width();
474 	} else {
475 		// limit max width to 30em in icon and mini icon mode
476 		fMaxWidth = textView->StringWidth("M") * 30;
477 
478 		if (textView->LineWidth() > fMaxWidth
479 			|| view->ViewMode() == kMiniIconMode) {
480 			// compensate for text going over right inset
481 			rect.OffsetBy(-2, 0);
482 		}
483 	}
484 
485 	// resize textView
486 	textView->MoveTo(rect.LeftTop());
487 	textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height());
488 	textView->SetTextRect(rect);
489 
490 	// set alignment before adding textView so it doesn't redraw
491 	switch (view->ViewMode()) {
492 		case kIconMode:
493 			textView->SetAlignment(B_ALIGN_CENTER);
494 			break;
495 
496 		case kMiniIconMode:
497 			textView->SetAlignment(B_ALIGN_LEFT);
498 			break;
499 
500 		case kListMode:
501 			textView->SetAlignment(fAlignment);
502 			break;
503 	}
504 
505 	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
506 		false, false, B_PLAIN_BORDER);
507 	view->AddChild(scrollView);
508 
509 	bool tooWide = textView->TextRect().Width() > fMaxWidth;
510 	textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
511 
512 	view->SetActivePose(pose);
513 		// tell view about pose
514 	SetActive(true);
515 		// for widget
516 
517 	textView->SelectAll();
518 	textView->ScrollToSelection();
519 		// scroll to beginning so that text is visible
520 	textView->MakeFocus();
521 
522 	// make this text widget invisible while we edit it
523 	SetVisible(false);
524 
525 	ASSERT(view->Window() != NULL);
526 		// how can I not have a Window here???
527 
528 	if (view->Window()) {
529 		// force immediate redraw so TextView appears instantly
530 		view->Window()->UpdateIfNeeded();
531 	}
532 }
533 
534 
535 void
536 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
537 	BPose* pose, int32 poseIndex)
538 {
539 	view->SetActiveTextWidget(NULL);
540 
541 	// find the text editing view
542 	BView* scrollView = view->FindView("BorderView");
543 	ASSERT(scrollView != NULL);
544 	if (scrollView == NULL)
545 		return;
546 
547 	BTextView* textView = dynamic_cast<BTextView*>(
548 		scrollView->FindView("WidgetTextView"));
549 	ASSERT(textView != NULL);
550 	if (textView == NULL)
551 		return;
552 
553 	BColumn* column = view->ColumnFor(fAttrHash);
554 	ASSERT(column != NULL);
555 	if (column == NULL)
556 		return;
557 
558 	if (saveChanges && fText->CommitEditedText(textView)) {
559 		// we have an actual change, re-sort
560 		view->CheckPoseSortOrder(pose, poseIndex);
561 	}
562 
563 	// make text widget visible again
564 	SetVisible(true);
565 	view->Invalidate(ColumnRect(poseLoc, column, view));
566 
567 	// force immediate redraw so TEView disappears
568 	scrollView->RemoveSelf();
569 	delete scrollView;
570 
571 	ASSERT(view->Window() != NULL);
572 	view->Window()->UpdateIfNeeded();
573 	view->MakeFocus();
574 
575 	SetActive(false);
576 }
577 
578 
579 void
580 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
581 	BPoseView* view, bool visible)
582 {
583 	BRect oldRect;
584 	if (view->ViewMode() != kListMode)
585 		oldRect = CalcOldRect(loc, column, view);
586 
587 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
588 		&& visible) {
589 		BRect invalRect(ColumnRect(loc, column, view));
590 		if (view->ViewMode() != kListMode)
591 			invalRect = invalRect | oldRect;
592 		view->Invalidate(invalRect);
593 	}
594 }
595 
596 
597 void
598 BTextWidget::SelectAll(BPoseView* view)
599 {
600 	BTextView* text = dynamic_cast<BTextView*>(
601 		view->FindView("WidgetTextView"));
602 	if (text != NULL)
603 		text->SelectAll();
604 }
605 
606 
607 void
608 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
609 	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
610 	bool direct)
611 {
612 	textRect.OffsetBy(offset);
613 
614 	// We are only concerned with setting the correct text color.
615 
616 	// For active views the selection is drawn as inverse text
617 	// (background color for the text, solid black for the background).
618 	// For inactive windows the text is drawn normally, then the
619 	// selection rect is alpha-blended on top. This all happens in
620 	// BPose::Draw before and after calling this function.
621 
622 	if (direct) {
623 		// draw selection box if selected
624 		if (selected) {
625 			drawView->SetDrawingMode(B_OP_COPY);
626 			drawView->FillRect(textRect, B_SOLID_LOW);
627 		} else
628 			drawView->SetDrawingMode(B_OP_OVER);
629 
630 		// set high color
631 		rgb_color highColor;
632 		highColor = view->TextColor(selected && view->Window()->IsActive());
633 
634 		if (clipboardMode == kMoveSelectionTo && !selected) {
635 			drawView->SetDrawingMode(B_OP_ALPHA);
636 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
637 			highColor.alpha = 64;
638 		}
639 		drawView->SetHighColor(highColor);
640 	} else if (selected && view->Window()->IsActive())
641 		drawView->SetHighColor(view->BackColor(true)); // inverse
642 	else if (!selected)
643 		drawView->SetHighColor(view->TextColor());
644 
645 	BPoint location;
646 	location.y = textRect.bottom - view->FontInfo().descent;
647 	location.x = textRect.left + 1;
648 
649 	const char* fittingText = fText->FittingText(view);
650 
651 	// TODO: Comparing view and drawView here to avoid rendering
652 	// the text outline when producing a drag bitmap. The check is
653 	// not fully correct, since an offscreen view is also used in some
654 	// other rare cases (something to do with columns). But for now, this
655 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
656 	if (direct && !selected && view->WidgetTextOutline()) {
657 		// draw a halo around the text by using the "false bold"
658 		// feature for text rendering. Either black or white is used for
659 		// the glow (whatever acts as contrast) with a some alpha value,
660 		drawView->SetDrawingMode(B_OP_ALPHA);
661 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
662 
663 		BFont font;
664 		drawView->GetFont(&font);
665 
666 		rgb_color textColor = view->TextColor();
667 		if (textColor.Brightness() < 100) {
668 			// dark text on light outline
669 			rgb_color glowColor = ui_color(B_SHINE_COLOR);
670 
671 			font.SetFalseBoldWidth(2.0);
672 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
673 			glowColor.alpha = 30;
674 			drawView->SetHighColor(glowColor);
675 
676 			drawView->DrawString(fittingText, location);
677 
678 			font.SetFalseBoldWidth(1.0);
679 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
680 			glowColor.alpha = 65;
681 			drawView->SetHighColor(glowColor);
682 
683 			drawView->DrawString(fittingText, location);
684 
685 			font.SetFalseBoldWidth(0.0);
686 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
687 		} else {
688 			// light text on dark outline
689 			rgb_color outlineColor = kBlack;
690 
691 			font.SetFalseBoldWidth(1.0);
692 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
693 			outlineColor.alpha = 30;
694 			drawView->SetHighColor(outlineColor);
695 
696 			drawView->DrawString(fittingText, location);
697 
698 			font.SetFalseBoldWidth(0.0);
699 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
700 
701 			outlineColor.alpha = 200;
702 			drawView->SetHighColor(outlineColor);
703 
704 			drawView->DrawString(fittingText, location + BPoint(1, 1));
705 		}
706 
707 		drawView->SetDrawingMode(B_OP_OVER);
708 		drawView->SetHighColor(textColor);
709 	}
710 
711 	drawView->DrawString(fittingText, location);
712 
713 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
714 		// TODO:
715 		// this should be exported to the WidgetAttribute class, probably
716 		// by having a per widget kind style
717 		if (direct) {
718 			rgb_color underlineColor = drawView->HighColor();
719 			underlineColor.alpha = 180;
720 			drawView->SetHighColor(underlineColor);
721 			drawView->SetDrawingMode(B_OP_ALPHA);
722 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
723 		}
724 
725 		textRect.right = textRect.left + fText->Width(view);
726 			// only underline text part
727 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
728 			B_MIXED_COLORS);
729 	}
730 }
731