* Copyright 2006, 2023, Haiku.
* Distributed under the terms of the MIT License.
*
* Authors:
* Stephan Aßmus <superstippi@gmx.de>
* Zardshard
*/
#include "ListViews.h"
#include <malloc.h>
#include <stdio.h>
#include <typeinfo>
#include <Bitmap.h>
#include <Clipboard.h>
#include <Cursor.h>
#include <Entry.h>
#include <MessageRunner.h>
#include <Messenger.h>
#include <ScrollBar.h>
#include <ScrollView.h>
#include <StackOrHeapArray.h>
#include <String.h>
#include <Window.h>
#include "cursors.h"
#include "Selection.h"
#define MAX_DRAG_HEIGHT 200.0
#define ALPHA 170
#define TEXT_OFFSET 5.0
static const rgb_color kDropIndicatorColor = make_color(255, 65, 54, 255);
static const rgb_color kDragFrameColor = make_color(17, 17, 17, 255);
SimpleItem::SimpleItem(const char *name)
:
BStringItem(name)
{
}
SimpleItem::~SimpleItem()
{
}
void
SimpleItem::DrawItem(BView* owner, BRect itemFrame, bool even)
{
DrawBackground(owner, itemFrame, even);
if (IsSelected())
owner->SetHighUIColor(B_LIST_SELECTED_ITEM_TEXT_COLOR);
else
owner->SetHighUIColor(B_LIST_ITEM_TEXT_COLOR);
font_height fh;
owner->GetFontHeight(&fh);
const char* text = Text();
BString truncatedString(text);
owner->TruncateString(&truncatedString, B_TRUNCATE_MIDDLE,
itemFrame.Width() - TEXT_OFFSET - 4);
float height = itemFrame.Height();
float textHeight = fh.ascent + fh.descent;
BPoint textPoint;
textPoint.x = itemFrame.left + TEXT_OFFSET;
textPoint.y = itemFrame.top
+ ceilf(height / 2 - textHeight / 2 + fh.ascent);
owner->DrawString(truncatedString.String(), textPoint);
}
void
SimpleItem::DrawBackground(BView* owner, BRect itemFrame, bool even)
{
rgb_color bgColor;
if (!IsEnabled()) {
rgb_color textColor = ui_color(B_LIST_ITEM_TEXT_COLOR);
rgb_color disabledColor;
if (textColor.red + textColor.green + textColor.blue > 128 * 3)
disabledColor = tint_color(textColor, B_DARKEN_2_TINT);
else
disabledColor = tint_color(textColor, B_LIGHTEN_2_TINT);
bgColor = disabledColor;
} else if (IsSelected())
bgColor = ui_color(B_LIST_SELECTED_BACKGROUND_COLOR);
else
bgColor = ui_color(B_LIST_BACKGROUND_COLOR);
if (even)
bgColor = tint_color(bgColor, 1.06);
owner->SetLowColor(bgColor);
owner->FillRect(itemFrame, B_SOLID_LOW);
}
DragSortableListView::DragSortableListView(BRect frame, const char* name,
list_view_type type, uint32 resizingMode, uint32 flags)
:
BListView(frame, name, type, resizingMode, flags),
fDropRect(0, 0, -1, -1),
fMouseWheelFilter(NULL),
fScrollPulse(NULL),
fDropIndex(-1),
fLastClickedItem(NULL),
fScrollView(NULL),
fDragCommand(B_SIMPLE_DATA),
fFocusedIndex(-1),
fSelection(NULL),
fSyncingToSelection(false),
fModifyingSelection(false)
{
SetViewColor(B_TRANSPARENT_32_BIT);
}
DragSortableListView::~DragSortableListView()
{
delete fMouseWheelFilter;
SetSelection(NULL);
}
void
DragSortableListView::AttachedToWindow()
{
if (!fMouseWheelFilter)
fMouseWheelFilter = new MouseWheelFilter(this);
Window()->AddCommonFilter(fMouseWheelFilter);
BListView::AttachedToWindow();
BRect bounds = Bounds();
BListView::FrameResized(bounds.Width(), bounds.Height());
}
void
DragSortableListView::DetachedFromWindow()
{
Window()->RemoveCommonFilter(fMouseWheelFilter);
}
void
DragSortableListView::FrameResized(float width, float height)
{
BListView::FrameResized(width, height);
}
void
DragSortableListView::TargetedByScrollView(BScrollView* scrollView)
{
fScrollView = scrollView;
BListView::TargetedByScrollView(scrollView);
}
void
DragSortableListView::WindowActivated(bool active)
{
if (BView* view = Parent())
view->Invalidate();
}
void
DragSortableListView::MessageReceived(BMessage* message)
{
if (message->what == fDragCommand) {
int32 count = CountItems();
if (fDropIndex < 0 || fDropIndex > count)
fDropIndex = count;
HandleDropMessage(message, fDropIndex);
fDropIndex = -1;
} else {
switch (message->what) {
case B_MOUSE_WHEEL_CHANGED:
{
BListView::MessageReceived(message);
BPoint where;
uint32 buttons;
GetMouse(&where, &buttons, false);
uint32 transit = Bounds().Contains(where) ? B_INSIDE_VIEW : B_OUTSIDE_VIEW;
MouseMoved(where, transit, &fDragMessageCopy);
break;
}
default:
BListView::MessageReceived(message);
break;
}
}
}
void
DragSortableListView::KeyDown(const char* bytes, int32 numBytes)
{
if (numBytes < 1)
return;
if ((bytes[0] == B_BACKSPACE) || (bytes[0] == B_DELETE))
RemoveSelected();
BListView::KeyDown(bytes, numBytes);
}
void
DragSortableListView::MouseDown(BPoint where)
{
int32 index = IndexOf(where);
BListItem* item = ItemAt(index);
if (index < 0 || item == NULL) {
fLastClickedItem = NULL;
return BListView::MouseDown(where);
}
int32 clicks = 1;
int32 buttons = 0;
Window()->CurrentMessage()->FindInt32("clicks", &clicks);
Window()->CurrentMessage()->FindInt32("buttons", &buttons);
if (clicks == 2 && item == fLastClickedItem) {
DoubleClicked(index);
} else {
fLastClickedItem = item;
}
if (ListType() == B_MULTIPLE_SELECTION_LIST
&& (buttons & B_SECONDARY_MOUSE_BUTTON) != 0) {
if (item->IsSelected())
Deselect(index);
else
Select(index, true);
} else
BListView::MouseDown(where);
}
void
DragSortableListView::MouseMoved(BPoint where, uint32 transit, const BMessage* msg)
{
int32 buttons = 0;
Window()->CurrentMessage()->FindInt32("buttons", &buttons);
if (buttons > 0 && msg && AcceptDragMessage(msg)) {
switch (transit) {
case B_ENTERED_VIEW:
case B_INSIDE_VIEW:
SetDragMessage(msg);
SetDropTargetRect(msg, where);
break;
case B_EXITED_VIEW:
SetDragMessage(NULL);
InvalidateDropRect();
case B_OUTSIDE_VIEW:
break;
}
} else {
SetDragMessage(NULL);
InvalidateDropRect();
BCursor cursor(B_HAND_CURSOR);
SetViewCursor(&cursor, true);
}
fLastMousePos = where;
BListView::MouseMoved(where, transit, msg);
}
void
DragSortableListView::MouseUp(BPoint where)
{
BListView::MouseUp(where);
SetDragMessage(NULL);
InvalidateDropRect();
BCursor cursor(B_HAND_CURSOR);
SetViewCursor(&cursor, true);
}
bool
DragSortableListView::MouseWheelChanged(float x, float y)
{
BPoint where;
uint32 buttons;
GetMouse(&where, &buttons, false);
if (Bounds().Contains(where))
return true;
else
return false;
}
void
DragSortableListView::ObjectChanged(const Observable* object)
{
if (object != fSelection || fModifyingSelection || fSyncingToSelection)
return;
fSyncingToSelection = true;
BList selectedItems;
int32 count = fSelection->CountSelected();
for (int32 i = 0; i < count; i++) {
int32 index = IndexOfSelectable(fSelection->SelectableAtFast(i));
if (index >= 0) {
BListItem* item = ItemAt(index);
if (item && !selectedItems.HasItem((void*)item))
selectedItems.AddItem((void*)item);
}
}
count = selectedItems.CountItems();
if (count == 0) {
if (CurrentSelection(0) >= 0)
DeselectAll();
} else {
count = CountItems();
for (int32 i = 0; i < count; i++) {
BListItem* item = ItemAt(i);
bool selected = selectedItems.RemoveItem((void*)item);
if (item->IsSelected() != selected) {
Select(i, true);
}
}
}
fSyncingToSelection = false;
}
void
DragSortableListView::SetDragCommand(uint32 command)
{
fDragCommand = command;
}
void
DragSortableListView::ModifiersChanged()
{
SetDropTargetRect(&fDragMessageCopy, fLastMousePos);
}
void
DragSortableListView::SetItemFocused(int32 index)
{
InvalidateItem(fFocusedIndex);
InvalidateItem(index);
fFocusedIndex = index;
}
bool
DragSortableListView::AcceptDragMessage(const BMessage* message) const
{
return message->what == fDragCommand;
}
void
DragSortableListView::SetDropTargetRect(const BMessage* message, BPoint where)
{
if (AcceptDragMessage(message)) {
bool copy = modifiers() & B_SHIFT_KEY;
bool replaceAll = !message->HasPointer("list") && !copy;
BRect rect = Bounds();
if (replaceAll) {
rect.bottom--;
fDropRect = rect;
fDropIndex = -1;
} else {
rect = ItemFrame(0);
where.y += rect.Height() / 2;
int32 index = IndexOf(where);
if (index < 0)
index = CountItems();
SetDropIndex(index);
const uchar* cursorData = copy ? kCopyCursor : B_HAND_CURSOR;
BCursor cursor(cursorData);
SetViewCursor(&cursor, true);
}
}
}
bool
DragSortableListView::HandleDropMessage(const BMessage* message,
int32 dropIndex)
{
DragSortableListView *list = NULL;
if (message->FindPointer("list", (void **)&list) != B_OK || list != this)
return false;
BList items;
int32 index;
for (int32 i = 0; message->FindInt32("index", i, &index) == B_OK; i++) {
BListItem* item = ItemAt(index);
if (item != NULL)
items.AddItem((void*)item);
}
if (items.CountItems() == 0)
return false;
if ((modifiers() & B_SHIFT_KEY) != 0)
CopyItems(items, dropIndex);
else
MoveItems(items, dropIndex);
return true;
}
bool
DragSortableListView::DoesAutoScrolling() const
{
return true;
}
void
DragSortableListView::MoveItems(BList& items, int32 index)
{
DeselectAll();
BList removedItems;
int32 count = items.CountItems();
for (int32 i = 0; i < count; i++) {
BListItem* item = (BListItem*)items.ItemAt(i);
int32 removeIndex = IndexOf(item);
if (RemoveItem(item) && removedItems.AddItem((void*)item)) {
if (removeIndex < index)
index--;
}
}
for (int32 i = 0;
BListItem* item = (BListItem*)removedItems.ItemAt(i); i++) {
if (AddItem(item, index)) {
Select(index, true);
index++;
} else
delete item;
}
}
void
DragSortableListView::CopyItems(BList& items, int32 index)
{
DeselectAll();
BList clonedItems;
int32 count = items.CountItems();
for (int32 i = 0; i < count; i++) {
BListItem* item = CloneItem(IndexOf((BListItem*)items.ItemAt(i)));
if (item && !clonedItems.AddItem((void*)item))
delete item;
}
for (int32 i = 0;
BListItem* item = (BListItem*)clonedItems.ItemAt(i); i++) {
if (AddItem(item, index)) {
Select(index, true);
index++;
} else
delete item;
}
}
void
DragSortableListView::RemoveItemList(BList& items)
{
int32 count = items.CountItems();
for (int32 i = 0; i < count; i++) {
BListItem* item = (BListItem*)items.ItemAt(i);
if (RemoveItem(item))
delete item;
}
}
void
DragSortableListView::RemoveSelected()
{
BList items;
for (int32 i = 0; BListItem* item = ItemAt(CurrentSelection(i)); i++)
items.AddItem((void*)item);
RemoveItemList(items);
}
void
DragSortableListView::SetSelection(Selection* selection)
{
if (fSelection == selection)
return;
if (fSelection)
fSelection->RemoveObserver(this);
fSelection = selection;
if (fSelection)
fSelection->AddObserver(this);
}
int32
DragSortableListView::IndexOfSelectable(Selectable* selectable) const
{
return -1;
}
Selectable*
DragSortableListView::SelectableFor(BListItem* item) const
{
return NULL;
}
void
DragSortableListView::SelectAll()
{
Select(0, CountItems() - 1);
}
int32
DragSortableListView::CountSelectedItems() const
{
int32 count = 0;
while (CurrentSelection(count) >= 0)
count++;
return count;
}
void
DragSortableListView::SelectionChanged()
{
if (!fSelection || fSyncingToSelection)
return;
fModifyingSelection = true;
BList selectables;
for (int32 i = 0; BListItem* item = ItemAt(CurrentSelection(i)); i++) {
Selectable* selectable = SelectableFor(item);
if (selectable)
selectables.AddItem((void*)selectable);
}
AutoNotificationSuspender _(fSelection);
int32 count = selectables.CountItems();
if (count == 0) {
if (!fSyncingToSelection)
fSelection->DeselectAll();
} else {
for (int32 i = 0; i < count; i++) {
Selectable* selectable = (Selectable*)selectables.ItemAtFast(i);
fSelection->Select(selectable, i > 0);
}
}
fModifyingSelection = false;
}
bool
DragSortableListView::DeleteItem(int32 index)
{
BListItem* item = ItemAt(index);
if (item && RemoveItem(item)) {
delete item;
return true;
}
return false;
}
void
DragSortableListView::SetDropRect(BRect rect)
{
fDropRect = rect;
}
void
DragSortableListView::SetDropIndex(int32 index)
{
if (fDropIndex != index) {
fDropIndex = index;
if (fDropIndex >= 0) {
int32 count = CountItems();
if (fDropIndex == count) {
BRect rect;
if (ItemAt(count - 1)) {
rect = ItemFrame(count - 1);
rect.top = rect.bottom;
rect.bottom = rect.top + 1;
} else {
rect = Bounds();
rect.bottom--;
}
fDropRect = rect;
} else {
BRect rect = ItemFrame(fDropIndex);
rect.top--;
rect.bottom = rect.top + 1;
fDropRect = rect;
}
}
}
}
void
DragSortableListView::InvalidateDropRect()
{
fDropRect = BRect(0, 0, -1, -1);
}
void
DragSortableListView::SetDragMessage(const BMessage* message)
{
if (message)
fDragMessageCopy = *message;
else
fDragMessageCopy.what = 0;
}
SimpleListView::SimpleListView(BRect frame, BMessage* selectionChangeMessage)
:
DragSortableListView(frame, "playlist listview",
B_MULTIPLE_SELECTION_LIST, B_FOLLOW_ALL,
B_WILL_DRAW | B_NAVIGABLE | B_FRAME_EVENTS | B_FULL_UPDATE_ON_RESIZE),
fSelectionChangeMessage(selectionChangeMessage)
{
}
SimpleListView::SimpleListView(BRect frame, const char* name,
BMessage* selectionChangeMessage, list_view_type type,
uint32 resizingMode, uint32 flags)
:
DragSortableListView(frame, name, type, resizingMode, flags),
fSelectionChangeMessage(selectionChangeMessage)
{
}
SimpleListView::~SimpleListView()
{
delete fSelectionChangeMessage;
}
void
SimpleListView::DetachedFromWindow()
{
DragSortableListView::DetachedFromWindow();
_MakeEmpty();
}
void
SimpleListView::Draw(BRect updateRect)
{
BRect emptyRect = updateRect;
int32 firstIndex = IndexOf(updateRect.LeftTop());
int32 lastIndex = IndexOf(updateRect.RightBottom());
if (firstIndex >= 0) {
BListItem* item;
BRect itemFrame(0, 0, -1, -1);
if (lastIndex < firstIndex)
lastIndex = CountItems() - 1;
for (int32 i = firstIndex; i <= lastIndex; i++) {
item = ItemAt(i);
if (item == NULL)
continue;
itemFrame = ItemFrame(i);
item->DrawItem(this, itemFrame, (i % 2) == 0);
if (i == fDropIndex) {
SetHighColor(kDropIndicatorColor);
StrokeLine(fDropRect.LeftTop(), fDropRect.RightTop());
}
}
emptyRect.top = itemFrame.bottom + 1;
}
if (emptyRect.IsValid()) {
SetLowUIColor(B_LIST_BACKGROUND_COLOR);
FillRect(emptyRect, B_SOLID_LOW);
}
#if 0
if (IsFocus()) {
SetHighUIColor(B_KEYBOARD_NAVIGATION_COLOR);
StrokeRect(Bounds());
}
#endif
}
bool
SimpleListView::InitiateDrag(BPoint where, int32 index, bool)
{
if (fFocusedIndex >= 0)
return false;
BListItem* item = ItemAt(CurrentSelection(0));
if (item == NULL) {
Select(index);
item = ItemAt(index);
}
if (item == NULL)
return false;
BMessage msg(fDragCommand);
MakeDragMessage(&msg);
float width = Bounds().Width();
BRect dragRect(0, 0, width, -1);
int32 numItems;
bool fade = false;
for (numItems = 0; BListItem* item = ItemAt(CurrentSelection(numItems)); numItems++) {
dragRect.bottom += ceilf(item->Height()) + 1;
if (dragRect.Height() > MAX_DRAG_HEIGHT) {
fade = true;
dragRect.bottom = MAX_DRAG_HEIGHT;
numItems++;
break;
}
}
BBitmap* dragBitmap = new BBitmap(dragRect, B_RGBA32, true);
if (dragBitmap && dragBitmap->IsValid()) {
if (BView* view = new BView(dragBitmap->Bounds(), "helper",
B_FOLLOW_NONE, B_WILL_DRAW)) {
dragBitmap->AddChild(view);
dragBitmap->Lock();
BRect itemFrame(dragRect) ;
itemFrame.bottom = 0.0;
BListItem* item;
for (int32 i = 0; i < numItems; i++) {
item = ItemAt(CurrentSelection(i));
if (item == NULL)
continue;
itemFrame.bottom = itemFrame.top + ceilf(item->Height());
if (itemFrame.bottom > dragRect.bottom)
itemFrame.bottom = dragRect.bottom;
item->DrawItem(view, itemFrame, (i % 2) == 0);
itemFrame.top = itemFrame.bottom + 1;
}
view->SetHighColor(kDragFrameColor);
view->StrokeRect(view->Bounds());
view->Sync();
uint8* bits = (uint8*)dragBitmap->Bits();
int32 height = (int32)dragBitmap->Bounds().Height() + 1;
int32 width = (int32)dragBitmap->Bounds().Width() + 1;
int32 bpr = dragBitmap->BytesPerRow();
if (fade) {
for (int32 y = 0; y < height - ALPHA / 2; y++, bits += bpr) {
uint8* line = bits + 3;
for (uint8 *end = line + 4 * width; line < end; line += 4)
*line = ALPHA;
}
for (int32 y = height - ALPHA / 2; y < height; y++, bits += bpr) {
uint8* line = bits + 3;
for (uint8 *end = line + 4 * width; line < end; line += 4)
*line = (height - y) << 1;
}
} else {
for (int32 y = 0; y < height; y++, bits += bpr) {
uint8* line = bits + 3;
for (uint8 *end = line + 4 * width; line < end; line += 4)
*line = ALPHA;
}
}
dragBitmap->Unlock();
}
} else {
delete dragBitmap;
dragBitmap = NULL;
}
if (dragBitmap)
DragMessage(&msg, dragBitmap, B_OP_ALPHA, B_ORIGIN);
else
DragMessage(&msg, dragRect.OffsetToCopy(where), this);
SetDragMessage(&msg);
return true;
}
void
SimpleListView::MessageReceived(BMessage* message)
{
switch (message->what) {
case B_COPY:
{
int count = CountSelectedItems();
if (count == 0)
return;
if (!be_clipboard->Lock())
break;
be_clipboard->Clear();
BMessage data;
ArchiveSelection(&data);
ssize_t size = data.FlattenedSize();
BStackOrHeapArray<char, 1024> archive(size);
if (!archive) {
be_clipboard->Unlock();
break;
}
data.Flatten(archive, size);
be_clipboard->Data()->AddData(
"application/x-vnd.icon_o_matic-listview-message", B_MIME_TYPE, archive, size);
be_clipboard->Commit();
be_clipboard->Unlock();
break;
}
default:
DragSortableListView::MessageReceived(message);
break;
}
}
BListItem*
SimpleListView::CloneItem(int32 atIndex) const
{
BListItem* clone = NULL;
if (SimpleItem* item = dynamic_cast<SimpleItem*>(ItemAt(atIndex)))
clone = new SimpleItem(item->Text());
return clone;
}
void
SimpleListView::MakeDragMessage(BMessage* message) const
{
if (message) {
message->AddPointer("list",
(void*)dynamic_cast<const DragSortableListView*>(this));
int32 index;
for (int32 i = 0; (index = CurrentSelection(i)) >= 0; i++)
message->AddInt32("index", index);
}
BMessage items;
ArchiveSelection(&items);
message->AddMessage("items", &items);
}
bool
SimpleListView::HandleDropMessage(const BMessage* message, int32 dropIndex)
{
if (DragSortableListView::HandleDropMessage(message, dropIndex))
return true;
BMessage items;
if (message->FindMessage("items", &items) != B_OK)
return false;
return InstantiateSelection(&items, dropIndex);
}
bool
SimpleListView::HandlePaste(const BMessage* archive)
{
return InstantiateSelection(archive, CountItems());
}
void
SimpleListView::_MakeEmpty()
{
int32 count = CountItems();
for (int32 i = count - 1; i >= 0; i--)
delete RemoveItem(i);
}