haiku/src/kits/tracker/NavMenu.cpp

852 lines
20 KiB
C++
Raw Normal View History

/*
Open Tracker License
Terms and Conditions
Copyright (c) 1991-2000, Be Incorporated. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice applies to all licensees
and shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Except as contained in this notice, the name of Be Incorporated shall not be
used in advertising or otherwise to promote the sale, use or other dealings in
this Software without prior written authorization from Be Incorporated.
Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or registered trademarks
of Be Incorporated in the United States and other countries. Other brand product
names are registered trademarks or trademarks of their respective holders.
All rights reserved.
*/
// NavMenu is a hierarchical menu of volumes, folders, files and queries
// displays icons, uses the SlowMenu API for full interruptability
#include <string.h>
#include <stdlib.h>
#include <Debug.h>
#include <StopWatch.h>
#include <Application.h>
#include <Directory.h>
#include <Query.h>
#include <Path.h>
#include <Screen.h>
#include <VolumeRoster.h>
#include <Volume.h>
#include "Attributes.h"
#include "Commands.h"
#include "ContainerWindow.h"
#include "DesktopPoseView.h"
#include "Tracker.h"
#include "FSUtils.h"
#include "IconMenuItem.h"
#include "MimeTypes.h"
#include "NavMenu.h"
#include "PoseView.h"
#include "Thread.h"
#include "FunctionObject.h"
#include "QueryPoseView.h"
namespace BPrivate {
const int32 kMinMenuWidth = 150;
enum nav_flags {
kVolumesOnly = 1,
kShowParent = 2
};
bool
SpringLoadedFolderCompareMessages(const BMessage *incoming, const BMessage *dragmessage)
{
if (!dragmessage || !incoming)
return false;
bool retvalue=false;
for (int32 inIndex=0; incoming->HasRef("refs", inIndex); inIndex++) {
entry_ref inRef;
if (incoming->FindRef("refs", inIndex, &inRef) != B_OK) {
retvalue = false;
break;
}
bool inRefMatch = false;
for (int32 dragIndex=0; dragmessage->HasRef("refs", dragIndex); dragIndex++) {
entry_ref dragRef;
if (dragmessage->FindRef("refs", dragIndex, &dragRef) != B_OK) {
inRefMatch = false;
break;
}
// if the incoming ref matches any ref in the drag ref
// then we can try the next incoming ref
if (inRef == dragRef) {
inRefMatch = true;
break;
}
}
retvalue = inRefMatch;
if (!inRefMatch)
break;
}
if (retvalue) {
// if all the refs match
// try and see if this is another instance of the same
// drag contents, but new drag
retvalue = false;
BPoint inPt, dPt;
if (incoming->FindPoint("click_pt", &inPt) == B_OK)
if (dragmessage->FindPoint("click_pt", &dPt) == B_OK)
retvalue = (inPt == dPt);
}
return retvalue;
}
void
SpringLoadedFolderSetMenuStates(const BMenu* menu, const BObjectList<BString> *typeslist)
{
if (!menu || !typeslist)
return;
// if a types list exists
// iterate through the list and see if each item
// can support any item in the list
// set the enabled state of the item
int32 count = menu->CountItems();
for (int32 index = 0 ; index < count ; index++) {
ModelMenuItem *item = dynamic_cast<ModelMenuItem *>(menu->ItemAt(index));
if (!item)
continue;
const Model *model = item->TargetModel();
if (!model)
continue;
if (model->IsSymLink()) {
// find out what the model is, resolve if symlink
BEntry entry(model->EntryRef(), true);
if (entry.InitCheck() == B_OK) {
if (entry.IsDirectory()) {
// folder? always keep enabled
item->SetEnabled(true);
} else {
// other, check its support
Model resolvedModel(&entry);
int32 supported = resolvedModel.SupportsMimeType(NULL, typeslist);
item->SetEnabled(supported != kDoesNotSupportType);
}
} else
// bad entry ref (bad symlink?), disable
item->SetEnabled(false);
} else if (model->IsDirectory() || model->IsRoot() || model->IsVolume())
// always enabled if a container
item->SetEnabled(true);
else if (model->IsFile() || model->IsExecutable()) {
int32 supported = model->SupportsMimeType(NULL, typeslist);
item->SetEnabled(supported != kDoesNotSupportType);
} else
item->SetEnabled(false);
}
}
void
SpringLoadedFolderAddUniqueTypeToList(entry_ref *ref, BObjectList<BString> *typeslist)
{
if (!ref || !typeslist)
return;
// get the mime type for the current ref
BNodeInfo nodeinfo;
BNode node(ref);
if (node.InitCheck() != B_OK)
return;
nodeinfo.SetTo(&node);
char mimestr[B_MIME_TYPE_LENGTH];
// add it to the list
if (nodeinfo.GetType(mimestr) == B_OK && strlen(mimestr) > 0) {
// if this is a symlink, add symlink to the list (below)
// resolve the symlink, add the resolved type
// to the list
if (strcmp(B_LINK_MIMETYPE, mimestr) == 0) {
BEntry entry(ref, true);
if (entry.InitCheck() == B_OK) {
entry_ref resolvedRef;
if (entry.GetRef(&resolvedRef) == B_OK)
SpringLoadedFolderAddUniqueTypeToList(&resolvedRef, typeslist);
}
}
// scan the current list, don't add dups
bool unique = true;
int32 count = typeslist->CountItems();
for (int32 index = 0 ; index < count ; index++) {
if (typeslist->ItemAt(index)->Compare(mimestr) == 0) {
unique = false;
break;
}
}
if (unique)
typeslist->AddItem(new BString(mimestr));
}
}
void
SpringLoadedFolderCacheDragData(const BMessage *incoming, BMessage **message, BObjectList<BString> **typeslist)
{
if (!incoming)
return;
delete *message;
delete *typeslist;
BMessage *localMessage = new BMessage(*incoming);
BObjectList<BString> *localTypesList = new BObjectList<BString>(10, true);
for (int32 index=0; incoming->HasRef("refs", index); index++) {
entry_ref ref;
if (incoming->FindRef("refs", index, &ref) != B_OK)
continue;
SpringLoadedFolderAddUniqueTypeToList(&ref, localTypesList);
}
*message = localMessage;
*typeslist = localTypesList;
}
}
// #pragma mark -
BNavMenu::BNavMenu(const char *title, uint32 message, const BHandler *target,
BWindow *parentWindow, const BObjectList<BString> *list)
: BSlowMenu(title),
fMessage(message),
fMessenger(target, target->Looper()),
fParentWindow(parentWindow),
fFlags(0),
fItemList(0),
fContainer(0),
fTypesList(list)
{
InitIconPreloader();
SetFont(be_plain_font);
// add the parent window to the invocation message so that it
// can be closed if option modifier held down during invocation
BContainerWindow *originatingWindow = dynamic_cast<BContainerWindow *>(fParentWindow);
if (originatingWindow)
fMessage.AddData("nodeRefsToClose", B_RAW_TYPE,
originatingWindow->TargetModel()->NodeRef(), sizeof (node_ref));
// too long to have triggers
SetTriggersEnabled(false);
}
BNavMenu::BNavMenu(const char *title, uint32 message, const BMessenger &messenger,
BWindow *parentWindow, const BObjectList<BString> *list)
: BSlowMenu(title),
fMessage(message),
fMessenger(messenger),
fParentWindow(parentWindow),
fFlags(0),
fItemList(0),
fContainer(0),
fTypesList(list)
{
InitIconPreloader();
SetFont(be_plain_font);
// add the parent window to the invocation message so that it
// can be closed if option modifier held down during invocation
BContainerWindow *originatingWindow = dynamic_cast<BContainerWindow *>(fParentWindow);
if (originatingWindow)
fMessage.AddData("nodeRefsToClose", B_RAW_TYPE,
originatingWindow->TargetModel()->NodeRef(), sizeof (node_ref));
// too long to have triggers
SetTriggersEnabled(false);
}
BNavMenu::~BNavMenu()
{
}
void
BNavMenu::AttachedToWindow()
{
BSlowMenu::AttachedToWindow();
SpringLoadedFolderSetMenuStates(this, fTypesList);
// if dragging (fTypesList != NULL)
// set the menu items enabled state
// relative to the ability to handle an item in the
// drag message
ResetTargets();
// allow an opportunity to reset the target for each of the items
}
void
BNavMenu::DetachedFromWindow()
{
// does this need to set this to null?
// the parent, handling dnd should set this
// appropriately
//
// if this changes, BeMenu and RecentsMenu
// in Deskbar should also change
fTypesList = NULL;
}
void
BNavMenu::ResetTargets()
{
SetTargetForItems(Target());
}
void
BNavMenu::ForceRebuild()
{
ClearMenuBuildingState();
fMenuBuilt = false;
}
bool
BNavMenu::NeedsToRebuild() const
{
return !fMenuBuilt;
}
void
BNavMenu::SetNavDir(const entry_ref *ref)
{
ForceRebuild();
// reset the slow menu building mechanism so we can add more stuff
fNavDir = *ref;
}
void
BNavMenu::ClearMenuBuildingState()
{
delete fContainer;
fContainer = NULL;
// item list is non-owning, need to delete the items because
// they didn't get added to the menu
if (fItemList) {
int32 count = fItemList->CountItems();
for (int32 index = count - 1; index >= 0; index--)
delete RemoveItem(index);
delete fItemList;
fItemList = NULL;
}
}
bool
BNavMenu::StartBuildingItemList()
{
BEntry entry;
if (fNavDir.device < 0 || entry.SetTo(&fNavDir) != B_OK
|| !entry.Exists())
return false;
fItemList = new BObjectList<BMenuItem>(50);
fIteratingDesktop = false;
BDirectory parent;
status_t status = entry.GetParent(&parent);
// if ref is the root item then build list of volume root dirs
fFlags = uint8((fFlags & ~kVolumesOnly) | (status == B_ENTRY_NOT_FOUND ? kVolumesOnly : 0));
if (fFlags & kVolumesOnly)
return true;
Model startModel(&entry, true);
if (startModel.InitCheck() != B_OK || !startModel.IsContainer())
return false;
if (startModel.IsQuery())
fContainer = new QueryEntryListCollection(&startModel);
else if (FSIsDeskDir(&entry)) {
fIteratingDesktop = true;
fContainer = DesktopPoseView::InitDesktopDirentIterator(0, startModel.EntryRef());
AddRootItemsIfNeeded();
} else if (FSIsTrashDir(&entry)) {
// the trash window needs to display a union of all the
// trash folders from all the mounted volumes
BVolumeRoster volRoster;
volRoster.Rewind();
BVolume volume;
fContainer = new EntryIteratorList();
while (volRoster.GetNextVolume(&volume) == B_OK) {
if (volume.IsReadOnly() || !volume.IsPersistent())
continue;
BDirectory trashDir;
if (FSGetTrashDir(&trashDir, volume.Device()) == B_OK)
dynamic_cast<EntryIteratorList *>(fContainer)->
AddItem(new DirectoryEntryList(trashDir));
}
} else
fContainer = new DirectoryEntryList(*dynamic_cast<BDirectory *>
(startModel.Node()));
if (fContainer == NULL || fContainer->InitCheck() != B_OK)
return false;
fContainer->Rewind();
return true;
}
void
BNavMenu::AddRootItemsIfNeeded()
{
BVolumeRoster roster;
roster.Rewind();
BVolume volume;
while (roster.GetNextVolume(&volume) == B_OK) {
BDirectory root;
BEntry entry;
if (!volume.IsPersistent()
|| volume.GetRootDirectory(&root) != B_OK
|| root.GetEntry(&entry) != B_OK)
continue;
Model model(&entry);
AddOneItem(&model);
}
}
bool
BNavMenu::AddNextItem()
{
if (fFlags & kVolumesOnly) {
BuildVolumeMenu();
return false;
}
// limit nav menus to 500 items only
if (fItemList->CountItems() > 500)
return false;
BEntry entry;
if (fContainer->GetNextEntry(&entry) != B_OK) {
// we're finished
return false;
}
if (TrackerSettings().HideDotFiles()) {
char name[B_FILE_NAME_LENGTH];
if (entry.GetName(name) == B_OK && name[0] == '.')
return true;
}
Model model(&entry, true);
if (model.InitCheck() != B_OK) {
// PRINT(("not showing hidden item %s, wouldn't open\n", model->Name()));
return true;
}
QueryEntryListCollection *queryContainer
= dynamic_cast<QueryEntryListCollection*>(fContainer);
if (queryContainer && !queryContainer->ShowResultsFromTrash()
&& FSInTrashDir(model.EntryRef())) {
// query entry is in trash and shall not be shown
return true;
}
ssize_t size = -1;
PoseInfo poseInfo;
if (model.Node())
size = model.Node()->ReadAttr(kAttrPoseInfo, B_RAW_TYPE, 0,
&poseInfo, sizeof(poseInfo));
model.CloseNode();
// item might be in invisible
// ToDo:
// use more of PoseView's filtering here
if ((size == sizeof(poseInfo)
&& !BPoseView::PoseVisible(&model, &poseInfo, false))
|| (fIteratingDesktop && !ShouldShowDesktopPose(fNavDir.device,
&model, &poseInfo))) {
// PRINT(("not showing hidden item %s\n", model.Name()));
return true;
}
AddOneItem(&model);
return true;
}
void
BNavMenu::AddOneItem(Model *model)
{
BMenuItem *item = NewModelItem(model, &fMessage, fMessenger, false,
dynamic_cast<BContainerWindow *>(fParentWindow),
fTypesList, &fTrackingHook);
if (item)
fItemList->AddItem(item);
}
ModelMenuItem *
BNavMenu::NewModelItem(Model *model, const BMessage *invokeMessage,
const BMessenger &target, bool suppressFolderHierarchy,
BContainerWindow *parentWindow, const BObjectList<BString> *typeslist,
TrackingHookData *hook)
{
if (model->InitCheck() != B_OK)
return 0;
entry_ref ref;
bool container = false;
if (model->IsSymLink()) {
Model *newResolvedModel = 0;
Model *result = model->LinkTo();
if (!result) {
newResolvedModel = new Model(model->EntryRef(), true, true);
if (newResolvedModel->InitCheck() != B_OK) {
// broken link, still can show though, bail
delete newResolvedModel;
result = 0;
} else
result = newResolvedModel;
}
if (result) {
BModelOpener opener(result);
// open the model, if it ain't open already
PoseInfo poseInfo;
ssize_t size = -1;
if (result->Node())
size = result->Node()->ReadAttr(kAttrPoseInfo, B_RAW_TYPE, 0,
&poseInfo, sizeof(poseInfo));
result->CloseNode();
if (size == sizeof(poseInfo) && !BPoseView::PoseVisible(result,
&poseInfo, false)) {
// link target sez it doesn't want to be visible,
// don't show the link
PRINT(("not showing hidden item %s\n", model->Name()));
delete newResolvedModel;
return 0;
}
ref = *result->EntryRef();
container = result->IsContainer();
}
model->SetLinkTo(result);
} else {
ref = *model->EntryRef();
container = model->IsContainer();
}
BMessage *message = new BMessage(*invokeMessage);
message->AddRef("refs", model->EntryRef());
// Truncate the name if necessary
BString truncatedString(model->Name());
be_plain_font->TruncateString(&truncatedString, B_TRUNCATE_END,
GetMaxMenuWidth());
ModelMenuItem *item = NULL;
if (!container || suppressFolderHierarchy) {
item = new ModelMenuItem(model, truncatedString.String(), message);
if (invokeMessage->what != B_REFS_RECEIVED)
item->SetEnabled(false);
// the above is broken for FavoritesMenu::AddNextItem, which uses a
// workaround - should fix this
} else {
BNavMenu *menu = new BNavMenu(truncatedString.String(),
invokeMessage->what, target, parentWindow, typeslist);
menu->SetNavDir(&ref);
if (hook)
menu->InitTrackingHook(hook->fTrackingHook, &(hook->fTarget),
hook->fDragMessage);
item = new ModelMenuItem(model, menu);
item->SetMessage(message);
}
return item;
}
void
BNavMenu::BuildVolumeMenu()
{
BVolumeRoster roster;
BVolume volume;
roster.Rewind();
while (roster.GetNextVolume(&volume) == B_OK) {
if (!volume.IsPersistent())
continue;
BDirectory startDir;
if (volume.GetRootDirectory(&startDir) == B_OK) {
BEntry entry;
startDir.GetEntry(&entry);
Model *model = new Model(&entry);
if (model->InitCheck() != B_OK) {
delete model;
continue;
}
BNavMenu *menu = new BNavMenu(model->Name(), fMessage.what,
fMessenger, fParentWindow, fTypesList);
menu->SetNavDir(model->EntryRef());
ASSERT(menu->Name());
ModelMenuItem *item = new ModelMenuItem(model, menu);
BMessage *message = new BMessage(fMessage);
message->AddRef("refs", model->EntryRef());
item->SetMessage(message);
fItemList->AddItem(item);
ASSERT(item->Label());
}
}
}
int
BNavMenu::CompareFolderNamesFirstOne(const BMenuItem *i1, const BMenuItem *i2)
{
const ModelMenuItem *item1 = dynamic_cast<const ModelMenuItem *>(i1);
const ModelMenuItem *item2 = dynamic_cast<const ModelMenuItem *>(i2);
if (item1 != NULL && item2 != NULL)
return item1->TargetModel()->CompareFolderNamesFirst(item2->TargetModel());
return strcasecmp(i1->Label(), i2->Label());
}
int
BNavMenu::CompareOne(const BMenuItem *i1, const BMenuItem *i2)
{
return strcasecmp(i1->Label(), i2->Label());
}
void
BNavMenu::DoneBuildingItemList()
{
// add sorted items to menu
if (TrackerSettings().SortFolderNamesFirst())
fItemList->SortItems(CompareFolderNamesFirstOne);
else
fItemList->SortItems(CompareOne);
// if the parent link should be shown, it will be the first
// entry in the menu - but don't add the item if we're already
// at the file system's root
if (fFlags & kShowParent) {
BDirectory directory(&fNavDir);
BEntry entry(&fNavDir);
if (!directory.IsRootDirectory()
&& entry.GetParent(&entry) == B_OK) {
Model model(&entry, true);
BLooper *looper;
AddNavParentDir(&model,fMessage.what,fMessenger.Target(&looper));
}
}
int32 count = fItemList->CountItems();
for (int32 index = 0; index < count; index++)
AddItem(fItemList->ItemAt(index));
fItemList->MakeEmpty();
if (!count) {
BMenuItem *item = new BMenuItem("Empty Folder", 0);
item->SetEnabled(false);
AddItem(item);
}
SetTargetForItems(fMessenger);
}
int32
BNavMenu::GetMaxMenuWidth(void)
{
int32 width = (int32)(BScreen().Frame().Width() / 4);
return (width < kMinMenuWidth) ? kMinMenuWidth : width;
}
void
BNavMenu::AddNavDir(const Model *model, uint32 what, BHandler *target,
bool populateSubmenu)
{
BMessage *message = new BMessage((uint32)what);
message->AddRef("refs", model->EntryRef());
ModelMenuItem *item = NULL;
if (populateSubmenu) {
BNavMenu *navMenu = new BNavMenu(model->Name(), what, target);
navMenu->SetNavDir(model->EntryRef());
navMenu->InitTrackingHook(fTrackingHook.fTrackingHook, &(fTrackingHook.fTarget),
fTrackingHook.fDragMessage);
item = new ModelMenuItem(model, navMenu);
item->SetMessage(message);
} else
item = new ModelMenuItem(model, model->Name(), message);
AddItem(item);
}
void
BNavMenu::AddNavParentDir(const char *name,const Model *model,uint32 what,BHandler *target)
{
BNavMenu *menu = new BNavMenu(name,what,target);
menu->SetNavDir(model->EntryRef());
menu->SetShowParent(true);
menu->InitTrackingHook(fTrackingHook.fTrackingHook, &(fTrackingHook.fTarget),
fTrackingHook.fDragMessage);
BMenuItem *item = new SpecialModelMenuItem(model,menu);
BMessage *message = new BMessage(what);
message->AddRef("refs",model->EntryRef());
item->SetMessage(message);
AddItem(item);
}
void
BNavMenu::AddNavParentDir(const Model *model, uint32 what, BHandler *target)
{
AddNavParentDir("parent folder",model,what,target);
}
void
BNavMenu::SetShowParent(bool show)
{
fFlags = uint8((fFlags & ~kShowParent) | (show ? kShowParent : 0));
}
void
BNavMenu::SetTypesList(const BObjectList<BString> *list)
{
fTypesList = list;
}
const BObjectList<BString> *
BNavMenu::TypesList() const
{
return fTypesList;
}
void
BNavMenu::SetTarget(const BMessenger &msngr)
{
fMessenger = msngr;
}
BMessenger
BNavMenu::Target()
{
return fMessenger;
}
TrackingHookData *
BNavMenu::InitTrackingHook(bool (*hook)(BMenu *, void *), const BMessenger *target,
const BMessage *dragMessage)
{
fTrackingHook.fTrackingHook = hook;
if (target)
fTrackingHook.fTarget = *target;
fTrackingHook.fDragMessage = dragMessage;
SetTrackingHookDeep(this, hook, &fTrackingHook);
return &fTrackingHook;
}
void
BNavMenu::SetTrackingHookDeep(BMenu *menu, bool (*func)(BMenu *, void *), void *state)
{
menu->SetTrackingHook(func, state);
int32 count = menu->CountItems();
for (int32 index = 0 ; index < count; index++) {
BMenuItem *item = menu->ItemAt(index);
if (!item)
continue;
BMenu *submenu = item->Submenu();
if (submenu)
SetTrackingHookDeep(submenu, func, state);
}
}