⛏️ index : haiku.git

/*
 * Copyright 2006-2012, 2023, Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Stephan Aßmus <superstippi@gmx.de>
 *		Zardshard
 */

#include "PathListView.h"

#include <new>
#include <stdio.h>

#include <Application.h>
#include <Catalog.h>
#include <ListItem.h>
#include <Locale.h>
#include <Menu.h>
#include <MenuItem.h>
#include <Message.h>
#include <Mime.h>
#include <Window.h>

#include "AddPathsCommand.h"
#include "CleanUpPathCommand.h"
#include "CommandStack.h"
#include "MovePathsCommand.h"
#include "Observer.h"
#include "PathSourceShape.h"
#include "RemovePathsCommand.h"
#include "ReversePathCommand.h"
#include "RotatePathIndicesCommand.h"
#include "Shape.h"
#include "Selection.h"
#include "UnassignPathCommand.h"
#include "Util.h"
#include "VectorPath.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Icon-O-Matic-PathsList"


using std::nothrow;

static const float kMarkWidth		= 14.0;
static const float kBorderOffset	= 3.0;
static const float kTextOffset		= 4.0;


class PathListItem : public SimpleItem, public Observer {
public:
	PathListItem(VectorPath* p, PathListView* listView, bool markEnabled)
		:
		SimpleItem(""),
		path(NULL),
		fListView(listView),
		fMarkEnabled(markEnabled),
		fMarked(false)
	{
		SetPath(p);
	}


	virtual ~PathListItem()
	{
		SetPath(NULL);
	}


	// SimpleItem interface
	virtual	void DrawItem(BView* owner, BRect itemFrame, bool even)
	{
		SimpleItem::DrawBackground(owner, itemFrame, even);

		float offset = kBorderOffset + kMarkWidth + kTextOffset;
		SimpleItem::DrawItem(owner, itemFrame.OffsetByCopy(offset, 0), even);

		if (!fMarkEnabled)
			return;

		// mark
		BRect markRect = itemFrame;
		float markRectBorderTint = B_DARKEN_1_TINT;
		float markRectFillTint = 1.04;
		float markTint = B_DARKEN_4_TINT;
					// Dark Themes
		rgb_color lowColor = owner->LowColor();
		if (lowColor.red + lowColor.green + lowColor.blue < 128 * 3) {
			markRectBorderTint = B_LIGHTEN_2_TINT;
			markRectFillTint = 0.85;
			markTint = 0.1;
		}
		markRect.left += kBorderOffset;
		markRect.right = markRect.left + kMarkWidth;
		markRect.top = (markRect.top + markRect.bottom - kMarkWidth) / 2.0;
		markRect.bottom = markRect.top + kMarkWidth;
		owner->SetHighColor(tint_color(owner->LowColor(), markRectBorderTint));
		owner->StrokeRect(markRect);
		markRect.InsetBy(1, 1);
		owner->SetHighColor(tint_color(owner->LowColor(), markRectFillTint));
		owner->FillRect(markRect);
		if (fMarked) {
			markRect.InsetBy(2, 2);
			owner->SetHighColor(tint_color(owner->LowColor(),
				markTint));
			owner->SetPenSize(2);
			owner->StrokeLine(markRect.LeftTop(), markRect.RightBottom());
			owner->StrokeLine(markRect.LeftBottom(), markRect.RightTop());
			owner->SetPenSize(1);
		}
	}


	// Observer interface
	virtual	void ObjectChanged(const Observable* object)
	{
		UpdateText();
	}


	// PathListItem
	void SetPath(VectorPath* p)
	{
		if (p == path)
			return;

		if (path) {
			path->RemoveObserver(this);
			path->ReleaseReference();
		}

		path = p;

		if (path) {
			path->AcquireReference();
			path->AddObserver(this);
			UpdateText();
		}
	}


	void UpdateText()
	{
		SetText(path->Name());
		Invalidate();
	}


	void SetMarkEnabled(bool enabled)
	{
		if (fMarkEnabled == enabled)
			return;
		fMarkEnabled = enabled;
		Invalidate();
	}


