ciderpress/app/ContentList.cpp

968 lines
29 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)
{
LOGI("MOUSE WHEEL");
return CWnd::OnMouseWheel(nFlags, zDelta, pt);
// return TRUE;
}
#endif
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;
}
void ContentList::PostNcDestroy(void)
{
LOGI("ContentList PostNcDestroy");
delete this;
}
static inline int MaxVal(int a, int b)
{
return a > b ? a : b;
}
int ContentList::OnCreate(LPCREATESTRUCT lpcs)
{
CString colHdrs[kNumVisibleColumns] = {
L"Pathname", L"Type", L"Aux", L"Mod Date",
L"Format", L"Size", L"Ratio", L"Packed", L"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 == NULL)
LOGW("GLITCH: couldn't get header ctrl");
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;
}
void ContentList::OnDestroy(void)
{
LOGD("ContentList OnDestroy");
ExportColumnWidths();
CListCtrl::OnDestroy();
}
void ContentList::OnSysColorChange(void)
{
fHdrImageList.DeleteImageList();
LoadHeaderImages();
}
void ContentList::OnColumnClick(NMHDR* pnmh, LRESULT* pResult)
{
NM_LISTVIEW* pnmlv = (NM_LISTVIEW*) pnmh;
LOGD("ContentList OnColumnClick");
if (fpLayout->GetSortColumn() == pnmlv->iSubItem)
fpLayout->SetAscending(!fpLayout->GetAscending());
else {
fpLayout->SetSortColumn(pnmlv->iSubItem);
fpLayout->SetAscending(true);
}
NewSortOrder();
*pResult = 0;
}
void ContentList::ExportColumnWidths(void)
{
//LOGI("ExportColumnWidths");
for (int i = 0; i < kNumVisibleColumns; i++)
fpLayout->SetColumnWidth(i, GetColumnWidth(i));
}
void ContentList::NewColumnWidths(void)
{
for (int i = 0; i < kNumVisibleColumns; i++) {
int width = fpLayout->GetColumnWidth(i);
if (width == ColumnLayout::kWidthDefaulted) {
width = GetDefaultWidth(i);
LOGD("Defaulting width %d to %d", i, width);
fpLayout->SetColumnWidth(i, width);
}
SetColumnWidth(i, width);
}
}
void ContentList::Reload(bool saveSelection)
{
LOGI("Reloading ContentList");
CWaitCursor waitc;
// fInvalid = false;
fpArchive->ClearReloadFlag();
long* savedSel = NULL;
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 != NULL) {
/* restore the selection */
RestoreSelection(savedSel, selCount);
delete[] savedSel;
}
/* try to put us back in the same place */
EnsureVisible(bottom, false);
EnsureVisible(top, false);
}
long* ContentList::GetSelectionSerials(long* pSelCount)
{
long* savedSel = NULL;
long maxCount;
maxCount = GetSelectedCount();
LOGD("GetSelectionSerials (maxCount=%d)", maxCount);
if (maxCount > 0) {
savedSel = new long[maxCount];
int idx = 0;
POSITION posn;
posn = GetFirstSelectedItemPosition();
ASSERT(posn != NULL);
if (posn == NULL)
return NULL;
while (posn != NULL) {
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;
}
void ContentList::RestoreSelection(const long* savedSel, long selCount)
{
LOGI("RestoreSelection (selCount=%d)", selCount);
if (savedSel == NULL)
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) {
LOGW("WHOA: unable to set selected on item=%d", i);
}
break;
}
}
}
}
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);
}
/*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;
}
}
}
/*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((unsigned char)*buf);
buf++;
}
}
/*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"%I64dk", pEntry->GetUncompressedLen() / 1024);
else
wsprintf(buf, L"$%04lX", pEntry->GetAuxType());
}
}
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);
}
}
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;
}
//LOGI("OnGetDispInfo");
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 ((int) 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());
}
#if 0 // no longer needed -- "display names" are converted to Unicode
/*
* 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++;
}
}
#endif
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, (LPCWSTR) modDate);
}
break;
case 4: // format
ASSERT(pEntry->GetFormatStr() != NULL);
wcscpy(plvdi->item.pszText, pEntry->GetFormatStr());
break;
case 5: // size
wsprintf(plvdi->item.pszText, L"%I64d", pEntry->GetUncompressedLen());
break;
case 6: // ratio
int crud;
MakeRatioDisplayString(pEntry, plvdi->item.pszText, &crud);
break;
case 7: // packed
wsprintf(plvdi->item.pszText, L"%I64d", 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) {
// LOGI("IMAGE req item=%d subitem=%d",
// plvdi->item.iItem, plvdi->item.iSubItem);
//}
*pResult = 0;
}
/*
* Helper functions for sort routine.
*/
static inline int CompareUnsignedLong(uint32_t u1, uint32_t 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 inline int CompareTime(time_t t1, time_t t2)
{
if (t1 < t2)
return -1;
else if (t1 > t2)
return 1;
else
return 0;
}
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 = CompareTime(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;
}
int ContentList::LoadData(void)
{
GenericEntry* pEntry;
LV_ITEM lvi;
int dirCount = 0;
int idx = 0;
DeleteAllItems(); // for Reload case
pEntry = fpArchive->GetEntries();
while (pEntry != NULL) {
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();
}
LOGI("ContentList got %d entries (%d files + %d unseen directories)",
idx, idx - dirCount, dirCount);
return 0;
}
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;
}
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 {
//LOGI(" Sorting on %d", i);
curItem.fmt |= HDF_IMAGE | HDF_BITMAP_ON_RIGHT;
if (fpLayout->GetAscending())
curItem.iImage = 0;
else
curItem.iImage = 1;
}
pHeader->SetItem(i, &curItem);
}
}
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);
LOGI("%ls was double-clicked", (LPCWSTR) str);
}
((MainWindow*) ::AfxGetMainWnd())->HandleDoubleClick();
*pResult = 0;
}
void ContentList::OnRightClick(NMHDR*, LRESULT* pResult)
{
/*
* -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]
*/
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);
LOGI("%ls was right-clicked", (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;
}
void ContentList::SelectAll(void)
{
int i;
for (i = GetItemCount()-1; i >= 0; i--) {
if (!SetItemState(i, LVIS_SELECTED, LVIS_SELECTED)) {
LOGI("Glitch: SetItemState failed on %d", i);
}
}
}
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)) {
LOGI("Glitch: SetItemState failed on %d", i);
}
}
}
void ContentList::SelectSubdirContents(void)
{
/*
* 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.)
*/
POSITION posn;
posn = GetFirstSelectedItemPosition();
if (posn == NULL) {
LOGI("SelectSubdirContents: nothing is selected");
return;
}
/* mark all selected items with LVIS_CUT */
while (posn != NULL) {
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)) {
// LOGI("GLITCH: SetItemState failed on %d", i);
// }
}
/* clear the LVIS_CUT flags */
posn = GetFirstSelectedItemPosition();
while (posn != NULL) {
int num = GetNextSelectedItem(/*ref*/ posn);
SetItemState(num, 0, LVIS_CUT);
}
}
void ContentList::SelectSubdir(const WCHAR* displayPrefix)
{
LOGI(" ContentList selecting all in '%ls'", 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);
}
}
void ContentList::ClearSelection(void)
{
for (int i = GetItemCount()-1; i >= 0; i--)
SetItemState(i, 0, LVIS_SELECTED);
}
void ContentList::FindNext(const WCHAR* str, bool down, bool matchCase,
bool wholeWord)
{
POSITION posn;
int i, num;
bool found = false;
LOGI("FindNext '%ls' d=%d c=%d w=%d", 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();
}
LOGI(" starting search from entry %d", 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) {
LOGI("Found, i=%d", i);
ClearSelection();
SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
EnsureVisible(i, false);
} else {
LOGI("Not found");
MainWindow* pMain = (MainWindow*)::AfxGetMainWnd();
pMain->FailureBeep();
}
}
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) = NULL;
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 == NULL)
break;
if ((match == src || *(match-1) == fssep) &&
(match[strLen] == '\0' || match[strLen] == fssep))
{
return true;
}
start++;
}
} else {
if ((*pSubCompare)(pEntry->GetDisplayName(), str) != NULL)
return true;
}
return false;
}