xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 58481f0f6ef1a61ba07283f012cafbc2ed874ead)
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 	bool delayedEdit)
216 {
217 	// wait until a double click time to see if we are double clicking
218 	// or selecting widget for editing
219 	// start editing early if mouse left widget or modifier down
220 
221 	if (!IsEditable())
222 		return;
223 
224 	if (delayedEdit) {
225 		bigtime_t doubleClickTime;
226 		get_click_speed(&doubleClickTime);
227 		doubleClickTime += system_time();
228 
229 		while (system_time() < doubleClickTime) {
230 			// loop for double-click time and watch the mouse and keyboard
231 
232 			BPoint point;
233 			uint32 buttons;
234 			view->GetMouse(&point, &buttons, false);
235 			if (buttons)
236 				// if mouse button goes down then a double click, exit
237 				// without editing
238 				return;
239 
240 			if (!bounds.Contains(point))
241 				// mouse has moved outside of text widget so go into edit mode
242 				break;
243 
244 			if (modifiers() & (B_SHIFT_KEY | B_COMMAND_KEY | B_CONTROL_KEY | B_MENU_KEY))
245 				// watch the keyboard (ignoring standard locking keys)
246 				break;
247 
248 			snooze(100000);
249 		}
250 	}
251 
252 	StartEdit(bounds, view, pose);
253 }
254 
255 
256 static filter_result
257 TextViewFilter(BMessage *message, BHandler **, BMessageFilter *filter)
258 {
259 	uchar key;
260 	if (message->FindInt8("byte", (int8 *)&key) != B_OK)
261 		return B_DISPATCH_MESSAGE;
262 
263 	BPoseView *poseView = dynamic_cast<BContainerWindow*>(filter->Looper())->
264 		PoseView();
265 
266 	if (key == B_RETURN || key == B_ESCAPE) {
267 		poseView->CommitActivePose(key == B_RETURN);
268 		return B_SKIP_MESSAGE;
269 	}
270 
271 	if (key == B_TAB) {
272 		if (poseView->ActivePose()) {
273 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
274 				poseView->ActivePose()->EditPreviousWidget(poseView);
275 			else
276 				poseView->ActivePose()->EditNextWidget(poseView);
277 		}
278 
279 		return B_SKIP_MESSAGE;
280 	}
281 
282 	// the BTextView doesn't respect window borders when resizing itself;
283 	// we try to work-around this "bug" here.
284 
285 	// find the text editing view
286 	BView *scrollView = poseView->FindView("BorderView");
287 	if (scrollView != NULL) {
288 		BTextView *textView = dynamic_cast<BTextView *>(scrollView->FindView("WidgetTextView"));
289 		if (textView != NULL) {
290 			BRect rect = scrollView->Frame();
291 
292 			if (rect.right + 3 > poseView->Bounds().right
293 				|| rect.left - 3 < 0)
294 				textView->MakeResizable(true, NULL);
295 		}
296 	}
297 
298 	return B_DISPATCH_MESSAGE;
299 }
300 
301 
302 void
303 BTextWidget::StartEdit(BRect bounds, BPoseView *view, BPose *pose)
304 {
305 	if (!IsEditable())
306 		return;
307 
308 	// don't allow editing of the trash directory name
309 	BEntry entry(pose->TargetModel()->EntryRef());
310 	if (entry.InitCheck() == B_OK && FSIsTrashDir(&entry))
311 		return;
312 
313 	// don't allow editing of the "Disks" icon name
314 	if (pose->TargetModel()->IsRoot())
315 		return;
316 
317 	if (!ConfirmChangeIfWellKnownDirectory(&entry, "rename"))
318 		return;
319 
320 	// get bounds with full text length
321 	BRect rect(bounds);
322 	BRect textRect(bounds);
323 	rect.OffsetBy(-2, -1);
324 	rect.right += 1;
325 
326 	BFont font;
327 	view->GetFont(&font);
328 	BTextView *textView = new BTextView(rect, "WidgetTextView", textRect, &font, 0,
329 		B_FOLLOW_ALL, B_WILL_DRAW);
330 
331 	textView->SetWordWrap(false);
332 	DisallowMetaKeys(textView);
333 	fText->SetUpEditing(textView);
334 
335 	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewFilter));
336 
337 	rect.right = rect.left + textView->LineWidth() + 3;
338 	// center new width, if necessary
339 	if (view->ViewMode() == kIconMode
340 		|| view->ViewMode() == kListMode && fAlignment == B_ALIGN_CENTER) {
341 		rect.OffsetBy(bounds.Width() / 2 - rect.Width() / 2, 0);
342 	}
343 
344 	rect.bottom = rect.top + textView->LineHeight() + 1;
345 	textRect = rect.OffsetToCopy(2, 1);
346 	textRect.right -= 3;
347 	textRect.bottom--;
348 	textView->SetTextRect(textRect);
349 
350 	textRect = view->Bounds();
351 	bool hitBorder = false;
352 	if (rect.left < 1)
353 		rect.left = 1, hitBorder = true;
354 	if (rect.right > textRect.right)
355 		rect.right = textRect.right - 2, hitBorder = true;
356 
357 	textView->MoveTo(rect.LeftTop());
358 	textView->ResizeTo(rect.Width(), rect.Height());
359 
360 	BScrollView *scrollView = new BScrollView("BorderView", textView, 0, 0, false,
361 		false, B_PLAIN_BORDER);
362 	view->AddChild(scrollView);
363 
364 	// configure text view
365 	switch (view->ViewMode()) {
366 		case kIconMode:
367 			textView->SetAlignment(B_ALIGN_CENTER);
368 			break;
369 
370 		case kMiniIconMode:
371 			textView->SetAlignment(B_ALIGN_LEFT);
372 			break;
373 
374 		case kListMode:
375 			textView->SetAlignment(fAlignment);
376 			break;
377 	}
378 	textView->MakeResizable(true, hitBorder ? NULL : scrollView);
379 
380 	view->SetActivePose(pose);		// tell view about pose
381 	SetActive(true);				// for widget
382 
383 	textView->SelectAll();
384 	textView->MakeFocus();
385 
386 	// make this text widget invisible while we edit it
387 	SetVisible(false);
388 
389 	ASSERT(view->Window());	// how can I not have a Window here???
390 
391 	if (view->Window())
392 		// force immediate redraw so TextView appears instantly
393 		view->Window()->UpdateIfNeeded();
394 }
395 
396 
397 void
398 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView *view,
399 	BPose *pose, int32 poseIndex)
400 {
401 	// find the text editing view
402 	BView *scrollView = view->FindView("BorderView");
403 	ASSERT(scrollView);
404 	if (!scrollView)
405 		return;
406 
407 	BTextView *textView = dynamic_cast<BTextView *>(scrollView->FindView("WidgetTextView"));
408 	ASSERT(textView);
409 	if (!textView)
410 		return;
411 
412 	BColumn *column = view->ColumnFor(fAttrHash);
413 	ASSERT(column);
414 	if (!column)
415 		return;
416 
417 	if (saveChanges && fText->CommitEditedText(textView)) {
418 		// we have an actual change, re-sort
419 		view->CheckPoseSortOrder(pose, poseIndex);
420 	}
421 
422 	// make text widget visible again
423 	SetVisible(true);
424 	view->Invalidate(ColumnRect(poseLoc, column, view));
425 
426 	// force immediate redraw so TEView disappears
427 	scrollView->RemoveSelf();
428 	delete scrollView;
429 
430 	ASSERT(view->Window());
431 	view->Window()->UpdateIfNeeded();
432 	view->MakeFocus();
433 
434 	SetActive(false);
435 }
436 
437 
438 void
439 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn *column, BPoseView *view)
440 {
441 	BRect oldRect;
442 	if (view->ViewMode() != kListMode)
443 		oldRect = CalcOldRect(loc, column, view);
444 
445 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)) {
446 		BRect invalRect(ColumnRect(loc, column, view));
447 		if (view->ViewMode() != kListMode)
448 			invalRect = invalRect | oldRect;
449 		view->Invalidate(invalRect);
450 	}
451 }
452 
453 
454 void
455 BTextWidget::SelectAll(BPoseView *view)
456 {
457 	BTextView *text = dynamic_cast<BTextView *>(view->FindView("WidgetTextView"));
458 	if (text)
459 		text->SelectAll();
460 }
461 
462 
463 void
464 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView *view,
465 	BView *drawView, bool selected, uint32 clipboardMode, BPoint offset, bool direct)
466 {
467 	textRect.OffsetBy(offset);
468 
469 	if (direct) {
470 #ifdef __HAIKU__
471 		// draw selection box if selected
472 		if (selected) {
473 #else
474 		// erase area we're going to draw in
475 		// NOTE: WidgetTextOutline() is reused for
476 		// erasing background on R5 here
477 		if (view->WidgetTextOutline() || selected) {
478 #endif
479 			drawView->SetDrawingMode(B_OP_COPY);
480 			eraseRect.OffsetBy(offset);
481 //			drawView->FillRect(eraseRect, B_SOLID_LOW);
482 			drawView->FillRect(textRect, B_SOLID_LOW);
483 		} else
484 			drawView->SetDrawingMode(B_OP_OVER);
485 
486 		// set high color
487 		rgb_color highColor;
488 		if (view->IsDesktopWindow()) {
489 			if (selected)
490 				highColor = kWhite;
491 			else
492 				highColor = view->DeskTextColor();
493 		} else if (selected && view->Window()->IsActive()) {
494 			highColor = kWhite;
495 		} else
496 			highColor = kBlack;
497 
498 		if (clipboardMode == kMoveSelectionTo && !selected) {
499 			drawView->SetDrawingMode(B_OP_ALPHA);
500 			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
501 			highColor.alpha = 64;
502 		}
503 		drawView->SetHighColor(highColor);
504 	}
505 
506 	BPoint loc;
507 	loc.y = textRect.bottom - view->FontInfo().descent;
508 	loc.x = textRect.left + 1;
509 
510 	const char* fittingText = fText->FittingText(view);
511 
512 #ifdef __HAIKU__
513 	// TODO: Comparing view and drawView here to avoid rendering
514 	// the text outline when producing a drag bitmap. The check is
515 	// not fully correct, since an offscreen view is also used in some
516 	// other rare cases (something to do with columns). But for now, this
517 	// fixes the broken drag bitmaps when dragging icons from the Desktop.
518 	if (!selected && view == drawView && view->WidgetTextOutline()) {
519 		// draw a halo around the text by using the "false bold"
520 		// feature for text rendering. Either black or white is used for
521 		// the glow (whatever acts as contrast) with a some alpha value,
522 		drawView->SetDrawingMode(B_OP_ALPHA);
523 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
524 
525 		BFont font;
526 		drawView->GetFont(&font);
527 
528 		rgb_color textColor = drawView->HighColor();
529 		if (textColor.red + textColor.green + textColor.blue < 128 * 3) {
530 			// dark text on light outline
531 			rgb_color glowColor = kWhite;
532 
533 			font.SetFalseBoldWidth(2.0);
534 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
535 			glowColor.alpha = 30;
536 			drawView->SetHighColor(glowColor);
537 
538 			drawView->DrawString(fittingText, loc);
539 
540 			font.SetFalseBoldWidth(1.0);
541 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
542 			glowColor.alpha = 65;
543 			drawView->SetHighColor(glowColor);
544 
545 			drawView->DrawString(fittingText, loc);
546 
547 			font.SetFalseBoldWidth(0.0);
548 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
549 		} else {
550 			// light text on dark outline
551 			rgb_color outlineColor = kBlack;
552 
553 			font.SetFalseBoldWidth(1.0);
554 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
555 			outlineColor.alpha = 30;
556 			drawView->SetHighColor(outlineColor);
557 
558 			drawView->DrawString(fittingText, loc);
559 
560 			font.SetFalseBoldWidth(0.0);
561 			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
562 
563 			outlineColor.alpha = 200;
564 			drawView->SetHighColor(outlineColor);
565 
566 			drawView->DrawString(fittingText, loc + BPoint(1, 1));
567 		}
568 
569 		drawView->SetDrawingMode(B_OP_OVER);
570 		drawView->SetHighColor(textColor);
571 	}
572 #endif // __HAIKU__
573 
574 	drawView->DrawString(fittingText, loc);
575 
576 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
577 		// ToDo:
578 		// this should be exported to the WidgetAttribute class, probably
579 		// by having a per widget kind style
580 		if (direct) {
581 			rgb_color underlineColor = drawView->HighColor();
582 			underlineColor.alpha = 180;
583 			drawView->SetHighColor(underlineColor);
584 			drawView->SetDrawingMode(B_OP_ALPHA);
585 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
586 		}
587 
588 		textRect.right = textRect.left + fText->Width(view);
589 			// only underline text part
590 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
591 			B_MIXED_COLORS);
592 	}
593 }
594