	void SetMarked(bool marked)
	{
		if (fMarked == marked)
			return;
		fMarked = marked;
		Invalidate();
	}


	void Invalidate()
	{
		if (fListView->LockLooper()) {
			fListView->InvalidateItem(
				fListView->IndexOf(this));
			fListView->UnlockLooper();
		}
	}

public:
	VectorPath* 	path;

private:
	PathListView*	fListView;
	bool			fMarkEnabled;
	bool			fMarked;
};


class ShapePathListener : public ContainerListener<VectorPath>,
	public ContainerListener<Shape> {
public:
	ShapePathListener(PathListView* listView)
		:
		fListView(listView),
		fShape(NULL)
	{
	}


	virtual ~ShapePathListener()
	{
		SetShape(NULL);
	}


	// ContainerListener<VectorPath> interface
	virtual void ItemAdded(VectorPath* path, int32 index)
	{
		fListView->_SetPathMarked(path, true);
	}


	virtual void ItemRemoved(VectorPath* path)
	{
		fListView->_SetPathMarked(path, false);
	}


	// ContainerListener<Shape> interface
	virtual void ItemAdded(Shape* shape, int32 index)
	{
	}


	virtual void ItemRemoved(Shape* shape)
	{
		fListView->SetCurrentShape(NULL);
	}


	// ShapePathListener
	void SetShape(PathSourceShape* shape)
	{
		if (fShape == shape)
			return;

		if (fShape)
			fShape->Paths()->RemoveListener(this);

		fShape = shape;

		if (fShape)
			fShape->Paths()->AddListener(this);
	}


	Shape* CurrentShape() const
	{
		return fShape;
	}

private:
	PathListView*		fListView;
	PathSourceShape*	fShape;
};


// #pragma mark -


enum {
	MSG_ADD					= 'addp',

	MSG_ADD_RECT			= 'addr',
	MSG_ADD_CIRCLE			= 'addc',
	MSG_ADD_ARC				= 'adda',

	MSG_DUPLICATE			= 'dupp',

	MSG_REVERSE				= 'rvrs',
	MSG_CLEAN_UP			= 'clup',
	MSG_ROTATE_INDICES_CW	= 'ricw',
	MSG_ROTATE_INDICES_CCW	= 'ricc',

	MSG_REMOVE				= 'remp',
};


PathListView::PathListView(BRect frame, const char* name, BMessage* message,
	BHandler* target)
	:
	SimpleListView(frame, name, NULL, B_SINGLE_SELECTION_LIST),
	fMessage(message),
	fMenu(NULL),

	fPathContainer(NULL),
	fShapeContainer(NULL),
	fCommandStack(NULL),

	fCurrentShape(NULL),
	fShapePathListener(new ShapePathListener(this))
{
	SetTarget(target);
}


PathListView::~PathListView()
{
	_MakeEmpty();
	delete fMessage;

	if (fPathContainer != NULL)
		fPathContainer->RemoveListener(this);

	if (fShapeContainer != NULL)
		fShapeContainer->RemoveListener(fShapePathListener);

	delete fShapePathListener;
}


void
PathListView::SelectionChanged()
{
	SimpleListView::SelectionChanged();

	if (!fSyncingToSelection) {
		// NOTE: single selection list
		PathListItem* item
			= dynamic_cast<PathListItem*>(ItemAt(CurrentSelection(0)));
		if (fMessage != NULL) {
			BMessage message(*fMessage);
			message.AddPointer("path", item ? (void*)item->path : NULL);
			Invoke(&message);
		}
	}

	_UpdateMenu();
}


