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