⛏️ index : haiku.git

/*
 * Copyright 2010, 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
 */


#include "NotificationWindow.h"

#include <algorithm>

#include <Alert.h>
#include <Application.h>
#include <Catalog.h>
#include <Debug.h>
#include <File.h>
#include <NodeMonitor.h>
#include <PropertyInfo.h>

#include "AppGroupView.h"
#include "AppUsage.h"
#include "BorderView.h"

#undef B_TRANSLATE_CONTEXT
#define B_TRANSLATE_CONTEXT "NotificationWindow"


property_info main_prop_list[] = {
	{"message", {B_GET_PROPERTY, 0}, {B_INDEX_SPECIFIER, 0}, 
		"get a message"},
	{"message", {B_COUNT_PROPERTIES, 0}, {B_DIRECT_SPECIFIER, 0},
		"count messages"},
	{"message", {B_CREATE_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0},
		"create a message"},
	{"message", {B_SET_PROPERTY, 0}, {B_INDEX_SPECIFIER, 0},
		"modify a message"},
	{0}
};


const float kCloseSize				= 8;
const float kExpandSize				= 8;
const float kPenSize				= 1;
const float kEdgePadding			= 5;
const float kSmallPadding			= 2;

NotificationWindow::NotificationWindow()
	:
	BWindow(BRect(10, 10, 30, 30), B_TRANSLATE_MARK("Notification"), 
		B_BORDERED_WINDOW, B_AVOID_FRONT | B_AVOID_FOCUS | B_NOT_CLOSABLE 
		| B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE | B_NOT_RESIZABLE, 
		B_ALL_WORKSPACES)
{
	fBorder = new BorderView(Bounds(), "Notification");

	AddChild(fBorder);

	Show();
	Hide();

	LoadSettings(true);
	LoadAppFilters(true);
}


NotificationWindow::~NotificationWindow()
{
	appfilter_t::iterator aIt;
	for (aIt = fAppFilters.begin(); aIt != fAppFilters.end(); aIt++)
		delete aIt->second;
}


bool
NotificationWindow::QuitRequested()
{
	appview_t::iterator aIt;
	for (aIt = fAppViews.begin(); aIt != fAppViews.end(); aIt++) {
		aIt->second->RemoveSelf();
		delete aIt->second;
	}

	BMessenger(be_app).SendMessage(B_QUIT_REQUESTED);
	return BWindow::QuitRequested();
}


void
NotificationWindow::WorkspaceActivated(int32 /*workspace*/, bool active)
{
	// Ensure window is in the correct position
	if (active)
		ResizeAll();
}


void
NotificationWindow::MessageReceived(BMessage* message)
{
	switch (message->what) {
		case B_NODE_MONITOR:
		{
			LoadSettings();
			LoadAppFilters();
			break;
		}
		case kResizeToFit:
			ResizeAll();
			break;
		case B_COUNT_PROPERTIES:
		{
			BMessage reply(B_REPLY);
			BMessage specifier;
			const char* property = NULL;
			bool messageOkay = true;

			if (message->FindMessage("specifiers", 0, &specifier) != B_OK)
				messageOkay = false;
			if (specifier.FindString("property", &property) != B_OK)
				messageOkay = false;
			if (strcmp(property, "message") != 0)
				messageOkay = false;

			if (messageOkay)
				reply.AddInt32("result", fViews.size());
			else {
				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
				reply.AddInt32("error", B_ERROR);
			}

			message->SendReply(&reply);
			break;
		}
		case B_CREATE_PROPERTY:
		case kNotificationMessage:
		{
			int32 type;
			const char* content = NULL;
			const char* title = NULL;
			const char* app = NULL;
			BMessage reply(B_REPLY);
			bool messageOkay = true;

			if (message->FindInt32("type", &type) != B_OK)
				type = B_INFORMATION_NOTIFICATION;
			if (message->FindString("content", &content) != B_OK)
				messageOkay = false;
			if (message->FindString("title", &title) != B_OK)
				messageOkay = false;
			if (message->FindString("app", &app) != B_OK
				&& message->FindString("appTitle", &app) != B_OK)
				messageOkay = false;

			if (messageOkay) {
				NotificationView* view = new NotificationView(this,
					(notification_type)type, app, title, content,
					new BMessage(*message));

				appfilter_t::iterator fIt = fAppFilters.find(app);
				bool allow = false;
				if (fIt == fAppFilters.end()) {
					app_info info;
					BMessenger messenger = message->ReturnAddress();
					if (messenger.IsValid())
						be_roster->GetRunningAppInfo(messenger.Team(), &info);
					else
						be_roster->GetAppInfo("application/x-vnd.Be-SHEL", &info);

					AppUsage* appUsage = new AppUsage(info.ref, app, true);
					fAppFilters[app] = appUsage;

					appUsage->Allowed(title, (notification_type)type);

					allow = true;
				} else
					allow = fIt->second->Allowed(title, (notification_type)type);

				if (allow) {
					appview_t::iterator aIt = fAppViews.find(app);
					AppGroupView* group = NULL;
					if (aIt == fAppViews.end()) {
						group = new AppGroupView(this, app);
						fAppViews[app] = group;
						fBorder->AddChild(group);
					} else
						group = aIt->second;

					group->AddInfo(view);

					ResizeAll();

					reply.AddInt32("error", B_OK);
				} else
					reply.AddInt32("error", B_NOT_ALLOWED);
			} else {
				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
				reply.AddInt32("error", B_ERROR);
			}

			message->SendReply(&reply);
			break;
		}
		case kRemoveView:
		{
			void* _ptr;
			message->FindPointer("view", &_ptr);

			NotificationView* info
				= reinterpret_cast<NotificationView*>(_ptr);

			fBorder->RemoveChild(info);

			std::vector<NotificationView*>::iterator i
				= find(fViews.begin(), fViews.end(), info);
			if (i != fViews.end())
				fViews.erase(i);

			delete info;

			ResizeAll();
			break;
		}
		default:
			BWindow::MessageReceived(message);
	}
}


