xref: /haiku/src/kits/tracker/TextWidget.cpp (revision 1214ef1b2100f2b3299fc9d8d6142e46f70a4c3f)
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 void
87 BTextWidget::RecalculateText(const BPoseView *view)
88 {
89 	fText->SetDirty(true);
90 	fText->CheckViewChanged(view);
91 }
92 
93 
94 const char *
95 BTextWidget::Text() const
96 {
97 	StringAttributeText *textAttribute = dynamic_cast<StringAttributeText *>(fText);
98 
99 	ASSERT(textAttribute);
100 	if (!textAttribute)
101 		return "";
102 
103 	return textAttribute->Value();
104 }
105 
106 
107 float
108 BTextWidget::TextWidth(const BPoseView *pose) const
109 {
110 	return fText->Width(pose);
111 }
112 
113 
114 float
115 BTextWidget::PreferredWidth(const BPoseView *pose) const
116 {
117 	return fText->PreferredWidth(pose) + 1;
118 }
119 
120 
121 BRect
122 BTextWidget::ColumnRect(BPoint poseLoc, const BColumn *column,
123 	const BPoseView *view)
124 {
125 	if (view->ViewMode() != kListMode) {
126 		// ColumnRect only makes sense in list view, return
127 		// CalcRect otherwise
128 		return CalcRect(poseLoc, column, view);
129 	}
130 	BRect result;
131 	result.left = column->Offset() + poseLoc.x;
132 	result.right = result.left + column->Width();
133 	result.bottom = poseLoc.y + view->ListElemHeight() - 1;
134 	result.top = result.bottom - view->FontHeight();
135 	return result;
136 }
137 
138 
139 BRect
140 BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn *column,
141 	const BPoseView *view, float textWidth)
142 {
143 	BRect result;
144 	if (view->ViewMode() == kListMode) {
145 		poseLoc.x += column->Offset();
146 
147 		switch (fAlignment) {
148 			case B_ALIGN_LEFT:
149 				result.left = poseLoc.x;
150 				result.right = result.left + textWidth + 1;
151 				break;
152 
153 			case B_ALIGN_CENTER:
154 				result.left = poseLoc.x + (column->Width() / 2) - (textWidth / 2);
155 				if (result.left < 0)
156 					result.left = 0;
157 				result.right = result.left + textWidth + 1;
158 				break;
159 
160 			case B_ALIGN_RIGHT:
161 				result.right = poseLoc.x + column->Width();
162 				result.left = result.right - textWidth - 1;
163 				if (result.left < 0)
164 					result.left = 0;
165 				break;
166 			default:
167 				TRESPASS();
168 		}
169 
170 		result.bottom = poseLoc.y + (view->ListElemHeight() - 1);
171 	} else {
172 		if (view->ViewMode() == kIconMode
173 			|| view->ViewMode() == kScaleIconMode) {
174 			// large/scaled icon mode
175 			result.left = poseLoc.x + (view->IconSizeInt() - textWidth) / 2;
176 		} else {
177 			// mini icon mode
178 			result.left = poseLoc.x + B_MINI_ICON + kMiniIconSeparator;
179 		}
180 
181 		result.right = result.left + textWidth;
182 		result.bottom = poseLoc.y + view->IconPoseHeight();
183 
184 	}
185 	result.top = result.bottom - view->FontHeight();
186 
187 	return result;
188 }
189 
190 
191 BRect
192 BTextWidget::CalcRect(BPoint poseLoc, const BColumn *column,
193 	const BPoseView *view)
194 {
195 	return CalcRectCommon(poseLoc, column, view, fText->Width(view));
196 }
197 
198 
199 BRect
200 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn *column,
201 	const BPoseView *view)
202 {
203 	return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth());
204 }
205 
206 
207 BRect
208 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn *column,
209 	const BPoseView* view)
210 {
211 	BRect result = CalcRect(poseLoc, column, view);
212 	if (result.Width() < kWidthMargin) {
213 		// if resulting rect too narrow, make it a bit wider
214 		// for comfortable clicking
215 		if (column && column->Width() < kWidthMargin)
216 			result.right = result.left + column->Width();
217 		else
218 			result.right = result.left + kWidthMargin;
219 	}
220 	return result;
221 }
222 
223 
224 void
225 BTextWidget::MouseUp(BRect bounds, BPoseView *view, BPose *pose, BPoint,
226 	bool delayedEdit)
227 {
228 	// wait until a double click time to see if we are double clicking
229 	// or selecting widget for editing
230 	// start editing early if mouse left widget or modifier down
231 
232 	if (!IsEditable())
233 		return;
234 
235 	if (delayedEdit) {
236 		bigtime_t doubleClickTime;
237 		get_click_speed(&doubleClickTime);
238 		doubleClickTime += system_time();
239 
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 			if (buttons)
247 				// if mouse button goes down then a double click, exit
248 				// without editing
249 				return;
250 
251 			if (!bounds.Contains(point))
252 				// mouse has moved outside of text widget so go into edit mode
253 				break;
254 
255 			if (modifiers() & (B_SHIFT_KEY | B_COMMAND_KEY | B_CONTROL_KEY | B_MENU_KEY))
256 				// watch the keyboard (ignoring standard locking keys)
257 				break;
258 
259 			snooze(100000);
260 		}
261 	}
262 
263 	StartEdit(bounds, view, pose);
264 }
265 
266 
267 static filter_result
268 TextViewFilter(BMessage *message, BHandler **, BMessageFilter *filter)
269 {
270 	uchar key;
271 	if (message->FindInt8("byte", (int8 *)&key) != B_OK)
272 		return B_DISPATCH_MESSAGE;
273 
274 	BPoseView *poseView = dynamic_cast<BContainerWindow*>(filter->Looper())->
275 		PoseView();
276 
277 	if (key == B_RETURN || key == B_ESCAPE) {
278 		poseView->CommitActivePose(key == B_RETURN);
279 		return B_SKIP_MESSAGE;
280 	}
281 
282 	if (key == B_TAB) {
283 		if (poseView->ActivePose()) {
284 			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
285 				poseView->ActivePose()->EditPreviousWidget(poseView);
286 			else
287 				poseView->ActivePose()->EditNextWidget(poseView);
288 		}
289 
290 		return B_SKIP_MESSAGE;
291 	}
292 
293 	// the BTextView doesn't respect window borders when resizing itself;
294 	// we try to work-around this "bug" here.
295 
296 	// find the text editing view
297 	BView *scrollView = poseView->FindView("BorderView");
298 	if (scrollView != NULL) {
299 		BTextView *textView = dynamic_cast<BTextView *>(scrollView->FindView("WidgetTextView"));
300 		if (textView != NULL) {
301 			BRect rect = scrollView->Frame();
302 
303 			if (rect.right + 3 > poseView->Bounds().right
304 				|| rect.left - 3 < 0)
305 				textView->MakeResizable(true, NULL);
306 		}
307 	}
308 
309 	return B_DISPATCH_MESSAGE;
310 }
311 
312 
313 void
314 BTextWidget::StartEdit(BRect bounds, BPoseView *view, BPose *pose)
315 {
316 	if (!IsEditable())
317 		return;
318 
319 	// don't allow editing of the trash directory name
320 	BEntry entry(pose->TargetModel()->EntryRef());
321 	if (entry.InitCheck() == B_OK && FSIsTrashDir(&entry))
322 		return;
323 
324 	// don't allow editing of the "Disks" icon name
325 	if (pose->TargetModel()->IsRoot())
326 		return;
327 
328 	if (!ConfirmChangeIfWellKnownDirectory(&entry, "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() == kScaleIconMode
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 		case kScaleIconMode:
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 {
454 	BRect oldRect;
455 	if (view->ViewMode() != kListMode)
456 		oldRect = CalcOldRect(loc, column, view);
457 
458 	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)) {
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 	if (!selected && view->WidgetTextOutline()) {
527 		// draw a halo around the text by using the "false bold"
528 		// feature for text rendering. Either black or white is used for
529 		// the glow (whatever acts as contrast) with a some alpha value,
530 		// two passes are used to achive a blur effect
531 		drawView->SetDrawingMode(B_OP_ALPHA);
532 		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
533 
534 		BFont font;
535 		drawView->GetFont(&font);
536 // NOTE: commented out first pass for halo, since just a plain
537 // outline looks better IMHO -stippi
538 //		font.SetFalseBoldWidth(2.0);
539 //		drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
540 		rgb_color textColor = drawView->HighColor();
541 		rgb_color glow = textColor.red
542 			+ textColor.green + textColor.blue > 128 * 3 ? kBlack : kWhite;
543 //		glow.alpha = 40;
544 //		drawView->SetHighColor(glow);
545 
546 //		drawView->DrawString(fittingText, loc);
547 
548 		font.SetFalseBoldWidth(1.0);
549 		drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
550 		glow.alpha = 220;
551 		drawView->SetHighColor(glow);
552 
553 		drawView->DrawString(fittingText, loc);
554 
555 		font.SetFalseBoldWidth(0.0);
556 		drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
557 		drawView->SetHighColor(textColor);
558 	}
559 #endif // __HAIKU__
560 
561 	drawView->DrawString(fittingText, loc);
562 
563 	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
564 		// ToDo:
565 		// this should be exported to the WidgetAttribute class, probably
566 		// by having a per widget kind style
567 		if (direct) {
568 			rgb_color underlineColor = drawView->HighColor();
569 			underlineColor.alpha = 180;
570 			drawView->SetHighColor(underlineColor);
571 			drawView->SetDrawingMode(B_OP_ALPHA);
572 			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
573 		}
574 
575 		textRect.right = textRect.left + fText->Width(view);
576 			// only underline text part
577 		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
578 			B_MIXED_COLORS);
579 	}
580 }
581