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