ciderpress/app/ContentList.cpp

1147 lines
33 KiB
C++

/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Implementation of list control showing archive contents.
*/
#include "stdafx.h"
#include "Main.h"
#include "ContentList.h"
const LPARAM kDescendingFlag = 0x0100;
BEGIN_MESSAGE_MAP(ContentList, CListCtrl)
ON_WM_CREATE()
ON_WM_DESTROY()
ON_WM_SYSCOLORCHANGE()
//ON_WM_MOUSEWHEEL()
ON_NOTIFY_REFLECT(NM_DBLCLK, OnDoubleClick)
ON_NOTIFY_REFLECT(NM_RCLICK, OnRightClick)
ON_NOTIFY_REFLECT(LVN_COLUMNCLICK, OnColumnClick)
ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetDispInfo)
END_MESSAGE_MAP()
#if 0
afx_msg BOOL
ContentList::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
WMSG0("MOUSE WHEEL\n");
return CWnd::OnMouseWheel(nFlags, zDelta, pt);
// return TRUE;
}
#endif
/*
* Put the window into "report" mode, and add a client edge since we're not
* using one on the frame window.
*/
BOOL
ContentList::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CListCtrl::PreCreateWindow(cs))
return FALSE;
cs.style &= ~LVS_TYPEMASK;
cs.style |= LVS_REPORT;
cs.style |= LVS_SHOWSELALWAYS;
cs.dwExStyle |= WS_EX_CLIENTEDGE;
return TRUE;
}
/*
* Auto-cleanup the object.
*/
void
ContentList::PostNcDestroy(void)
{
WMSG0("ContentList PostNcDestroy\n");
delete this;
}
static inline int
MaxVal(int a, int b)
{
return a > b ? a : b;
}
/*
* Create and populate list control.
*/
int
ContentList::OnCreate(LPCREATESTRUCT lpcs)
{
CString colHdrs[kNumVisibleColumns] = {
"Pathname", "Type", "Aux", "Mod Date",
"Format", "Size", "Ratio", "Packed", "Access"
}; // these should come from string table, not hard-coded
static int colFmt[kNumVisibleColumns] = {
LVCFMT_LEFT, LVCFMT_LEFT, LVCFMT_LEFT, LVCFMT_LEFT,
LVCFMT_LEFT, LVCFMT_RIGHT, LVCFMT_RIGHT, LVCFMT_RIGHT, LVCFMT_LEFT
};
if (CListCtrl::OnCreate(lpcs) == -1)
return -1;
/*
* Create all of the columns with an initial width of 1, then set
* them to the correct values with NewColumnWidths() (which handles
* defaulted values).
*/
for (int i = 0; i < kNumVisibleColumns; i++)
InsertColumn(i, colHdrs[i], colFmt[i], 1);
NewColumnWidths();
/* add images for list; this MUST be loaded before header images */
LoadListImages();
SetImageList(&fListImageList, LVSIL_SMALL);
/* add our up/down arrow bitmaps */
LoadHeaderImages();
CHeaderCtrl* pHeader = GetHeaderCtrl();
if (pHeader == nil)
WMSG0("GLITCH: couldn't get header ctrl\n");
ASSERT(pHeader != NULL);
pHeader->SetImageList(&fHdrImageList);
/* load the data and sort it */
if (LoadData() != 0) {
MessageBox(L"Not all entries were loaded.", L"Error",
MB_OK | MB_ICONSTOP);
/* keep going with what we've got; the error only affects display */
}
NewSortOrder();
/* grab the focus so we get keyboard and mouse wheel messages */
SetFocus();
/* highlight/select entire line, not just filename */
ListView_SetExtendedListViewStyleEx(m_hWnd,
LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);
return 0;
}
/*
* If we're being shut down, save off the column width info before the window
* gets destroyed.
*/
void
ContentList::OnDestroy(void)
{
WMSG0("ContentList OnDestroy\n");
ExportColumnWidths();
CListCtrl::OnDestroy();
}
/*
* The system colors are changing; delete the image list and re-load it.
*/
void
ContentList::OnSysColorChange(void)
{
fHdrImageList.DeleteImageList();
LoadHeaderImages();
}
/*
* They've clicked on a header. Figure out what kind of sort order we want
* to use.
*/
void
ContentList::OnColumnClick(NMHDR* pnmh, LRESULT* pResult)
{
NM_LISTVIEW* pnmlv = (NM_LISTVIEW*) pnmh;
WMSG0("OnColumnClick!!\n");
if (fpLayout->GetSortColumn() == pnmlv->iSubItem)
fpLayout->SetAscending(!fpLayout->GetAscending());
else {
fpLayout->SetSortColumn(pnmlv->iSubItem);
fpLayout->SetAscending(true);
}
NewSortOrder();
*pResult = 0;
}
/*
* Copy the current column widths out to the Preferences object.
*/
void
ContentList::ExportColumnWidths(void)
{
//WMSG0("ExportColumnWidths\n");
for (int i = 0; i < kNumVisibleColumns; i++)
fpLayout->SetColumnWidth(i, GetColumnWidth(i));
}
/*
* Call this when the column widths are changed programmatically (e.g. by
* the preferences page enabling or disabling columns).
*
* We want to set any defaulted entries to actual values so that, if the
* font properties change, column A doesn't resize when column B is tweaked
* in the Preferences dialog. (If it's still set to "default", then when
* we say "update all widths" the defaultedness will be re-evaluated.)
*/
void
ContentList::NewColumnWidths(void)
{
for (int i = 0; i < kNumVisibleColumns; i++) {
int width = fpLayout->GetColumnWidth(i);
if (width == ColumnLayout::kWidthDefaulted) {
width = GetDefaultWidth(i);
WMSG2("Defaulting width %d to %d\n", i, width);
fpLayout->SetColumnWidth(i, width);
}
SetColumnWidth(i, width);
}
}
#if 0 // replaced by GenericArchive reload flag
/*
* If we're in the middle of an update, invalidate the contents of the list
* so that we don't try to redraw from underlying storage that is no longer
* there.
*
* If we call DeleteAllItems the list will immediately blank itself. This
* rather sucks. Instead, we just mark it as invalid and have the "virtual"
* list goodies return empty strings. If the window has to redraw it won't
* do so properly, but most of the time it looks good and it beats flashing
* blank or crashing.
*/
void
ContentList::Invalidate(void)
{
fInvalid = true;
}
#endif
/*
* The archive contents have changed. Reload the list from the
* GenericArchive.
*
* Reloading causes the current selection and view position to be lost. This
* is sort of annoying if all we did is add a comment, so we try to save the
* selection and reapply it. To do this correctly we need some sort of
* unique identifier so we can spot the records that have come back.
*
* Nothing in GenericArchive should be considered valid at this point.
*/
void
ContentList::Reload(bool saveSelection)
{
WMSG0("Reloading ContentList\n");
CWaitCursor waitc;
// fInvalid = false;
fpArchive->ClearReloadFlag();
long* savedSel = nil;
long selCount = 0;
if (saveSelection) {
/* get the serials for the current selection (if any) */
savedSel = GetSelectionSerials(&selCount);
}
/* get the item that's currently at the top of the page */
int top = GetTopIndex();
int bottom = top + GetCountPerPage() -1;
/* reload the list */
LoadData();
NewSortOrder();
if (savedSel != nil) {
/* restore the selection */
RestoreSelection(savedSel, selCount);
delete[] savedSel;
}
/* try to put us back in the same place */
EnsureVisible(bottom, false);
EnsureVisible(top, false);
}
#if 1
/*
* Get the "selection serials" from the list of selected items.
*
* The caller is responsible for delete[]ing the return value.
*/
long*
ContentList::GetSelectionSerials(long* pSelCount)
{
long* savedSel = nil;
long maxCount;
maxCount = GetSelectedCount();
WMSG1("GetSelectionSerials (maxCount=%d)\n", maxCount);
if (maxCount > 0) {
savedSel = new long[maxCount];
int idx = 0;
POSITION posn;
posn = GetFirstSelectedItemPosition();
ASSERT(posn != nil);
if (posn == nil)
return nil;
while (posn != nil) {
int num = GetNextSelectedItem(posn);
GenericEntry* pEntry = (GenericEntry*) GetItemData(num);
if (idx == maxCount) {
ASSERT(false);
break;
}
savedSel[idx++] = pEntry->GetSelectionSerial();
}
ASSERT(idx == maxCount);
}
*pSelCount = maxCount;
return savedSel;
}
/*
* Restore the selection from the "savedSel" list.
*/
void
ContentList::RestoreSelection(const long* savedSel, long selCount)
{
WMSG1("RestoreSelection (selCount=%d)\n", selCount);
if (savedSel == nil)
return;
int i, j;
for (i = GetItemCount()-1; i >= 0; i--) {
GenericEntry* pEntry = (GenericEntry*) GetItemData(i);
for (j = 0; j < selCount; j++) {
if (pEntry->GetSelectionSerial() == savedSel[j] &&
pEntry->GetSelectionSerial() != -1)
{
/* match! */
if (SetItemState(i, LVIS_SELECTED, LVIS_SELECTED) == FALSE) {
WMSG1("WHOA: unable to set selected on item=%d\n", i);
}
break;
}
}
}
}
#endif
/*
* Call this when the sort order changes.
*/
void
ContentList::NewSortOrder(void)
{
CWaitCursor wait; // automatically changes mouse to hourglass
int column;
column = fpLayout->GetSortColumn();
if (!fpLayout->GetAscending())
column |= kDescendingFlag;
SetSortIcon();
SortItems(CompareFunc, column);
}
/*
* Get the file type display string.
*
* "buf" must be able to hold at least 4 characters plus the NUL (i.e. 5).
* Use kFileTypeBufLen.
*/
/*static*/ void
ContentList::MakeFileTypeDisplayString(const GenericEntry* pEntry, WCHAR* buf)
{
bool isDir =
pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir ||
pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory;
if (pEntry->GetSourceFS() == DiskImg::kFormatMacHFS && isDir) {
/* HFS directories don't have types; fake it */
wcscpy(buf, L"DIR/");
} else if (!(pEntry->GetFileType() >= 0 && pEntry->GetFileType() <= 0xff))
{
/* oversized type; assume it's HFS */
WCHAR typeBuf[kFileTypeBufLen];
MakeMacTypeString(pEntry->GetFileType(), typeBuf);
switch (pEntry->GetRecordKind()) {
case GenericEntry::kRecordKindFile:
wcscpy(buf, typeBuf);
break;
case GenericEntry::kRecordKindForkedFile:
wsprintf(buf, L"%ls+", typeBuf);
break;
case GenericEntry::kRecordKindUnknown:
// shouldn't happen
wsprintf(buf, L"%ls-", typeBuf);
break;
case GenericEntry::kRecordKindVolumeDir:
case GenericEntry::kRecordKindDirectory:
case GenericEntry::kRecordKindDisk:
default:
ASSERT(FALSE);
wcscpy(buf, L"!!!");
break;
}
} else {
/* typical ProDOS-style stuff */
switch (pEntry->GetRecordKind()) {
case GenericEntry::kRecordKindVolumeDir:
case GenericEntry::kRecordKindDirectory:
wsprintf(buf, L"%ls/", pEntry->GetFileTypeString());
break;
case GenericEntry::kRecordKindFile:
wsprintf(buf, L"%ls", pEntry->GetFileTypeString());
break;
case GenericEntry::kRecordKindForkedFile:
wsprintf(buf, L"%ls+", pEntry->GetFileTypeString());
break;
case GenericEntry::kRecordKindDisk:
wcscpy(buf, L"Disk");
break;
case GenericEntry::kRecordKindUnknown:
// usually a GSHK-archived empty data file does this
wsprintf(buf, L"%ls-", pEntry->GetFileTypeString());
break;
default:
ASSERT(FALSE);
wcscpy(buf, L"!!!");
break;
}
}
}
/*
* Convert an HFS file/creator type into a string.
*
* "buf" must be able to hold at least 4 characters plus the NUL. Use
* kFileTypeBufLen.
*/
/*static*/ void
ContentList::MakeMacTypeString(unsigned long val, WCHAR* buf)
{
/* expand longword with ASCII type bytes */
buf[0] = (unsigned char) (val >> 24);
buf[1] = (unsigned char) (val >> 16);
buf[2] = (unsigned char) (val >> 8);
buf[3] = (unsigned char) val;
buf[4] = '\0';
/* sanitize */
while (*buf != '\0') {
*buf = DiskImg::MacToASCII(*buf);
buf++;
}
}
/*
* Get the aux type display string.
*
* "buf" must be able to hold at least 5 characters plus the NUL (i.e. 6).
* Use kFileTypeBufLen.
*/
/*static*/ void
ContentList::MakeAuxTypeDisplayString(const GenericEntry* pEntry, WCHAR* buf)
{
bool isDir =
pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir ||
pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory;
if (pEntry->GetSourceFS() == DiskImg::kFormatMacHFS && isDir) {
/* HFS directories don't have types; fake it */
wcscpy(buf, L" ");
} else if (!(pEntry->GetFileType() >= 0 && pEntry->GetFileType() <= 0xff))
{
/* oversized type; assume it's HFS */
MakeMacTypeString(pEntry->GetAuxType(), buf);
} else {
if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDisk)
wsprintf(buf, L"%dk", pEntry->GetUncompressedLen() / 1024);
else
wsprintf(buf, L"$%04lX", pEntry->GetAuxType());
}
}
/*
* Generate the funky ratio display string. While we're at it, return a
* numeric value that we can sort on.
*
* "buf" must be able to hold at least 6 chars plus the NULL.
*/
void
ContentList::MakeRatioDisplayString(const GenericEntry* pEntry, WCHAR* buf,
int* pPerc)
{
LONGLONG totalLen, totalCompLen;
totalLen = pEntry->GetUncompressedLen();
totalCompLen = pEntry->GetCompressedLen();
if ((!totalLen && totalCompLen) || (totalLen && !totalCompLen)) {
wcscpy(buf, L"---"); /* weird */
*pPerc = -1;
} else if (totalLen < totalCompLen) {
wcscpy(buf, L">100%"); /* compression failed? */
*pPerc = 101;
} else {
*pPerc = ComputePercent(totalCompLen, totalLen);
wsprintf(buf, L"%d%%", *pPerc);
}
}
/*
* Return the value for a particular row and column.
*
* This gets called *a lot* while the list is being drawn, scrolled, etc.
* Don't do anything too expensive.
*/
void
ContentList::OnGetDispInfo(NMHDR* pnmh, LRESULT* pResult)
{
static const WCHAR kAccessBits[] = L"dnb iwr";
LV_DISPINFO* plvdi = (LV_DISPINFO*) pnmh;
CString str;
if (fpArchive->GetReloadFlag()) {
wcscpy(plvdi->item.pszText, L"");
*pResult = 0;
return;
}
//WMSG0("OnGetDispInfo\n");
if (plvdi->item.mask & LVIF_TEXT) {
GenericEntry* pEntry = (GenericEntry*) plvdi->item.lParam;
//GenericEntry* pEntry = fpArchive->GetEntry(plvdi->item.iItem);
switch (plvdi->item.iSubItem) {
case 0: // pathname
if (wcslen(pEntry->GetDisplayName()) > plvdi->item.cchTextMax) {
// looks like current limit is 264 chars, which we could hit
wcsncpy(plvdi->item.pszText, pEntry->GetDisplayName(),
plvdi->item.cchTextMax);
plvdi->item.pszText[plvdi->item.cchTextMax-1] = '\0';
} else {
wcscpy(plvdi->item.pszText, pEntry->GetDisplayName());
}
/*
* Sanitize the string. This is really only necessary for
* HFS, which has 8-bit "Macintosh Roman" filenames. The Win32
* controls can deal with it, but it looks better if we massage
* it a little.
*/
{
WCHAR* str = plvdi->item.pszText;
while (*str != '\0') {
*str = DiskImg::MacToASCII((unsigned char) (*str));
str++;
}
}
break;
case 1: // type
MakeFileTypeDisplayString(pEntry, plvdi->item.pszText);
break;
case 2: // auxtype
MakeAuxTypeDisplayString(pEntry, plvdi->item.pszText);
break;
case 3: // mod date
{
CString modDate;
FormatDate(pEntry->GetModWhen(), &modDate);
::lstrcpy(plvdi->item.pszText, (LPCTSTR) modDate);
}
break;
case 4: // format
ASSERT(pEntry->GetFormatStr() != nil);
wcscpy(plvdi->item.pszText, pEntry->GetFormatStr());
break;
case 5: // size
wsprintf(plvdi->item.pszText, L"%ld", pEntry->GetUncompressedLen());
break;
case 6: // ratio
int crud;
MakeRatioDisplayString(pEntry, plvdi->item.pszText, &crud);
break;
case 7: // packed
wsprintf(plvdi->item.pszText, L"%ld", pEntry->GetCompressedLen());
break;
case 8: // access
WCHAR bitLabels[sizeof(kAccessBits)];
int i, j, mask;
for (i = 0, j = 0, mask = 0x80; i < 8; i++, mask >>= 1) {
if (pEntry->GetAccess() & mask)
bitLabels[j++] = kAccessBits[i];
}
bitLabels[j] = '\0';
ASSERT(j < sizeof(bitLabels));
//::sprintf(plvdi->item.pszText, "0x%02x", pEntry->GetAccess());
wcscpy(plvdi->item.pszText, bitLabels);
break;
case 9: // NuRecordIdx [hidden]
break;
default:
ASSERT(false);
break;
}
}
//if (plvdi->item.mask & LVIF_IMAGE) {
// WMSG2("IMAGE req item=%d subitem=%d\n",
// plvdi->item.iItem, plvdi->item.iSubItem);
//}
*pResult = 0;
}
/*
* Helper functions for sort routine.
*/
static inline int
CompareUnsignedLong(unsigned long u1, unsigned long u2)
{
if (u1 < u2)
return -1;
else if (u1 > u2)
return 1;
else
return 0;
}
static inline int
CompareLONGLONG(LONGLONG u1, LONGLONG u2)
{
if (u1 < u2)
return -1;
else if (u1 > u2)
return 1;
else
return 0;
}
/*
* Static comparison function for list sorting.
*/
int CALLBACK
ContentList::CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
const GenericEntry* pEntry1 = (const GenericEntry*) lParam1;
const GenericEntry* pEntry2 = (const GenericEntry*) lParam2;
WCHAR tmpBuf1[16]; // needs >= 5 for file type compare, and
WCHAR tmpBuf2[16]; // >= 7 for ratio string
int result;
/* for descending order, flip the parameters */
if (lParamSort & kDescendingFlag) {
const GenericEntry* tmp;
lParamSort &= ~(kDescendingFlag);
tmp = pEntry1;
pEntry1 = pEntry2;
pEntry2 = tmp;
}
switch (lParamSort) {
case 0: // pathname
result = wcsicmp(pEntry1->GetDisplayName(), pEntry2->GetDisplayName());
break;
case 1: // file type
MakeFileTypeDisplayString(pEntry1, tmpBuf1);
MakeFileTypeDisplayString(pEntry2, tmpBuf2);
result = wcsicmp(tmpBuf1, tmpBuf2);
if (result != 0)
break;
/* else fall through to case 2 */
case 2: // aux type
if (pEntry1->GetRecordKind() == GenericEntry::kRecordKindDisk) {
if (pEntry2->GetRecordKind() == GenericEntry::kRecordKindDisk) {
result = pEntry1->GetAuxType() - pEntry2->GetAuxType();
} else {
result = -1;
}
} else if (pEntry2->GetRecordKind() == GenericEntry::kRecordKindDisk) {
result = 1;
} else {
result = pEntry1->GetAuxType() - pEntry2->GetAuxType();
}
break;
case 3: // mod date
result = CompareUnsignedLong(pEntry1->GetModWhen(),
pEntry2->GetModWhen());
break;
case 4: // format
result = ::lstrcmp(pEntry1->GetFormatStr(), pEntry2->GetFormatStr());
break;
case 5: // size
result = CompareLONGLONG(pEntry1->GetUncompressedLen(),
pEntry2->GetUncompressedLen());
break;
case 6: // ratio
int perc1, perc2;
MakeRatioDisplayString(pEntry1, tmpBuf1, &perc1);
MakeRatioDisplayString(pEntry2, tmpBuf2, &perc2);
result = perc1 - perc2;
break;
case 7: // packed
result = CompareLONGLONG(pEntry1->GetCompressedLen(),
pEntry2->GetCompressedLen());
break;
case 8: // access
result = CompareUnsignedLong(pEntry1->GetAccess(),
pEntry2->GetAccess());
break;
case kNumVisibleColumns: // file-order sort
default:
result = pEntry1->GetIndex() - pEntry2->GetIndex();
break;
}
return result;
}
/*
* Fill the columns with data from the archive entries. We use a "virtual"
* list control to avoid storing everything multiple times. However, we
* still create one item per entry so that the list control will do most
* of the sorting for us (otherwise we have to do the sorting ourselves).
*
* Someday we should probably move to a wholly virtual list view.
*/
int
ContentList::LoadData(void)
{
GenericEntry* pEntry;
LV_ITEM lvi;
int dirCount = 0;
int idx = 0;
DeleteAllItems(); // for Reload case
pEntry = fpArchive->GetEntries();
while (pEntry != nil) {
pEntry->SetIndex(idx);
lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
lvi.iItem = idx++;
lvi.iSubItem = 0;
if (pEntry->GetDamaged())
lvi.iImage = kListIconDamaged;
else if (pEntry->GetSuspicious())
lvi.iImage = kListIconSuspicious;
else if (pEntry->GetHasNonEmptyComment())
lvi.iImage = kListIconNonEmptyComment;
else if (pEntry->GetHasComment())
lvi.iImage = kListIconComment;
else
lvi.iImage = kListIconNone;
lvi.pszText = LPSTR_TEXTCALLBACK;
lvi.lParam = (LPARAM) pEntry;
if (InsertItem(&lvi) == -1) {
ASSERT(false);
return -1;
}
pEntry = pEntry->GetNext();
}
WMSG3("ContentList got %d entries (%d files + %d unseen directories)\n",
idx, idx - dirCount, dirCount);
return 0;
}
/*
* Return the default width for the specified column.
*/
int
ContentList::GetDefaultWidth(int col)
{
int retval;
switch (col) {
case 0: // pathname
retval = 200;
break;
case 1: // type (need "$XY" and long HFS types)
retval = MaxVal(GetStringWidth(L"XXMMMM+"), GetStringWidth(L"XXType"));
break;
case 2: // auxtype (hex or long HFS type)
retval = MaxVal(GetStringWidth(L"XX$CCCC"), GetStringWidth(L"XXAux"));
break;
case 3: // mod date
retval = GetStringWidth(L"XX88-MMM-88 88:88");
break;
case 4: // format
retval = GetStringWidth(L"XXUncompr");
break;
case 5: // uncompressed size
retval = GetStringWidth(L"XX88888888");
break;
case 6: // ratio
retval = MaxVal(GetStringWidth(L"XXRatio"), GetStringWidth(L"XX100%"));
break;
case 7: // packed
retval = GetStringWidth(L"XX88888888");
break;
case 8: // access
retval = MaxVal(GetStringWidth(L"XXAccess"), GetStringWidth(L"XXdnbiwr"));
break;
default:
ASSERT(false);
retval = 0;
}
return retval;
}
/*
* Set the up/down sorting arrow as appropriate.
*/
void
ContentList::SetSortIcon(void)
{
CHeaderCtrl* pHeader = GetHeaderCtrl();
ASSERT(pHeader != NULL);
HDITEM curItem;
/* update all column headers */
for (int i = 0; i < kNumVisibleColumns; i++) {
curItem.mask = HDI_IMAGE | HDI_FORMAT;
pHeader->GetItem(i, &curItem);
if (fpLayout->GetSortColumn() != i) {
curItem.fmt &= ~(HDF_IMAGE | HDF_BITMAP_ON_RIGHT);
} else {
//WMSG1(" Sorting on %d\n", i);
curItem.fmt |= HDF_IMAGE | HDF_BITMAP_ON_RIGHT;
if (fpLayout->GetAscending())
curItem.iImage = 0;
else
curItem.iImage = 1;
}
pHeader->SetItem(i, &curItem);
}
}
/*
* Handle a double-click on an item.
*
* The double-click should single-select the item, so we can throw it
* straight into the viewer. However, there are some uses for bulk
* double-clicking.
*/
void
ContentList::OnDoubleClick(NMHDR*, LRESULT* pResult)
{
/* test */
DWORD dwPos = ::GetMessagePos();
CPoint point ((int) LOWORD(dwPos), (int) HIWORD(dwPos));
ScreenToClient(&point);
int idx = HitTest(point);
if (idx != -1) {
CString str = GetItemText(idx, 0);
WMSG1("%ls was double-clicked\n", (LPCWSTR) str);
}
((MainWindow*) ::AfxGetMainWnd())->HandleDoubleClick();
*pResult = 0;
}
/*
* Handle a right-click on an item.
*
* -The first item in the menu performs the double-click action on the
* -item clicked on. The rest of the menu is simply a mirror of the items
* -in the "Actions" menu. To make this work, we let the main window handle
* -everything, but save a copy of the index of the menu item that was
* -clicked on.
*
* [We do this differently now?? ++ATM 20040722]
*/
void
ContentList::OnRightClick(NMHDR*, LRESULT* pResult)
{
DWORD dwPos = ::GetMessagePos();
CPoint point ((int) LOWORD(dwPos), (int) HIWORD(dwPos));
ScreenToClient(&point);
#if 0
int idx = HitTest(point);
if (idx != -1) {
CString str = GetItemText(idx, 0);
WMSG1("%ls was right-clicked\n", (LPCWSTR) str);
//fRightClickItem = idx;
#else
{
#endif
CMenu menu;
menu.LoadMenu(IDR_RIGHTCLICKMENU);
CMenu* pContextMenu = menu.GetSubMenu(0);
ClientToScreen(&point);
pContextMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON,
point.x, point.y, ::AfxGetMainWnd());
}
*pResult = 0;
}
/*
* Mark everything as selected.
*/
void
ContentList::SelectAll(void)
{
int i;
for (i = GetItemCount()-1; i >= 0; i--) {
if (!SetItemState(i, LVIS_SELECTED, LVIS_SELECTED)) {
WMSG1("Glitch: SetItemState failed on %d\n", i);
}
}
}
/*
* Toggle the "selected" state flag.
*/
void
ContentList::InvertSelection(void)
{
int i, oldState;
for (i = GetItemCount()-1; i >= 0; i--) {
oldState = GetItemState(i, LVIS_SELECTED);
if (!SetItemState(i, oldState ? 0 : LVIS_SELECTED, LVIS_SELECTED)) {
WMSG1("Glitch: SetItemState failed on %d\n", i);
}
}
}
/*
* Select the contents of any selected subdirs.
*
* We do the selection by prefix matching on the display name. This means
* we do one pass through the list for the contents of a subdir, including
* all of its subdirs. However, the subdirs we select as we're going will
* be indistinguishable from subdirs selected by the user, which could
* result in O(n^2) behavior.
*
* We mark the user's selection with LVIS_CUT, process them all, then go
* back and clear all of the LVIS_CUT flags. Of course, if they select
* the entire archive, we're approach O(n^2) anyway. If efficiency is a
* problem we will need to sort the list, do some work, then sort it back
* the way it was.
*
* This doesn't work for volume directories, because their display name
* isn't quite right. That's okay for now -- we document that we don't
* allow deletion of the volume directory. (We don't currently have a test
* to see if a GenericEntry is a volume dir; might want to add one.)
*/
void
ContentList::SelectSubdirContents(void)
{
POSITION posn;
posn = GetFirstSelectedItemPosition();
if (posn == nil) {
WMSG0("SelectSubdirContents: nothing is selected\n");
return;
}
/* mark all selected items with LVIS_CUT */
while (posn != nil) {
int num = GetNextSelectedItem(/*ref*/ posn);
SetItemState(num, LVIS_CUT, LVIS_CUT);
}
/* for each LVIS_CUT entry, select all prefix matches */
CString prefix;
for (int i = GetItemCount()-1; i >= 0; i--) {
GenericEntry* pEntry = (GenericEntry*) GetItemData(i);
bool origSel;
origSel = GetItemState(i, LVIS_CUT) != 0;
if (origSel &&
(pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory ||
pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir))
{
prefix = pEntry->GetDisplayName();
prefix += pEntry->GetFssep();
SelectSubdir(prefix);
}
// if (!SetItemState(i, oldState ? 0 : LVIS_SELECTED, LVIS_SELECTED)) {
// WMSG1("GLITCH: SetItemState failed on %d\n", i);
// }
}
/* clear the LVIS_CUT flags */
posn = GetFirstSelectedItemPosition();
while (posn != nil) {
int num = GetNextSelectedItem(/*ref*/ posn);
SetItemState(num, 0, LVIS_CUT);
}
}
/*
* Select every entry whose display name has "displayPrefix" as a prefix.
*/
void
ContentList::SelectSubdir(const WCHAR* displayPrefix)
{
WMSG1(" ContentList selecting all in '%ls'\n", displayPrefix);
int len = wcslen(displayPrefix);
for (int i = GetItemCount()-1; i >= 0; i--) {
GenericEntry* pEntry = (GenericEntry*) GetItemData(i);
if (wcsnicmp(displayPrefix, pEntry->GetDisplayName(), len) == 0)
SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
}
}
/*
* Mark all items as unselected.
*/
void
ContentList::ClearSelection(void)
{
for (int i = GetItemCount()-1; i >= 0; i--)
SetItemState(i, 0, LVIS_SELECTED);
}
/*
* Find the next matching entry. We start after the first selected item.
* If we find a matching entry, we clear the current selection and select it.
*/
void
ContentList::FindNext(const WCHAR* str, bool down, bool matchCase,
bool wholeWord)
{
POSITION posn;
int i, num;
bool found = false;
WMSG4("FindNext '%ls' d=%d c=%d w=%d\n", str, down, matchCase, wholeWord);
posn = GetFirstSelectedItemPosition();
num = GetNextSelectedItem(/*ref*/ posn);
if (num < 0) { // num will be -1 if nothing is selected
if (down)
num = -1;
else
num = GetItemCount();
}
WMSG1(" starting search from entry %d\n", num);
if (down) {
for (i = num+1; i < GetItemCount(); i++) {
found = CompareFindString(i, str, matchCase, wholeWord);
if (found)
break;
}
if (!found) { // wrap
for (i = 0; i <= num; i++) {
found = CompareFindString(i, str, matchCase, wholeWord);
if (found)
break;
}
}
} else {
for (i = num-1; i >= 0; i--) {
found = CompareFindString(i, str, matchCase, wholeWord);
if (found)
break;
}
if (!found) { // wrap
for (i = GetItemCount()-1; i >= num; i--) {
found = CompareFindString(i, str, matchCase, wholeWord);
if (found)
break;
}
}
}
if (found) {
WMSG1("Found, i=%d\n", i);
ClearSelection();
SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
EnsureVisible(i, false);
} else {
WMSG0("Not found\n");
MainWindow* pMain = (MainWindow*)::AfxGetMainWnd();
pMain->FailureBeep();
}
}
/*
* Compare "str" against the contents of entry "num".
*/
bool
ContentList::CompareFindString(int num, const WCHAR* str, bool matchCase,
bool wholeWord)
{
GenericEntry* pEntry = (GenericEntry*) GetItemData(num);
char fssep = pEntry->GetFssep();
const WCHAR* (*pSubCompare)(const WCHAR* str, const WCHAR* subStr) = nil;
if (matchCase)
pSubCompare = wcsstr;
else
pSubCompare = Stristr;
if (wholeWord) {
const WCHAR* src = pEntry->GetDisplayName();
const WCHAR* start = src;
size_t strLen = wcslen(str);
/* scan forward, looking for a match that starts & ends on fssep */
while (*start != '\0') {
const WCHAR* match;
match = (*pSubCompare)(start, str);
if (match == nil)
break;
if ((match == src || *(match-1) == fssep) &&
(match[strLen] == '\0' || match[strLen] == fssep))
{
return true;
}
start++;
}
} else {
if ((*pSubCompare)(pEntry->GetDisplayName(), str) != nil)
return true;
}
return false;
}