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