/*
* Copyright (C) 2010 Stephan Aßmus <superstippi@gmx.de>
*
* All rights reserved. Distributed under the terms of the MIT License.
*/
#include "DownloadProgressView.h"
#include <stdio.h>
#include <Alert.h>
#include <Application.h>
#include <Bitmap.h>
#include <Button.h>
#include <Catalog.h>
#include <Clipboard.h>
#include <Directory.h>
#include <DateTimeFormat.h>
#include <DurationFormat.h>
#include <Entry.h>
#include <FindDirectory.h>
#include <GroupLayoutBuilder.h>
#include <Locale.h>
#include <MenuItem.h>
#include <NodeInfo.h>
#include <NodeMonitor.h>
#include <Notification.h>
#include <PopUpMenu.h>
#include <Roster.h>
#include <SpaceLayoutItem.h>
#include <StatusBar.h>
#include <StringView.h>
#include <TimeFormat.h>
#include "BrowserWindow.h"
#include "WebDownload.h"
#include "WebPage.h"
#include "StringForSize.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Download Window"
enum {
OPEN_DOWNLOAD = 'opdn',
RESTART_DOWNLOAD = 'rsdn',
CANCEL_DOWNLOAD = 'cndn',
REMOVE_DOWNLOAD = 'rmdn',
COPY_URL_TO_CLIPBOARD = 'curl',
OPEN_CONTAINING_FOLDER = 'opfd',
};
const bigtime_t kMaxUpdateInterval = 100000LL;
const bigtime_t kSpeedReferenceInterval = 500000LL;
const bigtime_t kShowSpeedInterval = 8000000LL;
const bigtime_t kShowEstimatedFinishInterval = 4000000LL;
bigtime_t DownloadProgressView::sLastEstimatedFinishSpeedToggleTime = -1;
bool DownloadProgressView::sShowSpeed = true;
static const time_t kSecondsPerDay = 24 * 60 * 60;
static const time_t kSecondsPerHour = 60 * 60;
class IconView : public BView {
public:
IconView(const BEntry& entry)
:
BView("Download icon", B_WILL_DRAW),
fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32),
fDimmedIcon(false)
{
SetDrawingMode(B_OP_OVER);
SetTo(entry);
}
IconView()
:
BView("Download icon", B_WILL_DRAW),
fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32),
fDimmedIcon(false)
{
SetDrawingMode(B_OP_OVER);
memset(fIconBitmap.Bits(), 0, fIconBitmap.BitsLength());
}
IconView(BMessage* archive)
:
BView("Download icon", B_WILL_DRAW),
fIconBitmap(archive),
fDimmedIcon(true)
{
SetDrawingMode(B_OP_OVER);
}
void SetTo(const BEntry& entry)
{
BNode node(&entry);
BNodeInfo info(&node);
info.GetTrackerIcon(&fIconBitmap, B_LARGE_ICON);
Invalidate();
}
void SetIconDimmed(bool iconDimmed)
{
if (fDimmedIcon != iconDimmed) {
fDimmedIcon = iconDimmed;
Invalidate();
}
}
bool IsIconDimmed() const
{
return fDimmedIcon;
}
status_t SaveSettings(BMessage* archive)
{
return fIconBitmap.Archive(archive);
}
virtual void AttachedToWindow()
{
AdoptParentColors();
}
virtual void Draw(BRect updateRect)
{
if (fDimmedIcon) {
SetDrawingMode(B_OP_ALPHA);
SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
SetHighColor(0, 0, 0, 100);
}
DrawBitmapAsync(&fIconBitmap);
}
virtual BSize MinSize()
{
return BSize(fIconBitmap.Bounds().Width(),
fIconBitmap.Bounds().Height());
}
virtual BSize PreferredSize()
{
return MinSize();
}
virtual BSize MaxSize()
{
return MinSize();
}
BBitmap* Bitmap()
{
return &fIconBitmap;
}
private:
BBitmap fIconBitmap;
bool fDimmedIcon;
};
class SmallButton : public BButton {
public:
SmallButton(const char* label, BMessage* message = NULL)
:
BButton(label, message)
{
BFont font;
GetFont(&font);
float size = ceilf(font.Size() * 0.8);
font.SetSize(max_c(8, size));
SetFont(&font, B_FONT_SIZE);
}
};
// #pragma mark - DownloadProgressView
DownloadProgressView::DownloadProgressView(BWebDownload* download)
:
BGroupView(B_HORIZONTAL, 8),
fDownload(download),
fURL(download->URL()),
fPath(download->Path())
{
}
DownloadProgressView::DownloadProgressView(const BMessage* archive)
:
BGroupView(B_HORIZONTAL, 8),
fDownload(NULL),
fURL(),
fPath()
{
const char* string;
if (archive->FindString("path", &string) == B_OK)
fPath.SetTo(string);
if (archive->FindString("url", &string) == B_OK)
fURL = string;
}
bool
DownloadProgressView::Init(BMessage* archive)
{
fCurrentSize = 0;
fExpectedSize = 0;
fLastUpdateTime = 0;
fBytesPerSecond = 0.0;
for (size_t i = 0; i < kBytesPerSecondSlots; i++)
fBytesPerSecondSlot[i] = 0.0;
fCurrentBytesPerSecondSlot = 0;
fLastSpeedReferenceSize = 0;
fEstimatedFinishReferenceSize = 0;
fProcessStartTime = fLastSpeedReferenceTime
= fEstimatedFinishReferenceTime = system_time();
SetViewColor(245, 245, 245);
SetFlags(Flags() | B_FULL_UPDATE_ON_RESIZE | B_WILL_DRAW);
if (archive) {
fStatusBar = new BStatusBar("download progress", fPath.Leaf());
float value;
if (archive->FindFloat("value", &value) == B_OK)
fStatusBar->SetTo(value);
} else
fStatusBar = new BStatusBar("download progress", "Download");
fStatusBar->SetMaxValue(100);
fStatusBar->SetBarHeight(12);
// fPath is only valid when constructed from archive (fDownload == NULL)
BEntry entry(fPath.Path());
if (archive) {
if (!entry.Exists())
fIconView = new IconView(archive);
else
fIconView = new IconView(entry);
} else
fIconView = new IconView();
if (!fDownload && (fStatusBar->CurrentValue() < 100 || !entry.Exists())) {
fTopButton = new SmallButton(B_TRANSLATE("Restart"),
new BMessage(RESTART_DOWNLOAD));
} else {
fTopButton = new SmallButton(B_TRANSLATE("Open"),
new BMessage(OPEN_DOWNLOAD));
fTopButton->SetEnabled(fDownload == NULL);
}
if (fDownload) {
fBottomButton = new SmallButton(B_TRANSLATE("Cancel"),
new BMessage(CANCEL_DOWNLOAD));
} else {
fBottomButton = new SmallButton(B_TRANSLATE("Remove"),
new BMessage(REMOVE_DOWNLOAD));
fBottomButton->SetEnabled(fDownload == NULL);
}
fInfoView = new BStringView("info view", "");
fInfoView->SetViewColor(ViewColor());
BSize topButtonSize = fTopButton->PreferredSize();
BSize bottomButtonSize = fBottomButton->PreferredSize();
if (bottomButtonSize.width < topButtonSize.width)
fBottomButton->SetExplicitMaxSize(topButtonSize);
else
fTopButton->SetExplicitMaxSize(bottomButtonSize);
BGroupLayout* layout = GroupLayout();
layout->SetInsets(8, 5, 5, 6);
layout->AddView(fIconView);
BView* verticalGroup = BGroupLayoutBuilder(B_VERTICAL, 3)
.Add(fStatusBar)
.Add(fInfoView)
.TopView()
;
verticalGroup->SetViewColor(ViewColor());
layout->AddView(verticalGroup);
verticalGroup = BGroupLayoutBuilder(B_VERTICAL, 3)
.Add(fTopButton)
.Add(fBottomButton)
.TopView()
;
verticalGroup->SetViewColor(ViewColor());
layout->AddView(verticalGroup);
BFont font;
fInfoView->GetFont(&font);
float fontSize = font.Size() * 0.8f;
font.SetSize(max_c(8.0f, fontSize));
fInfoView->SetFont(&font, B_FONT_SIZE);
fInfoView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
return true;
}
status_t
DownloadProgressView::SaveSettings(BMessage* archive)
{
if (!archive)
return B_BAD_VALUE;
status_t ret = archive->AddString("path", fPath.Path());
if (ret == B_OK)
ret = archive->AddString("url", fURL.String());
if (ret == B_OK)
ret = archive->AddFloat("value", fStatusBar->CurrentValue());
if (ret == B_OK)
ret = fIconView->SaveSettings(archive);
return ret;
}
void
DownloadProgressView::AttachedToWindow()
{
if (fDownload) {
fDownload->SetProgressListener(BMessenger(this));
// Will start node monitor upon receiving the B_DOWNLOAD_STARTED
// message.
} else {
BEntry entry(fPath.Path());
if (entry.Exists())
_StartNodeMonitor(entry);
}
fTopButton->SetTarget(this);
fBottomButton->SetTarget(this);
}
void
DownloadProgressView::DetachedFromWindow()
{
_StopNodeMonitor();
}
void
DownloadProgressView::AllAttached()
{
fStatusBar->SetLowColor(ViewColor());
fInfoView->SetLowColor(ViewColor());
fInfoView->SetHighColor(0, 0, 0, 255);
SetViewColor(B_TRANSPARENT_COLOR);
SetLowColor(245, 245, 245);
SetHighColor(tint_color(LowColor(), B_DARKEN_1_TINT));
}
void
DownloadProgressView::Draw(BRect updateRect)
{
BRect bounds(Bounds());
bounds.bottom--;
FillRect(bounds, B_SOLID_LOW);
bounds.bottom++;
StrokeLine(bounds.LeftBottom(), bounds.RightBottom());
}
void
DownloadProgressView::MessageReceived(BMessage* message)
{
switch (message->what) {
case B_DOWNLOAD_STARTED:
{
BString path;
if (message->FindString("path", &path) != B_OK)
break;
fPath.SetTo(path);
BEntry entry(fPath.Path());
fIconView->SetTo(entry);
fStatusBar->Reset(fPath.Leaf());
_StartNodeMonitor(entry);
// Immediately switch to speed display whenever a new download
// starts.
sShowSpeed = true;
sLastEstimatedFinishSpeedToggleTime
= fProcessStartTime = fLastSpeedReferenceTime
= fEstimatedFinishReferenceTime = system_time();
break;
}
case B_DOWNLOAD_PROGRESS:
{
int64 currentSize;
int64 expectedSize;
if (message->FindInt64("current size", ¤tSize) == B_OK
&& message->FindInt64("expected size", &expectedSize) == B_OK) {
_UpdateStatus(currentSize, expectedSize);
}
break;
}
case B_DOWNLOAD_REMOVED:
// TODO: This is a bit asymetric. The removed notification
// arrives here, but it would be nicer if it arrived
// at the window...
Window()->PostMessage(message);
break;
case OPEN_DOWNLOAD:
{
// TODO: In case of executable files, ask the user first!
entry_ref ref;
status_t status = get_ref_for_path(fPath.Path(), &ref);
if (status == B_OK)
status = be_roster->Launch(&ref);
if (status != B_OK && status != B_ALREADY_RUNNING) {
BAlert* alert = new BAlert(B_TRANSLATE("Open download error"),
B_TRANSLATE("The download could not be opened."),
B_TRANSLATE("OK"));
alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
alert->Go(NULL);
}
break;
}
case RESTART_DOWNLOAD:
{
// We can't create a download without a full web context (mainly
// because it needs to access the cookie jar), and when we get here
// the original context is long gone (possibly the browser was
// restarted). So we create a new window to restart the download
// in a fresh context.
// FIXME this has of course the huge downside of leaving the new
// window open with a blank page. I can't think of a better
// solution right now...
BMessage* request = new BMessage(NEW_WINDOW);
request->AddString("url", fURL);
be_app->PostMessage(request);
break;
}
case CANCEL_DOWNLOAD:
CancelDownload();
break;
case REMOVE_DOWNLOAD:
{
Window()->PostMessage(SAVE_SETTINGS);
RemoveSelf();
delete this;
// TOAST!
return;
}
case B_NODE_MONITOR:
{
int32 opCode;
if (message->FindInt32("opcode", &opCode) != B_OK)
break;
switch (opCode) {
case B_ENTRY_REMOVED:
fIconView->SetIconDimmed(true);
CancelDownload();
break;
case B_ENTRY_MOVED:
{
// Follow the entry to the new location
dev_t device;
ino_t directory;
const char* name;
if (message->FindInt32("device",
reinterpret_cast<int32*>(&device)) != B_OK
|| message->FindInt64("to directory",
reinterpret_cast<int64*>(&directory)) != B_OK
|| message->FindString("name", &name) != B_OK
|| strlen(name) == 0) {
break;
}
// Construct the BEntry and update fPath
entry_ref ref(device, directory, name);
BEntry entry(&ref);
if (entry.GetPath(&fPath) != B_OK)
break;
// Find out if the directory is the Trash for this
// volume
char trashPath[B_PATH_NAME_LENGTH];
if (find_directory(B_TRASH_DIRECTORY, device, false,
trashPath, B_PATH_NAME_LENGTH) == B_OK) {
BPath trashDirectory(trashPath);
BPath parentDirectory;
fPath.GetParent(&parentDirectory);
if (parentDirectory == trashDirectory) {
// The entry was moved into the Trash.
// If the download is still in progress,
// cancel it.
fIconView->SetIconDimmed(true);
CancelDownload();
break;
} else if (fIconView->IsIconDimmed()) {
// Maybe it was moved out of the trash.
fIconView->SetIconDimmed(false);
}
}
// Inform download of the new path
if (fDownload)
fDownload->HasMovedTo(fPath);
float value = fStatusBar->CurrentValue();
fStatusBar->Reset(name);
fStatusBar->SetTo(value);
Window()->PostMessage(SAVE_SETTINGS);
break;
}
case B_ATTR_CHANGED:
{
BEntry entry(fPath.Path());
fIconView->SetIconDimmed(false);
fIconView->SetTo(entry);
break;
}
}
break;
}
// Context menu messages
case COPY_URL_TO_CLIPBOARD:
if (be_clipboard->Lock()) {
BMessage* data = be_clipboard->Data();
if (data != NULL) {
be_clipboard->Clear();
data->AddData("text/plain", B_MIME_TYPE, fURL.String(),
fURL.Length());
}
be_clipboard->Commit();
be_clipboard->Unlock();
}
break;
case OPEN_CONTAINING_FOLDER:
if (fPath.InitCheck() == B_OK) {
BEntry selected(fPath.Path());
if (!selected.Exists())
break;
BPath containingFolder;
if (fPath.GetParent(&containingFolder) != B_OK)
break;
entry_ref ref;
if (get_ref_for_path(containingFolder.Path(), &ref) != B_OK)
break;
// Ask Tracker to open the containing folder and select the
// file inside it.
BMessenger trackerMessenger("application/x-vnd.Be-TRAK");
if (trackerMessenger.IsValid()) {
BMessage selectionCommand(B_REFS_RECEIVED);
selectionCommand.AddRef("refs", &ref);
node_ref selectedRef;
if (selected.GetNodeRef(&selectedRef) == B_OK) {
selectionCommand.AddData("nodeRefToSelect", B_RAW_TYPE,
(void*)&selectedRef, sizeof(node_ref));
}
trackerMessenger.SendMessage(&selectionCommand);
}
}
break;
default:
BGroupView::MessageReceived(message);
}
}
void
DownloadProgressView::ShowContextMenu(BPoint screenWhere)
{
screenWhere += BPoint(2, 2);
BPopUpMenu* contextMenu = new BPopUpMenu("download context");
BMenuItem* copyURL = new BMenuItem(B_TRANSLATE("Copy URL to clipboard"),
new BMessage(COPY_URL_TO_CLIPBOARD));
copyURL->SetEnabled(fURL.Length() > 0);
contextMenu->AddItem(copyURL);
BMenuItem* openFolder = new BMenuItem(B_TRANSLATE("Open containing folder"),
new BMessage(OPEN_CONTAINING_FOLDER));
contextMenu->AddItem(openFolder);
contextMenu->SetTargetForItems(this);
contextMenu->Go(screenWhere, true, true, true);
}
BWebDownload*
DownloadProgressView::Download() const
{
return fDownload;
}
const BString&
DownloadProgressView::URL() const
{
return fURL;
}
bool
DownloadProgressView::IsMissing() const
{
return fIconView->IsIconDimmed();
}
bool
DownloadProgressView::IsFinished() const
{
return !fDownload && fStatusBar->CurrentValue() == 100;
}
void
DownloadProgressView::DownloadFinished()
{
fDownload = NULL;
if (fExpectedSize == -1) {
fStatusBar->SetTo(100.0);
fExpectedSize = fCurrentSize;
}
fTopButton->SetEnabled(true);
fBottomButton->SetLabel(B_TRANSLATE("Remove"));
fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD));
fBottomButton->SetEnabled(true);
fInfoView->SetText("");
fStatusBar->SetBarColor(ui_color(B_SUCCESS_COLOR));
BNotification success(B_INFORMATION_NOTIFICATION);
success.SetGroup(B_TRANSLATE("WebPositive"));
success.SetTitle(B_TRANSLATE("Download finished"));
success.SetContent(fPath.Leaf());
BEntry entry(fPath.Path());
entry_ref ref;
entry.GetRef(&ref);
success.SetOnClickFile(&ref);
success.SetIcon(fIconView->Bitmap());
success.Send();
}
void
DownloadProgressView::CancelDownload()
{
// Show the cancel notification, and set the progress bar red, only if the
// download was still running. In cases where the file is deleted after
// the download was finished, we don't want these things to happen.
if (fDownload) {
// Also cancel the download
fDownload->Cancel();
BNotification success(B_ERROR_NOTIFICATION);
success.SetGroup(B_TRANSLATE("WebPositive"));
success.SetTitle(B_TRANSLATE("Download aborted"));
success.SetContent(fPath.Leaf());
// Don't make a click on the notification open the file: it is not
// complete
success.SetIcon(fIconView->Bitmap());
success.Send();
fStatusBar->SetBarColor(ui_color(B_FAILURE_COLOR));
}
fDownload = NULL;
fTopButton->SetLabel(B_TRANSLATE("Restart"));
fTopButton->SetMessage(new BMessage(RESTART_DOWNLOAD));
fTopButton->SetEnabled(true);
fBottomButton->SetLabel(B_TRANSLATE("Remove"));
fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD));
fBottomButton->SetEnabled(true);
fInfoView->SetText("");
fPath.Unset();
}
/*static*/ void
DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse()
{
bigtime_t now = system_time();
if (sShowSpeed
&& sLastEstimatedFinishSpeedToggleTime + kShowSpeedInterval
<= now) {
sShowSpeed = false;
sLastEstimatedFinishSpeedToggleTime = now;
} else if (!sShowSpeed
&& sLastEstimatedFinishSpeedToggleTime
+ kShowEstimatedFinishInterval <= now) {
sShowSpeed = true;
sLastEstimatedFinishSpeedToggleTime = now;
}
}
// #pragma mark - private
void
DownloadProgressView::_UpdateStatus(off_t currentSize, off_t expectedSize)
{
fCurrentSize = currentSize;
fExpectedSize = expectedSize;
fStatusBar->SetTo(100.0 * currentSize / expectedSize);
bigtime_t currentTime = system_time();
if ((currentTime - fLastUpdateTime) > kMaxUpdateInterval) {
fLastUpdateTime = currentTime;
if (currentTime >= fLastSpeedReferenceTime + kSpeedReferenceInterval) {
// update current speed every kSpeedReferenceInterval
fCurrentBytesPerSecondSlot
= (fCurrentBytesPerSecondSlot + 1) % kBytesPerSecondSlots;
fBytesPerSecondSlot[fCurrentBytesPerSecondSlot]
= (double)(currentSize - fLastSpeedReferenceSize)
* 1000000LL / (currentTime - fLastSpeedReferenceTime);
fLastSpeedReferenceSize = currentSize;
fLastSpeedReferenceTime = currentTime;
fBytesPerSecond = 0.0;
size_t count = 0;
for (size_t i = 0; i < kBytesPerSecondSlots; i++) {
if (fBytesPerSecondSlot[i] != 0.0) {
fBytesPerSecond += fBytesPerSecondSlot[i];
count++;
}
}
if (count > 0)
fBytesPerSecond /= count;
}
_UpdateStatusText();
}
}
void
DownloadProgressView::_UpdateStatusText()
{
fInfoView->SetText("");
BString buffer;
if (sShowSpeed && fBytesPerSecond != 0.0) {
// Draw speed info
char sizeBuffer[128];
buffer = "(";
// Get strings for current and expected size and remove the unit
// from the current size string if it's the same as the expected
// size unit.
BString currentSize = string_for_size((double)fCurrentSize, sizeBuffer,
sizeof(sizeBuffer));
BString expectedSize = string_for_size((double)fExpectedSize, sizeBuffer,
sizeof(sizeBuffer));
int currentSizeUnitPos = currentSize.FindLast(' ');
int expectedSizeUnitPos = expectedSize.FindLast(' ');
if (currentSizeUnitPos >= 0 && expectedSizeUnitPos >= 0
&& strcmp(currentSize.String() + currentSizeUnitPos,
expectedSize.String() + expectedSizeUnitPos) == 0) {
currentSize.Truncate(currentSizeUnitPos);
}
buffer << currentSize;
buffer << " ";
buffer << B_TRANSLATE_COMMENT("of", "...as in '12kB of 256kB'");
buffer << " ";
buffer << expectedSize;
buffer << ", ";
buffer << string_for_size(fBytesPerSecond, sizeBuffer,
sizeof(sizeBuffer));
buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'");
float stringWidth = fInfoView->StringWidth(buffer.String());
if (stringWidth < fInfoView->Bounds().Width())
fInfoView->SetText(buffer.String());
else {
// complete string too wide, try with shorter version
buffer << string_for_size(fBytesPerSecond, sizeBuffer,
sizeof(sizeBuffer));
buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'");
stringWidth = fInfoView->StringWidth(buffer.String());
if (stringWidth < fInfoView->Bounds().Width())
fInfoView->SetText(buffer.String());
}
} else if (!sShowSpeed && fCurrentSize < fExpectedSize) {
double totalBytesPerSecond = (double)(fCurrentSize
- fEstimatedFinishReferenceSize)
* 1000000LL / (system_time() - fEstimatedFinishReferenceTime);
double secondsRemaining = (fExpectedSize - fCurrentSize)
/ totalBytesPerSecond;
time_t now = (time_t)real_time_clock();
time_t finishTime = (time_t)(now + secondsRemaining);
BString timeText;
if (finishTime - now > kSecondsPerDay) {
BDateTimeFormat().Format(timeText, finishTime,
B_MEDIUM_DATE_FORMAT, B_MEDIUM_TIME_FORMAT);
} else {
BTimeFormat().Format(timeText, finishTime,
B_MEDIUM_TIME_FORMAT);
}
BString statusString;
BDurationFormat formatter;
BString finishString;
if (finishTime - now > kSecondsPerHour) {
statusString.SetTo(B_TRANSLATE("(Finish: %date - Over %duration left)"));
formatter.Format(finishString, now * 1000000LL, finishTime * 1000000LL);
} else {
statusString.SetTo(B_TRANSLATE("(Finish: %date - %duration left)"));
formatter.Format(finishString, now * 1000000LL, finishTime * 1000000LL);
}
statusString.ReplaceFirst("%date", timeText);
statusString.ReplaceFirst("%duration", finishString);
float stringWidth = fInfoView->StringWidth(statusString.String());
if (stringWidth < fInfoView->Bounds().Width())
fInfoView->SetText(statusString.String());
else {
// complete string too wide, try with shorter version
statusString.SetTo(B_TRANSLATE("(Finish: %date)"));
statusString.ReplaceFirst("%date", timeText);
stringWidth = fInfoView->StringWidth(statusString.String());
if (stringWidth < fInfoView->Bounds().Width())
fInfoView->SetText(statusString.String());
}
}
}
void
DownloadProgressView::_StartNodeMonitor(const BEntry& entry)
{
node_ref nref;
if (entry.GetNodeRef(&nref) == B_OK)
watch_node(&nref, B_WATCH_ALL, this);
}
void
DownloadProgressView::_StopNodeMonitor()
{
stop_watching(this);
}
↑ V773 Visibility scope of the 'contextMenu' pointer was exited without releasing the memory. A memory leak is possible.
↑ V773 Visibility scope of the 'alert' pointer was exited without releasing the memory. A memory leak is possible.