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