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