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