home *** CD-ROM | disk | FTP | other *** search
/ SuperHack / SuperHack CD.bin / CODING / CPP / METAKIT.ZIP / EXAMPLES / CATFISH / CATFISH.CPP next >
Encoding:
C/C++ Source or Header  |  1996-12-09  |  38.6 KB  |  1,291 lines

  1. //  catfish.cpp  -  main application sample code
  2. //
  3. //  This is a part of the MetaKit library.
  4. //  Copyright (c) 1996 Meta Four Software.
  5. //  All rights reserved.
  6. /////////////////////////////////////////////////////////////////////////////
  7.  
  8. #include "stdafx.h"
  9. #include "catfish.h"
  10. #include "setupdlg.h"
  11.  
  12.     #include <dos.h>    // _dos_findfirst in GetCatalogDate
  13.     
  14. #ifdef _DEBUG
  15. #undef THIS_FILE
  16. static char BASED_CODE THIS_FILE[] = __FILE__;
  17. #endif
  18.  
  19. #pragma warning(disable: 4702) // MSVC 1.52 gets confused: unreachable code
  20.     
  21. /////////////////////////////////////////////////////////////////////////////
  22. // MSDN Q100770: Using Accelerator Keys When Modal Dialog Box Main Window
  23.  
  24.     HWND    ghDlg;          // Handle to main dialog box
  25.     HACCEL  ghAccelTable;   // Handle to accelerator table
  26.  
  27.     CTheApp ThisApp;
  28.  
  29. /////////////////////////////////////////////////////////////////////////////
  30. // Use a simple version of localized date, time, and number formating.
  31.  
  32.     static CString  sShortDate  = "MM/dd/yy";   // "d.M.yyyy", etc
  33.     static bool     iTime       = false;        // true if 24h format
  34.     static bool     iTLZero     = true;         // true if hour has 2 digits
  35.     static char     sThousand   = ',';          // thousands separator
  36.     static char     sTime       = ':';          // time separator
  37.  
  38.     static void SetInternationalSettings()
  39.     {
  40.         iTime = GetProfileInt("intl", "iTime", 0) != 0;
  41.         iTLZero = GetProfileInt("intl", "iTLZero", 1) != 0;
  42.         
  43.         char buf [30];
  44.         
  45.         if (GetProfileString("intl", "sShortDate", "MM/dd/yy", buf, sizeof buf))
  46.             sShortDate = buf;
  47.         
  48.         if (GetProfileString("intl", "sThousand", ",", buf, sizeof buf))
  49.             sThousand = *buf;
  50.         
  51.         if (GetProfileString("intl", "sTime", ":", buf, sizeof buf))
  52.             sTime = *buf;
  53.     }
  54.         
  55. /////////////////////////////////////////////////////////////////////////////
  56. // Convert a number to comma-separated format, grouped in units of three.
  57. // Optionally prefix with spaces (assuming two spaces is width of one digit).
  58. // Finally, the zero value can be changed to a '-' upon request.
  59. //
  60. // Note:    In many places, the code is simplified by the assumption that
  61. //          every digit has exactly the same width as two space characters.
  62. //          This works for the selected font (MS Sans Serif, font size 8).
  63. //          It allows us to present a nice columnar interface without having
  64. //          to figure out each of the string position in pixels. There are
  65. //          several more assumptions like this (e.g. "k   " is like "Mb").
  66.  
  67.     static CString CommaNum(DWORD num, int groups =0, BOOL zero =TRUE)
  68.     {
  69.         CString s;
  70.         s.Format("%lu", num);
  71.         
  72.         int g = 0;
  73.         int n = s.GetLength();
  74.         while (n > 3)
  75.         {
  76.             n -= 3;
  77.             s = s.Left(n) + sThousand + s.Mid(n);
  78.             ++g;
  79.         }
  80.         
  81.         if (--groups >= 0)
  82.         {
  83.             int w = ((3 - n) % 3) * 2;
  84.             if (g < groups)
  85.                 w += 7 * (groups - g);
  86.  
  87.             s = CString (' ', w) + s;
  88.         }
  89.         
  90.         if (!zero && (s == "0" || s.Right(2) == " 0"))
  91.             s = s.Left(s.GetLength() - 1) + " -";
  92.             
  93.         return s;
  94.     }
  95.     
  96. /////////////////////////////////////////////////////////////////////////////
  97. // Convert a DOS date and TIME words to short format strings.
  98. // Lets be nice to a lot of people and adopt their local conventions.
  99.  
  100.     static CString ShortDate(WORD date)
  101.     {
  102.         int w = 0;
  103.         
  104.         char buf [10];
  105.         char* q = buf;
  106.         
  107.             // decode the short date, deal with 1- and 2-digit fields
  108.         const char* p = sShortDate;
  109.         while (*p)
  110.         {
  111.             int i;
  112.             
  113.             switch (*p++)
  114.             {            
  115.                 default:    *q++ = *(p-1);
  116.                             continue;
  117.                 
  118.                 case 'd':   i = date & 0x1F;
  119.                             break;
  120.                             
  121.                 case 'M':   i = (date >> 5) & 0x0F;
  122.                             break;
  123.                             
  124.                 case 'y':   i = ((date >> 9) + 80) % 100;
  125.                             break; // 4-digit years are treated as 2-digit
  126.                             
  127.             }
  128.             
  129.             if (i < 10 && *p != *(p-1))
  130.                 ++w;
  131.             else
  132.                 *q++ = (char) (i / 10 + '0');
  133.             
  134.             *q++ = (char) (i % 10 + '0');
  135.  
  136.             while (*p == *(p-1))
  137.                 ++p;
  138.         }
  139.         
  140.             // centering is easy, since one digit is as wide as two spaces
  141.         CString t (' ', 2 * w);
  142.             // alignment depends on whether the year is first or last 
  143.         if (sShortDate[0] == 'y')
  144.             return CString (buf, q - buf) + t;
  145.         
  146.         return t + CString (buf, q - buf);
  147.     }
  148.     
  149.     static CString ShortTime(WORD time)
  150.     {
  151.         int h = time >> 11;
  152.         int m = (time >> 5) & 0x3F;
  153.         
  154.         if (!iTime)
  155.             h = (h + 11) % 12 + 1; // dec, then inc, so 0 becomes 12
  156.             
  157.         CString s;
  158.         s.Format("%02d%c%02d", h, sTime, m);
  159.         
  160.         if (!iTime)
  161.             s += h < 12 ? 'a' : 'p';
  162.         
  163.         if (!iTLZero && s[0] == '0')
  164.             s = "  " + s.Mid(1); // replace leading zero with two spaces
  165.             
  166.         return s;
  167.     }
  168.     
  169. /////////////////////////////////////////////////////////////////////////////
  170. // Make a string fit in the specified number of pixels on given device.
  171. // Characters at the end are replaced by an ellipsis to make the string fit.
  172. // There is some trickery in here to optimize this very common calculation.
  173.  
  174.     static BOOL FitString(CDC* dc, CString& text, int width)
  175.     {
  176.         CSize sz = dc->GetTextExtent(text, text.GetLength());
  177.         if (sz.cx <= width)
  178.             return TRUE;    // make the most common case fast
  179.         
  180.             // Assumption: "...xyz" is just as wide as "xyz..." 
  181.         CString s = "..." + text;
  182.         
  183.         int n = s.GetLength();
  184.         while (--n > 3)
  185.         {            
  186.             sz = dc->GetTextExtent(text, n);
  187.             if (sz.cx <= width)
  188.                 break;
  189.         }
  190.              
  191.         text = text.Left(n - 3) + "...";
  192.         return FALSE;
  193.     }
  194.     
  195. /////////////////////////////////////////////////////////////////////////////
  196. // Disables redraw and clears listbox, will reset normal state in destructor
  197.  
  198.     class ListBoxFreezer
  199.     {
  200.     public:
  201.         ListBoxFreezer (CListBox& lb)
  202.             : list (lb)
  203.         {
  204.             list.SetRedraw(FALSE);
  205.             list.ResetContent();
  206.         }
  207.         
  208.         ~ListBoxFreezer ()
  209.         {
  210.             list.SetRedraw(TRUE);
  211.             list.Invalidate();
  212.         }
  213.     
  214.     private:
  215.         CListBox& list;
  216.     };
  217.     
  218. /////////////////////////////////////////////////////////////////////////////
  219. // Return file date in display format, or an empty string if file not present
  220.  
  221. CString GetCatalogDate(CString& catName)
  222. {
  223.     CString s = catName;
  224.     s += FILE_TYPE;
  225.     
  226.     _find_t fbuf;
  227.     if (_dos_findfirst(s, _A_NORMAL, &fbuf) != 0)
  228.         return "";
  229.     
  230.         // pick up the name as it is stored on disk (properly capitalized)
  231.     s = fbuf.name;
  232.     ASSERT(s.Right(4).CompareNoCase(FILE_TYPE) == 0);
  233.     catName = s.Left(s.GetLength() - 4);
  234.         
  235.     return ShortDate((WORD) fbuf.wr_date) + "  "
  236.             + ShortTime((WORD) fbuf.wr_time);
  237. }
  238.  
  239. /////////////////////////////////////////////////////////////////////////////
  240. // The one and only application object
  241.  
  242. CTheApp::CTheApp ()
  243.     : CWinApp ("CatFish")
  244. {    
  245. }
  246.  
  247. BOOL CTheApp::InitInstance()
  248. {
  249.     SetDialogBkColor();
  250.     SetInternationalSettings();
  251.  
  252.     ghAccelTable = LoadAccelerators(AfxGetInstanceHandle(),
  253.                                     MAKEINTRESOURCE(IDD_MAIN_DIALOG));
  254.  
  255.         // the following is required to let a dialog box have an icon   
  256.     static WNDCLASS wndclass;
  257.     if (!wndclass.lpfnWndProc)
  258.     {
  259.         wndclass.lpfnWndProc    = DefDlgProc;
  260.         wndclass.cbWndExtra     = DLGWINDOWEXTRA ;
  261.         wndclass.hInstance      = m_hInstance;
  262.         wndclass.hIcon          = LoadIcon(AFX_IDI_STD_FRAME);
  263.         wndclass.lpszClassName  = "CATFISHCLASS";
  264.         
  265.         RegisterClass(&wndclass);
  266.     }
  267.     
  268.         // enter a modal loop right now 
  269.     CMainDlgWindow mainDlg;
  270.     m_pMainWnd = &mainDlg;
  271.     mainDlg.DoModal();
  272.     
  273.         // and then return false to skip the main application run loop
  274.     return FALSE;
  275. }
  276.  
  277. BOOL CTheApp::ProcessMessageFilter(int code, LPMSG lpMsg)
  278. {
  279.     if (code < 0)
  280.         CWinApp::ProcessMessageFilter(code, lpMsg);
  281.          
  282.     if (ghDlg && ghAccelTable)
  283.     {
  284.         if (::TranslateAccelerator(ghDlg, ghAccelTable, lpMsg))
  285.             return(TRUE);
  286.     }
  287.          
  288.     return CWinApp::ProcessMessageFilter(code, lpMsg);
  289. }
  290.  
  291. /////////////////////////////////////////////////////////////////////////////
  292.  
  293. BEGIN_MESSAGE_MAP(CMainDlgWindow, CDialog)
  294.     //{{AFX_MSG_MAP(CMainDlgWindow)
  295.     ON_WM_CLOSE()
  296.     ON_WM_DRAWITEM()
  297.     ON_LBN_SELCHANGE(IDC_CAT_LIST, OnSelchangeCatList)
  298.     ON_LBN_SELCHANGE(IDC_TREE_LIST, OnSelchangeTreeList)
  299.     ON_LBN_DBLCLK(IDC_TREE_LIST, OnDblclkTreeList)
  300.     ON_LBN_SELCHANGE(IDC_FILE_LIST, OnSelchangeFileList)
  301.     ON_BN_CLICKED(IDC_FIND_BTN, OnFindBtn)
  302.     ON_BN_CLICKED(IDC_SETUP_BTN, OnSetupBtn)
  303.     ON_LBN_DBLCLK(IDC_FILE_LIST, OnDblclkFileList)
  304.     ON_COMMAND(ID_FIND_NEXT, OnFindNext)
  305.     ON_COMMAND(ID_FIND_PREV, OnFindPrev)
  306.     ON_COMMAND(ID_SORT_BY_NAME, OnSortByName)
  307.     ON_COMMAND(ID_SORT_BY_SIZE, OnSortBySize)
  308.     ON_COMMAND(ID_SORT_BY_DATE, OnSortByDate)
  309.     ON_COMMAND(ID_SORT_REVERSE, OnSortReverse)
  310.     ON_WM_DESTROY()
  311.     ON_WM_LBUTTONDOWN()
  312.     ON_WM_CHAR()
  313.     ON_COMMAND(ID_FIND_CMD, OnFindBtn)
  314.     ON_COMMAND(ID_FILE_SETUP, OnSetupBtn)
  315.     ON_LBN_DBLCLK(IDC_CAT_LIST, OnSetupBtn)
  316.     ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
  317.     //}}AFX_MSG_MAP
  318.     ON_COMMAND(ID_HELP, OnHelp)
  319. END_MESSAGE_MAP()
  320.  
  321. /////////////////////////////////////////////////////////////////////////////
  322.  
  323. CMainDlgWindow::CMainDlgWindow ()
  324.     : CDialog (IDD_MAIN_DIALOG),
  325.       m_storage (0), m_fileDir (-1), m_treeDir (-1), m_dc (0),
  326.       m_sortProp (&pName), m_sortReverse (false)
  327. {
  328.     //{{AFX_DATA_INIT(CMainDlgWindow)
  329.     //}}AFX_DATA_INIT
  330. }
  331.  
  332. CMainDlgWindow::~CMainDlgWindow ()
  333. {
  334.     delete m_storage;
  335. }
  336.  
  337. void CMainDlgWindow::DoDataExchange(CDataExchange* pDX)
  338. {
  339.     CDialog::DoDataExchange(pDX);
  340.     //{{AFX_DATA_MAP(CMainDlgWindow)
  341.     DDX_Control(pDX, IDC_FIND_BTN, m_findBtn);
  342.     DDX_Control(pDX, IDC_PATH_FRAME, m_pathFrame);
  343.     DDX_Control(pDX, IDC_TREE_LIST, m_treeList);
  344.     DDX_Control(pDX, IDC_FILE_LIST, m_fileList);
  345.     DDX_Control(pDX, IDC_CAT_LIST, m_catList);
  346.     DDX_Control(pDX, IDC_MSG_TEXT, m_msgText);
  347.     DDX_Control(pDX, IDC_INFO_TEXT, m_infoText);
  348.     DDX_Control(pDX, IDC_TREE_PATH, m_treePath);
  349.     //}}AFX_DATA_MAP
  350. }
  351.  
  352. void CMainDlgWindow::OnCancel()
  353. {
  354.     ::MessageBeep(0);                  // don't go away on ESC key
  355. }
  356.  
  357. void CMainDlgWindow::OnClose()
  358. {
  359.     EndDialog(IDOK);
  360. }
  361.  
  362. void CMainDlgWindow::OnDestroy()
  363. {
  364.     CDialog::OnDestroy();
  365.     
  366.     SetCatalog("");
  367. }
  368.  
  369. BOOL CMainDlgWindow::OnInitDialog()
  370. {
  371.     CDialog::OnInitDialog();
  372.  
  373.     ghDlg = m_hWnd;
  374.     
  375.         // create a small font for several of the dialog box items
  376.     LOGFONT lf;
  377.     memset(&lf, 0, sizeof(LOGFONT));
  378.     lf.lfHeight = -8;
  379.     strcpy(lf.lfFaceName, "MS Sans Serif");
  380.     m_font.CreateFontIndirect(&lf);
  381.     
  382.     m_msgText.SetFont(&m_font, FALSE);
  383.     m_infoText.SetFont(&m_font, FALSE);
  384.     m_catList.SetFont(&m_font, FALSE);
  385.     m_treeList.SetFont(&m_font, FALSE);
  386.     m_fileList.SetFont(&m_font, FALSE);
  387.     
  388.         // determine the character height and set owner-draw lists accordingly
  389.     {
  390.         CClientDC dc (this);
  391.         CFont* oldFont = dc.SelectObject(&m_font);
  392.             
  393.         TEXTMETRIC tm;
  394.         VERIFY(dc.GetTextMetrics(&tm));
  395.                
  396.         dc.SelectObject(oldFont);
  397.             
  398.         m_catList.SetItemHeight(0, tm.tmHeight);
  399.         m_treeList.SetItemHeight(0, tm.tmHeight);
  400.         m_fileList.SetItemHeight(0, tm.tmHeight);
  401.     }
  402.     
  403.         // fill the list of catalogs
  404.     m_catList.Dir(0, "*" FILE_TYPE);
  405.     
  406.         // default file sort order is by filename
  407.     SortFileList(pName);
  408.  
  409.         // show contents now, before potential slow catalog loading starts
  410.     ShowWindow(ThisApp.m_nCmdShow);
  411.     UpdateWindow(); 
  412.     
  413.     m_catList.SetCurSel(0);
  414.     OnSelchangeCatList();
  415.  
  416.     m_infoText.SetWindowText("http://purl.net/meta4/metakit");
  417.     
  418.     if (m_catList.GetCount() == 0)
  419.         OnHelp();
  420.     
  421.     return TRUE;    // return TRUE  unless you set the focus to a control
  422. }
  423.  
  424.     // notification handler for owner-draw listboxes
  425. void CMainDlgWindow::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
  426. {
  427.     int n = (int) lpDrawItemStruct->itemID;
  428.     if (n == -1)
  429.        return;
  430.     
  431.     m_item = lpDrawItemStruct;
  432.     m_dc = CDC::FromHandle(m_item->hDC);
  433.     
  434.     if (m_item->itemAction == ODA_FOCUS)
  435.     {
  436.         m_dc->DrawFocusRect(&m_item->rcItem);
  437.         return;
  438.     }
  439.     
  440.     if (m_item->itemState & ODS_SELECTED)
  441.     {
  442.         m_dc->SetBkColor(GetSysColor(COLOR_HIGHLIGHT));
  443.         m_dc->SetTextColor(GetSysColor(COLOR_HIGHLIGHTTEXT));
  444.     }
  445.     else
  446.     {
  447.         m_dc->SetBkColor(GetSysColor(COLOR_WINDOW));
  448.         m_dc->SetTextColor(GetSysColor(COLOR_WINDOWTEXT));
  449.     }
  450.  
  451.     switch (nIDCtl)
  452.     {
  453.         case IDC_CAT_LIST:      OnDrawCatItem(n); break;
  454.         case IDC_TREE_LIST:     OnDrawDirItem(n); break;
  455.         case IDC_FILE_LIST:     OnDrawFileItem(n); break;
  456.     }
  457.     
  458.     if ((m_item->itemState & ODS_FOCUS) && m_item->itemAction != ODA_SELECT)
  459.         m_dc->DrawFocusRect(&m_item->rcItem);
  460. }
  461.  
  462.     // common code to draw a text string in a listbox item
  463. void CMainDlgWindow::DrawItemText(const CString& text, int off)
  464. {
  465.     RECT& rect = m_item->rcItem;
  466.     
  467.     m_dc->ExtTextOut(rect.left + off + 2, rect.top,
  468.                         off ? 0 : ETO_OPAQUE, &rect,
  469.                         text, text.GetLength(), 0);
  470. }
  471.  
  472.     // draw one item in the catalog listbox
  473. void CMainDlgWindow::OnDrawCatItem(int n)
  474. {
  475.     CString s;
  476.     m_catList.GetText(n, s);
  477.     
  478.     ASSERT(s.Right(4).CompareNoCase(FILE_TYPE) == 0);
  479.     s = s.Left(s.GetLength() - 4);
  480.     
  481.     CString date = GetCatalogDate(s);
  482.  
  483.     FitString(m_dc, s, 72);
  484.     DrawItemText(s);    
  485.     
  486.     DrawItemText(date, 72);
  487. }
  488.  
  489.     // draw one item in the tree listbox
  490. void CMainDlgWindow::OnDrawDirItem(int n)
  491. {
  492.     int dir = (int) m_treeList.GetItemData(n);
  493.     BOOL up = n == 0 || pParent (m_currCat[dir]) != m_treeDir;
  494.     
  495.     if (up && !(m_item->itemState & ODS_SELECTED))
  496.         m_dc->SetTextColor(GetSysColor(COLOR_GRAYTEXT));
  497.  
  498.     CString s = pName (m_currCat[dir]);
  499.     if (dir == 0)
  500.         s = "(root)";
  501.     
  502.     if (!up)
  503.         s = (m_dirCounts[dir] ? "+ " : "   ") + s;
  504.  
  505.     FitString(m_dc, s, 78);
  506.     DrawItemText(s);    
  507.     
  508.     DWORD t = m_kiloTotals[dir];
  509.     if (t <= 999999)
  510.         s = CommaNum(t, 2) + " k   ";
  511.     else
  512.         s = CommaNum((t + 999) / 1000, 2) + " Mb";
  513.         
  514.     s += CommaNum(m_dirCounts[dir], 2, FALSE).Mid(2) + "  "
  515.        + CommaNum(m_fileCounts[dir], 2, FALSE) + "    "
  516.        + ShortDate(m_lastDates[dir]);
  517.        
  518.     DrawItemText(s, 78);    
  519. }
  520.  
  521.     // draw one item in the file listbox
  522. void CMainDlgWindow::OnDrawFileItem(int n)
  523. {
  524.     c4_RowRef file = m_fileSort[n];
  525.     
  526.     CString s = pName (file);
  527.     FitString(m_dc, s, 85);
  528.     DrawItemText(s);
  529.     
  530.     s = CommaNum(pSize (file), 3) + "    " + ShortDate((WORD) pDate (file));
  531.     DrawItemText(s, 85);    
  532. }
  533.  
  534.     // pressing F1 leads to an brief help screen
  535. void CMainDlgWindow::OnHelp()
  536. {
  537.     CDialog dlg (IDD_WELCOME_DLG);
  538.     dlg.DoModal();
  539. }
  540.  
  541.     // there is of course also an about box
  542. void CMainDlgWindow::OnAppAbout()
  543. {
  544.     CDialog dlg (IDD_ABOUTBOX);
  545.     dlg.DoModal();
  546. }
  547.  
  548.     // find file entries
  549. void CMainDlgWindow::OnFindBtn()
  550. {
  551.     int n = m_catList.GetCurSel();
  552.     ASSERT(n != LB_ERR);
  553.  
  554.     CString s;
  555.     m_catList.GetText(n, s);      
  556.         
  557.     ASSERT(s.Right(4).CompareNoCase(FILE_TYPE) == 0);
  558.     s = s.Left(s.GetLength() - 4);
  559.     
  560.     s.MakeUpper();
  561.         
  562.     if (m_findDlg.Execute(s))
  563.         OnFindNext();
  564. }
  565.  
  566.     // setup catalogs
  567. void CMainDlgWindow::OnSetupBtn()
  568. {
  569.     CSetupDialog dlg;
  570.  
  571.     int n = m_catList.GetCurSel();
  572.     if (n != LB_ERR)
  573.     {                         
  574.         CString s;
  575.         m_catList.GetText(n, s);      
  576.         
  577.         ASSERT(s.Right(4).CompareNoCase(FILE_TYPE) == 0);
  578.         dlg.m_name = s.Left(s.GetLength() - 4);
  579.     }
  580.         
  581.     SetCatalog(""); // make sure no catalog is in use during setup
  582.     
  583.     dlg.DoModal();
  584.     
  585.     {
  586.         ListBoxFreezer frozen (m_catList);
  587.     
  588.         m_catList.Dir(0, "*" FILE_TYPE);
  589.     
  590.             // attempt to maintain the current selection
  591.         if (m_catList.SelectString(-1, dlg.m_name) == LB_ERR)
  592.             m_catList.SetCurSel(0);
  593.     }
  594.     
  595.     OnSelchangeCatList();
  596. }
  597.  
  598.     // adjust the title to show which catalog is selected
  599. void CMainDlgWindow::ConstructTitle()
  600. {
  601.     CString s = m_currCatName;
  602.     ASSERT(s.Right(4).CompareNoCase(FILE_TYPE) == 0);
  603.     s = s.Left(s.GetLength() - 4);
  604.     
  605.     GetCatalogDate(s); // for side-effect: proper file name capitalization
  606.         
  607.     CString root = pName (m_currCat[0]);
  608.     if (!root.IsEmpty())
  609.         s += " - " + root;
  610.     
  611.     s = "CatFish - " + s;
  612.     
  613.     CString title;
  614.     GetWindowText(title);
  615.  
  616.     if (title != s)
  617.         SetWindowText(s);   
  618. }
  619.  
  620.     // select a catalog and update the dialog contents
  621. void CMainDlgWindow::SetCatalog(const char* catName)
  622. {
  623.     if (m_currCatName == catName)
  624.         return; // don't bother, the catalog is currently loaded
  625.     
  626.     SetTreeDir(-1);
  627.  
  628.         // An important side effect is that m_fileView is cleared before the 
  629.         // storage class is destroyed. Otherwise, the entire view would be
  630.         // loaded into memory since the underlying file is about to go away.
  631.     SetFileDir(-1);
  632.  
  633.     m_currCat = c4_View (); // see comment about m_fileView 
  634.     delete m_storage;
  635.     m_storage = 0;
  636.     
  637.     m_dirCounts.RemoveAll();
  638.     m_fileCounts.RemoveAll();
  639.     m_lastDates.RemoveAll();
  640.     m_kiloTotals.RemoveAll();
  641.  
  642.     m_currCatName = catName;
  643.     if (m_currCatName.IsEmpty())
  644.         return;                      
  645.     
  646.         // loading and calculations may take some time
  647.     HCURSOR oldCursor = SetCursor(LoadCursor(0, IDC_WAIT));
  648.     
  649.     m_storage = DEBUG_NEW c4_Storage (m_currCatName, false);
  650.     m_currCat = m_storage->View("dirs");
  651.     
  652.     ConstructTitle();    
  653.     
  654.     int n = m_currCat.GetSize();
  655.     m_dirCounts.InsertAt(0, 0, n);
  656.     m_fileCounts.InsertAt(0, 0, n);
  657.     m_lastDates.InsertAt(0, 0, n);
  658.     m_kiloTotals.InsertAt(0, 0, n);
  659.     
  660.         // this loop calculates all cumulative totals and dates,
  661.         // mathematicians call this the "transitive closure" ...
  662.     while (--n >= 0)
  663.     {
  664.         c4_RowRef dir = m_currCat[n];
  665.         
  666.         int date = 0;
  667.         DWORD total = 0;
  668.  
  669.         c4_View files = pFiles (dir);
  670.         
  671.         for (int i = 0; i < files.GetSize(); ++i)
  672.         {
  673.             c4_RowRef file = files[i];
  674.             
  675.             total += pSize (file);
  676.             if (date < pDate (file))
  677.                 date = (int) pDate (file);
  678.         }
  679.         
  680.         ASSERT(i == files.GetSize());
  681.         m_fileCounts[n] += (WORD) i;
  682.         m_kiloTotals[n] += (total + 1023) / 1024;
  683.         
  684.         if (m_lastDates[n] < (WORD) date)
  685.             m_lastDates[n] = (WORD) date;
  686.         
  687.         int parDir = pParent (dir);
  688.         if (parDir != n)
  689.         {
  690.             m_dirCounts[parDir] += m_dirCounts[n] + 1;
  691.             m_fileCounts[parDir] += m_fileCounts[n];
  692.             m_kiloTotals[parDir] += m_kiloTotals[n];    
  693.     
  694.             if (m_lastDates[parDir] < m_lastDates[n])
  695.                 m_lastDates[parDir] = m_lastDates[n];
  696.         }
  697.     }
  698.     
  699.     SetCursor(oldCursor);
  700.     
  701.     if (m_currCat.GetSize() > 0)
  702.         SetTreeDir(0);
  703. }
  704.  
  705.     // select a directory in the tree and update the dialog contents
  706. void CMainDlgWindow::SetTreeDir(int dirNum)
  707. {
  708.     if (dirNum != m_treeDir)
  709.     {
  710.         m_treeDir = dirNum;
  711.         
  712.         ListBoxFreezer frozen (m_treeList);
  713.     
  714.         if (dirNum >= 0)
  715.         {                  
  716.                 // select the appropriate subdirectories and sort them by name
  717.             c4_View selsort = m_currCat.Select(pParent [dirNum]).SortOn(pName);
  718.  
  719.             for (int j = 0; j < selsort.GetSize(); ++j)
  720.             {
  721.                     // map each entry back to the m_currCat view
  722.                 int ix = m_currCat.GetIndexOf(selsort[j]);
  723.                 ASSERT(ix >= 0);
  724.                 
  725.                     // don't add the root entry, it doesn't sort correctly
  726.                 if (ix > 0)
  727.                 {
  728.                     int k = m_treeList.AddString("");
  729.                     m_treeList.SetItemData(k, ix);
  730.                 }
  731.             }
  732.             
  733.                 // insert the parent directories in reverse order in front
  734.             for (;;)
  735.             {
  736.                 m_treeList.InsertString(0, "");
  737.                 m_treeList.SetItemData(0, dirNum);
  738.                 
  739.                 if (dirNum == m_treeDir)
  740.                     m_treeList.SetCurSel(0);
  741.                     
  742.                 if (dirNum <= 0)
  743.                     break;
  744.                     
  745.                 dirNum = (int) pParent (m_currCat[dirNum]);
  746.             }
  747.                                              
  748.                 // make sure the focus item is the same as the selection
  749.                 // InsertString moves the selection but not the focus...
  750.             m_treeList.SetCurSel(m_treeList.GetCurSel());
  751.         }
  752.     }
  753.     
  754.     SetFileDir(m_treeDir);
  755. }
  756.  
  757.     // select a list of files and update the dialog contents
  758. void CMainDlgWindow::SetFileDir(int dirNum)
  759. {
  760.     if (dirNum != m_fileDir)
  761.     {
  762.         m_fileDir = dirNum;
  763.     
  764.         ListBoxFreezer frozen (m_fileList);
  765.     
  766.         if (dirNum >= 0)
  767.         {               
  768.             m_fileView = pFiles (m_currCat[dirNum]);
  769.         
  770.             CString root = fFullPath(m_currCat, 0);           
  771.             CString path = fFullPath(m_currCat, dirNum);           
  772.             
  773.                 // remove common root prefix
  774.             path = path.Mid(root.GetLength());
  775.             if (path.IsEmpty())
  776.                 path = "(root)";
  777.  
  778.             m_treePath.SetWindowText(path);
  779.             
  780.             for (int i = 0; i < m_fileView.GetSize(); ++i)
  781.                 m_fileList.AddString("");
  782.         }
  783.         else
  784.         {
  785.             m_fileSort = c4_View ();
  786.             m_fileView = c4_View ();
  787.             m_treePath.SetWindowText("");
  788.         }
  789.         
  790.             // this sets up the appropriate m_fileSort view
  791.         SortFileList(*m_sortProp);
  792.     }
  793.     
  794.         // always reset the file list selection
  795.     m_fileList.SetCurSel(-1);
  796.     
  797.     OnSelchangeFileList();
  798. }
  799.  
  800.     // the catalog selection changed
  801. void CMainDlgWindow::OnSelchangeCatList()
  802. {
  803.     CString s;
  804.  
  805.     int n = m_catList.GetCurSel();
  806.     if (n != LB_ERR)
  807.         m_catList.GetText(n, s);
  808.         
  809.     SetCatalog(s);
  810. }
  811.  
  812.     // the directory selection changed
  813. void CMainDlgWindow::OnSelchangeTreeList()
  814. {
  815.     int n = m_treeList.GetCurSel();
  816.     
  817.     m_findBtn.EnableWindow(n >= 0);
  818.     
  819.     if (n >= 0)
  820.         n = (int) m_treeList.GetItemData(n);
  821.     
  822.     SetFileDir(n);
  823. }
  824.  
  825.     // descend into an entry in the directory tree
  826. void CMainDlgWindow::OnDblclkTreeList()
  827. {
  828.     int n = m_treeList.GetCurSel();
  829.     if (n >= 0)
  830.     {
  831.         n = (int) m_treeList.GetItemData(n);
  832.             
  833.             // don't allow descending into a dir with no subdirs
  834.         if (m_dirCounts[n] == 0)
  835.         {
  836.             MessageBeep(0);
  837.             return;
  838.         }
  839.     }
  840.     
  841.     SetTreeDir(n);
  842. }
  843.  
  844.     // the file selection changed
  845. void CMainDlgWindow::OnSelchangeFileList()
  846. {
  847.     CString s;
  848.     
  849.     int n = m_fileList.GetCurSel();
  850.     if (n >= 0)
  851.     {
  852.         c4_RowRef file = m_fileSort[n];
  853.         s = pName (file);
  854.     }
  855.     else if (m_fileDir >= 0)
  856.         s.Format("%d files", m_fileSort.GetSize());
  857.     
  858.     m_infoText.SetWindowText(s);
  859. }
  860.  
  861. void CMainDlgWindow::OnDblclkFileList()
  862. {
  863.     int n = m_fileList.GetCurSel();
  864.     if (n >= 0)
  865.     {
  866.         c4_RowRef file = m_fileSort[n];
  867.         CString s = pName (file);
  868.         
  869.         CString path = fFullPath(m_currCat, m_fileDir); // also the working dir
  870.          
  871.         if ((UINT) ShellExecute(m_hWnd, 0, path + s, 0, path, SW_SHOWNORMAL) >= 32)
  872.             return; // selected file succesfully launched
  873.     }
  874.  
  875.     MessageBeep(0);
  876. }
  877.  
  878.     // Adjust specified menu entry and label text to indicate current sort order
  879. void CMainDlgWindow::AdjustDisplay(CCmdUI& cui, int ix_, c4_Property& prop_,
  880.                                     int label_, const char* text_)
  881. {
  882.     bool match = m_sortProp == &prop_;
  883.     
  884.     cui.m_nIndex = ix_;
  885.     cui.SetRadio(match);
  886.  
  887.         // include "+" or "-" in the label corresponding to the current sort field
  888.     CString s = text_;
  889.     if (match)
  890.         s += m_sortReverse ? " (-)" : " (+)";
  891.         
  892.     CWnd* wnd = GetDlgItem(label_);
  893.     ASSERT(wnd);
  894.     
  895.     wnd->SetWindowText(s);
  896. }
  897.  
  898.     // Sort the file list and adjust menu items and label texts
  899. void CMainDlgWindow::SortFileList(c4_Property& prop_, bool toggle_)
  900. {
  901.     if (m_sortProp != &prop_)
  902.     {
  903.         m_sortProp = &prop_;
  904.         m_sortReverse = false;
  905.     }
  906.     else if (toggle_)
  907.         m_sortReverse = !m_sortReverse;
  908.     
  909.         // update all menu check marks here, since CCmdUI doesn't work in dialogs
  910.     CMenu* menu = GetMenu();
  911.     ASSERT(menu);
  912.  
  913.     menu = menu->GetSubMenu(0); // the "File" menu
  914.     ASSERT(menu);
  915.     
  916.     CMenu* sub = menu->GetSubMenu(1); // the "Sort Files" entry
  917.     ASSERT(sub);
  918.         
  919.         // use CCmdUI, not CheckMenuItem, because it can set nice bullet marks
  920.     CCmdUI cui;
  921.     cui.m_pMenu = sub;
  922.     cui.m_nIndexMax = sub->GetMenuItemCount();
  923.     ASSERT(cui.m_nIndexMax == 5); // name, size, date, <sep>, reverse
  924.     
  925.     AdjustDisplay(cui, 0, pName, IDC_NAME_LABEL, "File &name");    
  926.     AdjustDisplay(cui, 1, pSize, IDC_SIZE_LABEL, "Size");    
  927.     AdjustDisplay(cui, 2, pDate, IDC_DATE_LABEL, "Date");
  928.     
  929.         // the "Reverse" menu item uses a regular check mark
  930.     cui.m_nIndex = 4;
  931.     cui.SetCheck(m_sortReverse);
  932.  
  933.         // sorting may take some time
  934.     HCURSOR oldCursor = SetCursor(LoadCursor(0, IDC_WAIT));
  935.         
  936.         // figure out the index of the row that was selected, if any
  937.     int n = m_fileList.GetCurSel();
  938.     if (n >= 0)
  939.     {
  940.         n = m_fileView.GetIndexOf(m_fileSort [n]);
  941.         ASSERT(n >= 0);
  942.     }
  943.        
  944.         // define the sort order and make sure the list is redrawn
  945.     if (m_sortReverse)
  946.         m_fileSort = m_fileView.SortOnReverse(prop_, prop_);
  947.     else
  948.         m_fileSort = m_fileView.SortOn(prop_);
  949.         
  950.     m_fileList.Invalidate(); 
  951.         
  952.         // restore the selection to the original item
  953.     if (n >= 0)
  954.     {
  955.         int m = m_fileSort.Find(m_fileView [n]); // where is that row now?
  956.         ASSERT(m >= 0);
  957.         
  958.         m_fileList.SetCurSel(m);
  959.     }
  960.     
  961.     SetCursor(oldCursor);
  962. }
  963.  
  964. void CMainDlgWindow::OnSortByName()
  965. {
  966.     SortFileList(pName);
  967. }
  968.  
  969. void CMainDlgWindow::OnSortBySize()
  970. {
  971.     SortFileList(pSize);
  972. }
  973.  
  974. void CMainDlgWindow::OnSortByDate()
  975. {
  976.     SortFileList(pDate);
  977. }
  978.  
  979. void CMainDlgWindow::OnSortReverse()
  980. {
  981.     SortFileList(*m_sortProp, true);
  982. }
  983.  
  984. void CMainDlgWindow::OnLButtonDown(UINT nFlags, CPoint point)
  985. {
  986.         // catch mouse clicks on the header texts to alter the file sort order
  987.     CWnd* wnd = ChildWindowFromPoint(point);
  988.     if (wnd)
  989.         switch (wnd->GetDlgCtrlID())
  990.         {
  991.             case IDC_NAME_LABEL:    SortFileList(pName, true); return;
  992.             case IDC_SIZE_LABEL:    SortFileList(pSize, true); return;
  993.             case IDC_DATE_LABEL:    SortFileList(pDate, true); return;
  994.             
  995.             case IDC_LOGO1:             // handle clicks on the fake logo
  996.             case IDC_LOGO2:
  997.             case IDC_LOGO3:         OnAppAbout(); return;
  998.         }                     
  999.     
  1000.     CDialog::OnLButtonDown(nFlags, point);
  1001. }
  1002.  
  1003. /////////////////////////////////////////////////////////////////////////////
  1004. // The following class maintains most of the state required to iterate
  1005. // over all entries to satisfy a find request. This is a pretty messy
  1006. // approach to be able to use this in both forward and backward modes.
  1007.         
  1008.     class CFindState
  1009.     {
  1010.     public:
  1011.         CFindState (CMainDlgWindow& dlg_)
  1012.             : _dlg (dlg_), findStorage (0)
  1013.         {
  1014.                 // searching may take some time
  1015.             oldCursor = SetCursor(LoadCursor(0, IDC_WAIT));
  1016.         }
  1017.         
  1018.         ~CFindState ()
  1019.         {                      
  1020.             SetCursor(oldCursor);
  1021.             
  1022.             findCat = c4_View();
  1023.             findList = c4_View();
  1024.             delete findStorage;
  1025.         }
  1026.         
  1027.         bool Initialize()
  1028.         {
  1029.             lastCat = _dlg.m_catList.GetCurSel();
  1030.             if (lastCat < 0 || _dlg.m_treeDir < 0 || _dlg.m_fileDir < 0)
  1031.             {
  1032.                 MessageBeep(0);
  1033.                 return false;
  1034.             }
  1035.         
  1036.             findCatName = _dlg.m_currCatName;
  1037.             findCat = _dlg.m_currCat;
  1038.             findList = _dlg.m_fileSort;
  1039.         
  1040.                 // prepare for iteration    
  1041.             lastDir = _dlg.m_fileDir;
  1042.             lastSel = _dlg.m_fileList.GetCurSel(); // can be -1
  1043.             
  1044.             return true;
  1045.         }
  1046.         
  1047.         void SetSort(const c4_View& view_)
  1048.         {
  1049.             ASSERT(_dlg.m_sortProp);
  1050.             c4_Property& prop = *_dlg.m_sortProp;
  1051.             
  1052.             if (_dlg.m_sortReverse)
  1053.                 findList = view_.SortOnReverse(prop, prop);
  1054.             else
  1055.                 findList = view_.SortOn(prop);
  1056.         }
  1057.         
  1058.         bool IsStartDir(int dir_, int cat_)
  1059.         {
  1060.             return dir_ == lastDir && cat_ == lastCat;
  1061.         }
  1062.         
  1063.         void Select(int sel_, int dir_)
  1064.         {
  1065.                 // adjust to the found catalog and directory
  1066.             _dlg.SetCatalog(findCatName);
  1067.             _dlg.SetTreeDir(dir_);
  1068.                 // then match the selection and update the status fields
  1069.             _dlg.m_fileList.SetCurSel(sel_);
  1070.             _dlg.OnSelchangeFileList();
  1071.  
  1072.             _dlg.m_fileList.SetFocus();     // so arrows work as expected
  1073.             _dlg.m_fileList.UpdateWindow(); // show before new find can start
  1074.         }
  1075.         
  1076.         void UseCatalog(int cat_)
  1077.         {
  1078.                 // show which catalog we're currently searching
  1079.             _dlg.m_catList.SetCurSel(cat_);
  1080.         
  1081.             findCat = c4_View();
  1082.             findList = c4_View();
  1083.             delete findStorage;
  1084.             findStorage = 0;
  1085.             
  1086.             _dlg.m_catList.GetText(cat_, findCatName);        
  1087.             findStorage = DEBUG_NEW c4_Storage (findCatName, false);
  1088.             findCat = findStorage->View("dirs");
  1089.         }
  1090.         
  1091.             // check if any key is pressed, this aborts a lengthy find
  1092.         bool WantsToQuit() const
  1093.         {
  1094.             MSG msg;
  1095.                 // careful, there may still be keyup's in the queue
  1096.             if (!::PeekMessage(&msg, NULL, WM_CHAR, WM_CHAR, PM_NOREMOVE))
  1097.                 return false;                                       
  1098.             
  1099.             while (::PeekMessage(&msg, NULL, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE))
  1100.                 ; // flush all key events
  1101.                 
  1102.             return true; 
  1103.         }
  1104.         
  1105.         CMainDlgWindow& _dlg;
  1106.         int lastCat, lastDir, lastSel;
  1107.         c4_Storage* findStorage;
  1108.         CString findCatName;
  1109.         c4_View findCat, findList;
  1110.         HCURSOR oldCursor;
  1111.     };
  1112.     
  1113. /////////////////////////////////////////////////////////////////////////////
  1114. // Several aspects of the find code below affect search performance:
  1115. //
  1116. //  1.  Each catalog is opened without calculating any statistics
  1117. //  2.  Only match fields are accessed, for optimal use of on-demand loading
  1118. //  3.  Sorting is only performed *after* at least one entry has been found
  1119. //
  1120. // As a result, searching is quite fast (and THAT is an understatement).
  1121.  
  1122. void CMainDlgWindow::OnFindNext()
  1123. {
  1124.     CFindState state (*this);
  1125.     if (!state.Initialize())
  1126.         return;
  1127.  
  1128.         // prepare for iteration    
  1129.     int cat = state.lastCat;    
  1130.     int dir = state.lastDir;
  1131.     int sel = state.lastSel;
  1132.     
  1133.     bool mustSort = false; // avoid resorting when a sorted list is available
  1134.     bool first = true; // watch out for infinite loop if never found an entry
  1135.     
  1136.     for (;;) // loop over each catalog
  1137.     {
  1138.         int dirCount = state.findCat.GetSize();
  1139.         
  1140.         while (dir < dirCount) // loop over each subdirectory
  1141.         {
  1142.             c4_View files = pFiles (state.findCat [dir]);
  1143.             int selCount = files.GetSize();
  1144.             
  1145.                 // on first entry into dir, first scan for match in unsorted list
  1146.                 // this *drastically* improves performance if most dirs are a miss
  1147.             if (sel < 0 && m_findDlg.NeedsCompare())
  1148.             {
  1149.                 while (++sel < selCount) // loop over each file
  1150.                     if (m_findDlg.Match(files[sel]))
  1151.                     {
  1152.                         sel = -1; // something matches, prepare to search sorted
  1153.                         break;
  1154.                     }
  1155.                 
  1156.                 // at this point sel is either -1 or selCount
  1157.             }
  1158.             
  1159.                 // only sort if we're really going to use this to scan
  1160.             if (mustSort && sel < selCount)
  1161.                 state.SetSort(files);
  1162.             
  1163.             while (++sel < selCount) // loop over each file
  1164.                 if (m_findDlg.Match(state.findList [sel]))
  1165.                 {
  1166.                     if (!first && sel >= state.lastSel &&
  1167.                                     state.IsStartDir(dir, cat))
  1168.                         break; // oops, second time around in start dir, fail
  1169.                         
  1170.                     state.Select(sel, dir);
  1171.                     return;                    
  1172.                 }
  1173.             
  1174.                 // if we fell through start dir for the second time, then fail
  1175.                 // this scans for too many entries but works on empty start dir
  1176.             if (state.WantsToQuit() || !first && state.IsStartDir(dir, cat))
  1177.             {
  1178.                     // wrapped around, nothing found
  1179.                 m_catList.SetCurSel(state.lastCat);
  1180.                 MessageBeep(0);
  1181.                 return;
  1182.             }
  1183.             
  1184.             first = false;
  1185.             
  1186.             sel = -1;
  1187.             ++dir; // directories are scanned in breadth first order, hmmm...
  1188.             mustSort = true;
  1189.         }
  1190.         
  1191.         dir = 0;
  1192.         
  1193.         if (m_findDlg.m_singleCat)
  1194.             continue; // don't switch to another catalog file
  1195.                  
  1196.         if (++cat >= m_catList.GetCount())
  1197.             cat = 0;
  1198.         
  1199.         state.UseCatalog(cat);
  1200.     }
  1201. }
  1202.  
  1203. void CMainDlgWindow::OnFindPrev()
  1204. {
  1205.     CFindState state (*this);
  1206.     if (!state.Initialize())
  1207.         return;
  1208.  
  1209.         // prepare for iteration    
  1210.     int cat = state.lastCat;    
  1211.     int dir = state.lastDir;
  1212.     int sel = state.lastSel;
  1213.     
  1214.     bool mustSort = false; // avoid resorting when a sorted list is available
  1215.     bool first = true; // watch out for infinite loop if never found an entry
  1216.     
  1217.     for (;;) // loop over each catalog
  1218.     {
  1219.         if (dir < 0)
  1220.             dir = state.findCat.GetSize() - 1;
  1221.             
  1222.         while (dir >= 0) // loop over each subdirectory
  1223.         {
  1224.             c4_View files = pFiles (state.findCat [dir]);
  1225.             int selCount = files.GetSize();
  1226.             
  1227.             if (sel < 0)
  1228.                 sel = selCount;
  1229.                 
  1230.                 // on first entry into dir, first scan for match in unsorted list
  1231.                 // this *drastically* improves performance if most dirs are a miss
  1232.             if (sel >= selCount && m_findDlg.NeedsCompare())
  1233.             {
  1234.                 while (--sel >= 0) // loop over each file
  1235.                     if (m_findDlg.Match(files[sel]))
  1236.                     {
  1237.                         sel = selCount; // matches, prepare to search sorted
  1238.                         break;
  1239.                     }
  1240.                 
  1241.                 // at this point sel is either -1 or selCount
  1242.             }
  1243.             
  1244.                 // only sort if we're really going to use this to scan
  1245.             if (mustSort && sel >= 0)
  1246.                 state.SetSort(files);
  1247.             
  1248.             while (--sel >= 0) // loop over each file
  1249.                 if (m_findDlg.Match(state.findList[sel]))
  1250.                 {
  1251.                     if (!first && sel <= state.lastSel &&
  1252.                                     state.IsStartDir(dir, cat))
  1253.                         break; // oops, second time around in start dir, fail
  1254.                         
  1255.                     state.Select(sel, dir);
  1256.                     return;                    
  1257.                 }
  1258.             
  1259.                 // if we fell through start dir for the second time, then fail
  1260.                 // this scans for too many entries but works on empty start dir
  1261.             if (state.WantsToQuit() || !first && state.IsStartDir(dir, cat))
  1262.             {
  1263.                     // wrapped around, nothing found
  1264.                 m_catList.SetCurSel(state.lastCat);
  1265.                 MessageBeep(0);
  1266.                 return;
  1267.             }
  1268.             
  1269.             first = false;
  1270.             
  1271.             sel = -1;
  1272.             --dir; // directories are scanned in breadth first order, hmmm...
  1273.             mustSort = true;
  1274.         }
  1275.         
  1276.         ASSERT(dir == -1);
  1277.         
  1278.         if (m_findDlg.m_singleCat)
  1279.             continue; // don't switch to another catalog file
  1280.         
  1281.         if (cat == 0)
  1282.             cat = m_catList.GetCount();
  1283.         --cat;
  1284.         
  1285.         state.UseCatalog(cat);
  1286.     }
  1287. }
  1288.  
  1289. /////////////////////////////////////////////////////////////////////////////
  1290. // $Id: catfish.cpp,v 1.2 1996/12/04 14:49:22 jcw Exp $
  1291.