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