/* Open Tracker License Terms and Conditions Copyright (c) 1991-2000, Be Incorporated. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice applies to all licensees and shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of Be Incorporated shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Be Incorporated. Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or registered trademarks of Be Incorporated in the United States and other countries. Other brand product names are registered trademarks or trademarks of their respective holders. All rights reserved. */ // ListView title drawing and mouse manipulation classes #include "TitleView.h" #include #include #include #include #include #include #include "Commands.h" #include "ContainerWindow.h" #include "PoseView.h" #include "Utilities.h" const rgb_color kTitleBackground = {220, 220, 220, 255}; const rgb_color kDarkTitleBackground = {180, 180, 180, 255}; const rgb_color kHighlightColor = {100, 100, 210, 255}; const rgb_color kLightGray = {150, 150, 150, 255}; const rgb_color kGray = {100, 100, 100, 255}; const rgb_color kDarkGray = {70, 70, 70, 255}; const unsigned char kHorizontalResizeCursor[] = { 16, 1, 7, 7, 0, 0, 1, 0, 1, 0, 1, 0, 9, 32, 25, 48, 57, 56, 121, 60, 57, 56, 25, 48, 9, 32, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 3, 128, 3, 128, 3, 128, 15, 224, 31, 240, 63, 248, 127, 252, 255, 254, 127, 252, 63, 248, 31, 240, 15, 224, 3, 128, 3, 128, 3, 128, 0, 0 }; BTitleView::BTitleView(BRect frame, BPoseView *view) : BView(frame, "TitleView", B_FOLLOW_LEFT_RIGHT, B_WILL_DRAW), fPoseView(view), fTitleList(10, true), fHorizontalResizeCursor(kHorizontalResizeCursor), fPreviouslyClickedColumnTitle(0) { SetHighColor(kTitleBackground); SetLowColor(kTitleBackground); SetViewColor(kTitleBackground); BFont font(be_plain_font); font.SetSize(9); SetFont(&font); Reset(); } BTitleView::~BTitleView() { } void BTitleView::Reset() { fTitleList.MakeEmpty(); for (int32 index = 0; ; index++) { BColumn *column = fPoseView->ColumnAt(index); if (!column) break; fTitleList.AddItem(new BColumnTitle(this, column)); } } void BTitleView::AddTitle(BColumn *column, const BColumn *after) { int32 count = fTitleList.CountItems(); int32 index; if (after) for (index = 0; index < count; index++) { BColumn *titleColumn = fTitleList.ItemAt(index)->Column(); if (after == titleColumn) { index++; break; } } else index = count; fTitleList.AddItem(new BColumnTitle(this, column), index); Invalidate(); } void BTitleView::RemoveTitle(BColumn *column) { int32 count = fTitleList.CountItems(); for (int32 index = 0; index < count; index++) { BColumnTitle *title = fTitleList.ItemAt(index); if (title->Column() == column) { fTitleList.RemoveItem(title); break; } } Invalidate(); } void BTitleView::Draw(BRect rect) { Draw(rect, false); } void BTitleView::Draw(BRect, bool useOffscreen, bool updateOnly, const BColumnTitle *pressedColumn, void (*trackRectBlitter)(BView *, BRect), BRect passThru) { BRect bounds(Bounds()); BView *view; if (useOffscreen) { ASSERT(offscreen); BRect frame(bounds); frame.right += frame.left; // this is kind of messy way of avoiding being clipped by the ammount the // title is scrolled to the left // ToDo: // fix this view = offscreen->BeginUsing(frame); view->SetOrigin(-bounds.left, 0); view->SetLowColor(LowColor()); view->SetHighColor(HighColor()); BFont font(be_plain_font); font.SetSize(9); view->SetFont(&font); } else view = this; // fill background with light gray background if (!updateOnly) view->FillRect(bounds, B_SOLID_LOW); view->BeginLineArray(4); view->AddLine(bounds.LeftTop(), bounds.RightTop(), kGray); view->AddLine(bounds.LeftBottom(), bounds.RightBottom(), kGray); // draw lighter gray and white inset lines bounds.InsetBy(0, 1); view->AddLine(bounds.LeftBottom(), bounds.RightBottom(), kLightGray); view->AddLine(bounds.LeftTop(), bounds.RightTop(), kWhite); view->EndLineArray(); int32 count = fTitleList.CountItems(); float minx = bounds.right; float maxx = bounds.left; for (int32 index = 0; index < count; index++) { BColumnTitle *title = fTitleList.ItemAt(index); title->Draw(view, title == pressedColumn); BRect titleBounds(title->Bounds()); if (titleBounds.left < minx) minx = titleBounds.left; if (titleBounds.right > maxx) maxx = titleBounds.right; } // first and last shades before and after first column BRect tmp(bounds); tmp.right = maxx; tmp.left = minx; tmp.InsetBy(-1, 0); view->BeginLineArray(2); view->AddLine(tmp.LeftTop(), tmp.LeftBottom(), kGray); view->AddLine(tmp.RightTop(), tmp.RightBottom(), kWhite); view->EndLineArray(); if (useOffscreen) { if (trackRectBlitter) (trackRectBlitter)(view, passThru); view->Sync(); DrawBitmap(offscreen->Bitmap()); offscreen->DoneUsing(); } else if (trackRectBlitter) (trackRectBlitter)(view, passThru); } void BTitleView::MouseDown(BPoint where) { if (!Window()->IsActive()) { // wasn't active, just activate and bail Window()->Activate(); return; } // finish any pending edits fPoseView->CommitActivePose(); BColumnTitle *title = FindColumnTitle(where); BColumnTitle *resizedTitle = InColumnResizeArea(where); uint32 buttons; GetMouse(&where, &buttons); // Check if the user clicked the secondary mouse button. // if so, display the attribute menu: if (buttons & B_SECONDARY_MOUSE_BUTTON) { BContainerWindow *window = dynamic_cast (Window()); BPopUpMenu *menu = new BPopUpMenu("Attributes", false, false); menu->SetFont(be_plain_font); window->NewAttributeMenu(menu); window->AddMimeTypesToMenu(menu); window->MarkAttributeMenu(menu); menu->SetTargetForItems(window->PoseView()); menu->Go(ConvertToScreen(where), true, false); return; } bigtime_t doubleClickSpeed; get_click_speed(&doubleClickSpeed); if (resizedTitle) { bool force = static_cast(buttons & B_TERTIARY_MOUSE_BUTTON); if (force || buttons & B_PRIMARY_MOUSE_BUTTON) { if (force || fPreviouslyClickedColumnTitle != 0) { if (force || system_time() - fPreviousLeftClickTime < doubleClickSpeed) { if (fPoseView->ResizeColumnToWidest(resizedTitle->Column())) { Invalidate(); return; } } } fPreviousLeftClickTime = system_time(); fPreviouslyClickedColumnTitle = resizedTitle; } } else if (!title) return; ColumnTrackState *trackState; if (resizedTitle) trackState = new ColumnResizeState(this, resizedTitle, where); else trackState = new ColumnDragState(this, title, where); // track the mouse // - if it is pressed shortly and not moved, it is a click // all else is a track bigtime_t pastClickTime = system_time() + doubleClickSpeed; bool pastClick = false; for (;;) { BPoint old(where); GetMouse(&where, &buttons); if (!buttons) { if (!pastClick) trackState->Clicked(where, buttons); else trackState->Done(where); break; } BRect oldMarging(old, old); oldMarging.InsetBy(-1, -1); if ((pastClick && where != old) || !oldMarging.Contains(where)) { // if not pressing yet, use a margin to start, else // call moved on any mouse movement pastClick = true; trackState->MouseMoved(where, buttons); } if (!pastClick && system_time() > pastClickTime) pastClick = true; snooze(15000); } delete trackState; } void BTitleView::MouseMoved(BPoint where, uint32 code, const BMessage *message) { switch (code) { default: if (InColumnResizeArea(where) && Window()->IsActive()) SetViewCursor(&fHorizontalResizeCursor); else SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); break; case B_EXITED_VIEW: SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); break; } _inherited::MouseMoved(where, code, message); } BColumnTitle * BTitleView::InColumnResizeArea(BPoint where) const { int32 count = fTitleList.CountItems(); for (int32 index = 0; index < count; index++) { BColumnTitle *title = fTitleList.ItemAt(index); if (title->InColumnResizeArea(where)) return title; } return NULL; } BColumnTitle * BTitleView::FindColumnTitle(BPoint where) const { int32 count = fTitleList.CountItems(); for (int32 index = 0; index < count; index++) { BColumnTitle *title = fTitleList.ItemAt(index); if (title->Bounds().Contains(where)) return title; } return NULL; } BColumnTitle * BTitleView::FindColumnTitle(const BColumn *column) const { int32 count = fTitleList.CountItems(); for (int32 index = 0; index < count; index++) { BColumnTitle *title = fTitleList.ItemAt(index); if (title->Column() == column) return title; } return NULL; } bool BColumnTitle::InColumnResizeArea(BPoint where) const { BRect edge(Bounds()); edge.left = edge.right - kEdgeSize; edge.right += kEdgeSize; return edge.Contains(where); } BColumnTitle::BColumnTitle(BTitleView *view, BColumn *column) : fColumn(column), fParent(view) { } BRect BColumnTitle::Bounds() const { BRect bounds(fColumn->Offset() - kTitleColumnLeftExtraMargin, 0, 0, kTitleViewHeight); bounds.right = bounds.left + fColumn->Width() + kTitleColumnExtraMargin; return bounds; } void BColumnTitle::Draw(BView *view, bool pressed) { BRect bounds(Bounds()); BPoint loc(0, bounds.bottom - 4); if (pressed) view->SetLowColor(kDarkTitleBackground); else view->SetLowColor(kTitleBackground); view->FillRect(bounds, B_SOLID_LOW); BString titleString(fColumn->Title()); view->TruncateString(&titleString, B_TRUNCATE_END, bounds.Width() - kTitleColumnExtraMargin); float resultingWidth = view->StringWidth(titleString.String()); switch (fColumn->Alignment()) { case B_ALIGN_LEFT: loc.x = bounds.left + 1 + kTitleColumnLeftExtraMargin; break; case B_ALIGN_CENTER: loc.x = bounds.left + (bounds.Width() / 2) - (resultingWidth / 2); break; case B_ALIGN_RIGHT: loc.x = bounds.right - resultingWidth - kTitleColumnRightExtraMargin; break; } view->MovePenTo(loc); view->SetHighColor(0, 0, 0); view->DrawString(titleString.String()); // show sort columns bool secondary = (fColumn->AttrHash() == fParent->PoseView()->SecondarySort()); if (secondary || (fColumn->AttrHash() == fParent->PoseView()->PrimarySort())) { BPoint pt1(loc); BPoint pt2(view->PenLocation()); pt1.x--; pt2.x--; pt1.y++; pt2.y++; if (secondary) view->StrokeLine(pt1, pt2, B_MIXED_COLORS); else view->StrokeLine(pt1, pt2); } BRect rect(bounds); view->SetHighColor(kGray); view->StrokeRect(rect); view->BeginLineArray(4); // draw lighter gray and white inset lines rect.InsetBy(1, 1); view->AddLine(rect.LeftBottom(), rect.RightBottom(), pressed ? kLightGray : kLightGray); view->AddLine(rect.LeftTop(), rect.RightTop(), pressed ? kDarkGray: kWhite); view->AddLine(rect.LeftTop(), rect.LeftBottom(), pressed ? kDarkGray : kWhite); view->AddLine(rect.RightTop(), rect.RightBottom(), pressed ? kLightGray : kLightGray); view->EndLineArray(); } ColumnTrackState::ColumnTrackState(BTitleView *view, BColumnTitle *title, BPoint where) : fTitleView(view), fTitle(title), fLastPos(where) { } void ColumnTrackState::MouseMoved(BPoint where, uint32 buttons) { if (!ValueChanged(where)) return; Moved(where, buttons); fLastPos = where; } ColumnResizeState::ColumnResizeState(BTitleView *view, BColumnTitle *title, BPoint where) : ColumnTrackState(view, title, where), fLastLineDrawPos(-1), fInitialTrackOffset((title->fColumn->Offset() + title->fColumn->Width()) - where.x) { DrawLine(); } bool ColumnResizeState::ValueChanged(BPoint where) { float newWidth = where.x + fInitialTrackOffset - fTitle->fColumn->Offset(); if (newWidth < kMinColumnWidth) newWidth = kMinColumnWidth; return newWidth != fTitle->fColumn->Width(); } static void _DrawLine(BPoseView *view, BPoint from, BPoint to) { rgb_color highColor = view->HighColor(); view->SetHighColor(kHighlightColor); view->StrokeLine(from, to); view->SetHighColor(highColor); } static void _UndrawLine(BPoseView *view, BPoint from, BPoint to) { view->StrokeLine(from, to, B_SOLID_LOW); } void ColumnResizeState::Moved(BPoint where, uint32) { float newWidth = where.x + fInitialTrackOffset - fTitle->fColumn->Offset(); if (newWidth < kMinColumnWidth) newWidth = kMinColumnWidth; BPoseView *poseView = fTitleView->PoseView(); // bool shrink = (newWidth < fTitle->fColumn->Width()); // resize the column poseView->ResizeColumn(fTitle->fColumn, newWidth, &fLastLineDrawPos, _DrawLine, _UndrawLine); BRect bounds(fTitleView->Bounds()); bounds.left = fTitle->fColumn->Offset(); // force title redraw fTitleView->Draw(bounds, true, false); } void ColumnResizeState::Done(BPoint) { UndrawLine(); } void ColumnResizeState::Clicked(BPoint, uint32) { UndrawLine(); } void ColumnResizeState::DrawLine() { BPoseView *poseView = fTitleView->PoseView(); ASSERT(!poseView->IsDesktopWindow()); BRect poseViewBounds(poseView->Bounds()); // remember the line location poseViewBounds.left = fTitle->Bounds().right; fLastLineDrawPos = poseViewBounds.left; // draw the line in the new location _DrawLine(poseView, poseViewBounds.LeftTop(), poseViewBounds.LeftBottom()); } void ColumnResizeState::UndrawLine() { if (fLastLineDrawPos < 0) return; BRect poseViewBounds(fTitleView->PoseView()->Bounds()); poseViewBounds.left = fLastLineDrawPos; _UndrawLine(fTitleView->PoseView(), poseViewBounds.LeftTop(), poseViewBounds.LeftBottom()); } ColumnDragState::ColumnDragState(BTitleView *view, BColumnTitle *columnTitle, BPoint where) : ColumnTrackState(view, columnTitle, where), fInitialMouseTrackOffset(where.x), fTrackingRemovedColumn(false) { ASSERT(columnTitle); ASSERT(fTitle); ASSERT(fTitle->Column()); DrawPressNoOutline(); } static void _DrawOutline(BView *view, BRect where) { where.InsetBy(1, 1); rgb_color highColor = view->HighColor(); view->SetHighColor(kHighlightColor); view->StrokeRect(where); view->SetHighColor(highColor); } // ToDo: // Autoscroll when dragging column left/right // fix dragging back a column before the first column (now adds as last) // make column swaps/adds not invalidate/redraw columns to the left void ColumnDragState::Moved(BPoint where, uint32) { // figure out where we are with the mouse BRect titleBounds(fTitleView->Bounds()); bool overTitleView = titleBounds.Contains(where); BColumnTitle *overTitle = overTitleView ? fTitleView->FindColumnTitle(where) : 0; BRect titleBoundsWithMargin(titleBounds); titleBoundsWithMargin.InsetBy(0, -kRemoveTitleMargin); bool inMarginRect = overTitleView || titleBoundsWithMargin.Contains(where); bool drawOutline = false; bool undrawOutline = false; if (fTrackingRemovedColumn) { if (overTitleView) { // tracked back with a removed title into the title bar, add it // back fTitleView->EndRectTracking(); fColumnArchive.Seek(0, SEEK_SET); BColumn *column = BColumn::InstantiateFromStream(&fColumnArchive); ASSERT(column); const BColumn *after = NULL; if (overTitle) after = overTitle->Column(); fTitleView->PoseView()->AddColumn(column, after); fTrackingRemovedColumn = false; fTitle = fTitleView->FindColumnTitle(column); fInitialMouseTrackOffset += fTitle->Bounds().left; drawOutline = true; } } else { if (!inMarginRect) { // dragged a title out of the hysteresis margin around the // title bar - remove it and start dragging it as a dotted outline BRect rect(fTitle->Bounds()); rect.OffsetBy(where.x - fInitialMouseTrackOffset, where.y - 5); fColumnArchive.Seek(0, SEEK_SET); fTitle->Column()->ArchiveToStream(&fColumnArchive); fInitialMouseTrackOffset -= fTitle->Bounds().left; if (fTitleView->PoseView()->RemoveColumn(fTitle->Column(), false)) { fTitle = 0; fTitleView->BeginRectTracking(rect); fTrackingRemovedColumn = true; undrawOutline = true; } } else if (overTitle && overTitle != fTitle // over a different column && (overTitle->Bounds().left >= fTitle->Bounds().right // over the one to the right || where.x < overTitle->Bounds().left + fTitle->Bounds().Width())){ // over the one to the left, far enough to not snap right back BColumn *column = fTitle->Column(); fInitialMouseTrackOffset -= fTitle->Bounds().left; // swap the columns fTitleView->PoseView()->MoveColumnTo(column, overTitle->Column()); // re-grab the title object looking it up by the column fTitle = fTitleView->FindColumnTitle(column); // recalc initialMouseTrackOffset fInitialMouseTrackOffset += fTitle->Bounds().left; drawOutline = true; } else drawOutline = true; } if (drawOutline) DrawOutline(where.x - fInitialMouseTrackOffset); else if (undrawOutline) UndrawOutline(); } void ColumnDragState::Done(BPoint) { if (fTrackingRemovedColumn) fTitleView->EndRectTracking(); UndrawOutline(); } void ColumnDragState::Clicked(BPoint, uint32) { BPoseView *poseView = fTitleView->PoseView(); uint32 hash = fTitle->Column()->AttrHash(); uint32 primarySort = poseView->PrimarySort(); uint32 secondarySort = poseView->SecondarySort(); bool shift = (modifiers() & B_SHIFT_KEY) != 0; // For now: // if we hit the primary sort field again // then if shift key was down, switch primary and secondary if (hash == primarySort) { if (shift && secondarySort) { poseView->SetPrimarySort(secondarySort); poseView->SetSecondarySort(primarySort); } else poseView->SetReverseSort(!poseView->ReverseSort()); } else if (shift) { // hit secondary sort column with shift key, disable if (hash == secondarySort) poseView->SetSecondarySort(0); else poseView->SetSecondarySort(hash); } else { poseView->SetPrimarySort(hash); poseView->SetReverseSort(false); } if (poseView->PrimarySort() == poseView->SecondarySort()) poseView->SetSecondarySort(0); UndrawOutline(); poseView->SortPoses(); poseView->Invalidate(); } void ColumnDragState::Pressing(BPoint, uint32) { } bool ColumnDragState::ValueChanged(BPoint) { return true; } void ColumnDragState::DrawPressNoOutline() { fTitleView->Draw(fTitleView->Bounds(), true, false, fTitle); } void ColumnDragState::DrawOutline(float pos) { BRect outline(fTitle->Bounds()); outline.OffsetBy(pos, 0); fTitleView->Draw(fTitleView->Bounds(), true, false, fTitle, _DrawOutline, outline); } void ColumnDragState::UndrawOutline() { fTitleView->Draw(fTitleView->Bounds(), true, false); } OffscreenBitmap *BTitleView::offscreen = new OffscreenBitmap;