BHandler*
NotificationWindow::ResolveSpecifier(BMessage* msg, int32 index,
	BMessage* spec, int32 form, const char* prop)
{
	BPropertyInfo prop_info(main_prop_list);
	BHandler* handler = NULL;

	if (strcmp(prop,"message") == 0) {
		switch (msg->what) {
			case B_CREATE_PROPERTY:
			{
				msg->PopSpecifier();
				handler = this;
				break;
			}
			case B_SET_PROPERTY:
			case B_GET_PROPERTY:
			{
				int32 i;

				if (spec->FindInt32("index", &i) != B_OK)
					i = -1;

				if (i >= 0 && i < (int32)fViews.size()) {
					msg->PopSpecifier();
					handler = fViews[i];
				} else
					handler = NULL;
				break;
			}
			case B_COUNT_PROPERTIES:
				msg->PopSpecifier();
				handler = this;
				break;
			default:
				break;
		}
	}

	if (!handler)
		handler = BWindow::ResolveSpecifier(msg, index, spec, form, prop);

	return handler;
}


icon_size
NotificationWindow::IconSize()
{
	return fIconSize;
}


int32
NotificationWindow::Timeout()
{
	return fTimeout;
}


infoview_layout
NotificationWindow::Layout()
{
	return fLayout;
}


float
NotificationWindow::ViewWidth()
{
	return fWidth;
}


void
NotificationWindow::ResizeAll()
{
	if (fAppViews.empty()) {
		if (!IsHidden())
			Hide();
		return;
	}

	appview_t::iterator aIt;
	bool shouldHide = true;

	for (aIt = fAppViews.begin(); aIt != fAppViews.end(); aIt++) {
		AppGroupView* app = aIt->second;
		if (app->HasChildren()) {
			shouldHide = false;
			break;
		}
	}

	if (shouldHide) {
		if (!IsHidden())
			Hide();
		return;
	}

	if (IsHidden())
		Show();

	float width = 0;
	float height = 0;

	for (aIt = fAppViews.begin(); aIt != fAppViews.end(); aIt++) {
		AppGroupView* view = aIt->second;
		float w = -1;
		float h = -1;

		if (!view->HasChildren()) {
			if (!view->IsHidden())
				view->Hide();
		} else {
			view->GetPreferredSize(&w, &h);
			width = max_c(width, h);

			view->ResizeToPreferred();
			view->MoveTo(0, height);

			height += h;

			if (view->IsHidden())
				view->Show();
		}
	}

	ResizeTo(ViewWidth(), height);
	PopupAnimation(Bounds().Width(), Bounds().Height());
}


void
NotificationWindow::PopupAnimation(float width, float height)
{
	float x = 0, y = 0, sx, sy;
	float pad = 0;
	BDeskbar deskbar;
	BRect frame = deskbar.Frame();

	switch (deskbar.Location()) {
		case B_DESKBAR_TOP:
			// Put it just under, top right corner
			sx = frame.right;
			sy = frame.bottom + pad;
			y = sy;
			x = sx - width - pad;
			break;
		case B_DESKBAR_BOTTOM:
			// Put it just above, lower left corner
			sx = frame.right;
			sy = frame.top - height - pad;
			y = sy;
			x = sx - width - pad;
			break;
		case B_DESKBAR_LEFT_TOP:
			// Put it just to the right of the deskbar
			sx = frame.right + pad;
			sy = frame.top - height;
			x = sx;
			y = frame.top + pad;
			break;
		case B_DESKBAR_RIGHT_TOP:
			// Put it just to the left of the deskbar
			sx = frame.left - width - pad;
			sy = frame.top - height;
			x = sx;
			y = frame.top + pad;
			break;
		case B_DESKBAR_LEFT_BOTTOM:
			// Put it to the right of the deskbar.
			sx = frame.right + pad;
			sy = frame.bottom;
			x = sx;
			y = sy - height - pad;
			break;
		case B_DESKBAR_RIGHT_BOTTOM:
			// Put it to the left of the deskbar.
			sx = frame.left - width - pad;
			sy = frame.bottom;
			y = sy - height - pad;
			x = sx;
			break;
		default:
			break;
	}

	MoveTo(x, y);

	if (IsHidden() && fViews.size() != 0)
		Show();
	// Activate();// it hides floaters from apps :-(
}


