/* * Copyright 2010-2017, Haiku, Inc. All Rights Reserved. * Copyright 2008-2009, Pier Luigi Fiorini. All Rights Reserved. * Copyright 2004-2008, Michael Davidson. All Rights Reserved. * Copyright 2004-2007, Mikael Eiman. All Rights Reserved. * Distributed under the terms of the MIT License. * * Authors: * Michael Davidson, slaad@bong.com.au * Mikael Eiman, mikael@eiman.tv * Pier Luigi Fiorini, pierluigi.fiorini@gmail.com * Stephan Aßmus * Adrien Destugues * Brian Hill, supernova@tycho.email */ #include "NotificationView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "AppGroupView.h" #include "NotificationWindow.h" const int kIconStripeWidth = 32; const float kCloseSize = 6; const float kEdgePadding = 2; const float kSmallPadding = 2; property_info message_prop_list[] = { { "type", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get the notification type"}, { "app", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get notification's app"}, { "title", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get notification's title"}, { "content", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get notification's contents"}, { "icon", {B_GET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get icon as an archived bitmap"}, { "progress", {B_GET_PROPERTY, B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, "get the progress (between 0.0 and 1.0)"}, { 0 } }; NotificationView::NotificationView(BNotification* notification, bigtime_t timeout, float iconSize, bool disableTimeout) : BView("NotificationView", B_WILL_DRAW), fNotification(notification), fTimeout(timeout), fIconSize(iconSize), fDisableTimeout(disableTimeout), fRunner(NULL), fBitmap(NULL), fCloseClicked(false), fPreviewModeOn(false) { if (fNotification->Icon() != NULL) fBitmap = new BBitmap(fNotification->Icon()); BGroupLayout* layout = new BGroupLayout(B_VERTICAL); SetLayout(layout); SetViewUIColor(B_PANEL_BACKGROUND_COLOR); SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR)); switch (fNotification->Type()) { case B_IMPORTANT_NOTIFICATION: fStripeColor = ui_color(B_CONTROL_HIGHLIGHT_COLOR); break; case B_ERROR_NOTIFICATION: fStripeColor = ui_color(B_FAILURE_COLOR); break; case B_PROGRESS_NOTIFICATION: { BStatusBar* progress = new BStatusBar("progress"); progress->SetBarHeight(12.0f); progress->SetMaxValue(1.0f); progress->Update(fNotification->Progress()); BNumberFormat numberFormat; BString label; double progressPercent = fNotification->Progress(); if (numberFormat.FormatPercent(label, progressPercent) != B_OK) label.SetToFormat("%d%%", (int)(progressPercent * 100)); progress->SetTrailingText(label.String()); layout->AddView(progress); } // fall through. case B_INFORMATION_NOTIFICATION: fStripeColor = tint_color(ui_color(B_PANEL_BACKGROUND_COLOR), B_DARKEN_1_TINT); break; } } NotificationView::~NotificationView() { delete fRunner; delete fBitmap; delete fNotification; LineInfoList::iterator lIt; for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) delete (*lIt); } void NotificationView::AttachedToWindow() { SetText(); if (!fDisableTimeout) { BMessage msg(kRemoveView); msg.AddPointer("view", this); fRunner = new BMessageRunner(BMessenger(Parent()), &msg, fTimeout, 1); } } void NotificationView::MessageReceived(BMessage* msg) { switch (msg->what) { case B_GET_PROPERTY: { BMessage specifier; const char* property; BMessage reply(B_REPLY); bool msgOkay = true; if (msg->FindMessage("specifiers", 0, &specifier) != B_OK) msgOkay = false; if (specifier.FindString("property", &property) != B_OK) msgOkay = false; if (msgOkay) { if (strcmp(property, "type") == 0) reply.AddInt32("result", fNotification->Type()); if (strcmp(property, "group") == 0) reply.AddString("result", fNotification->Group()); if (strcmp(property, "title") == 0) reply.AddString("result", fNotification->Title()); if (strcmp(property, "content") == 0) reply.AddString("result", fNotification->Content()); if (strcmp(property, "progress") == 0) reply.AddFloat("result", fNotification->Progress()); if ((strcmp(property, "icon") == 0) && fBitmap) { BMessage archive; if (fBitmap->Archive(&archive) == B_OK) reply.AddMessage("result", &archive); } reply.AddInt32("error", B_OK); } else { reply.what = B_MESSAGE_NOT_UNDERSTOOD; reply.AddInt32("error", B_ERROR); } msg->SendReply(&reply); break; } case B_SET_PROPERTY: { BMessage specifier; const char* property; BMessage reply(B_REPLY); bool msgOkay = true; if (msg->FindMessage("specifiers", 0, &specifier) != B_OK) msgOkay = false; if (specifier.FindString("property", &property) != B_OK) msgOkay = false; if (msgOkay) { const char* value = NULL; if (strcmp(property, "group") == 0) if (msg->FindString("data", &value) == B_OK) fNotification->SetGroup(value); if (strcmp(property, "title") == 0) if (msg->FindString("data", &value) == B_OK) fNotification->SetTitle(value); if (strcmp(property, "content") == 0) if (msg->FindString("data", &value) == B_OK) fNotification->SetContent(value); if (strcmp(property, "icon") == 0) { BMessage archive; if (msg->FindMessage("data", &archive) == B_OK) { delete fBitmap; fBitmap = new BBitmap(&archive); } } SetText(); Invalidate(); reply.AddInt32("error", B_OK); } else { reply.what = B_MESSAGE_NOT_UNDERSTOOD; reply.AddInt32("error", B_ERROR); } msg->SendReply(&reply); break; } default: BView::MessageReceived(msg); } } void NotificationView::Draw(BRect updateRect) { BRect progRect; SetDrawingMode(B_OP_ALPHA); SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); BRect stripeRect = Bounds(); stripeRect.right = kIconStripeWidth; SetHighColor(tint_color(ui_color(B_PANEL_BACKGROUND_COLOR), B_DARKEN_1_TINT)); FillRect(stripeRect); SetHighColor(fStripeColor); stripeRect.right = 2; FillRect(stripeRect); SetHighColor(ui_color(B_PANEL_TEXT_COLOR)); // Rectangle for icon and overlay icon BRect iconRect(0, 0, 0, 0); // Draw icon if (fBitmap) { float ix = 18; float iy = (Bounds().Height() - fIconSize) / 4.0; // Icon is vertically centered in view if (fNotification->Type() == B_PROGRESS_NOTIFICATION) { // Move icon up by half progress bar height if it's present iy -= (progRect.Height() + kEdgePadding); } iconRect.Set(ix, iy, ix + fIconSize - 1.0, iy + fIconSize - 1.0); DrawBitmapAsync(fBitmap, fBitmap->Bounds(), iconRect); } // Draw content LineInfoList::iterator lIt; for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) { LineInfo *l = (*lIt); SetFont(&l->font); // Truncate the string. We have already line-wrapped the text but if // there is a very long 'word' we can only truncate it. BString text(l->text); TruncateString(&text, B_TRUNCATE_END, Bounds().Width() - l->location.x); DrawString(text.String(), text.Length(), l->location); } AppGroupView* groupView = dynamic_cast(Parent()); if (groupView != NULL && groupView->ChildrenCount() > 1) _DrawCloseButton(updateRect); SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT)); BPoint left(Bounds().left, Bounds().top); BPoint right(Bounds().right, Bounds().top); StrokeLine(left, right); Sync(); } void NotificationView::_DrawCloseButton(const BRect& updateRect) { PushState(); BRect closeRect = Bounds(); closeRect.InsetBy(3 * kEdgePadding, 3 * kEdgePadding); closeRect.left = closeRect.right - kCloseSize; closeRect.bottom = closeRect.top + kCloseSize; rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR); float tint = B_DARKEN_2_TINT; if (fCloseClicked) { BRect buttonRect(closeRect.InsetByCopy(-4, -4)); be_control_look->DrawButtonFrame(this, buttonRect, updateRect, base, base, BControlLook::B_ACTIVATED | BControlLook::B_BLEND_FRAME); be_control_look->DrawButtonBackground(this, buttonRect, updateRect, base, BControlLook::B_ACTIVATED); tint *= 1.2; closeRect.OffsetBy(1, 1); } base = tint_color(base, tint); SetHighColor(base); SetPenSize(2); StrokeLine(closeRect.LeftTop(), closeRect.RightBottom()); StrokeLine(closeRect.LeftBottom(), closeRect.RightTop()); PopState(); } void NotificationView::MouseDown(BPoint point) { // Preview Mode ignores any mouse clicks if (fPreviewModeOn) return; int32 buttons; Window()->CurrentMessage()->FindInt32("buttons", &buttons); switch (buttons) { case B_PRIMARY_MOUSE_BUTTON: { BRect closeRect = Bounds().InsetByCopy(2,2); closeRect.left = closeRect.right - kCloseSize; closeRect.bottom = closeRect.top + kCloseSize; if (!closeRect.Contains(point)) { entry_ref launchRef; BString launchString; BMessage argMsg(B_ARGV_RECEIVED); BMessage refMsg(B_REFS_RECEIVED); entry_ref appRef; bool useArgv = false; BList messages; entry_ref ref; if (fNotification->OnClickApp() != NULL && be_roster->FindApp(fNotification->OnClickApp(), &appRef) == B_OK) { useArgv = true; } if (fNotification->OnClickFile() != NULL && be_roster->FindApp( (entry_ref*)fNotification->OnClickFile(), &appRef) == B_OK) { useArgv = true; } for (int32 i = 0; i < fNotification->CountOnClickRefs(); i++) refMsg.AddRef("refs", fNotification->OnClickRefAt(i)); messages.AddItem((void*)&refMsg); if (useArgv) { int32 argc = fNotification->CountOnClickArgs() + 1; BString arg; BPath p(&appRef); argMsg.AddString("argv", p.Path()); argMsg.AddInt32("argc", argc); for (int32 i = 0; i < argc - 1; i++) { argMsg.AddString("argv", fNotification->OnClickArgAt(i)); } messages.AddItem((void*)&argMsg); } if (fNotification->OnClickApp() != NULL) be_roster->Launch(fNotification->OnClickApp(), &messages); else be_roster->Launch(fNotification->OnClickFile(), &messages); } else { fCloseClicked = true; } // Remove the info view after a click BMessage remove_msg(kRemoveView); remove_msg.AddPointer("view", this); BMessenger msgr(Parent()); msgr.SendMessage(&remove_msg); break; } } } BHandler* NotificationView::ResolveSpecifier(BMessage* msg, int32 index, BMessage* spec, int32 form, const char* prop) { BPropertyInfo prop_info(message_prop_list); if (prop_info.FindMatch(msg, index, spec, form, prop) >= 0) { msg->PopSpecifier(); return this; } return BView::ResolveSpecifier(msg, index, spec, form, prop); } status_t NotificationView::GetSupportedSuites(BMessage* msg) { msg->AddString("suites", "suite/x-vnd.Haiku-notification_server"); BPropertyInfo prop_info(message_prop_list); msg->AddFlat("messages", &prop_info); return BView::GetSupportedSuites(msg); } void NotificationView::SetText(float newMaxWidth) { if (newMaxWidth < 0 && Parent()) newMaxWidth = Parent()->Bounds().IntegerWidth(); if (newMaxWidth <= 0) newMaxWidth = kDefaultWidth; // Delete old lines LineInfoList::iterator lIt; for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) delete (*lIt); fLines.clear(); float iconRight = kIconStripeWidth; if (fBitmap != NULL) iconRight += fIconSize; else iconRight += 32; font_height fh; be_bold_font->GetHeight(&fh); float fontHeight = ceilf(fh.leading) + ceilf(fh.descent) + ceilf(fh.ascent); float y = fontHeight + kEdgePadding * 2; // Title LineInfo* titleLine = new LineInfo; titleLine->text = fNotification->Title(); titleLine->font = *be_bold_font; titleLine->location = BPoint(iconRight + kEdgePadding, y); fLines.push_front(titleLine); y += fontHeight; // Rest of text is rendered with be_plain_font. be_plain_font->GetHeight(&fh); fontHeight = ceilf(fh.leading) + ceilf(fh.descent) + ceilf(fh.ascent); // Split text into chunks between certain characters and compose the lines. const char kSeparatorCharacters[] = " \n-\\"; BString textBuffer = fNotification->Content(); textBuffer.ReplaceAll("\t", " "); const char* chunkStart = textBuffer.String(); float maxWidth = newMaxWidth - kEdgePadding - iconRight; LineInfo* line = NULL; ssize_t length = textBuffer.Length(); while (chunkStart - textBuffer.String() < length) { size_t chunkLength = strcspn(chunkStart, kSeparatorCharacters) + 1; // Start a new line if we didn't start one before BString tempText; if (line != NULL) tempText.SetTo(line->text); tempText.Append(chunkStart, chunkLength); if (line == NULL || chunkStart[0] == '\n' || StringWidth(tempText) > maxWidth) { line = new LineInfo; line->font = *be_plain_font; line->location = BPoint(iconRight + kEdgePadding, y); fLines.push_front(line); y += fontHeight; // Skip the eventual new-line character at the beginning of this chunk if (chunkStart[0] == '\n') { chunkStart++; chunkLength--; } // Skip more new-line characters and move the line further down while (chunkStart[0] == '\n') { chunkStart++; chunkLength--; line->location.y += fontHeight; y += fontHeight; } // Strip space at beginning of a new line while (chunkStart[0] == ' ') { chunkLength--; chunkStart++; } } if (chunkStart[0] == '\0') break; // Append the chunk to the current line, which was either a new // line or the one from the previous iteration line->text.Append(chunkStart, chunkLength); chunkStart += chunkLength; } fHeight = y + (kEdgePadding * 2); // Make sure icon fits if (fBitmap != NULL) { float minHeight = fBitmap->Bounds().Height() + 2 * kEdgePadding; if (fHeight < minHeight) fHeight = minHeight; } // Make sure the progress bar is below the text, and the window is big // enough. static_cast(GetLayout())->SetInsets(kIconStripeWidth + 8, fHeight, 8, 8); _CalculateSize(); } void NotificationView::SetPreviewModeOn(bool enabled) { fPreviewModeOn = enabled; } const char* NotificationView::MessageID() const { return fNotification->MessageID(); } void NotificationView::_CalculateSize() { float height = fHeight; if (fNotification->Type() == B_PROGRESS_NOTIFICATION) { font_height fh; be_plain_font->GetHeight(&fh); float fontHeight = fh.ascent + fh.descent + fh.leading; height += 9 + (kSmallPadding * 2) + (kEdgePadding * 1) + fontHeight * 2; } SetExplicitMinSize(BSize(0, height)); SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, height)); }