xref: /haiku/src/kits/tracker/TextWidget.cpp (revision efafab643ce980e3f3c916795ed302599f6b4f66)
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 	// get bounds with full text length
370 	BRect rect(bounds);
371 	BRect textRect(bounds);
372 	rect.OffsetBy(-2, -1);
373 	rect.right += 1;
374 
375 	BFont font;
376 	view->GetFont(&font);
377 	BTextView* textView = new BTextView(rect, "WidgetTextView", textRect,
378 		&font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
379 
380 	textView->SetWordWrap(false);
381 	DisallowMetaKeys(textView);
382 	fText->SetUpEditing(textView);
383 
384 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewFilter));
385 
386 	rect.right = rect.left + textView->LineWidth() + 3;
387 	// center new width, if necessary
388 	if (view->ViewMode() == kIconMode
389 		|| (view->ViewMode() == kListMode && fAlignment == B_ALIGN_CENTER)) {
390 		rect.OffsetBy(bounds.Width() / 2 - rect.Width() / 2, 0);
391 	}
392 
393 	rect.bottom = rect.top + textView->LineHeight() + 1;
394 	textRect = rect.OffsetToCopy(2, 1);
395 	textRect.right -= 3;
396 	textRect.bottom--;
397 	textView->SetTextRect(textRect);
398 
399 	BPoint origin = view->LeftTop();
400 	textRect = view->Bounds();
401 
402 	bool hitBorder = false;
403 	if (rect.left <= origin.x)
404 		rect.left = origin.x + 1, hitBorder = true;
405 	if (rect.right >= textRect.right)
406 		rect.right = textRect.right - 1, hitBorder = true;
407 
408 	textView->MoveTo(rect.LeftTop());
409 	textView->ResizeTo(rect.Width(), rect.Height());
410 
411 	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
412 		false, false, B_PLAIN_BORDER);
413 	view->AddChild(scrollView);
414 
415 	// configure text view
416 	switch (view->ViewMode()) {
417 		case kIconMode:
418 			textView->SetAlignment(B_ALIGN_CENTER);
419 			break;
420 
421 		case kMiniIconMode:
422 			textView->SetAlignment(B_ALIGN_LEFT);
423 			break;
424 
425 		case kListMode:
426 			textView->SetAlignment(fAlignment);
427 			break;
428 	}
429 	textView->MakeResizable(true, hitBorder ? NULL : scrollView);
430 
431 	view->SetActivePose(pose);
432 		// tell view about pose
433 	SetActive(true);
434 		// for widget
435 
436 	textView->SelectAll();
437 	textView->MakeFocus();
438 
439 	// make this text widget invisible while we edit it
440 	SetVisible(false);
441 
442 	ASSERT(view->Window() != NULL);
443 		// how can I not have a Window here???
444 
445 	if (view->Window()) {
446 		// force immediate redraw so TextView appears instantly
447 		view->Window()->UpdateIfNeeded();
448 	}
449 }
450 
451 
452 void
453 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
454 	BPose* pose, int32 poseIndex)
455 {
456 	// find the text editing view
457 	BView* scrollView = view->FindView("BorderView");
458 	ASSERT(scrollView != NULL);
459 	if (scrollView == NULL)
460 		return;
461 
462 	BTextView* textView = dynamic_cast<BTextView*>(
463 		scrollView->FindView("WidgetTextView"));
464 	ASSERT(textView != NULL);
465 	if (textView == NULL)
466 		return;
467 
468 	BColumn* column = view->ColumnFor(fAttrHash);
469 	ASSERT(column != NULL);
470 	if (column == NULL)
471 		return;
472 
473 	if (saveChanges && fText->CommitEditedText(textView)) {
474 		// we have an actual change, re-sort
475 		view->CheckPoseSortOrder(pose, poseIndex);
476 	}
477 
478 	// make text widget visible again
479 	SetVisible(true);
480 	view->Invalidate(ColumnRect(poseLoc, column, view));
481 
482 	// force immediate redraw so TEView disappears
483 	scrollView->RemoveSelf();
484 	delete scrollView;
485 
486 	ASSERT(view->Window() != NULL);
487 	view->Window()->UpdateIfNeeded();
488 	view->MakeFocus();
489 
490 	SetActive(false);
491 }
492 
493 
494 void
495 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
496 	BPoseView* view, bool visible)
497 {
498 	BRect oldRect;
499 	if (view->ViewMode() != kListMode)
500 		oldRect = CalcOldRect(loc, column, view);
501 
502 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
503 		&& visible) {
504 		BRect invalRect(ColumnRect(loc, column, view));
505 		if (view->ViewMode() != kListMode)
506 			invalRect = invalRect | oldRect;
507 		view->Invalidate(invalRect);
508 	}
509 }
510 
511 
512 void
513 BTextWidget::SelectAll(BPoseView* view)
514 {
515 	BTextView* text = dynamic_cast<BTextView*>(
516 		view->FindView("WidgetTextView"));
517 	if (text != NULL)
518 		text->SelectAll();
519 }
520 
521 
522 void
523 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
524 	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
525 	bool direct)
526 {
527 	textRect.OffsetBy(offset);
528 
529 	if (direct) {
530 		// draw selection box if selected
531 		if (selected) {
532 			drawView->SetDrawingMode(B_OP_COPY);
533 //			eraseRect.OffsetBy(offset);
534 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
535 			drawView->FillRect(textRect, B_SOLID_LOW);
536 		} else
537 			drawView->SetDrawingMode(B_OP_OVER);
538 
539 		// set high color
540 		rgb_color highColor;
541 		if (view->IsDesktopWindow()) {
542 			if (selected)
543 				highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
544 			else
545 				highColor = view->DeskTextColor();
546 		} else if (selected && view->Window()->IsActive()) {
547 			highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
548 		} else
549 			highColor = kBlack;
550 
551 		if (clipboardMode == kMoveSelectionTo && !selected) {
552 			drawView->SetDrawingMode(B_OP_ALPHA);
553 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
554 			highColor.alpha = 64;
555 		}
556 		drawView->SetHighColor(highColor);
557 	}
558 
559 	BPoint loc;
560 	loc.y = textRect.bottom - view->FontInfo().descent;
561 	loc.x = textRect.left + 1;
562 
563 	const char* fittingText = fText->FittingText(view);
564 
565 	// TODO: Comparing view and drawView here to avoid rendering
566 	// the text outline when producing a drag bitmap. The check is
567 	// not fully correct, since an offscreen view is also used in some
568 	// other rare cases (something to do with columns). But for now, this
569 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
570 	if (!selected && view == drawView && view->WidgetTextOutline()) {
571 		// draw a halo around the text by using the "false bold"
572 		// feature for text rendering. Either black or white is used for
573 		// the glow (whatever acts as contrast) with a some alpha value,
574 		drawView->SetDrawingMode(B_OP_ALPHA);
575 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
576 
577 		BFont font;
578 		drawView->GetFont(&font);
579 
580 		rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
581 		if (view->IsDesktopWindow())
582 			textColor = view->DeskTextColor();
583 
584 		if (textColor.Brightness() < 100) {
585 			// dark text on light outline
586 			rgb_color glowColor = ui_color(B_SHINE_COLOR);
587 
588 			font.SetFalseBoldWidth(2.0);
589 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
590 			glowColor.alpha = 30;
591 			drawView->SetHighColor(glowColor);
592 
593 			drawView->DrawString(fittingText, loc);
594 
595 			font.SetFalseBoldWidth(1.0);
596 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
597 			glowColor.alpha = 65;
598 			drawView->SetHighColor(glowColor);
599 
600 			drawView->DrawString(fittingText, loc);
601 
602 			font.SetFalseBoldWidth(0.0);
603 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
604 		} else {
605 			// light text on dark outline
606 			rgb_color outlineColor = kBlack;
607 
608 			font.SetFalseBoldWidth(1.0);
609 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
610 			outlineColor.alpha = 30;
611 			drawView->SetHighColor(outlineColor);
612 
613 			drawView->DrawString(fittingText, loc);
614 
615 			font.SetFalseBoldWidth(0.0);
616 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
617 
618 			outlineColor.alpha = 200;
619 			drawView->SetHighColor(outlineColor);
620 
621 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
622 		}
623 
624 		drawView->SetDrawingMode(B_OP_OVER);
625 		drawView->SetHighColor(textColor);
626 	}
627 
628 	drawView->DrawString(fittingText, loc);
629 
630 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
631 		// TODO:
632 		// this should be exported to the WidgetAttribute class, probably
633 		// by having a per widget kind style
634 		if (direct) {
635 			rgb_color underlineColor = drawView->HighColor();
636 			underlineColor.alpha = 180;
637 			drawView->SetHighColor(underlineColor);
638 			drawView->SetDrawingMode(B_OP_ALPHA);
639 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
640 		}
641 
642 		textRect.right = textRect.left + fText->Width(view);
643 			// only underline text part
644 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
645 			B_MIXED_COLORS);
646 	}
647 }
648