void
PathListView::MouseDown(BPoint where)
{
	if (fCurrentShape == NULL) {
		SimpleListView::MouseDown(where);
		return;
	}

	bool handled = false;
	int32 index = IndexOf(where);
	PathListItem* item = dynamic_cast<PathListItem*>(ItemAt(index));
	if (item != NULL) {
		BRect itemFrame(ItemFrame(index));
		itemFrame.right = itemFrame.left + kBorderOffset + kMarkWidth
			+ kTextOffset / 2.0;

		VectorPath* path = item->path;
		if (itemFrame.Contains(where) && fCommandStack) {
			// add or remove the path to the shape
			::Command* command;
			if (fCurrentShape->Paths()->HasItem(path)) {
				command = new UnassignPathCommand(fCurrentShape, path);
			} else {
				VectorPath* paths[1];
				paths[0] = path;
				command = new AddPathsCommand(fCurrentShape->Paths(),
					paths, 1, false, fCurrentShape->Paths()->CountItems());
			}
			fCommandStack->Perform(command);
			handled = true;
		}
	}

	if (!handled)
		SimpleListView::MouseDown(where);
}


void
PathListView::MessageReceived(BMessage* message)
{
	switch (message->what) {
		case MSG_ADD:
			if (fCommandStack != NULL) {
				VectorPath* path;
				AddPathsCommand* command;
				new_path(fPathContainer, &path, &command);
				fCommandStack->Perform(command);
			}
			break;

		case MSG_ADD_RECT:
			if (fCommandStack != NULL) {
				VectorPath* path;
				AddPathsCommand* command;
				new_path(fPathContainer, &path, &command);
				if (path != NULL) {
					path->AddPoint(BPoint(16, 16));
					path->AddPoint(BPoint(16, 48));
					path->AddPoint(BPoint(48, 48));
					path->AddPoint(BPoint(48, 16));
					path->SetClosed(true);
				}
				fCommandStack->Perform(command);
			}
			break;

		case MSG_ADD_CIRCLE:
			// TODO: ask for number of secions
			if (fCommandStack != NULL) {
				VectorPath* path;
				AddPathsCommand* command;
				new_path(fPathContainer, &path, &command);
				if (path != NULL) {
					// add four control points defining a circle:
					//   a 
					// b   d
					//   c
					BPoint a(32, 16);
					BPoint b(16, 32);
					BPoint c(32, 48);
					BPoint d(48, 32);
					
					path->AddPoint(a);
					path->AddPoint(b);
					path->AddPoint(c);
					path->AddPoint(d);
			
					path->SetClosed(true);
			
					float controlDist = 0.552284 * 16;
					path->SetPoint(0, a, a + BPoint(controlDist, 0.0),
										 a + BPoint(-controlDist, 0.0), true);
					path->SetPoint(1, b, b + BPoint(0.0, -controlDist),
										 b + BPoint(0.0, controlDist), true);
					path->SetPoint(2, c, c + BPoint(-controlDist, 0.0),
										 c + BPoint(controlDist, 0.0), true);
					path->SetPoint(3, d, d + BPoint(0.0, controlDist),
										 d + BPoint(0.0, -controlDist), true);
				}
				fCommandStack->Perform(command);
			}
			break;

		case MSG_DUPLICATE:
			if (fCommandStack != NULL) {
				PathListItem* item = dynamic_cast<PathListItem*>(
					ItemAt(CurrentSelection(0)));
				if (item == NULL)
					break;

				VectorPath* path;
				AddPathsCommand* command;
				new_path(fPathContainer, &path, &command, item->path);
				fCommandStack->Perform(command);
			}
			break;

		case MSG_REVERSE:
			if (fCommandStack != NULL) {
				PathListItem* item = dynamic_cast<PathListItem*>(
					ItemAt(CurrentSelection(0)));
				if (item == NULL)
					break;

				ReversePathCommand* command
					= new (nothrow) ReversePathCommand(item->path);
				fCommandStack->Perform(command);
			}
			break;

		case MSG_CLEAN_UP:
			if (fCommandStack != NULL) {
				PathListItem* item = dynamic_cast<PathListItem*>(
					ItemAt(CurrentSelection(0)));
				if (item == NULL)
					break;

				CleanUpPathCommand* command
					= new (nothrow) CleanUpPathCommand(item->path);
				fCommandStack->Perform(command);
			}
			break;

		case MSG_ROTATE_INDICES_CW:
		case MSG_ROTATE_INDICES_CCW:
			if (fCommandStack != NULL) {
				PathListItem* item = dynamic_cast<PathListItem*>(
					ItemAt(CurrentSelection(0)));
				if (item == NULL)
					break;

				RotatePathIndicesCommand* command
					= new (nothrow) RotatePathIndicesCommand(item->path,
					message->what == MSG_ROTATE_INDICES_CW);
				fCommandStack->Perform(command);
			}
			break;

		case MSG_REMOVE:
			RemoveSelected();
			break;

		default:
			SimpleListView::MessageReceived(message);
			break;
	}
}


