xref: /haiku/src/kits/tracker/TextWidget.cpp (revision b3fe70844e087a579563b43cf4c1b2525946ca27)
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 			StartEdit(fParams.bounds, fParams.poseView, fParams.pose);
238 		}
239 	} else {
240 		fLastClickedTime = 0;
241 		fParams.poseView->SetTextWidgetToCheck(NULL);
242 	}
243 }
244 
245 
246 void
247 BTextWidget::CancelWait()
248 {
249 	fLastClickedTime = 0;
250 	fParams.poseView->SetTextWidgetToCheck(NULL);
251 }
252 
253 
254 void
255 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint)
256 {
257 	// Register the time of that click.  The PoseView, through its Pulse()
258 	// will allow us to StartEdit() if no other click have been registered since
259 	// then.
260 
261 	// TODO: re-enable modifiers, one should be enough
262 	view->SetTextWidgetToCheck(NULL);
263 	if (IsEditable() && pose->IsSelected()) {
264 		bigtime_t doubleClickSpeed;
265 		get_click_speed(&doubleClickSpeed);
266 
267 		if (fLastClickedTime == 0) {
268 			fLastClickedTime = system_time();
269 			if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime())
270 				fLastClickedTime = 0;
271 		} else
272 			fLastClickedTime = 0;
273 
274 		if (fLastClickedTime == 0)
275 			return;
276 
277 		view->SetTextWidgetToCheck(this);
278 
279 		fParams.pose = pose;
280 		fParams.bounds = bounds;
281 		fParams.poseView = view;
282 	} else
283 		fLastClickedTime = 0;
284 }
285 
286 
287 static filter_result
288 TextViewFilter(BMessage* message, BHandler**, BMessageFilter* filter)
289 {
290 	uchar key;
291 	if (message->FindInt8("byte", (int8*)&key) != B_OK)
292 		return B_DISPATCH_MESSAGE;
293 
294 	BPoseView* poseView = dynamic_cast<BContainerWindow*>(
295 		filter->Looper())->PoseView();
296 
297 	if (key == B_RETURN || key == B_ESCAPE) {
298 		poseView->CommitActivePose(key == B_RETURN);
299 		return B_SKIP_MESSAGE;
300 	}
301 
302 	if (key == B_TAB) {
303 		if (poseView->ActivePose()) {
304 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
305 				poseView->ActivePose()->EditPreviousWidget(poseView);
306 			else
307 				poseView->ActivePose()->EditNextWidget(poseView);
308 		}
309 
310 		return B_SKIP_MESSAGE;
311 	}
312 
313 	// the BTextView doesn't respect window borders when resizing itself;
314 	// we try to work-around this "bug" here.
315 
316 	// find the text editing view
317 	BView* scrollView = poseView->FindView("BorderView");
318 	if (scrollView != NULL) {
319 		BTextView* textView = dynamic_cast<BTextView*>(
320 			scrollView->FindView("WidgetTextView"));
321 		if (textView != NULL) {
322 			BRect rect = scrollView->Frame();
323 
324 			if (rect.right + 3 > poseView->Bounds().right
325 				|| rect.left - 3 < 0)
326 				textView->MakeResizable(true, NULL);
327 		}
328 	}
329 
330 	return B_DISPATCH_MESSAGE;
331 }
332 
333 
334 void
335 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose)
336 {
337 	view->SetTextWidgetToCheck(NULL, this);
338 	if (!IsEditable() || IsActive())
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 	BPoint origin = view->LeftTop();
385 	textRect = view->Bounds();
386 
387 	bool hitBorder = false;
388 	if (rect.left <= origin.x)
389 		rect.left = origin.x + 1, hitBorder = true;
390 	if (rect.right >= textRect.right)
391 		rect.right = textRect.right - 1, hitBorder = true;
392 
393 	textView->MoveTo(rect.LeftTop());
394 	textView->ResizeTo(rect.Width(), rect.Height());
395 
396 	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
397 		false, false, B_PLAIN_BORDER);
398 	view->AddChild(scrollView);
399 
400 	// configure text view
401 	switch (view->ViewMode()) {
402 		case kIconMode:
403 			textView->SetAlignment(B_ALIGN_CENTER);
404 			break;
405 
406 		case kMiniIconMode:
407 			textView->SetAlignment(B_ALIGN_LEFT);
408 			break;
409 
410 		case kListMode:
411 			textView->SetAlignment(fAlignment);
412 			break;
413 	}
414 	textView->MakeResizable(true, hitBorder ? NULL : scrollView);
415 
416 	view->SetActivePose(pose);		// tell view about pose
417 	SetActive(true);				// for widget
418 
419 	textView->SelectAll();
420 	textView->MakeFocus();
421 
422 	// make this text widget invisible while we edit it
423 	SetVisible(false);
424 
425 	ASSERT(view->Window());	// how can I not have a Window here???
426 
427 	if (view->Window()) {
428 		// force immediate redraw so TextView appears instantly
429 		view->Window()->UpdateIfNeeded();
430 	}
431 }
432 
433 
434 void
435 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
436 	BPose* pose, int32 poseIndex)
437 {
438 	// find the text editing view
439 	BView* scrollView = view->FindView("BorderView");
440 	ASSERT(scrollView);
441 	if (!scrollView)
442 		return;
443 
444 	BTextView* textView = dynamic_cast<BTextView*>(
445 		scrollView->FindView("WidgetTextView"));
446 	ASSERT(textView);
447 	if (!textView)
448 		return;
449 
450 	BColumn* column = view->ColumnFor(fAttrHash);
451 	ASSERT(column);
452 	if (!column)
453 		return;
454 
455 	if (saveChanges && fText->CommitEditedText(textView)) {
456 		// we have an actual change, re-sort
457 		view->CheckPoseSortOrder(pose, poseIndex);
458 	}
459 
460 	// make text widget visible again
461 	SetVisible(true);
462 	view->Invalidate(ColumnRect(poseLoc, column, view));
463 
464 	// force immediate redraw so TEView disappears
465 	scrollView->RemoveSelf();
466 	delete scrollView;
467 
468 	ASSERT(view->Window());
469 	view->Window()->UpdateIfNeeded();
470 	view->MakeFocus();
471 
472 	SetActive(false);
473 }
474 
475 
476 void
477 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
478 	BPoseView* view, bool visible)
479 {
480 	BRect oldRect;
481 	if (view->ViewMode() != kListMode)
482 		oldRect = CalcOldRect(loc, column, view);
483 
484 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
485 		&& visible) {
486 		BRect invalRect(ColumnRect(loc, column, view));
487 		if (view->ViewMode() != kListMode)
488 			invalRect = invalRect | oldRect;
489 		view->Invalidate(invalRect);
490 	}
491 }
492 
493 
494 void
495 BTextWidget::SelectAll(BPoseView* view)
496 {
497 	BTextView* text = dynamic_cast<BTextView*>(
498 		view->FindView("WidgetTextView"));
499 	if (text)
500 		text->SelectAll();
501 }
502 
503 
504 void
505 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
506 	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
507 	bool direct)
508 {
509 	textRect.OffsetBy(offset);
510 
511 	if (direct) {
512 		// draw selection box if selected
513 		if (selected) {
514 			drawView->SetDrawingMode(B_OP_COPY);
515 //			eraseRect.OffsetBy(offset);
516 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
517 			drawView->FillRect(textRect, B_SOLID_LOW);
518 		} else
519 			drawView->SetDrawingMode(B_OP_OVER);
520 
521 		// set high color
522 		rgb_color highColor;
523 		if (view->IsDesktopWindow()) {
524 			if (selected)
525 				highColor = kWhite;
526 			else
527 				highColor = view->DeskTextColor();
528 		} else if (selected && view->Window()->IsActive()) {
529 			highColor = kWhite;
530 		} else
531 			highColor = kBlack;
532 
533 		if (clipboardMode == kMoveSelectionTo && !selected) {
534 			drawView->SetDrawingMode(B_OP_ALPHA);
535 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
536 			highColor.alpha = 64;
537 		}
538 		drawView->SetHighColor(highColor);
539 	}
540 
541 	BPoint loc;
542 	loc.y = textRect.bottom - view->FontInfo().descent;
543 	loc.x = textRect.left + 1;
544 
545 	const char* fittingText = fText->FittingText(view);
546 
547 	// TODO: Comparing view and drawView here to avoid rendering
548 	// the text outline when producing a drag bitmap. The check is
549 	// not fully correct, since an offscreen view is also used in some
550 	// other rare cases (something to do with columns). But for now, this
551 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
552 	if (!selected && view == drawView && view->WidgetTextOutline()) {
553 		// draw a halo around the text by using the "false bold"
554 		// feature for text rendering. Either black or white is used for
555 		// the glow (whatever acts as contrast) with a some alpha value,
556 		drawView->SetDrawingMode(B_OP_ALPHA);
557 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
558 
559 		BFont font;
560 		drawView->GetFont(&font);
561 
562 		rgb_color textColor = drawView->HighColor();
563 		if (textColor.red + textColor.green + textColor.blue < 128 * 3) {
564 			// dark text on light outline
565 			rgb_color glowColor = kWhite;
566 
567 			font.SetFalseBoldWidth(2.0);
568 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
569 			glowColor.alpha = 30;
570 			drawView->SetHighColor(glowColor);
571 
572 			drawView->DrawString(fittingText, loc);
573 
574 			font.SetFalseBoldWidth(1.0);
575 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
576 			glowColor.alpha = 65;
577 			drawView->SetHighColor(glowColor);
578 
579 			drawView->DrawString(fittingText, loc);
580 
581 			font.SetFalseBoldWidth(0.0);
582 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
583 		} else {
584 			// light text on dark outline
585 			rgb_color outlineColor = kBlack;
586 
587 			font.SetFalseBoldWidth(1.0);
588 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
589 			outlineColor.alpha = 30;
590 			drawView->SetHighColor(outlineColor);
591 
592 			drawView->DrawString(fittingText, loc);
593 
594 			font.SetFalseBoldWidth(0.0);
595 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
596 
597 			outlineColor.alpha = 200;
598 			drawView->SetHighColor(outlineColor);
599 
600 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
601 		}
602 
603 		drawView->SetDrawingMode(B_OP_OVER);
604 		drawView->SetHighColor(textColor);
605 	}
606 
607 	drawView->DrawString(fittingText, loc);
608 
609 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
610 		// TODO:
611 		// this should be exported to the WidgetAttribute class, probably
612 		// by having a per widget kind style
613 		if (direct) {
614 			rgb_color underlineColor = drawView->HighColor();
615 			underlineColor.alpha = 180;
616 			drawView->SetHighColor(underlineColor);
617 			drawView->SetDrawingMode(B_OP_ALPHA);
618 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
619 		}
620 
621 		textRect.right = textRect.left + fText->Width(view);
622 			// only underline text part
623 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
624 			B_MIXED_COLORS);
625 	}
626 }
627