xref: /haiku/src/kits/tracker/TextWidget.cpp (revision d06cbe081b7ea043aea2012359744091de6d604d)
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 	BPoseView* poseView = dynamic_cast<BContainerWindow*>(
301 		filter->Looper())->PoseView();
302 
303 	if (key == B_RETURN || key == B_ESCAPE) {
304 		poseView->CommitActivePose(key == B_RETURN);
305 		return B_SKIP_MESSAGE;
306 	}
307 
308 	if (key == B_TAB) {
309 		if (poseView->ActivePose()) {
310 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
311 				poseView->ActivePose()->EditPreviousWidget(poseView);
312 			else
313 				poseView->ActivePose()->EditNextWidget(poseView);
314 		}
315 
316 		return B_SKIP_MESSAGE;
317 	}
318 
319 	// the BTextView doesn't respect window borders when resizing itself;
320 	// we try to work-around this "bug" here.
321 
322 	// find the text editing view
323 	BView* scrollView = poseView->FindView("BorderView");
324 	if (scrollView != NULL) {
325 		BTextView* textView = dynamic_cast<BTextView*>(
326 			scrollView->FindView("WidgetTextView"));
327 		if (textView != NULL) {
328 			BRect rect = scrollView->Frame();
329 
330 			if (rect.right + 3 > poseView->Bounds().right
331 				|| rect.left - 3 < 0)
332 				textView->MakeResizable(true, NULL);
333 		}
334 	}
335 
336 	return B_DISPATCH_MESSAGE;
337 }
338 
339 
340 void
341 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose)
342 {
343 	view->SetTextWidgetToCheck(NULL, this);
344 	if (!IsEditable() || IsActive())
345 		return;
346 
347 	BEntry entry(pose->TargetModel()->EntryRef());
348 	if (entry.InitCheck() == B_OK
349 		&& !ConfirmChangeIfWellKnownDirectory(&entry,
350 			B_TRANSLATE_COMMENT("rename",
351 				"As in 'if you rename this folder...' (en) "
352 				"'Wird dieser Ordner umbenannt...' (de)"),
353 			B_TRANSLATE_COMMENT("rename",
354 				"As in 'to rename this folder...' (en) "
355 				"'Um diesen Ordner umzubenennen...' (de)"),
356 			B_TRANSLATE_COMMENT("Rename",
357 				"Button label, 'Rename' (en), 'Umbenennen' (de)")))
358 		return;
359 
360 	// get bounds with full text length
361 	BRect rect(bounds);
362 	BRect textRect(bounds);
363 	rect.OffsetBy(-2, -1);
364 	rect.right += 1;
365 
366 	BFont font;
367 	view->GetFont(&font);
368 	BTextView* textView = new BTextView(rect, "WidgetTextView", textRect,
369 		&font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
370 
371 	textView->SetWordWrap(false);
372 	DisallowMetaKeys(textView);
373 	fText->SetUpEditing(textView);
374 
375 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewFilter));
376 
377 	rect.right = rect.left + textView->LineWidth() + 3;
378 	// center new width, if necessary
379 	if (view->ViewMode() == kIconMode
380 		|| (view->ViewMode() == kListMode && fAlignment == B_ALIGN_CENTER)) {
381 		rect.OffsetBy(bounds.Width() / 2 - rect.Width() / 2, 0);
382 	}
383 
384 	rect.bottom = rect.top + textView->LineHeight() + 1;
385 	textRect = rect.OffsetToCopy(2, 1);
386 	textRect.right -= 3;
387 	textRect.bottom--;
388 	textView->SetTextRect(textRect);
389 
390 	BPoint origin = view->LeftTop();
391 	textRect = view->Bounds();
392 
393 	bool hitBorder = false;
394 	if (rect.left <= origin.x)
395 		rect.left = origin.x + 1, hitBorder = true;
396 	if (rect.right >= textRect.right)
397 		rect.right = textRect.right - 1, hitBorder = true;
398 
399 	textView->MoveTo(rect.LeftTop());
400 	textView->ResizeTo(rect.Width(), rect.Height());
401 
402 	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
403 		false, false, B_PLAIN_BORDER);
404 	view->AddChild(scrollView);
405 
406 	// configure text view
407 	switch (view->ViewMode()) {
408 		case kIconMode:
409 			textView->SetAlignment(B_ALIGN_CENTER);
410 			break;
411 
412 		case kMiniIconMode:
413 			textView->SetAlignment(B_ALIGN_LEFT);
414 			break;
415 
416 		case kListMode:
417 			textView->SetAlignment(fAlignment);
418 			break;
419 	}
420 	textView->MakeResizable(true, hitBorder ? NULL : scrollView);
421 
422 	view->SetActivePose(pose);
423 		// tell view about pose
424 	SetActive(true);
425 		// for widget
426 
427 	textView->SelectAll();
428 	textView->MakeFocus();
429 
430 	// make this text widget invisible while we edit it
431 	SetVisible(false);
432 
433 	ASSERT(view->Window());
434 		// how can I not have a Window here???
435 
436 	if (view->Window()) {
437 		// force immediate redraw so TextView appears instantly
438 		view->Window()->UpdateIfNeeded();
439 	}
440 }
441 
442 
443 void
444 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
445 	BPose* pose, int32 poseIndex)
446 {
447 	// find the text editing view
448 	BView* scrollView = view->FindView("BorderView");
449 	ASSERT(scrollView);
450 	if (!scrollView)
451 		return;
452 
453 	BTextView* textView = dynamic_cast<BTextView*>(
454 		scrollView->FindView("WidgetTextView"));
455 	ASSERT(textView);
456 	if (!textView)
457 		return;
458 
459 	BColumn* column = view->ColumnFor(fAttrHash);
460 	ASSERT(column);
461 	if (!column)
462 		return;
463 
464 	if (saveChanges && fText->CommitEditedText(textView)) {
465 		// we have an actual change, re-sort
466 		view->CheckPoseSortOrder(pose, poseIndex);
467 	}
468 
469 	// make text widget visible again
470 	SetVisible(true);
471 	view->Invalidate(ColumnRect(poseLoc, column, view));
472 
473 	// force immediate redraw so TEView disappears
474 	scrollView->RemoveSelf();
475 	delete scrollView;
476 
477 	ASSERT(view->Window());
478 	view->Window()->UpdateIfNeeded();
479 	view->MakeFocus();
480 
481 	SetActive(false);
482 }
483 
484 
485 void
486 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
487 	BPoseView* view, bool visible)
488 {
489 	BRect oldRect;
490 	if (view->ViewMode() != kListMode)
491 		oldRect = CalcOldRect(loc, column, view);
492 
493 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
494 		&& visible) {
495 		BRect invalRect(ColumnRect(loc, column, view));
496 		if (view->ViewMode() != kListMode)
497 			invalRect = invalRect | oldRect;
498 		view->Invalidate(invalRect);
499 	}
500 }
501 
502 
503 void
504 BTextWidget::SelectAll(BPoseView* view)
505 {
506 	BTextView* text = dynamic_cast<BTextView*>(
507 		view->FindView("WidgetTextView"));
508 	if (text)
509 		text->SelectAll();
510 }
511 
512 
513 void
514 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
515 	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
516 	bool direct)
517 {
518 	textRect.OffsetBy(offset);
519 
520 	if (direct) {
521 		// draw selection box if selected
522 		if (selected) {
523 			drawView->SetDrawingMode(B_OP_COPY);
524 //			eraseRect.OffsetBy(offset);
525 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
526 			drawView->FillRect(textRect, B_SOLID_LOW);
527 		} else
528 			drawView->SetDrawingMode(B_OP_OVER);
529 
530 		// set high color
531 		rgb_color highColor;
532 		if (view->IsDesktopWindow()) {
533 			if (selected)
534 				highColor = kWhite;
535 			else
536 				highColor = view->DeskTextColor();
537 		} else if (selected && view->Window()->IsActive()) {
538 			highColor = kWhite;
539 		} else
540 			highColor = kBlack;
541 
542 		if (clipboardMode == kMoveSelectionTo && !selected) {
543 			drawView->SetDrawingMode(B_OP_ALPHA);
544 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
545 			highColor.alpha = 64;
546 		}
547 		drawView->SetHighColor(highColor);
548 	}
549 
550 	BPoint loc;
551 	loc.y = textRect.bottom - view->FontInfo().descent;
552 	loc.x = textRect.left + 1;
553 
554 	const char* fittingText = fText->FittingText(view);
555 
556 	// TODO: Comparing view and drawView here to avoid rendering
557 	// the text outline when producing a drag bitmap. The check is
558 	// not fully correct, since an offscreen view is also used in some
559 	// other rare cases (something to do with columns). But for now, this
560 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
561 	if (!selected && view == drawView && view->WidgetTextOutline()) {
562 		// draw a halo around the text by using the "false bold"
563 		// feature for text rendering. Either black or white is used for
564 		// the glow (whatever acts as contrast) with a some alpha value,
565 		drawView->SetDrawingMode(B_OP_ALPHA);
566 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
567 
568 		BFont font;
569 		drawView->GetFont(&font);
570 
571 		rgb_color textColor = drawView->HighColor();
572 		if (textColor.red + textColor.green + textColor.blue < 128 * 3) {
573 			// dark text on light outline
574 			rgb_color glowColor = kWhite;
575 
576 			font.SetFalseBoldWidth(2.0);
577 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
578 			glowColor.alpha = 30;
579 			drawView->SetHighColor(glowColor);
580 
581 			drawView->DrawString(fittingText, loc);
582 
583 			font.SetFalseBoldWidth(1.0);
584 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
585 			glowColor.alpha = 65;
586 			drawView->SetHighColor(glowColor);
587 
588 			drawView->DrawString(fittingText, loc);
589 
590 			font.SetFalseBoldWidth(0.0);
591 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
592 		} else {
593 			// light text on dark outline
594 			rgb_color outlineColor = kBlack;
595 
596 			font.SetFalseBoldWidth(1.0);
597 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
598 			outlineColor.alpha = 30;
599 			drawView->SetHighColor(outlineColor);
600 
601 			drawView->DrawString(fittingText, loc);
602 
603 			font.SetFalseBoldWidth(0.0);
604 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
605 
606 			outlineColor.alpha = 200;
607 			drawView->SetHighColor(outlineColor);
608 
609 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
610 		}
611 
612 		drawView->SetDrawingMode(B_OP_OVER);
613 		drawView->SetHighColor(textColor);
614 	}
615 
616 	drawView->DrawString(fittingText, loc);
617 
618 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
619 		// TODO:
620 		// this should be exported to the WidgetAttribute class, probably
621 		// by having a per widget kind style
622 		if (direct) {
623 			rgb_color underlineColor = drawView->HighColor();
624 			underlineColor.alpha = 180;
625 			drawView->SetHighColor(underlineColor);
626 			drawView->SetDrawingMode(B_OP_ALPHA);
627 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
628 		}
629 
630 		textRect.right = textRect.left + fText->Width(view);
631 			// only underline text part
632 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
633 			B_MIXED_COLORS);
634 	}
635 }
636