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