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