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 "HeaderView.h"
37
38#include <algorithm>
39
40#include <Alert.h>
41#include <Application.h>
42#include <Catalog.h>
43#include <ControlLook.h>
44#include <Locale.h>
45#include <PopUpMenu.h>
46#include <ScrollView.h>
47#include <Volume.h>
48#include <VolumeRoster.h>
49#include <Window.h>
50
51#include "Commands.h"
52#include "FSUtils.h"
53#include "GeneralInfoView.h"
54#include "IconMenuItem.h"
55#include "Model.h"
56#include "NavMenu.h"
57#include "PoseView.h"
58#include "Tracker.h"
59
60
61#undef B_TRANSLATION_CONTEXT
62#define B_TRANSLATION_CONTEXT "InfoWindow"
63
64
65// Amount you have to move the mouse before a drag starts
66const float kDragSlop = 3.0f;
67
68
69HeaderView::HeaderView(Model* model)
70	:
71	BView("header", B_WILL_DRAW),
72	fModel(model),
73	fIconModel(model),
74	fTitleEditView(NULL),
75	fTrackingState(no_track),
76	fMouseDown(false),
77	fIsDropTarget(false),
78	fDoubleClick(false),
79	fDragging(false)
80{
81	const float labelSpacing = be_control_look->DefaultLabelSpacing();
82	fIconRect = BRect(BPoint(labelSpacing * 3.0f, labelSpacing),
83		be_control_look->ComposeIconSize(B_LARGE_ICON));
84	SetExplicitSize(BSize(B_SIZE_UNSET, fIconRect.Width() + 2 * fIconRect.top));
85
86	// The title rect
87	// The magic numbers are used to properly calculate the rect so that
88	// when the editing text view is displayed, the position of the text
89	// does not change.
90	BFont currentFont;
91	font_height fontMetrics;
92	GetFont(&currentFont);
93	currentFont.GetHeight(&fontMetrics);
94
95	fTitleRect.left = fIconRect.right + labelSpacing;
96	fTitleRect.top = 0;
97	fTitleRect.bottom = fontMetrics.ascent + 1;
98	fTitleRect.right = std::min(
99		fTitleRect.left + currentFont.StringWidth(fModel->Name()),
100		Bounds().Width() - labelSpacing);
101	// Offset so that it centers with the icon
102	fTitleRect.OffsetBy(0,
103		fIconRect.top + ((fIconRect.Height() - fTitleRect.Height()) / 2));
104	// Make some room for the border for when we are in edit mode
105	// (Negative numbers increase the size of the rect)
106	fTitleRect.InsetBy(-1, -2);
107
108	// If the model is a symlink, then we deference the model to
109	// get the targets icon
110	if (fModel->IsSymLink()) {
111		Model* resolvedModel = new Model(model->EntryRef(), true, true);
112		if (resolvedModel->InitCheck() == B_OK)
113			fIconModel = resolvedModel;
114		// broken link, just show the symlink
115		else
116			delete resolvedModel;
117	}
118}
119
120
121HeaderView::~HeaderView()
122{
123	if (fIconModel != fModel)
124		delete fIconModel;
125}
126
127
128void
129HeaderView::ModelChanged(Model* model, BMessage* message)
130{
131	// Update the icon stuff
132	if (fIconModel != fModel) {
133		delete fIconModel;
134		fIconModel = NULL;
135	}
136
137	fModel = model;
138	if (fModel->IsSymLink()) {
139		// if we are looking at a symlink, deference the model and look
140		// at the target
141		Model* resolvedModel = new Model(model->EntryRef(), true, true);
142		if (resolvedModel->InitCheck() == B_OK) {
143			if (fIconModel != fModel)
144				delete fIconModel;
145			fIconModel = resolvedModel;
146		} else {
147			fIconModel = model;
148			delete resolvedModel;
149		}
150	}
151
152	Invalidate();
153}
154
155
156void
157HeaderView::ReLinkTargetModel(Model* model)
158{
159	fModel = model;
160	if (fModel->IsSymLink()) {
161		Model* resolvedModel = new Model(model->EntryRef(), true, true);
162		if (resolvedModel->InitCheck() == B_OK) {
163			if (fIconModel != fModel)
164				delete fIconModel;
165			fIconModel = resolvedModel;
166		} else {
167			fIconModel = fModel;
168			delete resolvedModel;
169		}
170	}
171	Invalidate();
172}
173
174
175void
176HeaderView::BeginEditingTitle()
177{
178	if (fTitleEditView != NULL)
179		return;
180
181	BFont font(be_plain_font);
182	font.SetSize(font.Size() + 2);
183	BRect textFrame(fTitleRect);
184	textFrame.right = Bounds().Width() - 5;
185	BRect textRect(textFrame);
186	textRect.OffsetTo(0, 0);
187	textRect.InsetBy(1, 1);
188
189	// Just make it some really large size, since we don't do any line
190	// wrapping. The text filter will make sure to scroll the cursor
191	// into position
192
193	textRect.right = 2000;
194	fTitleEditView = new BTextView(textFrame, "text_editor",
195		textRect, &font, 0, B_FOLLOW_ALL, B_WILL_DRAW);
196	fTitleEditView->SetText(fModel->Name());
197	DisallowFilenameKeys(fTitleEditView);
198
199	// Reset the width of the text rect
200	textRect = fTitleEditView->TextRect();
201	textRect.right = fTitleEditView->LineWidth() + 20;
202	fTitleEditView->SetTextRect(textRect);
203	fTitleEditView->SetWordWrap(false);
204	// Add filter for catching B_RETURN and B_ESCAPE key's
205	fTitleEditView->AddFilter(
206		new BMessageFilter(B_KEY_DOWN, HeaderView::TextViewFilter));
207
208	BScrollView* scrollView = new BScrollView("BorderView", fTitleEditView,
209		0, 0, false, false, B_PLAIN_BORDER);
210	AddChild(scrollView);
211	fTitleEditView->SelectAll();
212	fTitleEditView->MakeFocus();
213
214	Window()->UpdateIfNeeded();
215}
216
217
218void
219HeaderView::FinishEditingTitle(bool commit)
220{
221	if (fTitleEditView == NULL || !commit)
222		return;
223
224	const char* name = fTitleEditView->Text();
225	size_t length = (size_t)fTitleEditView->TextLength();
226
227	status_t result = EditModelName(fModel, name, length);
228	bool reopen = (result == B_NAME_TOO_LONG || result == B_NAME_IN_USE);
229
230	if (result == B_OK) {
231		// Adjust the size of the text rect
232		BFont currentFont(be_plain_font);
233		currentFont.SetSize(currentFont.Size() + 2);
234		float stringWidth = currentFont.StringWidth(fTitleEditView->Text());
235		fTitleRect.right = std::min(fTitleRect.left + stringWidth,
236			Bounds().Width() - 5);
237	}
238
239	// Remove view
240	BView* scrollView = fTitleEditView->Parent();
241	if (scrollView != NULL) {
242		RemoveChild(scrollView);
243		delete scrollView;
244		fTitleEditView = NULL;
245	}
246
247	if (reopen)
248		BeginEditingTitle();
249}
250
251
252void
253HeaderView::Draw(BRect)
254{
255	// Set the low color for anti-aliasing
256	SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR));
257
258	// Clear the old contents
259	SetHighColor(ui_color(B_PANEL_BACKGROUND_COLOR));
260	FillRect(Bounds());
261
262	rgb_color labelColor = ui_color(B_PANEL_TEXT_COLOR);
263
264	// Draw the icon, straddling the border
265	SetDrawingMode(B_OP_OVER);
266	IconCache::sIconCache->Draw(fIconModel, this, fIconRect.LeftTop(),
267		kNormalIcon, fIconRect.Size(), true);
268	SetDrawingMode(B_OP_COPY);
269
270	// Font information
271	font_height fontMetrics;
272	BFont currentFont;
273	float lineBase = 0;
274
275	// Draw the main title if the user is not currently editing it
276	if (fTitleEditView == NULL) {
277		SetFont(be_bold_font);
278		SetFontSize(be_bold_font->Size());
279		GetFont(&currentFont);
280		currentFont.GetHeight(&fontMetrics);
281		lineBase = fTitleRect.bottom - fontMetrics.descent;
282		SetHighColor(labelColor);
283		MovePenTo(BPoint(fIconRect.right + 6, lineBase));
284
285		// Recalculate the rect width
286		fTitleRect.right = std::min(fTitleRect.left
287				+ currentFont.StringWidth(fModel->Name()),
288			Bounds().Width() - 5);
289		// Check for possible need of truncation
290		if (StringWidth(fModel->Name()) > fTitleRect.Width()) {
291			BString nameString(fModel->Name());
292			TruncateString(&nameString, B_TRUNCATE_END,
293				fTitleRect.Width() - 2);
294			DrawString(nameString.String());
295		} else
296			DrawString(fModel->Name());
297	}
298
299}
300
301
302void
303HeaderView::MakeFocus(bool focus)
304{
305	if (!focus && fTitleEditView != NULL)
306		FinishEditingTitle(true);
307}
308
309
310void
311HeaderView::WindowActivated(bool active)
312{
313	if (active)
314		return;
315
316	if (fTitleEditView != NULL)
317		FinishEditingTitle(true);
318}
319
320
321void
322HeaderView::MouseDown(BPoint where)
323{
324	// Assume this isn't part of a double click
325	fDoubleClick = false;
326
327	if (fTitleRect.Contains(where) && fTitleEditView == NULL)
328		BeginEditingTitle();
329	else if (fTitleEditView != NULL)
330		FinishEditingTitle(true);
331	else if (fIconRect.Contains(where)) {
332		uint32 buttons;
333		Window()->CurrentMessage()->FindInt32("buttons", (int32*)&buttons);
334		if (SecondaryMouseButtonDown(modifiers(), buttons)) {
335			// Show contextual menu
336			BPopUpMenu* contextMenu
337				= new BPopUpMenu("FileContext", false, false);
338			if (contextMenu) {
339				BuildContextMenu(contextMenu);
340				contextMenu->SetAsyncAutoDestruct(true);
341				contextMenu->Go(ConvertToScreen(where), true, true,
342					ConvertToScreen(fIconRect));
343			}
344		} else {
345			// Check to see if the point is actually on part of the icon,
346			// versus just in the container rect. The icons are always
347			// the large version
348			BPoint offsetPoint;
349			offsetPoint.x = where.x - fIconRect.left;
350			offsetPoint.y = where.y - fIconRect.top;
351			if (IconCache::sIconCache->IconHitTest(offsetPoint, fIconModel,
352					kNormalIcon, fIconRect.Size())) {
353				// Can't drag the trash anywhere..
354				fTrackingState = fModel->IsTrash()
355					? open_only_track : icon_track;
356
357				// Check for possible double click
358				if (abs((int32)(fClickPoint.x - where.x)) < kDragSlop
359					&& abs((int32)(fClickPoint.y - where.y)) < kDragSlop) {
360					int32 clickCount;
361					Window()->CurrentMessage()->FindInt32("clicks",
362						&clickCount);
363
364					// This checks the* previous* click point
365					if (clickCount == 2) {
366						offsetPoint.x = fClickPoint.x - fIconRect.left;
367						offsetPoint.y = fClickPoint.y - fIconRect.top;
368						fDoubleClick
369							= IconCache::sIconCache->IconHitTest(offsetPoint,
370							fIconModel, kNormalIcon, fIconRect.Size());
371					}
372				}
373			}
374		}
375	}
376
377	fClickPoint = where;
378	fMouseDown = true;
379	SetMouseEventMask(B_POINTER_EVENTS, B_NO_POINTER_HISTORY);
380}
381
382
383void
384HeaderView::MouseMoved(BPoint where, uint32, const BMessage* dragMessage)
385{
386	if (dragMessage != NULL && dragMessage->ReturnAddress() != BMessenger(this)
387		&& dragMessage->what == B_SIMPLE_DATA
388		&& BPoseView::CanHandleDragSelection(fModel, dragMessage,
389			(modifiers() & B_CONTROL_KEY) != 0)) {
390		// highlight drag target
391		bool overTarget = fIconRect.Contains(where);
392		SetDrawingMode(B_OP_OVER);
393		if (overTarget != fIsDropTarget) {
394			IconCache::sIconCache->Draw(fIconModel, this, fIconRect.LeftTop(),
395				overTarget ? kSelectedIcon : kNormalIcon, fIconRect.Size(), true);
396			fIsDropTarget = overTarget;
397		}
398	}
399
400	switch (fTrackingState) {
401		case icon_track:
402			if (fMouseDown && !fDragging
403				&& (abs((int32)(where.x - fClickPoint.x)) > kDragSlop
404					|| abs((int32)(where.y - fClickPoint.y)) > kDragSlop)) {
405				// Find the required height
406				BFont font;
407				GetFont(&font);
408
409				float height = CurrentFontHeight()
410					+ fIconRect.Height() + 8;
411				BRect rect(0, 0, std::min(fIconRect.Width()
412						+ font.StringWidth(fModel->Name()) + 4,
413					fIconRect.Width() * 3), height);
414				BBitmap* dragBitmap = new BBitmap(rect, B_RGBA32, true);
415				dragBitmap->Lock();
416				BView* view = new BView(dragBitmap->Bounds(), "",
417					B_FOLLOW_NONE, 0);
418				dragBitmap->AddChild(view);
419				view->SetOrigin(0, 0);
420				BRect clipRect(view->Bounds());
421				BRegion newClip;
422				newClip.Set(clipRect);
423				view->ConstrainClippingRegion(&newClip);
424
425				// Transparent draw magic
426				view->SetHighColor(0, 0, 0, 0);
427				view->FillRect(view->Bounds());
428				view->SetDrawingMode(B_OP_ALPHA);
429				rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
430				textColor.alpha = 128;
431					// set transparency by value
432				view->SetHighColor(textColor);
433				view->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_COMPOSITE);
434
435				// Draw the icon
436				float hIconOffset = (rect.Width() - fIconRect.Width()) / 2;
437				IconCache::sIconCache->Draw(fIconModel, view,
438					BPoint(hIconOffset, 0), kNormalIcon, fIconRect.Size(), true);
439
440				// See if we need to truncate the string
441				BString nameString(fModel->Name());
442				if (view->StringWidth(fModel->Name()) > rect.Width()) {
443					view->TruncateString(&nameString, B_TRUNCATE_END,
444						rect.Width() - 5);
445				}
446
447				// Draw the label
448				font_height fontHeight;
449				font.GetHeight(&fontHeight);
450				float leftText = (view->StringWidth(nameString.String())
451					- fIconRect.Width()) / 2;
452				view->MovePenTo(BPoint(hIconOffset - leftText + 2,
453					fIconRect.Height() + (fontHeight.ascent + 2)));
454				view->DrawString(nameString.String());
455
456				view->Sync();
457				dragBitmap->Unlock();
458
459				BMessage dragMessage(B_REFS_RECEIVED);
460				dragMessage.AddPoint("click_pt", fClickPoint);
461				BPoint tmpLoc;
462				uint32 button;
463				GetMouse(&tmpLoc, &button);
464				if (button)
465					dragMessage.AddInt32("buttons", (int32)button);
466
467				dragMessage.AddInt32("be:actions",
468					(modifiers() & B_OPTION_KEY) != 0
469						? B_COPY_TARGET : B_MOVE_TARGET);
470				dragMessage.AddRef("refs", fModel->EntryRef());
471				DragMessage(&dragMessage, dragBitmap, B_OP_ALPHA,
472					BPoint((fClickPoint.x - fIconRect.left)
473					+ hIconOffset, fClickPoint.y - fIconRect.top), this);
474				fDragging = true;
475			}
476			break;
477
478		case open_only_track :
479			// Special type of entry that can't be renamed or drag and dropped
480			// It can only be opened by double clicking on the icon
481			break;
482
483		case no_track:
484			// No mouse tracking, do nothing
485			break;
486	}
487}
488
489
490void
491HeaderView::MouseUp(BPoint where)
492{
493	if ((fTrackingState == icon_track
494			|| fTrackingState == open_only_track)
495		&& fIconRect.Contains(where)) {
496		// If it was a double click, then tell Tracker to open the item
497		// The CurrentMessage() here does* not* have a "clicks" field,
498		// which is why we are tracking the clicks with this temp var
499		if (fDoubleClick) {
500			// Double click, launch.
501			BMessage message(B_REFS_RECEIVED);
502			message.AddRef("refs", fModel->EntryRef());
503
504			// add a messenger to the launch message that will be used to
505			// dispatch scripting calls from apps to the PoseView
506			message.AddMessenger("TrackerViewToken", BMessenger(this));
507			be_app->PostMessage(&message);
508			fDoubleClick = false;
509		}
510	}
511
512	// End mouse tracking
513	fMouseDown = false;
514	fDragging = false;
515	fTrackingState = no_track;
516}
517
518
519void
520HeaderView::MessageReceived(BMessage* message)
521{
522	if (message->WasDropped()
523		&& message->what == B_SIMPLE_DATA
524		&& message->ReturnAddress() != BMessenger(this)
525		&& fIconRect.Contains(ConvertFromScreen(message->DropPoint()))
526		&& BPoseView::CanHandleDragSelection(fModel, message,
527			(modifiers() & B_CONTROL_KEY) != 0)) {
528		BPoseView::HandleDropCommon(message, fModel, 0, this,
529			message->DropPoint());
530		Invalidate(fIconRect);
531		return;
532	}
533
534	BView::MessageReceived(message);
535}
536
537
538
539status_t
540HeaderView::BuildContextMenu(BMenu* parent)
541{
542	if (parent == NULL)
543		return B_BAD_VALUE;
544
545	// Add navigation menu if this is not a symlink
546	// Symlink's to directories are OK however!
547	BEntry entry(fModel->EntryRef());
548	entry_ref ref;
549	entry.GetRef(&ref);
550	Model model(&entry);
551	bool navigate = false;
552	if (model.InitCheck() == B_OK) {
553		if (model.IsSymLink()) {
554			// Check if it's to a directory
555			if (entry.SetTo(model.EntryRef(), true) == B_OK) {
556				navigate = entry.IsDirectory();
557				entry.GetRef(&ref);
558			}
559		} else if (model.IsDirectory() || model.IsVolume())
560			navigate = true;
561	}
562	ModelMenuItem* navigationItem = NULL;
563	if (navigate) {
564		navigationItem = new ModelMenuItem(new Model(model),
565			new BNavMenu(model.Name(), B_REFS_RECEIVED, be_app, Window()));
566
567		// setup a navigation menu item which will dynamically load items
568		// as menu items are traversed
569		BNavMenu* navMenu = dynamic_cast<BNavMenu*>(navigationItem->Submenu());
570		if (navMenu != NULL)
571			navMenu->SetNavDir(&ref);
572
573		navigationItem->SetLabel(model.Name());
574		navigationItem->SetEntry(&entry);
575
576		parent->AddItem(navigationItem, 0);
577		parent->AddItem(new BSeparatorItem(), 1);
578
579		BMessage* message = new BMessage(B_REFS_RECEIVED);
580		message->AddRef("refs", &ref);
581		navigationItem->SetMessage(message);
582		navigationItem->SetTarget(be_app);
583	}
584
585	parent->AddItem(new BMenuItem(B_TRANSLATE("Open"),
586		new BMessage(kOpenSelection), 'O'));
587
588	if (!model.IsDesktop() && !model.IsRoot() && !model.IsTrash()) {
589		parent->AddItem(new BMenuItem(B_TRANSLATE("Edit name"),
590			new BMessage(kEditItem), 'E'));
591		parent->AddSeparatorItem();
592
593		if (fModel->IsVolume()) {
594			BMenuItem* item = new BMenuItem(B_TRANSLATE("Unmount"),
595				new BMessage(kUnmountVolume), 'U');
596			parent->AddItem(item);
597			// volume model, enable/disable the Unmount item
598			BVolume boot;
599			BVolumeRoster().GetBootVolume(&boot);
600			BVolume volume;
601			volume.SetTo(fModel->NodeRef()->device);
602			if (volume == boot)
603				item->SetEnabled(false);
604		}
605	}
606
607	if (!model.IsRoot() && !model.IsVolume() && !model.IsTrash())
608		parent->AddItem(new BMenuItem(B_TRANSLATE("Identify"),
609			new BMessage(kIdentifyEntry)));
610
611	if (model.IsTrash())
612		parent->AddItem(new BMenuItem(B_TRANSLATE("Empty Trash"),
613			new BMessage(kEmptyTrash)));
614
615	BMenuItem* sizeItem = NULL;
616	if (model.IsDirectory() && !model.IsVolume() && !model.IsRoot())  {
617		parent->AddItem(sizeItem
618				= new BMenuItem(B_TRANSLATE("Recalculate folder size"),
619			new BMessage(kRecalculateSize)));
620	}
621
622	if (model.IsSymLink()) {
623		parent->AddItem(sizeItem
624				= new BMenuItem(B_TRANSLATE("Set new link target"),
625			new BMessage(kSetLinkTarget)));
626	}
627
628	parent->AddItem(new BSeparatorItem());
629	parent->AddItem(new BMenuItem(B_TRANSLATE("Permissions"),
630		new BMessage(kPermissionsSelected), 'P'));
631
632	parent->SetFont(be_plain_font);
633	parent->SetTargetForItems(this);
634
635	// Reset the nav menu to be_app
636	if (navigate)
637		navigationItem->SetTarget(be_app);
638	if (sizeItem)
639		sizeItem->SetTarget(Window());
640
641	return B_OK;
642}
643
644
645filter_result
646HeaderView::TextViewFilter(BMessage* message, BHandler**,
647	BMessageFilter* filter)
648{
649	uchar key;
650	HeaderView* attribView = static_cast<HeaderView*>(
651		static_cast<BWindow*>(filter->Looper())->FindView("header"));
652
653	// Adjust the size of the text rect
654	BRect nuRect(attribView->TextView()->TextRect());
655	nuRect.right = attribView->TextView()->LineWidth() + 20;
656	attribView->TextView()->SetTextRect(nuRect);
657
658	// Make sure the cursor is in view
659	attribView->TextView()->ScrollToSelection();
660	if (message->FindInt8("byte", (int8*)&key) != B_OK)
661		return B_DISPATCH_MESSAGE;
662
663	if (key == B_RETURN || key == B_ESCAPE) {
664		attribView->FinishEditingTitle(key == B_RETURN);
665		return B_SKIP_MESSAGE;
666	}
667
668	return B_DISPATCH_MESSAGE;
669}
670
671
672float
673HeaderView::CurrentFontHeight()
674{
675	BFont font;
676	GetFont(&font);
677	font_height fontHeight;
678	font.GetHeight(&fontHeight);
679
680	return fontHeight.ascent + fontHeight.descent + fontHeight.leading + 2;
681}
682