void
NotificationWindow::LoadSettings(bool startMonitor)
{
	_LoadGeneralSettings(startMonitor);
	_LoadDisplaySettings(startMonitor);
}


void
NotificationWindow::LoadAppFilters(bool startMonitor)
{
	BPath path;

	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
		return;

	path.Append(kSettingsDirectory);

	if (create_directory(path.Path(), 0755) != B_OK)
		return;

	path.Append(kFiltersSettings);

	BFile file(path.Path(), B_READ_ONLY);
	BMessage settings;
	if (settings.Unflatten(&file) != B_OK)
		return;

	type_code type;
	int32 count = 0;

	if (settings.GetInfo("app_usage", &type, &count) != B_OK)
		return;

	for (int32 i = 0; i < count; i++) {
		AppUsage* app = new AppUsage();
		settings.FindFlat("app_usage", i, app);
		fAppFilters[app->Name()] = app;
	}

	if (startMonitor) {
		node_ref nref;
		BEntry entry(path.Path());
		entry.GetNodeRef(&nref);

		if (watch_node(&nref, B_WATCH_ALL, BMessenger(this)) != B_OK) {
			BAlert* alert = new BAlert(B_TRANSLATE("Warning"),
					B_TRANSLATE("Couldn't start filter monitor."
						" Live filter changes disabled."), B_TRANSLATE("Darn."));
			alert->Go();
		}
	}
}


void
NotificationWindow::SaveAppFilters()
{
	BPath path;

	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
		return;

	path.Append(kSettingsDirectory);
	path.Append(kFiltersSettings);

	BMessage settings;
	BFile file(path.Path(), B_WRITE_ONLY);

	appfilter_t::iterator fIt;
	for (fIt = fAppFilters.begin(); fIt != fAppFilters.end(); fIt++)
		settings.AddFlat("app_usage", fIt->second);

	settings.Flatten(&file);
}


void
NotificationWindow::_LoadGeneralSettings(bool startMonitor)
{
	BPath path;
	BMessage settings;

	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
		return;

	path.Append(kSettingsDirectory);
	if (create_directory(path.Path(), 0755) == B_OK) {
		path.Append(kGeneralSettings);

		BFile file(path.Path(), B_READ_ONLY);
		settings.Unflatten(&file);
	}

	if (settings.FindInt32(kTimeoutName, &fTimeout) != B_OK)
		fTimeout = kDefaultTimeout;

	// Notify the view about the change
	views_t::iterator it;
	for (it = fViews.begin(); it != fViews.end(); ++it) {
		NotificationView* view = (*it);
		view->SetText(view->Application(), view->Title(), view->Text());
		view->Invalidate();
	}

	if (startMonitor) {
		node_ref nref;
		BEntry entry(path.Path());
		entry.GetNodeRef(&nref);

		if (watch_node(&nref, B_WATCH_ALL, BMessenger(this)) != B_OK) {
			BAlert* alert = new BAlert(B_TRANSLATE("Warning"),
						B_TRANSLATE("Couldn't start general settings monitor.\n"
						"Live filter changes disabled."), B_TRANSLATE("OK"));
			alert->Go();
		}
	}
}


void
NotificationWindow::_LoadDisplaySettings(bool startMonitor)
{
	BPath path;
	BMessage settings;

	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
		return;

	path.Append(kSettingsDirectory);
	if (create_directory(path.Path(), 0755) == B_OK) {
		path.Append(kDisplaySettings);

		BFile file(path.Path(), B_READ_ONLY);
		settings.Unflatten(&file);
	}

	int32 setting;

	if (settings.FindFloat(kWidthName, &fWidth) != B_OK)
		fWidth = kDefaultWidth;

	if (settings.FindInt32(kIconSizeName, &setting) != B_OK)
		fIconSize = kDefaultIconSize;
	else
		fIconSize = (icon_size)setting;

	if (settings.FindInt32(kLayoutName, &setting) != B_OK)
		fLayout = kDefaultLayout;
	else {
		switch (setting) {
			case 0:
				fLayout = TitleAboveIcon;
				break;
			case 1:
				fLayout = AllTextRightOfIcon;
				break;
			default:
				fLayout = kDefaultLayout;
		}
	}

	// Notify the view about the change
	views_t::iterator it;
	for (it = fViews.begin(); it != fViews.end(); ++it) {
		NotificationView* view = (*it);
		view->SetText(view->Application(), view->Title(), view->Text());
		view->Invalidate();
	}

	if (startMonitor) {
		node_ref nref;
		BEntry entry(path.Path());
		entry.GetNodeRef(&nref);

		if (watch_node(&nref, B_WATCH_ALL, BMessenger(this)) != B_OK) {
			BAlert* alert = new BAlert(B_TRANSLATE("Warning"),
				B_TRANSLATE("Couldn't start display settings monitor.\n"
					"Live filter changes disabled."), B_TRANSLATE("OK"));
			alert->Go();
		}
	}
}