status_t
PathListView::ArchiveSelection(BMessage* into, bool deep) const
{
	into->what = PathListView::kSelectionArchiveCode;

	int32 count = CountSelectedItems();
	for (int32 i = 0; i < count; i++) {
		PathListItem* item = dynamic_cast<PathListItem*>(
			ItemAt(CurrentSelection(i)));
		if (item != NULL) {
			BMessage archive;
			if (item->path->Archive(&archive, deep) == B_OK)
				into->AddMessage("path", &archive);
		} else
			return B_ERROR;
	}

	return B_OK;
}


bool
PathListView::InstantiateSelection(const BMessage* archive, int32 dropIndex)
{
	if (archive->what != PathListView::kSelectionArchiveCode
		|| fCommandStack == NULL || fPathContainer == NULL)
		return false;

	// Drag may have come from another instance, like in another window.
	// Reconstruct the Styles from the archive and add them at the drop
	// index.
	int index = 0;
	BList paths;
	while (true) {
		BMessage pathArchive;
		if (archive->FindMessage("path", index, &pathArchive) != B_OK)
			break;

		VectorPath* path = new(std::nothrow) VectorPath(&pathArchive);
		if (path == NULL)
			break;

		if (!paths.AddItem(path)) {
			delete path;
			break;
		}

		index++;
	}

	int32 count = paths.CountItems();
	if (count == 0)
		return false;

	AddPathsCommand* command = new(nothrow) AddPathsCommand(fPathContainer,
		(VectorPath**)paths.Items(), count, true, dropIndex);
	if (command == NULL) {
		for (int32 i = 0; i < count; i++)
			delete (VectorPath*)paths.ItemAtFast(i);
		return false;
	}

	fCommandStack->Perform(command);

	return true;
}


void
PathListView::MoveItems(BList& items, int32 toIndex)
{
	if (fCommandStack == NULL || fPathContainer == NULL)
		return;

	int32 count = items.CountItems();
	VectorPath** paths = new (nothrow) VectorPath*[count];
	if (paths == NULL)
		return;

	for (int32 i = 0; i < count; i++) {
		PathListItem* item
			= dynamic_cast<PathListItem*>((BListItem*)items.ItemAtFast(i));
		paths[i] = item ? item->path : NULL;
	}

	MovePathsCommand* command = new (nothrow) MovePathsCommand(fPathContainer,
		paths, count, toIndex);
	if (command == NULL) {
		delete[] paths;
		return;
	}

	fCommandStack->Perform(command);
}


void
PathListView::CopyItems(BList& items, int32 toIndex)
{
	if (fCommandStack == NULL || fPathContainer == NULL)
		return;

	int32 count = items.CountItems();
	VectorPath* paths[count];

	for (int32 i = 0; i < count; i++) {
		PathListItem* item
			= dynamic_cast<PathListItem*>((BListItem*)items.ItemAtFast(i));
		paths[i] = item ? new (nothrow) VectorPath(*item->path) : NULL;
	}

	AddPathsCommand* command = new(nothrow) AddPathsCommand(fPathContainer,
		paths, count, true, toIndex);
	if (command == NULL) {
		for (int32 i = 0; i < count; i++)
			delete paths[i];
		return;
	}

	fCommandStack->Perform(command);
}


void
PathListView::RemoveItemList(BList& items)
{
	if (fCommandStack == NULL || fPathContainer == NULL)
		return;

	int32 count = items.CountItems();
	int32 indices[count];
	for (int32 i = 0; i < count; i++)
		indices[i] = IndexOf((BListItem*)items.ItemAtFast(i));

	RemovePathsCommand* command = new (nothrow) RemovePathsCommand(
		fPathContainer, indices, count);

	fCommandStack->Perform(command);
}


