xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 13581b3d2a71545960b98fefebc5225b5bf29072)
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);
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() + ActualFontHeight(view)) / 2);
141 	result.top = result.bottom - floorf(ActualFontHeight(view));
142 	return result;
143 }
144 
145 
146 BRect
147 BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn* column,
148 	const BPoseView* view, float textWidth)
149 {
150 	textWidth -= 1;
151 	BRect result;
152 	float viewWidth = textWidth;
153 
154 	if (view->ViewMode() == kListMode) {
155 		viewWidth = std::min(column->Width(), textWidth);
156 
157 		poseLoc.x += column->Offset();
158 
159 		switch (fAlignment) {
160 			case B_ALIGN_LEFT:
161 				result.left = poseLoc.x;
162 				result.right = result.left + viewWidth;
163 				break;
164 
165 			case B_ALIGN_CENTER:
166 				result.left = poseLoc.x
167 					+ roundf((column->Width() - viewWidth) / 2);
168 				if (result.left < 0)
169 					result.left = 0;
170 
171 				result.right = result.left + viewWidth;
172 				break;
173 
174 			case B_ALIGN_RIGHT:
175 				result.right = poseLoc.x + column->Width();
176 				result.left = result.right - viewWidth;
177 				if (result.left < 0)
178 					result.left = 0;
179 				break;
180 
181 			default:
182 				TRESPASS();
183 				break;
184 		}
185 
186 		result.bottom = poseLoc.y
187 			+ roundf((view->ListElemHeight() + ActualFontHeight(view)) / 2);
188 	} else {
189 		viewWidth = std::min(view->StringWidth("M") * 30, textWidth);
190 		if (view->ViewMode() == kIconMode) {
191 			// icon mode
192 			result.left = poseLoc.x
193 				+ roundf((view->IconSizeInt() - viewWidth) / 2);
194 		} else {
195 			// mini icon mode
196 			result.left = poseLoc.x + view->IconSizeInt() + kMiniIconSeparator;
197 		}
198 		result.bottom = poseLoc.y + view->IconPoseHeight();
199 
200 		result.right = result.left + viewWidth;
201 	}
202 
203 	result.top = result.bottom - floorf(ActualFontHeight(view));
204 
205 	return result;
206 }
207 
208 
209 BRect
210 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column,
211 	const BPoseView* view)
212 {
213 	return CalcRectCommon(poseLoc, column, view, fText->Width(view));
214 }
215 
216 
217 BRect
218 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column,
219 	const BPoseView* view)
220 {
221 	return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth());
222 }
223 
224 
225 BRect
226 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column,
227 	const BPoseView* view)
228 {
229 	BRect result = CalcRect(poseLoc, column, view);
230 	if (result.Width() < kWidthMargin) {
231 		// if resulting rect too narrow, make it a bit wider
232 		// for comfortable clicking
233 		if (column != NULL && column->Width() < kWidthMargin)
234 			result.right = result.left + column->Width();
235 		else
236 			result.right = result.left + kWidthMargin;
237 	}
238 
239 	return result;
240 }
241 
242 
243 void
244 BTextWidget::CheckExpiration()
245 {
246 	if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) {
247 		bigtime_t doubleClickSpeed;
248 		get_click_speed(&doubleClickSpeed);
249 
250 		bigtime_t delta = system_time() - fLastClickedTime;
251 
252 		if (delta > doubleClickSpeed) {
253 			// at least 'doubleClickSpeed' microseconds ellapsed and no click
254 			// was registered since.
255 			fLastClickedTime = 0;
256 			StartEdit(fParams.bounds, fParams.poseView, fParams.pose);
257 		}
258 	} else {
259 		fLastClickedTime = 0;
260 		fParams.poseView->SetTextWidgetToCheck(NULL);
261 	}
262 }
263 
264 
265 void
266 BTextWidget::CancelWait()
267 {
268 	fLastClickedTime = 0;
269 	fParams.poseView->SetTextWidgetToCheck(NULL);
270 }
271 
272 
273 void
274 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint)
275 {
276 	// Register the time of that click.  The PoseView, through its Pulse()
277 	// will allow us to StartEdit() if no other click have been registered since
278 	// then.
279 
280 	// TODO: re-enable modifiers, one should be enough
281 	view->SetTextWidgetToCheck(NULL);
282 	if (IsEditable() && pose->IsSelected()) {
283 		bigtime_t doubleClickSpeed;
284 		get_click_speed(&doubleClickSpeed);
285 
286 		if (fLastClickedTime == 0) {
287 			fLastClickedTime = system_time();
288 			if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime())
289 				fLastClickedTime = 0;
290 		} else
291 			fLastClickedTime = 0;
292 
293 		if (fLastClickedTime == 0)
294 			return;
295 
296 		view->SetTextWidgetToCheck(this);
297 
298 		fParams.pose = pose;
299 		fParams.bounds = bounds;
300 		fParams.poseView = view;
301 	} else
302 		fLastClickedTime = 0;
303 }
304 
305 
306 static filter_result
307 TextViewKeyDownFilter(BMessage* message, BHandler**, BMessageFilter* filter)
308 {
309 	uchar key;
310 	if (message->FindInt8("byte", (int8*)&key) != B_OK)
311 		return B_DISPATCH_MESSAGE;
312 
313 	ThrowOnAssert(filter != NULL);
314 
315 	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
316 		filter->Looper());
317 	ThrowOnAssert(window != NULL);
318 
319 	BPoseView* view = window->PoseView();
320 	ThrowOnAssert(view != NULL);
321 
322 	if (key == B_RETURN || key == B_ESCAPE) {
323 		view->CommitActivePose(key == B_RETURN);
324 		return B_SKIP_MESSAGE;
325 	}
326 
327 	if (key == B_TAB) {
328 		if (view->ActivePose()) {
329 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
330 				view->ActivePose()->EditPreviousWidget(view);
331 			else
332 				view->ActivePose()->EditNextWidget(view);
333 		}
334 
335 		return B_SKIP_MESSAGE;
336 	}
337 
338 	// the BTextView doesn't respect window borders when resizing itself;
339 	// we try to work-around this "bug" here.
340 
341 	// find the text editing view
342 	BView* scrollView = view->FindView("BorderView");
343 	if (scrollView != NULL) {
344 		BTextView* textView = dynamic_cast<BTextView*>(
345 			scrollView->FindView("WidgetTextView"));
346 		if (textView != NULL) {
347 			ASSERT(view->ActiveTextWidget() != NULL);
348 			float maxWidth = view->ActiveTextWidget()->MaxWidth();
349 			bool tooWide = textView->TextRect().Width() > maxWidth;
350 			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
351 		}
352 	}
353 
354 	return B_DISPATCH_MESSAGE;
355 }
356 
357 
358 static filter_result
359 TextViewPasteFilter(BMessage* message, BHandler**, BMessageFilter* filter)
360 {
361 	ThrowOnAssert(filter != NULL);
362 
363 	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
364 		filter->Looper());
365 	ThrowOnAssert(window != NULL);
366 
367 	BPoseView* view = window->PoseView();
368 	ThrowOnAssert(view != NULL);
369 
370 	// the BTextView doesn't respect window borders when resizing itself;
371 	// we try to work-around this "bug" here.
372 
373 	// find the text editing view
374 	BView* scrollView = view->FindView("BorderView");
375 	if (scrollView != NULL) {
376 		BTextView* textView = dynamic_cast<BTextView*>(
377 			scrollView->FindView("WidgetTextView"));
378 		if (textView != NULL) {
379 			float textWidth = textView->TextRect().Width();
380 
381 			// subtract out selected text region width
382 			int32 start, finish;
383 			textView->GetSelection(&start, &finish);
384 			if (start != finish) {
385 				BRegion selectedRegion;
386 				textView->GetTextRegion(start, finish, &selectedRegion);
387 				textWidth -= selectedRegion.Frame().Width();
388 			}
389 
390 			// add pasted text width
391 			if (be_clipboard->Lock()) {
392 				BMessage* clip = be_clipboard->Data();
393 				if (clip != NULL) {
394 					const char* text = NULL;
395 					ssize_t length = 0;
396 
397 					if (clip->FindData("text/plain", B_MIME_TYPE,
398 							(const void**)&text, &length) == B_OK) {
399 						textWidth += textView->StringWidth(text);
400 					}
401 				}
402 
403 				be_clipboard->Unlock();
404 			}
405 
406 			// check if pasted text is too wide
407 			ASSERT(view->ActiveTextWidget() != NULL);
408 			float maxWidth = view->ActiveTextWidget()->MaxWidth();
409 			bool tooWide = textWidth > maxWidth;
410 
411 			if (tooWide) {
412 				// resize text view to max width
413 
414 				// move scroll view if not left aligned
415 				float oldWidth = textView->Bounds().Width();
416 				float newWidth = maxWidth;
417 				float right = oldWidth - newWidth;
418 
419 				if (textView->Alignment() == B_ALIGN_CENTER)
420 					scrollView->MoveBy(roundf(right / 2), 0);
421 				else if (textView->Alignment() == B_ALIGN_RIGHT)
422 					scrollView->MoveBy(right, 0);
423 
424 				// resize scroll view
425 				float grow = newWidth - oldWidth;
426 				scrollView->ResizeBy(grow, 0);
427 			}
428 
429 			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
430 		}
431 	}
432 
433 	return B_DISPATCH_MESSAGE;
434 }
435 
436 
437 void
438 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose)
439 {
440 	view->SetTextWidgetToCheck(NULL, this);
441 	if (!IsEditable() || IsActive())
442 		return;
443 
444 	view->SetActiveTextWidget(this);
445 
446 	BRect rect(bounds);
447 	rect.OffsetBy(view->ViewMode() == kListMode ? -2 : 0, -2);
448 	BTextView* textView = new BTextView(rect, "WidgetTextView", rect,
449 		be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
450 
451 	textView->SetWordWrap(false);
452 	textView->SetInsets(2, 2, 2, 2);
453 	DisallowMetaKeys(textView);
454 	fText->SetupEditing(textView);
455 
456 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter));
457 
458 	if (view->SelectedVolumeIsReadOnly()) {
459 		textView->MakeEditable(false);
460 		textView->MakeSelectable(true);
461 		// tint text view background color to indicate not editable
462 		textView->SetViewColor(tint_color(textView->ViewColor(),
463 			ReadOnlyTint(textView->ViewColor())));
464 	} else
465 		textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter));
466 
467 	// get full text length
468 	rect.right = rect.left + textView->LineWidth();
469 	rect.bottom = rect.top + textView->LineHeight() - 1 + 4;
470 
471 	if (view->ViewMode() == kListMode) {
472 		// limit max width to column width in list mode
473 		BColumn* column = view->ColumnFor(fAttrHash);
474 		ASSERT(column != NULL);
475 		fMaxWidth = column->Width();
476 	} else {
477 		// limit max width to 30em in icon and mini icon mode
478 		fMaxWidth = textView->StringWidth("M") * 30;
479 
480 		if (textView->LineWidth() > fMaxWidth
481 			|| view->ViewMode() == kMiniIconMode) {
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 	// We are only concerned with setting the correct text color.
617 
618 	// For active views the selection is drawn as inverse text
619 	// (background color for the text, solid black for the background).
620 	// For inactive windows the text is drawn normally, then the
621 	// selection rect is alpha-blended on top. This all happens in
622 	// BPose::Draw before and after calling this function.
623 
624 	if (direct) {
625 		// draw selection box if selected
626 		if (selected) {
627 			drawView->SetDrawingMode(B_OP_COPY);
628 			drawView->FillRect(textRect, B_SOLID_LOW);
629 		} else
630 			drawView->SetDrawingMode(B_OP_OVER);
631 
632 		// set high color
633 		rgb_color highColor;
634 		highColor = view->TextColor(selected && view->Window()->IsActive());
635 
636 		if (clipboardMode == kMoveSelectionTo && !selected) {
637 			drawView->SetDrawingMode(B_OP_ALPHA);
638 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
639 			highColor.alpha = 64;
640 		}
641 		drawView->SetHighColor(highColor);
642 	} else if (selected && view->Window()->IsActive())
643 		drawView->SetHighColor(view->BackColor(true)); // inverse
644 	else if (!selected)
645 		drawView->SetHighColor(view->TextColor());
646 
647 	BPoint location;
648 	location.y = textRect.bottom - view->FontInfo().descent + 1;
649 	location.x = textRect.left;
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 (direct && !selected && 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 = view->TextColor();
669 		if (textColor.IsDark()) {
670 			// dark text on light outline
671 			rgb_color glowColor = ui_color(B_SHINE_COLOR);
672 
673 			font.SetFalseBoldWidth(2.0);
674 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
675 			glowColor.alpha = 30;
676 			drawView->SetHighColor(glowColor);
677 
678 			drawView->DrawString(fittingText, location);
679 
680 			font.SetFalseBoldWidth(1.0);
681 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
682 			glowColor.alpha = 65;
683 			drawView->SetHighColor(glowColor);
684 
685 			drawView->DrawString(fittingText, location);
686 
687 			font.SetFalseBoldWidth(0.0);
688 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
689 		} else {
690 			// light text on dark outline
691 			rgb_color outlineColor = kBlack;
692 
693 			font.SetFalseBoldWidth(1.0);
694 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
695 			outlineColor.alpha = 30;
696 			drawView->SetHighColor(outlineColor);
697 
698 			drawView->DrawString(fittingText, location);
699 
700 			font.SetFalseBoldWidth(0.0);
701 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
702 
703 			outlineColor.alpha = 200;
704 			drawView->SetHighColor(outlineColor);
705 
706 			drawView->DrawString(fittingText, location + BPoint(1, 1));
707 		}
708 
709 		drawView->SetDrawingMode(B_OP_OVER);
710 		drawView->SetHighColor(textColor);
711 	}
712 
713 	drawView->DrawString(fittingText, location);
714 
715 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
716 		// TODO:
717 		// this should be exported to the WidgetAttribute class, probably
718 		// by having a per widget kind style
719 		if (direct) {
720 			rgb_color underlineColor = drawView->HighColor();
721 			underlineColor.alpha = 180;
722 			drawView->SetHighColor(underlineColor);
723 			drawView->SetDrawingMode(B_OP_ALPHA);
724 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
725 		}
726 
727 		textRect.right = textRect.left + fText->Width(view);
728 			// only underline text part
729 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
730 			B_MIXED_COLORS);
731 	}
732 }
733