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