1/*
2Open Tracker License
3
4Terms and Conditions
5
6Copyright (c) 1991-2000, Be Incorporated. All rights reserved.
7
8Permission is hereby granted, free of charge, to any person obtaining a copy of
9this software and associated documentation files (the "Software"), to deal in
10the Software without restriction, including without limitation the rights to
11use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12of the Software, and to permit persons to whom the Software is furnished to do
13so, subject to the following conditions:
14
15The above copyright notice and this permission notice applies to all licensees
16and shall be included in all copies or substantial portions of the Software.
17
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY,
20FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
22AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
23WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25Except as contained in this notice, the name of Be Incorporated shall not be
26used in advertising or otherwise to promote the sale, use or other dealings in
27this Software without prior written authorization from Be Incorporated.
28
29Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or registered trademarks
30of Be Incorporated in the United States and other countries. Other brand product
31names are registered trademarks or trademarks of their respective holders.
32All rights reserved.
33*/
34
35
36#include "TextWidget.h"
37
38#include <string.h>
39#include <stdlib.h>
40
41#include <Alert.h>
42#include <Catalog.h>
43#include <Clipboard.h>
44#include <Debug.h>
45#include <Directory.h>
46#include <MessageFilter.h>
47#include <ScrollView.h>
48#include <TextView.h>
49#include <Volume.h>
50#include <Window.h>
51
52#include "Attributes.h"
53#include "ContainerWindow.h"
54#include "Commands.h"
55#include "FSUtils.h"
56#include "PoseView.h"
57#include "Utilities.h"
58
59
60#undef B_TRANSLATION_CONTEXT
61#define B_TRANSLATION_CONTEXT "TextWidget"
62
63
64const float kWidthMargin = 20;
65
66
67//	#pragma mark - BTextWidget
68
69
70BTextWidget::BTextWidget(Model* model, BColumn* column, BPoseView* view)
71	:
72	fText(WidgetAttributeText::NewWidgetText(model, column, view)),
73	fAttrHash(column->AttrHash()),
74	fAlignment(column->Alignment()),
75	fEditable(column->Editable()),
76	fVisible(true),
77	fActive(false),
78	fSymLink(model->IsSymLink()),
79	fMaxWidth(0),
80	fLastClickedTime(0)
81{
82}
83
84
85BTextWidget::~BTextWidget()
86{
87	if (fLastClickedTime != 0)
88		fParams.poseView->SetTextWidgetToCheck(NULL, this);
89
90	delete fText;
91}
92
93
94int
95BTextWidget::Compare(const BTextWidget& with, BPoseView* view) const
96{
97	return fText->Compare(*with.fText, view);
98}
99
100
101const char*
102BTextWidget::Text(const BPoseView* view) const
103{
104	StringAttributeText* textAttribute
105		= dynamic_cast<StringAttributeText*>(fText);
106	if (textAttribute == NULL)
107		return NULL;
108
109	return textAttribute->ValueAsText(view);
110}
111
112
113float
114BTextWidget::TextWidth(const BPoseView* pose) const
115{
116	return fText->Width(pose);
117}
118
119
120float
121BTextWidget::PreferredWidth(const BPoseView* pose) const
122{
123	return fText->PreferredWidth(pose) + 1;
124}
125
126
127BRect
128BTextWidget::ColumnRect(BPoint poseLoc, const BColumn* column,
129	const BPoseView* view)
130{
131	if (view->ViewMode() != kListMode) {
132		// ColumnRect only makes sense in list view, return
133		// CalcRect otherwise
134		return CalcRect(poseLoc, column, view);
135	}
136	BRect result;
137	result.left = column->Offset() + poseLoc.x;
138	result.right = result.left + column->Width();
139	result.bottom = poseLoc.y
140		+ roundf((view->ListElemHeight() + view->FontHeight()) / 2);
141	result.top = result.bottom - floorf(view->FontHeight());
142	return result;
143}
144
145
146BRect
147BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn* column,
148	const BPoseView* view, float textWidth)
149{
150	BRect result;
151	float viewWidth = textWidth;
152
153	if (view->ViewMode() == kListMode) {
154		viewWidth = std::min(column->Width(), textWidth);
155
156		poseLoc.x += column->Offset();
157
158		switch (fAlignment) {
159			case B_ALIGN_LEFT:
160				result.left = poseLoc.x;
161				result.right = result.left + viewWidth;
162				break;
163
164			case B_ALIGN_CENTER:
165				result.left = poseLoc.x
166					+ roundf((column->Width() - viewWidth) / 2);
167				if (result.left < 0)
168					result.left = 0;
169
170				result.right = result.left + viewWidth;
171				break;
172
173			case B_ALIGN_RIGHT:
174				result.right = poseLoc.x + column->Width();
175				result.left = result.right - viewWidth;
176				if (result.left < 0)
177					result.left = 0;
178				break;
179
180			default:
181				TRESPASS();
182				break;
183		}
184
185		result.bottom = poseLoc.y
186			+ roundf((view->ListElemHeight() + view->FontHeight()) / 2);
187	} else {
188		viewWidth = std::min(view->StringWidth("M") * 30, textWidth);
189		if (view->ViewMode() == kIconMode) {
190			// icon mode
191			result.left = poseLoc.x
192				+ roundf((view->IconSizeInt() - viewWidth) / 2);
193		} else {
194			// mini icon mode
195			result.left = poseLoc.x + view->IconSizeInt() + kMiniIconSeparator;
196		}
197		result.bottom = poseLoc.y + view->IconPoseHeight();
198
199		result.right = result.left + viewWidth;
200	}
201
202	result.top = result.bottom - floorf(view->FontHeight());
203
204	return result;
205}
206
207
208BRect
209BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column,
210	const BPoseView* view)
211{
212	return CalcRectCommon(poseLoc, column, view, fText->Width(view));
213}
214
215
216BRect
217BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column,
218	const BPoseView* view)
219{
220	return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth());
221}
222
223
224BRect
225BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column,
226	const BPoseView* view)
227{
228	BRect result = CalcRect(poseLoc, column, view);
229	if (result.Width() < kWidthMargin) {
230		// if resulting rect too narrow, make it a bit wider
231		// for comfortable clicking
232		if (column != NULL && column->Width() < kWidthMargin)
233			result.right = result.left + column->Width();
234		else
235			result.right = result.left + kWidthMargin;
236	}
237
238	return result;
239}
240
241
242void
243BTextWidget::CheckExpiration()
244{
245	if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) {
246		bigtime_t doubleClickSpeed;
247		get_click_speed(&doubleClickSpeed);
248
249		bigtime_t delta = system_time() - fLastClickedTime;
250
251		if (delta > doubleClickSpeed) {
252			// at least 'doubleClickSpeed' microseconds ellapsed and no click
253			// was registered since.
254			fLastClickedTime = 0;
255			StartEdit(fParams.bounds, fParams.poseView, fParams.pose);
256		}
257	} else {
258		fLastClickedTime = 0;
259		fParams.poseView->SetTextWidgetToCheck(NULL);
260	}
261}
262
263
264void
265BTextWidget::CancelWait()
266{
267	fLastClickedTime = 0;
268	fParams.poseView->SetTextWidgetToCheck(NULL);
269}
270
271
272void
273BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint)
274{
275	// Register the time of that click.  The PoseView, through its Pulse()
276	// will allow us to StartEdit() if no other click have been registered since
277	// then.
278
279	// TODO: re-enable modifiers, one should be enough
280	view->SetTextWidgetToCheck(NULL);
281	if (IsEditable() && pose->IsSelected()) {
282		bigtime_t doubleClickSpeed;
283		get_click_speed(&doubleClickSpeed);
284
285		if (fLastClickedTime == 0) {
286			fLastClickedTime = system_time();
287			if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime())
288				fLastClickedTime = 0;
289		} else
290			fLastClickedTime = 0;
291
292		if (fLastClickedTime == 0)
293			return;
294
295		view->SetTextWidgetToCheck(this);
296
297		fParams.pose = pose;
298		fParams.bounds = bounds;
299		fParams.poseView = view;
300	} else
301		fLastClickedTime = 0;
302}
303
304
305static filter_result
306TextViewKeyDownFilter(BMessage* message, BHandler**, BMessageFilter* filter)
307{
308	uchar key;
309	if (message->FindInt8("byte", (int8*)&key) != B_OK)
310		return B_DISPATCH_MESSAGE;
311
312	ThrowOnAssert(filter != NULL);
313
314	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
315		filter->Looper());
316	ThrowOnAssert(window != NULL);
317
318	BPoseView* view = window->PoseView();
319	ThrowOnAssert(view != NULL);
320
321	if (key == B_RETURN || key == B_ESCAPE) {
322		view->CommitActivePose(key == B_RETURN);
323		return B_SKIP_MESSAGE;
324	}
325
326	if (key == B_TAB) {
327		if (view->ActivePose()) {
328			if (message->FindInt32("modifiers") & B_SHIFT_KEY)
329				view->ActivePose()->EditPreviousWidget(view);
330			else
331				view->ActivePose()->EditNextWidget(view);
332		}
333
334		return B_SKIP_MESSAGE;
335	}
336
337	// the BTextView doesn't respect window borders when resizing itself;
338	// we try to work-around this "bug" here.
339
340	// find the text editing view
341	BView* scrollView = view->FindView("BorderView");
342	if (scrollView != NULL) {
343		BTextView* textView = dynamic_cast<BTextView*>(
344			scrollView->FindView("WidgetTextView"));
345		if (textView != NULL) {
346			ASSERT(view->ActiveTextWidget() != NULL);
347			float maxWidth = view->ActiveTextWidget()->MaxWidth();
348			bool tooWide = textView->TextRect().Width() > maxWidth;
349			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
350		}
351	}
352
353	return B_DISPATCH_MESSAGE;
354}
355
356
357static filter_result
358TextViewPasteFilter(BMessage* message, BHandler**, BMessageFilter* filter)
359{
360	ThrowOnAssert(filter != NULL);
361
362	BContainerWindow* window = dynamic_cast<BContainerWindow*>(
363		filter->Looper());
364	ThrowOnAssert(window != NULL);
365
366	BPoseView* view = window->PoseView();
367	ThrowOnAssert(view != NULL);
368
369	// the BTextView doesn't respect window borders when resizing itself;
370	// we try to work-around this "bug" here.
371
372	// find the text editing view
373	BView* scrollView = view->FindView("BorderView");
374	if (scrollView != NULL) {
375		BTextView* textView = dynamic_cast<BTextView*>(
376			scrollView->FindView("WidgetTextView"));
377		if (textView != NULL) {
378			float textWidth = textView->TextRect().Width();
379
380			// subtract out selected text region width
381			int32 start, finish;
382			textView->GetSelection(&start, &finish);
383			if (start != finish) {
384				BRegion selectedRegion;
385				textView->GetTextRegion(start, finish, &selectedRegion);
386				textWidth -= selectedRegion.Frame().Width();
387			}
388
389			// add pasted text width
390			if (be_clipboard->Lock()) {
391				BMessage* clip = be_clipboard->Data();
392				if (clip != NULL) {
393					const char* text = NULL;
394					ssize_t length = 0;
395
396					if (clip->FindData("text/plain", B_MIME_TYPE,
397							(const void**)&text, &length) == B_OK) {
398						textWidth += textView->StringWidth(text);
399					}
400				}
401
402				be_clipboard->Unlock();
403			}
404
405			// check if pasted text is too wide
406			ASSERT(view->ActiveTextWidget() != NULL);
407			float maxWidth = view->ActiveTextWidget()->MaxWidth();
408			bool tooWide = textWidth > maxWidth;
409
410			if (tooWide) {
411				// resize text view to max width
412
413				// move scroll view if not left aligned
414				float oldWidth = textView->Bounds().Width();
415				float newWidth = maxWidth;
416				float right = oldWidth - newWidth;
417
418				if (textView->Alignment() == B_ALIGN_CENTER)
419					scrollView->MoveBy(roundf(right / 2), 0);
420				else if (textView->Alignment() == B_ALIGN_RIGHT)
421					scrollView->MoveBy(right, 0);
422
423				// resize scroll view
424				float grow = newWidth - oldWidth;
425				scrollView->ResizeBy(grow, 0);
426			}
427
428			textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
429		}
430	}
431
432	return B_DISPATCH_MESSAGE;
433}
434
435
436void
437BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose)
438{
439	view->SetTextWidgetToCheck(NULL, this);
440	if (!IsEditable() || IsActive())
441		return;
442
443	view->SetActiveTextWidget(this);
444
445	// TODO fix text rect being off by a pixel on some files
446
447	BRect rect(bounds);
448	rect.OffsetBy(view->ViewMode() == kListMode ? -1 : 1, -4);
449	BTextView* textView = new BTextView(rect, "WidgetTextView", rect,
450		be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
451
452	textView->SetWordWrap(false);
453	textView->SetInsets(2, 2, 2, 2);
454	DisallowMetaKeys(textView);
455	fText->SetupEditing(textView);
456
457	textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter));
458
459	if (view->SelectedVolumeIsReadOnly()) {
460		textView->MakeEditable(false);
461		textView->MakeSelectable(true);
462		// tint text view background color to indicate not editable
463		textView->SetViewColor(tint_color(textView->ViewColor(),
464			ReadOnlyTint(textView->ViewColor())));
465	} else
466		textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter));
467
468	// get full text length
469	rect.right = rect.left + textView->LineWidth();
470	rect.bottom = rect.top + textView->LineHeight() - 1 + 4;
471
472	if (view->ViewMode() == kListMode) {
473		// limit max width to column width in list mode
474		BColumn* column = view->ColumnFor(fAttrHash);
475		ASSERT(column != NULL);
476		fMaxWidth = column->Width();
477	} else {
478		// limit max width to 30em in icon and mini icon mode
479		fMaxWidth = textView->StringWidth("M") * 30;
480
481		if (textView->LineWidth() > fMaxWidth
482			|| view->ViewMode() == kMiniIconMode) {
483			// compensate for text going over right inset
484			rect.OffsetBy(-2, 0);
485		}
486	}
487
488	// resize textView
489	textView->MoveTo(rect.LeftTop());
490	textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height());
491	textView->SetTextRect(rect);
492
493	// set alignment before adding textView so it doesn't redraw
494	switch (view->ViewMode()) {
495		case kIconMode:
496			textView->SetAlignment(B_ALIGN_CENTER);
497			break;
498
499		case kMiniIconMode:
500			textView->SetAlignment(B_ALIGN_LEFT);
501			break;
502
503		case kListMode:
504			textView->SetAlignment(fAlignment);
505			break;
506	}
507
508	BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0,
509		false, false, B_PLAIN_BORDER);
510	view->AddChild(scrollView);
511
512	bool tooWide = textView->TextRect().Width() > fMaxWidth;
513	textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView);
514
515	view->SetActivePose(pose);
516		// tell view about pose
517	SetActive(true);
518		// for widget
519
520	textView->SelectAll();
521	textView->ScrollToSelection();
522		// scroll to beginning so that text is visible
523	textView->MakeFocus();
524
525	// make this text widget invisible while we edit it
526	SetVisible(false);
527
528	ASSERT(view->Window() != NULL);
529		// how can I not have a Window here???
530
531	if (view->Window()) {
532		// force immediate redraw so TextView appears instantly
533		view->Window()->UpdateIfNeeded();
534	}
535}
536
537
538void
539BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view,
540	BPose* pose, int32 poseIndex)
541{
542	view->SetActiveTextWidget(NULL);
543
544	// find the text editing view
545	BView* scrollView = view->FindView("BorderView");
546	ASSERT(scrollView != NULL);
547	if (scrollView == NULL)
548		return;
549
550	BTextView* textView = dynamic_cast<BTextView*>(
551		scrollView->FindView("WidgetTextView"));
552	ASSERT(textView != NULL);
553	if (textView == NULL)
554		return;
555
556	BColumn* column = view->ColumnFor(fAttrHash);
557	ASSERT(column != NULL);
558	if (column == NULL)
559		return;
560
561	if (saveChanges && fText->CommitEditedText(textView)) {
562		// we have an actual change, re-sort
563		view->CheckPoseSortOrder(pose, poseIndex);
564	}
565
566	// make text widget visible again
567	SetVisible(true);
568	view->Invalidate(ColumnRect(poseLoc, column, view));
569
570	// force immediate redraw so TEView disappears
571	scrollView->RemoveSelf();
572	delete scrollView;
573
574	ASSERT(view->Window() != NULL);
575	view->Window()->UpdateIfNeeded();
576	view->MakeFocus();
577
578	SetActive(false);
579}
580
581
582void
583BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column,
584	BPoseView* view, bool visible)
585{
586	BRect oldRect;
587	if (view->ViewMode() != kListMode)
588		oldRect = CalcOldRect(loc, column, view);
589
590	if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view)
591		&& visible) {
592		BRect invalRect(ColumnRect(loc, column, view));
593		if (view->ViewMode() != kListMode)
594			invalRect = invalRect | oldRect;
595		view->Invalidate(invalRect);
596	}
597}
598
599
600void
601BTextWidget::SelectAll(BPoseView* view)
602{
603	BTextView* text = dynamic_cast<BTextView*>(
604		view->FindView("WidgetTextView"));
605	if (text != NULL)
606		text->SelectAll();
607}
608
609
610void
611BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view,
612	BView* drawView, bool selected, uint32 clipboardMode, BPoint offset,
613	bool direct)
614{
615	textRect.OffsetBy(offset);
616
617	// We are only concerned with setting the correct text color.
618
619	// For active views the selection is drawn as inverse text
620	// (background color for the text, solid black for the background).
621	// For inactive windows the text is drawn normally, then the
622	// selection rect is alpha-blended on top. This all happens in
623	// BPose::Draw before and after calling this function.
624
625	if (direct) {
626		// draw selection box if selected
627		if (selected) {
628			drawView->SetDrawingMode(B_OP_COPY);
629			drawView->FillRect(textRect, B_SOLID_LOW);
630		} else
631			drawView->SetDrawingMode(B_OP_OVER);
632
633		// set high color
634		rgb_color highColor;
635		highColor = view->TextColor(selected && view->Window()->IsActive());
636
637		if (clipboardMode == kMoveSelectionTo && !selected) {
638			drawView->SetDrawingMode(B_OP_ALPHA);
639			drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
640			highColor.alpha = 64;
641		}
642		drawView->SetHighColor(highColor);
643	} else if (selected && view->Window()->IsActive())
644		drawView->SetHighColor(view->BackColor(true)); // inverse
645	else if (!selected)
646		drawView->SetHighColor(view->TextColor());
647
648	BPoint location;
649	location.y = textRect.bottom - view->FontInfo().descent;
650	location.x = textRect.left + 1;
651
652	const char* fittingText = fText->FittingText(view);
653
654	// TODO: Comparing view and drawView here to avoid rendering
655	// the text outline when producing a drag bitmap. The check is
656	// not fully correct, since an offscreen view is also used in some
657	// other rare cases (something to do with columns). But for now, this
658	// fixes the broken drag bitmaps when dragging icons from the Desktop.
659	if (direct && !selected && view->WidgetTextOutline()) {
660		// draw a halo around the text by using the "false bold"
661		// feature for text rendering. Either black or white is used for
662		// the glow (whatever acts as contrast) with a some alpha value,
663		drawView->SetDrawingMode(B_OP_ALPHA);
664		drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
665
666		BFont font;
667		drawView->GetFont(&font);
668
669		rgb_color textColor = view->TextColor();
670		if (textColor.IsDark()) {
671			// dark text on light outline
672			rgb_color glowColor = ui_color(B_SHINE_COLOR);
673
674			font.SetFalseBoldWidth(2.0);
675			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
676			glowColor.alpha = 30;
677			drawView->SetHighColor(glowColor);
678
679			drawView->DrawString(fittingText, location);
680
681			font.SetFalseBoldWidth(1.0);
682			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
683			glowColor.alpha = 65;
684			drawView->SetHighColor(glowColor);
685
686			drawView->DrawString(fittingText, location);
687
688			font.SetFalseBoldWidth(0.0);
689			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
690		} else {
691			// light text on dark outline
692			rgb_color outlineColor = kBlack;
693
694			font.SetFalseBoldWidth(1.0);
695			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
696			outlineColor.alpha = 30;
697			drawView->SetHighColor(outlineColor);
698
699			drawView->DrawString(fittingText, location);
700
701			font.SetFalseBoldWidth(0.0);
702			drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH);
703
704			outlineColor.alpha = 200;
705			drawView->SetHighColor(outlineColor);
706
707			drawView->DrawString(fittingText, location + BPoint(1, 1));
708		}
709
710		drawView->SetDrawingMode(B_OP_OVER);
711		drawView->SetHighColor(textColor);
712	}
713
714	drawView->DrawString(fittingText, location);
715
716	if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) {
717		// TODO:
718		// this should be exported to the WidgetAttribute class, probably
719		// by having a per widget kind style
720		if (direct) {
721			rgb_color underlineColor = drawView->HighColor();
722			underlineColor.alpha = 180;
723			drawView->SetHighColor(underlineColor);
724			drawView->SetDrawingMode(B_OP_ALPHA);
725			drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
726		}
727
728		textRect.right = textRect.left + fText->Width(view);
729			// only underline text part
730		drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(),
731			B_MIXED_COLORS);
732	}
733}
734