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