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