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 "InfoWindow.h"
37
38#include <string.h>
39#include <stdio.h>
40#include <stdlib.h>
41
42#include <Alert.h>
43#include <Catalog.h>
44#include <Debug.h>
45#include <Directory.h>
46#include <File.h>
47#include <Font.h>
48#include <Locale.h>
49#include <MenuField.h>
50#include <Mime.h>
51#include <NodeInfo.h>
52#include <NodeMonitor.h>
53#include <Path.h>
54#include <PopUpMenu.h>
55#include <Region.h>
56#include <Roster.h>
57#include <Screen.h>
58#include <ScrollView.h>
59#include <StringFormat.h>
60#include <SymLink.h>
61#include <TabView.h>
62#include <TextView.h>
63#include <Volume.h>
64#include <VolumeRoster.h>
65
66#include "Attributes.h"
67#include "AttributesView.h"
68#include "AutoLock.h"
69#include "Commands.h"
70#include "DialogPane.h"
71#include "FSUtils.h"
72#include "GeneralInfoView.h"
73#include "IconCache.h"
74#include "Model.h"
75#include "NavMenu.h"
76#include "PoseView.h"
77#include "StringForSize.h"
78#include "Tracker.h"
79#include "WidgetAttributeText.h"
80
81
82#undef B_TRANSLATION_CONTEXT
83#define B_TRANSLATION_CONTEXT "InfoWindow"
84
85
86const uint32 kNewTargetSelected = 'selc';
87
88//	#pragma mark - BInfoWindow
89
90
91BInfoWindow::BInfoWindow(Model* model, int32 group_index,
92	LockingList<BWindow>* list)
93	:
94	BWindow(BInfoWindow::InfoWindowRect(),
95		"InfoWindow", B_TITLED_WINDOW,
96		B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS,
97		B_CURRENT_WORKSPACE),
98	fModel(model),
99	fStopCalc(false),
100	fIndex(group_index),
101	fCalcThreadID(-1),
102	fWindowList(list),
103	fPermissionsView(NULL),
104	fFilePanel(NULL),
105	fFilePanelOpen(false)
106{
107	SetPulseRate(1000000);
108		// we use pulse to check freebytes on volume
109
110	TTracker::WatchNode(model->NodeRef(), B_WATCH_ALL | B_WATCH_MOUNT, this);
111
112	// window list is Locked by Tracker around this constructor
113	if (list != NULL)
114		list->AddItem(this);
115
116	AddShortcut('E', 0, new BMessage(kEditItem));
117	AddShortcut('O', 0, new BMessage(kOpenSelection));
118	AddShortcut('U', 0, new BMessage(kUnmountVolume));
119	AddShortcut('P', 0, new BMessage(kPermissionsSelected));
120
121	BGroupLayout* layout = new BGroupLayout(B_VERTICAL, 0);
122	SetLayout(layout);
123
124	BModelOpener modelOpener(TargetModel());
125	if (TargetModel()->InitCheck() != B_OK)
126		return;
127
128	fHeaderView = new HeaderView(TargetModel());
129	AddChild(fHeaderView);
130	BTabView* tabView = new BTabView("tabs");
131	tabView->SetBorder(B_NO_BORDER);
132	AddChild(tabView);
133
134	fGeneralInfoView = new GeneralInfoView(TargetModel());
135	tabView->AddTab(fGeneralInfoView);
136
137	BRect permissionsBounds(0,
138		fGeneralInfoView->Bounds().bottom,
139		fGeneralInfoView->Bounds().right,
140		fGeneralInfoView->Bounds().bottom + 103);
141
142	fPermissionsView = new FilePermissionsView(
143		permissionsBounds, fModel);
144	tabView->AddTab(fPermissionsView);
145
146	tabView->AddTab(new AttributesView(TargetModel()));
147
148	// This window accepts messages before being shown, so let's start the
149	// looper immediately.
150	Run();
151}
152
153
154BInfoWindow::~BInfoWindow()
155{
156	// Check to make sure the file panel is destroyed
157	delete fFilePanel;
158	delete fModel;
159}
160
161
162BRect
163BInfoWindow::InfoWindowRect()
164{
165	// starting size of window
166	return BRect(70, 50, 385, 240);
167}
168
169
170void
171BInfoWindow::Quit()
172{
173	stop_watching(this);
174
175	if (fWindowList) {
176		AutoLock<LockingList<BWindow> > lock(fWindowList);
177		fWindowList->RemoveItem(this);
178	}
179
180	fStopCalc = true;
181
182	// wait until CalcSize thread has terminated before closing window
183	status_t result;
184	wait_for_thread(fCalcThreadID, &result);
185
186	_inherited::Quit();
187}
188
189
190bool
191BInfoWindow::IsShowing(const node_ref* node) const
192{
193	return *TargetModel()->NodeRef() == *node;
194}
195
196
197void
198BInfoWindow::Show()
199{
200	if (TargetModel()->InitCheck() != B_OK) {
201		Close();
202		return;
203	}
204
205	AutoLock<BWindow> lock(this);
206
207	// position window appropriately based on index
208	BRect windRect(InfoWindowRect());
209	if ((fIndex + 2) % 2 == 1) {
210		windRect.OffsetBy(320, 0);
211		fIndex--;
212	}
213
214	windRect.OffsetBy(fIndex * 8, fIndex * 8);
215
216	// make sure window is visible on screen
217	BScreen screen(this);
218	if (!windRect.Intersects(screen.Frame()))
219		windRect.OffsetTo(50, 50);
220
221	MoveTo(windRect.LeftTop());
222
223	// volume case is handled by view
224	if (!TargetModel()->IsVolume() && !TargetModel()->IsRoot()) {
225		if (TargetModel()->IsDirectory()) {
226			// if this is a folder then spawn thread to calculate size
227			SetSizeString(B_TRANSLATE("calculating" B_UTF8_ELLIPSIS));
228			fCalcThreadID = spawn_thread(BInfoWindow::CalcSize, "CalcSize",
229				B_NORMAL_PRIORITY, this);
230			resume_thread(fCalcThreadID);
231		} else {
232			fGeneralInfoView->SetLastSize(TargetModel()->StatBuf()->st_size);
233
234			BString sizeStr;
235			GetSizeString(sizeStr, fGeneralInfoView->LastSize(), 0);
236			SetSizeString(sizeStr.String());
237		}
238	}
239
240	BString buffer(B_TRANSLATE_COMMENT("%name info", "InfoWindow Title"));
241	buffer.ReplaceFirst("%name", TargetModel()->Name());
242	SetTitle(buffer.String());
243
244	lock.Unlock();
245	_inherited::Show();
246}
247
248
249void
250BInfoWindow::MessageReceived(BMessage* message)
251{
252	switch (message->what) {
253		case kRestoreState:
254			Show();
255			break;
256
257		case kOpenSelection:
258		{
259			BMessage refsMessage(B_REFS_RECEIVED);
260			refsMessage.AddRef("refs", fModel->EntryRef());
261
262			// add a messenger to the launch message that will be used to
263			// dispatch scripting calls from apps to the PoseView
264			refsMessage.AddMessenger("TrackerViewToken", BMessenger(this));
265			be_app->PostMessage(&refsMessage);
266			break;
267		}
268
269		case kEditItem:
270		{
271			BEntry entry(fModel->EntryRef());
272			fHeaderView->BeginEditingTitle();
273			break;
274		}
275
276		case kIdentifyEntry:
277		{
278			bool force = (modifiers() & B_OPTION_KEY) != 0;
279			BEntry entry;
280			if (entry.SetTo(fModel->EntryRef(), true) == B_OK) {
281				BPath path;
282				if (entry.GetPath(&path) == B_OK)
283					update_mime_info(path.Path(), true, false, force ? 2 : 1);
284			}
285			break;
286		}
287
288		case kRecalculateSize:
289		{
290			fStopCalc = true;
291			// Wait until any current CalcSize thread has terminated before
292			// starting a new one
293			status_t result;
294			wait_for_thread(fCalcThreadID, &result);
295
296			// Start recalculating..
297			fStopCalc = false;
298			SetSizeString(B_TRANSLATE("calculating" B_UTF8_ELLIPSIS));
299			fCalcThreadID = spawn_thread(BInfoWindow::CalcSize, "CalcSize",
300				B_NORMAL_PRIORITY, this);
301			resume_thread(fCalcThreadID);
302			break;
303		}
304
305		case kSetLinkTarget:
306			OpenFilePanel(fModel->EntryRef());
307			break;
308
309		// An item was dropped into the window
310		case B_SIMPLE_DATA:
311			// If we are not a SymLink, just ignore the request
312			if (!fModel->IsSymLink())
313				break;
314			// supposed to fall through
315		// An item was selected from the file panel
316		// fall-through
317		case kNewTargetSelected:
318		{
319			// Extract the BEntry, and set its full path to the string value
320			BEntry targetEntry;
321			entry_ref ref;
322			BPath path;
323
324			if (message->FindRef("refs", &ref) == B_OK
325				&& targetEntry.SetTo(&ref, true) == B_OK
326				&& targetEntry.Exists()) {
327				// We now have to re-target the broken symlink. Unfortunately,
328				// there's no way to change the target of an existing symlink.
329				// So we have to delete the old one and create a new one.
330				// First, stop watching the broken node
331				// (we don't want this window to quit when the node
332				// is removed.)
333				stop_watching(this);
334
335				// Get the parent
336				BDirectory parent;
337				BEntry tmpEntry(TargetModel()->EntryRef());
338				if (tmpEntry.GetParent(&parent) != B_OK)
339					break;
340
341				// Preserve the name
342				BString name(TargetModel()->Name());
343
344				// Extract path for new target
345				BEntry target(&ref);
346				BPath targetPath;
347				if (target.GetPath(&targetPath) != B_OK)
348					break;
349
350				// Preserve the original attributes
351				AttributeStreamMemoryNode memoryNode;
352				{
353					BModelOpener opener(TargetModel());
354					AttributeStreamFileNode original(TargetModel()->Node());
355					memoryNode << original;
356				}
357
358				// Delete the broken node.
359				BEntry oldEntry(TargetModel()->EntryRef());
360				oldEntry.Remove();
361
362				// Create new node
363				BSymLink link;
364				parent.CreateSymLink(name.String(), targetPath.Path(), &link);
365
366				// Update our Model()
367				BEntry symEntry(&parent, name.String());
368				fModel->SetTo(&symEntry);
369
370				BModelWriteOpener opener(TargetModel());
371
372				// Copy the attributes back
373				AttributeStreamFileNode newNode(TargetModel()->Node());
374				newNode << memoryNode;
375
376				// Start watching this again
377				TTracker::WatchNode(TargetModel()->NodeRef(),
378					B_WATCH_ALL | B_WATCH_MOUNT, this);
379
380				// Tell the attribute view about this new model
381				fGeneralInfoView->ReLinkTargetModel(TargetModel());
382				fHeaderView->ReLinkTargetModel(TargetModel());
383			}
384			break;
385		}
386
387		case B_CANCEL:
388			// File panel window has closed
389			delete fFilePanel;
390			fFilePanel = NULL;
391			// It's no longer open
392			fFilePanelOpen = false;
393			break;
394
395		case kUnmountVolume:
396			// Sanity check that this isn't the boot volume
397			// (The unmount menu item has been disabled in this
398			// case, but the shortcut is still active)
399			if (fModel->IsVolume()) {
400				BVolume boot;
401				BVolumeRoster().GetBootVolume(&boot);
402				BVolume volume(fModel->NodeRef()->device);
403				if (volume != boot) {
404					TTracker* tracker = dynamic_cast<TTracker*>(be_app);
405					if (tracker != NULL)
406						tracker->SaveAllPoseLocations();
407
408					BMessage unmountMessage(kUnmountVolume);
409					unmountMessage.AddInt32("device_id", volume.Device());
410					be_app->PostMessage(&unmountMessage);
411				}
412			}
413			break;
414
415		case kEmptyTrash:
416			FSEmptyTrash();
417			break;
418
419		case B_NODE_MONITOR:
420			switch (message->FindInt32("opcode")) {
421				case B_ENTRY_REMOVED:
422				{
423					node_ref itemNode;
424					message->FindInt32("device", &itemNode.device);
425					message->FindInt64("node", &itemNode.node);
426					// our window itself may be deleted
427					if (*TargetModel()->NodeRef() == itemNode)
428						Close();
429					break;
430				}
431
432				case B_ENTRY_MOVED:
433				case B_STAT_CHANGED:
434				case B_ATTR_CHANGED:
435					fGeneralInfoView->ModelChanged(TargetModel(), message);
436						// must be called before the
437						// FilePermissionView::ModelChanged()
438						// call, because it changes the model...
439						// (bad style!)
440					fHeaderView->ModelChanged(TargetModel(), message);
441
442					if (fPermissionsView != NULL)
443						fPermissionsView->ModelChanged(TargetModel());
444					break;
445
446				case B_DEVICE_UNMOUNTED:
447				{
448					// We were watching a volume that is no longer
449					// mounted, we might as well quit
450					node_ref itemNode;
451					// Only the device information is available
452					message->FindInt32("device", &itemNode.device);
453					if (TargetModel()->NodeRef()->device == itemNode.device)
454						Close();
455					break;
456				}
457
458				default:
459					break;
460			}
461			break;
462
463		case kPermissionsSelected:
464		{
465			BTabView* tabView = (BTabView*)FindView("tabs");
466			tabView->Select(1);
467			break;
468		}
469
470		default:
471			_inherited::MessageReceived(message);
472			break;
473	}
474}
475
476
477void
478BInfoWindow::GetSizeString(BString& result, off_t size, int32 fileCount)
479{
480	static BStringFormat sizeFormat(B_TRANSLATE(
481		"{0, plural, one{(# byte)} other{(# bytes)}}"));
482	static BStringFormat countFormat(B_TRANSLATE(
483		"{0, plural, one{for # file} other{for # files}}"));
484
485	char sizeBuffer[128];
486	result << string_for_size((double)size, sizeBuffer, sizeof(sizeBuffer));
487
488	if (size >= kKBSize) {
489		result << " ";
490
491		sizeFormat.Format(result, size);
492			// "bytes" translation could come from string_for_size
493			// which could be part of the localekit itself
494	}
495
496	if (fileCount != 0) {
497		result << " ";
498		countFormat.Format(result, fileCount);
499	}
500}
501
502
503int32
504BInfoWindow::CalcSize(void* castToWindow)
505{
506	BInfoWindow* window = static_cast<BInfoWindow*>(castToWindow);
507	BDirectory dir(window->TargetModel()->EntryRef());
508	BDirectory trashDir;
509	FSGetTrashDir(&trashDir, window->TargetModel()->EntryRef()->device);
510	if (dir.InitCheck() != B_OK) {
511		if (window->StopCalc())
512			return B_ERROR;
513
514		AutoLock<BWindow> lock(window);
515		if (!lock)
516			return B_ERROR;
517
518		window->SetSizeString(B_TRANSLATE("Error calculating folder size."));
519		return B_ERROR;
520	}
521
522	BEntry dirEntry, trashEntry;
523	dir.GetEntry(&dirEntry);
524	trashDir.GetEntry(&trashEntry);
525
526	BString sizeString;
527
528	// check if user has asked for trash dir info
529	if (dirEntry != trashEntry) {
530		// if not, perform normal info calculations
531		off_t size = 0;
532		int32 fileCount = 0;
533		int32 dirCount = 0;
534		CopyLoopControl loopControl;
535		FSRecursiveCalcSize(window, &loopControl, &dir, &size, &fileCount,
536			&dirCount);
537
538		// got the size value, update the size string
539		GetSizeString(sizeString, size, fileCount);
540	} else {
541		// in the trash case, iterate through and sum up
542		// size/counts for all present trash dirs
543		off_t totalSize = 0, currentSize;
544		int32 totalFileCount = 0, currentFileCount;
545		int32 totalDirCount = 0, currentDirCount;
546		BVolumeRoster volRoster;
547		volRoster.Rewind();
548		BVolume volume;
549		while (volRoster.GetNextVolume(&volume) == B_OK) {
550			if (!volume.IsPersistent())
551				continue;
552
553			currentSize = 0;
554			currentFileCount = 0;
555			currentDirCount = 0;
556
557			BDirectory trashDir;
558			if (FSGetTrashDir(&trashDir, volume.Device()) == B_OK) {
559				CopyLoopControl loopControl;
560				FSRecursiveCalcSize(window, &loopControl, &trashDir,
561					&currentSize, &currentFileCount, &currentDirCount);
562				totalSize += currentSize;
563				totalFileCount += currentFileCount;
564				totalDirCount += currentDirCount;
565			}
566		}
567		GetSizeString(sizeString, totalSize, totalFileCount);
568	}
569
570	if (window->StopCalc()) {
571		// window closed, bail
572		return B_OK;
573	}
574
575	AutoLock<BWindow> lock(window);
576	if (lock.IsLocked())
577		window->SetSizeString(sizeString.String());
578
579	return B_OK;
580}
581
582
583void
584BInfoWindow::SetSizeString(const char* sizeString)
585{
586	fGeneralInfoView->SetSizeString(sizeString);
587}
588
589
590void
591BInfoWindow::OpenFilePanel(const entry_ref* ref)
592{
593	// Open a file dialog box to allow the user to select a new target
594	// for the sym link
595	if (fFilePanel == NULL) {
596		BMessenger runner(this);
597		BMessage message(kNewTargetSelected);
598		fFilePanel = new BFilePanel(B_OPEN_PANEL, &runner, ref,
599			B_FILE_NODE | B_SYMLINK_NODE | B_DIRECTORY_NODE,
600			false, &message);
601
602		if (fFilePanel != NULL) {
603			fFilePanel->SetButtonLabel(B_DEFAULT_BUTTON,
604				B_TRANSLATE("Select"));
605			fFilePanel->Window()->ResizeTo(500, 300);
606			BString title(B_TRANSLATE_COMMENT("Link \"%name\" to:",
607				"File dialog title for new sym link"));
608			title.ReplaceFirst("%name", fModel->Name());
609			fFilePanel->Window()->SetTitle(title.String());
610			fFilePanel->Show();
611			fFilePanelOpen = true;
612		}
613	} else if (!fFilePanelOpen) {
614		fFilePanel->Show();
615		fFilePanelOpen = true;
616	} else {
617		fFilePanelOpen = true;
618		fFilePanel->Window()->Activate(true);
619	}
620}
621
622
623