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