BListItem*
PathListView::CloneItem(int32 index) const
{
	if (PathListItem* item = dynamic_cast<PathListItem*>(ItemAt(index))) {
		return new(nothrow) PathListItem(item->path,
			const_cast<PathListView*>(this), fCurrentShape != NULL);
	}
	return NULL;
}


int32
PathListView::IndexOfSelectable(Selectable* selectable) const
{
	VectorPath* path = dynamic_cast<VectorPath*>(selectable);
	if (path == NULL)
		return -1;

	int32 count = CountItems();
	for (int32 i = 0; i < count; i++) {
		if (SelectableFor(ItemAt(i)) == path)
			return i;
	}

	return -1;
}


Selectable*
PathListView::SelectableFor(BListItem* item) const
{
	PathListItem* pathItem = dynamic_cast<PathListItem*>(item);
	if (pathItem != NULL)
		return pathItem->path;
	return NULL;
}


// #pragma mark -


void
PathListView::ItemAdded(VectorPath* path, int32 index)
{
	// NOTE: we are in the thread that messed with the
	// ShapeContainer, so no need to lock the
	// container, when this is changed to asynchronous
	// notifications, then it would need to be read-locked!
	if (!LockLooper())
		return;

	if (_AddPath(path, index))
		Select(index);

	UnlockLooper();
}


void
PathListView::ItemRemoved(VectorPath* path)
{
	// NOTE: we are in the thread that messed with the
	// ShapeContainer, so no need to lock the
	// container, when this is changed to asynchronous
	// notifications, then it would need to be read-locked!
	if (!LockLooper())
		return;

	// NOTE: we're only interested in VectorPath objects
	_RemovePath(path);

	UnlockLooper();
}


// #pragma mark -


void
PathListView::SetPathContainer(Container<VectorPath>* container)
{
	if (fPathContainer == container)
		return;

	// detach from old container
	if (fPathContainer != NULL)
		fPathContainer->RemoveListener(this);

	_MakeEmpty();

	fPathContainer = container;

	if (fPathContainer == NULL)
		return;

	fPathContainer->AddListener(this);

	// sync
//	if (!fPathContainer->ReadLock())
//		return;

	int32 count = fPathContainer->CountItems();
	for (int32 i = 0; i < count; i++)
		_AddPath(fPathContainer->ItemAtFast(i), i);

//	fPathContainer->ReadUnlock();
}


void
PathListView::SetShapeContainer(Container<Shape>* container)
{
	if (fShapeContainer == container)
		return;

	// detach from old container
	if (fShapeContainer != NULL)
		fShapeContainer->RemoveListener(fShapePathListener);

	fShapeContainer = container;

	if (fShapeContainer != NULL)
		fShapeContainer->AddListener(fShapePathListener);
}


void
PathListView::SetCommandStack(CommandStack* stack)
{
	fCommandStack = stack;
}


