xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 38eb9fb0eb050aefd2ca57bd87210a1cce158aa4)
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 			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 - floorf(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 			|| 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 	if (direct) {
618 		// draw selection box if selected
619 		if (selected) {
620 			drawView->SetDrawingMode(B_OP_COPY);
621 //			eraseRect.OffsetBy(offset);
622 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
623 			drawView->FillRect(textRect, B_SOLID_LOW);
624 		} else
625 			drawView->SetDrawingMode(B_OP_OVER);
626 
627 		// set high color
628 		rgb_color highColor;
629 		// for active views, the selection is drawn as inverse text (background color for the text,
630 		// solid black for the background).
631 		// For inactive windows, the text is drawn normally, then the selection rect is
632 		// alpha-blended on top of it.
633 		// This all happens in BPose::Draw before and after calling this function, here we are
634 		// only concerned with setting the correct color for the text.
635 		if (selected && view->Window()->IsActive())
636 			highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
637 		else
638 			highColor = view->DeskTextColor();
639 
640 		if (clipboardMode == kMoveSelectionTo && !selected) {
641 			drawView->SetDrawingMode(B_OP_ALPHA);
642 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
643 			highColor.alpha = 64;
644 		}
645 		drawView->SetHighColor(highColor);
646 	}
647 
648 	BPoint loc;
649 	loc.y = textRect.bottom - view->FontInfo().descent;
650 	loc.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 (!selected && view == drawView && 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 = ui_color(B_PANEL_TEXT_COLOR);
670 		if (view->IsDesktopWindow())
671 			textColor = view->DeskTextColor();
672 
673 		if (textColor.Brightness() < 100) {
674 			// dark text on light outline
675 			rgb_color glowColor = ui_color(B_SHINE_COLOR);
676 
677 			font.SetFalseBoldWidth(2.0);
678 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
679 			glowColor.alpha = 30;
680 			drawView->SetHighColor(glowColor);
681 
682 			drawView->DrawString(fittingText, loc);
683 
684 			font.SetFalseBoldWidth(1.0);
685 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
686 			glowColor.alpha = 65;
687 			drawView->SetHighColor(glowColor);
688 
689 			drawView->DrawString(fittingText, loc);
690 
691 			font.SetFalseBoldWidth(0.0);
692 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
693 		} else {
694 			// light text on dark outline
695 			rgb_color outlineColor = kBlack;
696 
697 			font.SetFalseBoldWidth(1.0);
698 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
699 			outlineColor.alpha = 30;
700 			drawView->SetHighColor(outlineColor);
701 
702 			drawView->DrawString(fittingText, loc);
703 
704 			font.SetFalseBoldWidth(0.0);
705 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
706 
707 			outlineColor.alpha = 200;
708 			drawView->SetHighColor(outlineColor);
709 
710 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
711 		}
712 
713 		drawView->SetDrawingMode(B_OP_OVER);
714 		drawView->SetHighColor(textColor);
715 	}
716 
717 	drawView->DrawString(fittingText, loc);
718 
719 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
720 		// TODO:
721 		// this should be exported to the WidgetAttribute class, probably
722 		// by having a per widget kind style
723 		if (direct) {
724 			rgb_color underlineColor = drawView->HighColor();
725 			underlineColor.alpha = 180;
726 			drawView->SetHighColor(underlineColor);
727 			drawView->SetDrawingMode(B_OP_ALPHA);
728 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
729 		}
730 
731 		textRect.right = textRect.left + fText->Width(view);
732 			// only underline text part
733 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
734 			B_MIXED_COLORS);
735 	}
736 }
737