xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 87f4776937505e3014251c9c3434be78ae29d7d0)
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 <Debug.h>
44 #include <Directory.h>
45 #include <MessageFilter.h>
46 #include <ScrollView.h>
47 #include <TextView.h>
48 #include <Volume.h>
49 #include <Window.h>
50 
51 #include "Attributes.h"
52 #include "ContainerWindow.h"
53 #include "Commands.h"
54 #include "FSUtils.h"
55 #include "PoseView.h"
56 #include "Utilities.h"
57 
58 
59 #undef B_TRANSLATION_CONTEXT
60 #define B_TRANSLATION_CONTEXT "TextWidget"
61 
62 
63 const float kWidthMargin = 20;
64 
65 
66 //	#pragma mark - BTextWidget
67 
68 
69 BTextWidget::BTextWidget(Model* model, BColumn* column, BPoseView* view)
70 	:
71 	fText(WidgetAttributeText::NewWidgetText(model, column, view)),
72 	fAttrHash(column->AttrHash()),
73 	fAlignment(column->Alignment()),
74 	fEditable(column->Editable()),
75 	fVisible(true),
76 	fActive(false),
77 	fSymLink(model->IsSymLink()),
78 	fLastClickedTime(0)
79 {
80 }
81 
82 
83 BTextWidget::~BTextWidget()
84 {
85 	if (fLastClickedTime != 0)
86 		fParams.poseView->SetTextWidgetToCheck(NULL, this);
87 
88 	delete fText;
89 }
90 
91 
92 int
93 BTextWidget::Compare(const BTextWidget& with, BPoseView* view) const
94 {
95 	return fText->Compare(*with.fText, view);
96 }
97 
98 
99 const char*
100 BTextWidget::Text(const BPoseView* view) const
101 {
102 	StringAttributeText* textAttribute
103 		= dynamic_cast<StringAttributeText*>(fText);
104 	if (textAttribute == NULL)
105 		return NULL;
106 
107 	return textAttribute->ValueAsText(view);
108 }
109 
110 
111 float
112 BTextWidget::TextWidth(const BPoseView* pose) const
113 {
114 	return fText->Width(pose);
115 }
116 
117 
118 float
119 BTextWidget::PreferredWidth(const BPoseView* pose) const
120 {
121 	return fText->PreferredWidth(pose) + 1;
122 }
123 
124 
125 BRect
126 BTextWidget::ColumnRect(BPoint poseLoc, const BColumn* column,
127 	const BPoseView* view)
128 {
129 	if (view->ViewMode() != kListMode) {
130 		// ColumnRect only makes sense in list view, return
131 		// CalcRect otherwise
132 		return CalcRect(poseLoc, column, view);
133 	}
134 	BRect result;
135 	result.left = column->Offset() + poseLoc.x;
136 	result.right = result.left + column->Width();
137 	result.bottom = poseLoc.y
138 		+ roundf((view->ListElemHeight() + view->FontHeight()) / 2);
139 	result.top = result.bottom - view->FontHeight();
140 	return result;
141 }
142 
143 
144 BRect
145 BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn* column,
146 	const BPoseView* view, float textWidth)
147 {
148 	BRect result;
149 	if (view->ViewMode() == kListMode) {
150 		poseLoc.x += column->Offset();
151 
152 		switch (fAlignment) {
153 			case B_ALIGN_LEFT:
154 				result.left = poseLoc.x;
155 				result.right = result.left + textWidth + 1;
156 				break;
157 
158 			case B_ALIGN_CENTER:
159 				result.left = poseLoc.x + (column->Width() / 2)
160 					- (textWidth / 2);
161 				if (result.left < 0)
162 					result.left = 0;
163 
164 				result.right = result.left + textWidth + 1;
165 				break;
166 
167 			case B_ALIGN_RIGHT:
168 				result.right = poseLoc.x + column->Width();
169 				result.left = result.right - textWidth - 1;
170 				if (result.left < 0)
171 					result.left = 0;
172 				break;
173 
174 			default:
175 				TRESPASS();
176 				break;
177 		}
178 
179 		result.bottom = poseLoc.y
180 			+ roundf((view->ListElemHeight() + view->FontHeight()) / 2);
181 	} else {
182 		if (view->ViewMode() == kIconMode) {
183 			// large/scaled icon mode
184 			result.left = poseLoc.x + (view->IconSizeInt() - textWidth) / 2;
185 		} else {
186 			// mini icon mode
187 			result.left = poseLoc.x + B_MINI_ICON + kMiniIconSeparator;
188 		}
189 
190 		result.right = result.left + textWidth;
191 		result.bottom = poseLoc.y + view->IconPoseHeight();
192 	}
193 	result.top = result.bottom - view->FontHeight();
194 
195 	return result;
196 }
197 
198 
199 BRect
200 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column,
201 	const BPoseView* view)
202 {
203 	return CalcRectCommon(poseLoc, column, view, fText->Width(view));
204 }
205 
206 
207 BRect
208 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column,
209 	const BPoseView* view)
210 {
211 	return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth());
212 }
213 
214 
215 BRect
216 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column,
217 	const BPoseView* view)
218 {
219 	BRect result = CalcRect(poseLoc, column, view);
220 	if (result.Width() < kWidthMargin) {
221 		// if resulting rect too narrow, make it a bit wider
222 		// for comfortable clicking
223 		if (column != NULL && column->Width() < kWidthMargin)
224 			result.right = result.left + column->Width();
225 		else
226 			result.right = result.left + kWidthMargin;
227 	}
228 
229 	return result;
230 }
231 
232 
233 void
234 BTextWidget::CheckExpiration()
235 {
236 	if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) {
237 		bigtime_t doubleClickSpeed;
238 		get_click_speed(&doubleClickSpeed);
239 
240 		bigtime_t delta = system_time() - fLastClickedTime;
241 
242 		if (delta > doubleClickSpeed) {
243 			// at least 'doubleClickSpeed' microseconds ellapsed and no click
244 			// was registered since.
245 			fLastClickedTime = 0;
246 			StartEdit(fParams.bounds, fParams.poseView, fParams.pose);
247 		}
248 	} else {
249 		fLastClickedTime = 0;
250 		fParams.poseView->SetTextWidgetToCheck(NULL);
251 	}
252 }
253 
254 
255 void
256 BTextWidget::CancelWait()
257 {
258 	fLastClickedTime = 0;
259 	fParams.poseView->SetTextWidgetToCheck(NULL);
260 }
261 
262 
263 void
264 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint)
265 {
266 	// Register the time of that click.  The PoseView, through its Pulse()
267 	// will allow us to StartEdit() if no other click have been registered since
268 	// then.
269 
270 	// TODO: re-enable modifiers, one should be enough
271 	view->SetTextWidgetToCheck(NULL);
272 	if (IsEditable() && pose->IsSelected()) {
273 		bigtime_t doubleClickSpeed;
274 		get_click_speed(&doubleClickSpeed);
275 
276 		if (fLastClickedTime == 0) {
277 			fLastClickedTime = system_time();
278 			if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime())
279 				fLastClickedTime = 0;
280 		} else
281 			fLastClickedTime = 0;
282 
283 		if (fLastClickedTime == 0)
284 			return;
285 
286 		view->SetTextWidgetToCheck(this);
287 
288 		fParams.pose = pose;
289 		fParams.bounds = bounds;
290 		fParams.poseView = view;
291 	} else
292 		fLastClickedTime = 0;
293 }
294 
295 
296 static filter_result
297 TextViewFilter(BMessage* message, BHandler**, BMessageFilter* filter)
298 {
299 	uchar key;
300 	if (message->FindInt8("byte", (int8*)&key) != B_OK)
301 		return B_DISPATCH_MESSAGE;
302 
303 	ThrowOnAssert(filter != NULL);
304 
305 	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
306 		filter->Looper());
307 	ThrowOnAssert(window != NULL);
308 
309 	BPoseView* poseView = window->PoseView();
310 	ThrowOnAssert(poseView != NULL);
311 
312 	if (key == B_RETURN || key == B_ESCAPE) {
313 		poseView->CommitActivePose(key == B_RETURN);
314 		return B_SKIP_MESSAGE;
315 	}
316 
317 	if (key == B_TAB) {
318 		if (poseView->ActivePose()) {
319 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
320 				poseView->ActivePose()->EditPreviousWidget(poseView);
321 			else
322 				poseView->ActivePose()->EditNextWidget(poseView);
323 		}
324 
325 		return B_SKIP_MESSAGE;
326 	}
327 
328 	// the BTextView doesn't respect window borders when resizing itself;
329 	// we try to work-around this "bug" here.
330 
331 	// find the text editing view
332 	BView* scrollView = poseView->FindView("BorderView");
333 	if (scrollView != NULL) {
334 		BTextView* textView = dynamic_cast<BTextView*>(
335 			scrollView->FindView("WidgetTextView"));
336 		if (textView != NULL) {
337 			BRect textRect = textView->TextRect();
338 			BRect rect = scrollView->Frame();
339 
340 			if (rect.right + 5 > poseView->Bounds().right
341 				|| rect.left - 5 < 0)
342 				textView->MakeResizable(true, NULL);
343 
344 			if (textRect.Width() + 10 < rect.Width()) {
345 				textView->MakeResizable(true, scrollView);
346 				// make sure no empty white space stays on the right
347 				textView->ScrollToOffset(0);
348 			}
349 		}
350 	}
351 
352 	return B_DISPATCH_MESSAGE;
353 }
354 
355 
356 void
357 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose)
358 {
359 	view->SetTextWidgetToCheck(NULL, this);
360 	if (!IsEditable() || IsActive())
361 		return;
362 
363 	BEntry entry(pose->TargetModel()->EntryRef());
364 	if (entry.InitCheck() == B_OK
365 		&& !ConfirmChangeIfWellKnownDirectory(&entry, kRename)) {
366 		return;
367 	}
368 
369 	// TODO fix text rect being off by a pixel on some files
370 
371 	// get bounds with full text length
372 	BRect rect(bounds);
373 	BRect textRect(bounds);
374 
375 	// label offset
376 	float hOffset = 0;
377 	float vOffset = view->ViewMode() == kListMode ? -1 : -2;
378 	rect.OffsetBy(hOffset, vOffset);
379 
380 	BTextView* textView = new BTextView(rect, "WidgetTextView", textRect,
381 		be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
382 
383 	textView->SetWordWrap(false);
384 	textView->SetInsets(2, 2, 2, 2);
385 	DisallowMetaKeys(textView);
386 	fText->SetUpEditing(textView);
387 
388 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewFilter));
389 
390 	rect.right = rect.left + textView->LineWidth();
391 	rect.bottom = rect.top + textView->LineHeight() - 1;
392 
393 	// enlarge rect by inset amount
394 	rect.InsetBy(-2, -2);
395 
396 	// undo label offset
397 	textRect = rect.OffsetToCopy(-hOffset, -vOffset);
398 
399 	textView->SetTextRect(textRect);
400 
401 	BPoint origin = view->LeftTop();
402 	textRect = view->Bounds();
403 
404 	bool hitBorder = false;
405 	if (rect.left <= origin.x)
406 		rect.left = origin.x + 1, hitBorder = true;
407 	if (rect.right >= textRect.right)
408 		rect.right = textRect.right - 1, hitBorder = true;
409 
410 	textView->MoveTo(rect.LeftTop());
411 	textView->ResizeTo(rect.Width(), rect.Height());
412 
413 	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
414 		false, false, B_PLAIN_BORDER);
415 	view->AddChild(scrollView);
416 
417 	// configure text view
418 	switch (view->ViewMode()) {
419 		case kIconMode:
420 			textView->SetAlignment(B_ALIGN_CENTER);
421 			break;
422 
423 		case kMiniIconMode:
424 			textView->SetAlignment(B_ALIGN_LEFT);
425 			break;
426 
427 		case kListMode:
428 			textView->SetAlignment(fAlignment);
429 			break;
430 	}
431 	textView->MakeResizable(true, hitBorder ? NULL : scrollView);
432 
433 	view->SetActivePose(pose);
434 		// tell view about pose
435 	SetActive(true);
436 		// for widget
437 
438 	textView->SelectAll();
439 	textView->ScrollToSelection();
440 		// scroll to beginning so that text is visible
441 	textView->ScrollBy(-1, -2);
442 		// scroll in rect to center text
443 	textView->MakeFocus();
444 
445 	// make this text widget invisible while we edit it
446 	SetVisible(false);
447 
448 	ASSERT(view->Window() != NULL);
449 		// how can I not have a Window here???
450 
451 	if (view->Window()) {
452 		// force immediate redraw so TextView appears instantly
453 		view->Window()->UpdateIfNeeded();
454 	}
455 }
456 
457 
458 void
459 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
460 	BPose* pose, int32 poseIndex)
461 {
462 	// find the text editing view
463 	BView* scrollView = view->FindView("BorderView");
464 	ASSERT(scrollView != NULL);
465 	if (scrollView == NULL)
466 		return;
467 
468 	BTextView* textView = dynamic_cast<BTextView*>(
469 		scrollView->FindView("WidgetTextView"));
470 	ASSERT(textView != NULL);
471 	if (textView == NULL)
472 		return;
473 
474 	BColumn* column = view->ColumnFor(fAttrHash);
475 	ASSERT(column != NULL);
476 	if (column == NULL)
477 		return;
478 
479 	if (saveChanges && fText->CommitEditedText(textView)) {
480 		// we have an actual change, re-sort
481 		view->CheckPoseSortOrder(pose, poseIndex);
482 	}
483 
484 	// make text widget visible again
485 	SetVisible(true);
486 	view->Invalidate(ColumnRect(poseLoc, column, view));
487 
488 	// force immediate redraw so TEView disappears
489 	scrollView->RemoveSelf();
490 	delete scrollView;
491 
492 	ASSERT(view->Window() != NULL);
493 	view->Window()->UpdateIfNeeded();
494 	view->MakeFocus();
495 
496 	SetActive(false);
497 }
498 
499 
500 void
501 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
502 	BPoseView* view, bool visible)
503 {
504 	BRect oldRect;
505 	if (view->ViewMode() != kListMode)
506 		oldRect = CalcOldRect(loc, column, view);
507 
508 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
509 		&& visible) {
510 		BRect invalRect(ColumnRect(loc, column, view));
511 		if (view->ViewMode() != kListMode)
512 			invalRect = invalRect | oldRect;
513 		view->Invalidate(invalRect);
514 	}
515 }
516 
517 
518 void
519 BTextWidget::SelectAll(BPoseView* view)
520 {
521 	BTextView* text = dynamic_cast<BTextView*>(
522 		view->FindView("WidgetTextView"));
523 	if (text != NULL)
524 		text->SelectAll();
525 }
526 
527 
528 void
529 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
530 	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
531 	bool direct)
532 {
533 	textRect.OffsetBy(offset);
534 
535 	if (direct) {
536 		// draw selection box if selected
537 		if (selected) {
538 			drawView->SetDrawingMode(B_OP_COPY);
539 //			eraseRect.OffsetBy(offset);
540 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
541 			drawView->FillRect(textRect, B_SOLID_LOW);
542 		} else
543 			drawView->SetDrawingMode(B_OP_OVER);
544 
545 		// set high color
546 		rgb_color highColor;
547 		// for active views, the selection is drawn as inverse text (background color for the text,
548 		// solid black for the background).
549 		// For inactive windows, the text is drawn normally, then the selection rect is
550 		// alpha-blended on top of it.
551 		// This all happens in BPose::Draw before and after calling this function, here we are
552 		// only concerned with setting the correct color for the text.
553 		if (selected && view->Window()->IsActive())
554 			highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
555 		else
556 			highColor = view->DeskTextColor();
557 
558 		if (clipboardMode == kMoveSelectionTo && !selected) {
559 			drawView->SetDrawingMode(B_OP_ALPHA);
560 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
561 			highColor.alpha = 64;
562 		}
563 		drawView->SetHighColor(highColor);
564 	}
565 
566 	BPoint loc;
567 	loc.y = textRect.bottom - view->FontInfo().descent;
568 	loc.x = textRect.left + 1;
569 
570 	const char* fittingText = fText->FittingText(view);
571 
572 	// TODO: Comparing view and drawView here to avoid rendering
573 	// the text outline when producing a drag bitmap. The check is
574 	// not fully correct, since an offscreen view is also used in some
575 	// other rare cases (something to do with columns). But for now, this
576 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
577 	if (!selected && view == drawView && view->WidgetTextOutline()) {
578 		// draw a halo around the text by using the "false bold"
579 		// feature for text rendering. Either black or white is used for
580 		// the glow (whatever acts as contrast) with a some alpha value,
581 		drawView->SetDrawingMode(B_OP_ALPHA);
582 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
583 
584 		BFont font;
585 		drawView->GetFont(&font);
586 
587 		rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
588 		if (view->IsDesktopWindow())
589 			textColor = view->DeskTextColor();
590 
591 		if (textColor.Brightness() < 100) {
592 			// dark text on light outline
593 			rgb_color glowColor = ui_color(B_SHINE_COLOR);
594 
595 			font.SetFalseBoldWidth(2.0);
596 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
597 			glowColor.alpha = 30;
598 			drawView->SetHighColor(glowColor);
599 
600 			drawView->DrawString(fittingText, loc);
601 
602 			font.SetFalseBoldWidth(1.0);
603 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
604 			glowColor.alpha = 65;
605 			drawView->SetHighColor(glowColor);
606 
607 			drawView->DrawString(fittingText, loc);
608 
609 			font.SetFalseBoldWidth(0.0);
610 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
611 		} else {
612 			// light text on dark outline
613 			rgb_color outlineColor = kBlack;
614 
615 			font.SetFalseBoldWidth(1.0);
616 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
617 			outlineColor.alpha = 30;
618 			drawView->SetHighColor(outlineColor);
619 
620 			drawView->DrawString(fittingText, loc);
621 
622 			font.SetFalseBoldWidth(0.0);
623 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
624 
625 			outlineColor.alpha = 200;
626 			drawView->SetHighColor(outlineColor);
627 
628 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
629 		}
630 
631 		drawView->SetDrawingMode(B_OP_OVER);
632 		drawView->SetHighColor(textColor);
633 	}
634 
635 	drawView->DrawString(fittingText, loc);
636 
637 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
638 		// TODO:
639 		// this should be exported to the WidgetAttribute class, probably
640 		// by having a per widget kind style
641 		if (direct) {
642 			rgb_color underlineColor = drawView->HighColor();
643 			underlineColor.alpha = 180;
644 			drawView->SetHighColor(underlineColor);
645 			drawView->SetDrawingMode(B_OP_ALPHA);
646 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
647 		}
648 
649 		textRect.right = textRect.left + fText->Width(view);
650 			// only underline text part
651 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
652 			B_MIXED_COLORS);
653 	}
654 }
655