void
PathListView::SetMenu(BMenu* menu)
{
	fMenu = menu;
	if (fMenu == NULL)
		return;

	fAddItem = new BMenuItem(B_TRANSLATE("Add"),
		new BMessage(MSG_ADD));
	fAddRectItem = new BMenuItem(B_TRANSLATE("Add rect"),
		new BMessage(MSG_ADD_RECT));
	fAddCircleItem = new BMenuItem(B_TRANSLATE("Add circle"/*B_UTF8_ELLIPSIS*/),
		new BMessage(MSG_ADD_CIRCLE));
//	fAddArcItem = new BMenuItem("Add arc" B_UTF8_ELLIPSIS,
//		new BMessage(MSG_ADD_ARC));
	fDuplicateItem = new BMenuItem(B_TRANSLATE("Duplicate"),
		new BMessage(MSG_DUPLICATE));
	fReverseItem = new BMenuItem(B_TRANSLATE("Reverse"),
		new BMessage(MSG_REVERSE));
	fCleanUpItem = new BMenuItem(B_TRANSLATE("Clean up"),
		new BMessage(MSG_CLEAN_UP));
	fRotateIndicesRightItem = new BMenuItem(B_TRANSLATE("Rotate indices forwards"),
		new BMessage(MSG_ROTATE_INDICES_CCW), 'R');
	fRotateIndicesLeftItem = new BMenuItem(B_TRANSLATE("Rotate indices backwards"),
		new BMessage(MSG_ROTATE_INDICES_CW), 'R', B_SHIFT_KEY);
	fRemoveItem = new BMenuItem(B_TRANSLATE("Remove"),
		new BMessage(MSG_REMOVE));

	fMenu->AddItem(fAddItem);
	fMenu->AddItem(fAddRectItem);
	fMenu->AddItem(fAddCircleItem);
//	fMenu->AddItem(fAddArcItem);

	fMenu->AddSeparatorItem();

	fMenu->AddItem(fDuplicateItem);
	fMenu->AddItem(fReverseItem);
	fMenu->AddItem(fCleanUpItem);

	fMenu->AddSeparatorItem();

	fMenu->AddItem(fRotateIndicesRightItem);
	fMenu->AddItem(fRotateIndicesLeftItem);

	fMenu->AddSeparatorItem();

	fMenu->AddItem(fRemoveItem);

	fMenu->SetTargetForItems(this);

	_UpdateMenu();
}


void
PathListView::SetCurrentShape(Shape* shape)
{
	if (fCurrentShape == shape)
		return;

	fCurrentShape = dynamic_cast<PathSourceShape*>(shape);
	fShapePathListener->SetShape(fCurrentShape);

	_UpdateMarks();
}


// #pragma mark -


bool
PathListView::_AddPath(VectorPath* path, int32 index)
{
	if (path == NULL)
		return false;

	PathListItem* item = new(nothrow) PathListItem(path, this,
		fCurrentShape != NULL);
	if (item == NULL)
		return false;

	if (!AddItem(item, index)) {
		delete item;
		return false;
	}
	
	return true;
}


bool
PathListView::_RemovePath(VectorPath* path)
{
	PathListItem* item = _ItemForPath(path);
	if (item != NULL && RemoveItem(item)) {
		delete item;
		return true;
	}
	return false;
}


PathListItem*
PathListView::_ItemForPath(VectorPath* path) const
{
	int32 count = CountItems();
	for (int32 i = 0; i < count; i++) {
		 PathListItem* item = dynamic_cast<PathListItem*>(ItemAt(i));
		if (item == NULL)
			continue;
		if (item->path == path)
			return item;
	}
	return NULL;
}


// #pragma mark -


void
PathListView::_UpdateMarks()
{
	int32 count = CountItems();
	if (fCurrentShape != NULL) {
		// enable display of marks and mark items whoes
		// path is contained in fCurrentShape
		for (int32 i = 0; i < count; i++) {
			PathListItem* item = dynamic_cast<PathListItem*>(ItemAt(i));
			if (item == NULL)
				continue;
			item->SetMarkEnabled(true);
			item->SetMarked(fCurrentShape->Paths()->HasItem(item->path));
		}
	} else {
		// disable display of marks
		for (int32 i = 0; i < count; i++) {
			PathListItem* item = dynamic_cast<PathListItem*>(ItemAt(i));
			if (item == NULL)
				continue;
			item->SetMarkEnabled(false);
		}
	}

	Invalidate();
}


void
PathListView::_SetPathMarked(VectorPath* path, bool marked)
{
	PathListItem* item = _ItemForPath(path);
	if (item != NULL)
		item->SetMarked(marked);
}


void
PathListView::_UpdateMenu()
{
	if (fMenu == NULL)
		return;

	bool hasSelection = CurrentSelection(0) >= 0;

	fDuplicateItem->SetEnabled(hasSelection);
	fReverseItem->SetEnabled(hasSelection);
	fCleanUpItem->SetEnabled(hasSelection);
	fRotateIndicesLeftItem->SetEnabled(hasSelection);
	fRotateIndicesRightItem->SetEnabled(hasSelection);
	fRemoveItem->SetEnabled(hasSelection);
}