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