/* * Copyright (c) 2008 Stephan Aßmus . * Copyright (c) 2009 Philippe Saint-Pierre, stpere@gmail.com * All rights reserved. Distributed under the terms of the MIT license. * * Copyright (c) 1999 Mike Steed. You are free to use and distribute this software * as long as it is accompanied by it's documentation and this copyright notice. * The software comes with no warranty, etc. */ #include "PieView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Commands.h" #include "DiskUsage.h" #include "InfoWindow.h" #include "MainWindow.h" #include "Scanner.h" #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "Pie View" static const int32 kIdxGetInfo = 0; static const int32 kIdxOpen = 1; static const int32 kIdxOpenWith = 2; static const int32 kIdxRescan = 3; class AppMenuItem : public BMenuItem { public: AppMenuItem(const char* appSig, int category); virtual ~AppMenuItem(); virtual void GetContentSize(float* _width, float* _height); virtual void DrawContent(); int Category() const { return fCategory; } const entry_ref* AppRef() const { return &fAppRef; } bool IsValid() const { return fIsValid; } private: int fCategory; BBitmap* fIcon; entry_ref fAppRef; bool fIsValid; }; AppMenuItem::AppMenuItem(const char* appSig, int category) : BMenuItem(kEmptyStr, NULL), fCategory(category), fIcon(NULL), fIsValid(false) { if (be_roster->FindApp(appSig, &fAppRef) == B_NO_ERROR) { fIcon = new BBitmap(BRect(0.0, 0.0, 15.0, 15.0), B_RGBA32); if (BNodeInfo::GetTrackerIcon(&fAppRef, fIcon, B_MINI_ICON) == B_OK) { BEntry appEntry(&fAppRef); if (appEntry.InitCheck() == B_OK) { char name[B_FILE_NAME_LENGTH]; appEntry.GetName(name); SetLabel(name); fIsValid = true; } } } } AppMenuItem::~AppMenuItem() { delete fIcon; } void AppMenuItem::GetContentSize(float* _width, float* _height) { BMenuItem::GetContentSize(_width, _height); if (_width) *_width += fIcon->Bounds().Width(); if (_height) *_height = max_c(*_height, fIcon->Bounds().Height()); } void AppMenuItem::DrawContent() { float yOffset, height; GetContentSize(NULL, &height); yOffset = (height - fIcon->Bounds().Height()) / 2; Menu()->SetDrawingMode(B_OP_OVER); Menu()->MovePenBy(0.0, yOffset); Menu()->DrawBitmap(fIcon); Menu()->MovePenBy(fIcon->Bounds().Width() + kSmallHMargin, -yOffset); BMenuItem::DrawContent(); } // #pragma mark - PieView PieView::PieView(BVolume* volume) : BView(NULL, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_SUBPIXEL_PRECISE), fWindow(NULL), fScanner(NULL), fVolume(volume), fMouseOverInfo(), fClicked(false), fDragging(false), fUpdateFileAt(false) { fMouseOverMenu = new BPopUpMenu(kEmptyStr, false, false); fMouseOverMenu->AddItem(new BMenuItem(B_TRANSLATE("Get info"), NULL), kIdxGetInfo); fMouseOverMenu->AddItem(new BMenuItem(B_TRANSLATE("Open"), NULL), kIdxOpen); fFileUnavailableMenu = new BPopUpMenu(kEmptyStr, false, false); BMenuItem* item = new BMenuItem(B_TRANSLATE("file unavailable"), NULL); item->SetEnabled(false); fFileUnavailableMenu->AddItem(item); BFont font; GetFont(&font); font.SetSize(ceilf(font.Size() * 1.33)); font.SetFace(B_BOLD_FACE); SetFont(&font); struct font_height fh; font.GetHeight(&fh); fFontHeight = ceilf(fh.ascent) + ceilf(fh.descent) + ceilf(fh.leading); } void PieView::AttachedToWindow() { fWindow = (MainWindow*)Window(); if (Parent()) { SetViewColor(Parent()->ViewColor()); SetLowColor(Parent()->ViewColor()); } } PieView::~PieView() { delete fMouseOverMenu; delete fFileUnavailableMenu; if (fScanner != NULL) fScanner->RequestQuit(); } void PieView::MessageReceived(BMessage* message) { switch (message->what) { case kBtnCancel: if (fScanner != NULL) fScanner->Cancel(); break; case kBtnRescan: if (fVolume != NULL) { if (fScanner != NULL) fScanner->Refresh(); else _ShowVolume(fVolume); fWindow->EnableCancel(); Invalidate(); } break; case kScanDone: fWindow->EnableRescan(); // fall-through case kScanProgress: Invalidate(); break; default: BView::MessageReceived(message); } } void PieView::MouseDown(BPoint where) { BMessage* current = Window()->CurrentMessage(); uint32 buttons; if (current->FindInt32("buttons", (int32*)&buttons) != B_OK) buttons = B_PRIMARY_MOUSE_BUTTON; FileInfo* info = _FileAt(where); if (info == NULL || info->pseudo) return; if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) { fClicked = true; fDragStart = where; fClickedFile = info; SetMouseEventMask(B_POINTER_EVENTS); } else if (buttons & B_SECONDARY_MOUSE_BUTTON) { where = ConvertToScreen(where); _ShowContextMenu(info, where); } } void PieView::MouseUp(BPoint where) { if (fClicked && !fDragging) { // The primary mouse button was released and there's no dragging happening. FileInfo* info = _FileAt(where); if (info != NULL) { BMessage* current = Window()->CurrentMessage(); uint32 modifiers; if (current->FindInt32("modifiers", (int32*)&modifiers) != B_OK) modifiers = 0; if ((modifiers & B_COMMAND_KEY) != 0) { // launch the app on command-click _Launch(info); } else { // zoom in or out if (info == fScanner->CurrentDir()) { fScanner->ChangeDir(info->parent); fLastWhere = where; fUpdateFileAt = true; Invalidate(); } else if (info->children.size() > 0) { fScanner->ChangeDir(info); fLastWhere = where; fUpdateFileAt = true; Invalidate(); } } } } fClicked = false; fDragging = false; } void PieView::MouseMoved(BPoint where, uint32 transit, const BMessage* dragMessage) { if (fClicked) { // Primary mouse button is down. if (fDragging) return; // If the mouse has moved far enough, initiate dragging. BPoint diff = where - fDragStart; float distance = sqrtf(diff.x * diff.x + diff.y * diff.x); if (distance > kDragThreshold) { fDragging = true; BBitmap* icon = new BBitmap(BRect(0.0, 0.0, 31.0, 31.0), B_RGBA32); if (BNodeInfo::GetTrackerIcon(&fClickedFile->ref, icon, B_LARGE_ICON) == B_OK) { BMessage msg(B_SIMPLE_DATA); msg.AddRef("refs", &fClickedFile->ref); DragMessage(&msg, icon, B_OP_BLEND, BPoint(15.0, 15.0)); } else delete icon; } } else { // Mouse button is not down, display file info. if (transit == B_EXITED_VIEW) { // Clear status view fWindow->ShowInfo(NULL); } else { // Display file information. fWindow->ShowInfo(_FileAt(where)); } } } void PieView::Draw(BRect updateRect) { if (fScanner != NULL) { // There is a current volume. if (fScanner->IsBusy()) { // Show progress of scanning. _DrawProgressBar(updateRect); } else if (fScanner->Snapshot() != NULL) { _DrawPieChart(updateRect); if (fUpdateFileAt) { fWindow->ShowInfo(_FileAt(fLastWhere)); fUpdateFileAt = false; } } } } void PieView::SetPath(BPath path) { if (fScanner == NULL) _ShowVolume(fVolume); if (fScanner != NULL) { string desiredPath(path.Path()); fScanner->SetDesiredPath(desiredPath); Invalidate(); } } // #pragma mark - private void PieView::_ShowVolume(BVolume* volume) { if (volume != NULL) { if (fScanner == NULL) fScanner = new Scanner(volume, this); if (fScanner->Snapshot() == NULL) fScanner->Refresh(); } Invalidate(); } void PieView::_DrawProgressBar(BRect updateRect) { // Show the progress of the scanning operation. fMouseOverInfo.clear(); // Draw the progress bar. BRect b = Bounds(); float bx = floorf((b.left + b.Width() - kProgBarWidth) / 2.0); float by = floorf((b.top + b.Height() - kProgBarHeight) / 2.0); float ex = bx + kProgBarWidth; float ey = by + kProgBarHeight; float mx = bx + floorf((kProgBarWidth - 2.0) * fScanner->Progress() + 0.5); const rgb_color kBarColor = {50, 150, 255, 255}; BRect barFrame(bx, by, ex, ey); be_control_look->DrawStatusBar(this, barFrame, updateRect, ui_color(B_PANEL_BACKGROUND_COLOR), kBarColor, mx); // Tell what we are doing. const char* task = fScanner->Task(); float strWidth = StringWidth(task); bx = (b.left + b.Width() - strWidth) / 2.0; by -= fFontHeight + 2.0 * kSmallVMargin; SetHighColor(ui_color(B_PANEL_TEXT_COLOR)); DrawString(task, BPoint(bx, by)); } void PieView::_DrawPieChart(BRect updateRect) { BRect pieRect = Bounds(); if (!updateRect.Intersects(pieRect)) return; pieRect.InsetBy(kPieOuterMargin, kPieOuterMargin); // constraint proportions if (pieRect.Width() > pieRect.Height()) { float moveBy = (pieRect.Width() - pieRect.Height()) / 2; pieRect.left += moveBy; pieRect.right -= moveBy; } else { float moveBy = (pieRect.Height() - pieRect.Width()) / 2; pieRect.top -= moveBy; pieRect.bottom += moveBy; } int colorIdx = 0; FileInfo* currentDir = fScanner->CurrentDir(); FileInfo* parent = currentDir; while (parent != NULL) { parent = parent->parent; colorIdx++; } _DrawDirectory(pieRect, currentDir, 0.0, 0.0, colorIdx % kBasePieColorCount, 0); } float PieView::_DrawDirectory(BRect b, FileInfo* info, float parentSpan, float beginAngle, int colorIdx, int level) { if (b.Width() < 2.0 * (kPieCenterSize + level * kPieRingSize + kPieOuterMargin + kPieInnerMargin)) { return 0.0; } if (info != NULL && info->color >= 0 && level == 0) colorIdx = info->color % kBasePieColorCount; else if (info != NULL) info->color = colorIdx; VolumeSnapshot* snapshot = fScanner->Snapshot(); float cx = floorf(b.left + b.Width() / 2.0 + 0.5); float cy = floorf(b.top + b.Height() / 2.0 + 0.5); float mySpan; if (level == 0) { // Make room for mouse over info. fMouseOverInfo.clear(); fMouseOverInfo[0] = SegmentList(); // Draw the center circle. const char* displayName; if (info == NULL) { // NULL represents the entire volume. Show used and free space in // the center circle, with the used segment representing the // volume's root directory. off_t volCapacity = snapshot->capacity; mySpan = 360.0 * (volCapacity - snapshot->freeBytes) / volCapacity; SetHighColor(kEmptySpcColor); FillEllipse(BPoint(cx, cy), kPieCenterSize, kPieCenterSize); SetHighColor(kBasePieColor[0]); FillArc(BPoint(cx, cy), kPieCenterSize, kPieCenterSize, 0.0, mySpan); // Show total volume capacity. char label[B_PATH_NAME_LENGTH]; string_for_size(volCapacity, label, sizeof(label)); SetHighColor(kPieBGColor); SetDrawingMode(B_OP_OVER); DrawString(label, BPoint(cx - StringWidth(label) / 2.0, cy + fFontHeight + kSmallVMargin)); SetDrawingMode(B_OP_COPY); displayName = snapshot->name.c_str(); // Record in-use space and free space for use during MouseMoved(). info = snapshot->rootDir; info->color = colorIdx; fMouseOverInfo[0].push_back(Segment(0.0, mySpan, info)); if (mySpan < 360.0 - kMinSegmentSpan) { fMouseOverInfo[0].push_back(Segment(mySpan, 360.0, snapshot->freeSpace)); } } else { // Show a normal directory. SetHighColor(kBasePieColor[colorIdx]); FillEllipse(BRect(cx - kPieCenterSize, cy - kPieCenterSize, cx + kPieCenterSize + 0.5, cy + kPieCenterSize + 0.5)); displayName = info->ref.name; mySpan = 360.0; // Record the segment for use during MouseMoved(). fMouseOverInfo[0].push_back(Segment(0.0, mySpan, info)); } SetPenSize(1.0); SetHighColor(kOutlineColor); StrokeEllipse(BPoint(cx, cy), kPieCenterSize + 0.5, kPieCenterSize + 0.5); // Show the name of the volume or directory. BString label(displayName); BFont font; GetFont(&font); font.TruncateString(&label, B_TRUNCATE_END, 2.0 * (kPieCenterSize - kSmallHMargin)); float labelWidth = font.StringWidth(label.String()); SetHighColor(kPieBGColor); SetDrawingMode(B_OP_OVER); DrawString(label.String(), BPoint(cx - labelWidth / 2.0, cy)); SetDrawingMode(B_OP_COPY); beginAngle = 0.0; } else { // Draw an exterior segment. float parentSize; if (info->parent == NULL) parentSize = (float)snapshot->capacity; else parentSize = (float)info->parent->size; mySpan = parentSpan * (float)info->size / parentSize; if (mySpan >= kMinSegmentSpan) { const float tint = 1.4f - level * 0.08f; float radius = kPieCenterSize + level * kPieRingSize - kPieRingSize / 2.0; // Draw the grey border SetHighColor(tint_color(kOutlineColor, tint)); SetPenSize(kPieRingSize + 1.5f); StrokeArc(BPoint(cx, cy), radius, radius, beginAngle - 0.001f * radius, mySpan + 0.002f * radius); // Draw the colored area rgb_color color = tint_color(kBasePieColor[colorIdx], tint); SetHighColor(color); SetPenSize(kPieRingSize); StrokeArc(BPoint(cx, cy), radius, radius, beginAngle, mySpan); // Record the segment for use during MouseMoved(). if (fMouseOverInfo.find(level) == fMouseOverInfo.end()) fMouseOverInfo[level] = SegmentList(); fMouseOverInfo[level].push_back( Segment(beginAngle, beginAngle + mySpan, info)); } } // Draw children. vector::iterator i = info->children.begin(); while (i != info->children.end()) { float childSpan = _DrawDirectory(b, *i, mySpan, beginAngle, colorIdx, level + 1); if (childSpan >= kMinSegmentSpan) { beginAngle += childSpan; colorIdx = (colorIdx + 1) % kBasePieColorCount; } i++; } return mySpan; } FileInfo* PieView::_FileAt(const BPoint& where) { BRect b = Bounds(); float cx = b.left + b.Width() / 2.0; float cy = b.top + b.Height() / 2.0; float dx = where.x - cx; float dy = where.y - cy; float dist = sqrt(dx * dx + dy * dy); int level; if (dist < kPieCenterSize) level = 0; else level = 1 + (int)((dist - kPieCenterSize) / kPieRingSize); float angle = rad2deg(atan(dy / dx)); angle = ((dx < 0.0) ? 180.0 : (dy < 0.0) ? 0.0 : 360.0) - angle; if (fMouseOverInfo.find(level) == fMouseOverInfo.end()) { // No files in this level (ring) of the pie. return NULL; } SegmentList s = fMouseOverInfo[level]; SegmentList::iterator i = s.begin(); while (i != s.end() && (angle < (*i).begin || (*i).end < angle)) i++; if (i == s.end()) { // Nothing at this angle. return NULL; } return (*i).info; } void PieView::_AddAppToList(vector& list, const char* appSig, int category) { // skip self. if (strcmp(appSig, kAppSignature) == 0) return; AppMenuItem* item = new AppMenuItem(appSig, category); if (item->IsValid()) { vector::iterator i = list.begin(); while (i != list.end()) { if (*item->AppRef() == *(*i)->AppRef()) { // Skip duplicates. delete item; return; } i++; } list.push_back(item); } else { // Skip items that weren't constructed successfully. delete item; } } BMenu* PieView::_BuildOpenWithMenu(FileInfo* info) { vector appList; // Get preferred app. BMimeType* type = info->Type(); char appSignature[B_MIME_TYPE_LENGTH]; if (type->GetPreferredApp(appSignature) == B_OK) _AddAppToList(appList, appSignature, 1); // Get apps that handle this subtype and supertype. BMessage msg; if (type->GetSupportingApps(&msg) == B_OK) { int32 subs, supers, i; msg.FindInt32("be:sub", &subs); msg.FindInt32("be:super", &supers); const char* appSig; for (i = 0; i < subs; i++) { msg.FindString("applications", i, &appSig); _AddAppToList(appList, appSig, 2); } int hold = i; for (i = 0; i < supers; i++) { msg.FindString("applications", i + hold, &appSig); _AddAppToList(appList, appSig, 3); } } // Get apps that handle any type. if (BMimeType::GetWildcardApps(&msg) == B_OK) { const char* appSig; for (int32 i = 0; true; i++) { if (msg.FindString("applications", i, &appSig) == B_OK) _AddAppToList(appList, appSig, 4); else break; } } delete type; BMenu* openWith = new BMenu(B_TRANSLATE("Open with")); if (appList.size() == 0) { BMenuItem* item = new BMenuItem(B_TRANSLATE("no supporting apps"), NULL); item->SetEnabled(false); openWith->AddItem(item); } else { vector::iterator i = appList.begin(); int category = (*i)->Category(); while (i != appList.end()) { if (category != (*i)->Category()) { openWith->AddSeparatorItem(); category = (*i)->Category(); } openWith->AddItem(*i); i++; } } return openWith; } void PieView::_ShowContextMenu(FileInfo* info, BPoint p) { BRect openRect(p.x - 5.0, p.y - 5.0, p.x + 5.0, p.y + 5.0); // Display the open-with menu only if the file is still available. BNode node(&info->ref); if (node.InitCheck() == B_OK) { // Add "Open With" submenu. BMenu* openWith = _BuildOpenWithMenu(info); fMouseOverMenu->AddItem(openWith, kIdxOpenWith); // Add a "Rescan" option for folders. BMenuItem* rescan = NULL; if (info->children.size() > 0) { rescan = new BMenuItem(B_TRANSLATE("Rescan"), NULL); fMouseOverMenu->AddItem(rescan, kIdxRescan); } BMenuItem* item = fMouseOverMenu->Go(p, false, true, openRect); if (item != NULL) { switch (fMouseOverMenu->IndexOf(item)) { case kIdxGetInfo: _OpenInfo(info, p); break; case kIdxOpen: _Launch(info); break; case kIdxRescan: fScanner->Refresh(info); fWindow->EnableCancel(); Invalidate(); break; default: // must be "Open With" submenu _Launch(info, ((AppMenuItem*)item)->AppRef()); break; } } if (rescan != NULL) { fMouseOverMenu->RemoveItem(rescan); delete rescan; } fMouseOverMenu->RemoveItem(openWith); delete openWith; } else // The file is no longer available. fFileUnavailableMenu->Go(p, false, true, openRect); } void PieView::_Launch(FileInfo* info, const entry_ref* appRef) { BMessage msg(B_REFS_RECEIVED); msg.AddRef("refs", &info->ref); if (appRef == NULL) { // Let the registrar pick an app based on the file's MIME type. BMimeType* type = info->Type(); be_roster->Launch(type->Type(), &msg); delete type; } else { // Launch a designated app to handle this file. be_roster->Launch(appRef, &msg); } } void PieView::_OpenInfo(FileInfo* info, BPoint p) { BMessenger tracker(kTrackerSignature); if (!tracker.IsValid()) { new InfoWin(p, info, Window()); } else { BMessage message(kGetInfo); message.AddRef("refs", &info->ref); tracker.SendMessage(&message); } }