/* * CiderPress * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. * See the file LICENSE for distribution terms. */ /* * File actions. These are actually part of MainWindow, but for readability * these are split into their own file. */ #include "stdafx.h" #include "Main.h" #include "ViewFilesDialog.h" //#include "ViewOptionsDialog.h" #include "ChooseDirDialog.h" #include "AddFilesDialog.h" #include "CreateSubdirDialog.h" #include "ExtractOptionsDialog.h" #include "UseSelectionDialog.h" #include "RecompressOptionsDialog.h" #include "ConvDiskOptionsDialog.h" #include "ConvFileOptionsDialog.h" #include "EditCommentDialog.h" #include "EditPropsDialog.h" #include "RenameVolumeDialog.h" #include "ConfirmOverwriteDialog.h" #include "ImageFormatDialog.h" #include "FileNameConv.h" #include "GenericArchive.h" #include "NufxArchive.h" #include "DiskArchive.h" #include "ChooseAddTargetDialog.h" #include "CassetteDialog.h" #include "BasicImport.h" //#include "../util/UtilLib.h" #include "../diskimg/TwoImg.h" #include /* * ========================================================================== * View * ========================================================================== */ /* * View a file stored in the archive. * * Control bounces back through Get*FileText() to get the actual * data to view. */ void MainWindow::OnActionsView(void) { HandleView(); } void MainWindow::OnUpdateActionsView(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && fpContentList->GetSelectedCount() > 0); } /* * Handle a request to view stuff. * * If "query" is set, we ask the user to confirm some choices. If not, we * just go with the defaults. * * We include "damaged" files so that we can show the user a nice message * about how the file is damaged. */ void MainWindow::HandleView(void) { ASSERT(fpContentList != nil); SelectionSet selSet; int threadMask = GenericEntry::kAnyThread | GenericEntry::kAllowDamaged | GenericEntry::kAllowDirectory | GenericEntry::kAllowVolumeDir; selSet.CreateFromSelection(fpContentList, threadMask); selSet.Dump(); if (selSet.GetNumEntries() == 0) { MessageBox(L"Nothing viewable found.", L"No match", MB_OK | MB_ICONEXCLAMATION); return; } //fpSelSet = &selSet; ViewFilesDialog vfd(this); vfd.SetSelectionSet(&selSet); vfd.SetTextTypeFace(fPreferences.GetPrefString(kPrViewTextTypeFace)); vfd.SetTextPointSize(fPreferences.GetPrefLong(kPrViewTextPointSize)); vfd.SetNoWrapText(fPreferences.GetPrefBool(kPrNoWrapText)); vfd.DoModal(); //fpSelSet = nil; // remember which font they used (sticky pref, not in registry) fPreferences.SetPrefString(kPrViewTextTypeFace, vfd.GetTextTypeFace()); fPreferences.SetPrefLong(kPrViewTextPointSize, vfd.GetTextPointSize()); WMSG2("Preferences: saving view font %d-point '%ls'\n", fPreferences.GetPrefLong(kPrViewTextPointSize), fPreferences.GetPrefString(kPrViewTextTypeFace)); } /* * ========================================================================== * Open as disk image * ========================================================================== */ /* * View a file stored in the archive. * * Control bounces back through Get*FileText() to get the actual * data to view. */ void MainWindow::OnActionsOpenAsDisk(void) { ASSERT(fpContentList != nil); ASSERT(fpContentList->GetSelectedCount() == 1); GenericEntry* pEntry = GetSelectedItem(fpContentList); if (pEntry->GetHasDiskImage()) TmpExtractAndOpen(pEntry, GenericEntry::kDiskImageThread, kModeDiskImage); else TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeDiskImage); } void MainWindow::OnUpdateActionsOpenAsDisk(CCmdUI* pCmdUI) { const int kMinLen = 512 * 7; bool allow = false; if (fpContentList != nil && fpContentList->GetSelectedCount() == 1) { GenericEntry* pEntry = GetSelectedItem(fpContentList); if (pEntry != nil) { if ((pEntry->GetHasDataFork() || pEntry->GetHasDiskImage()) && pEntry->GetUncompressedLen() > kMinLen) { allow = true; } } } pCmdUI->Enable(allow); } /* * ========================================================================== * Add Files * ========================================================================== */ /* * Add files to an archive. */ void MainWindow::OnActionsAddFiles(void) { WMSG0("Add files!\n"); AddFilesDialog addFiles(this); DiskImgLib::A2File* pTargetSubdir = nil; /* * Special handling for adding files to disk images. */ if (fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) { if (!ChooseAddTarget(&pTargetSubdir, &addFiles.fpTargetDiskFS)) return; } addFiles.fStoragePrefix = ""; addFiles.fIncludeSubfolders = fPreferences.GetPrefBool(kPrAddIncludeSubFolders); addFiles.fStripFolderNames = fPreferences.GetPrefBool(kPrAddStripFolderNames); addFiles.fOverwriteExisting = fPreferences.GetPrefBool(kPrAddOverwriteExisting); addFiles.fTypePreservation = fPreferences.GetPrefLong(kPrAddTypePreservation); addFiles.fConvEOL = fPreferences.GetPrefLong(kPrAddConvEOL); /* if they can't convert EOL when adding files, disable the option */ if (!fpOpenArchive->GetCapability(GenericArchive::kCapCanConvEOLOnAdd)) { addFiles.fConvEOL = AddFilesDialog::kConvEOLNone; addFiles.fConvEOLEnable = false; } /* * Disable editing of the storage prefix field. Force pathname * stripping to be on for non-hierarchical filesystems (i.e. everything * but ProDOS and HFS). */ if (addFiles.fpTargetDiskFS != nil) { DiskImg::FSFormat format; format = addFiles.fpTargetDiskFS->GetDiskImg()->GetFSFormat(); if (pTargetSubdir != nil) { ASSERT(!pTargetSubdir->IsVolumeDirectory()); addFiles.fStoragePrefix = pTargetSubdir->GetPathName(); } addFiles.fStripFolderNamesEnable = false; addFiles.fStoragePrefixEnable = false; switch (format) { case DiskImg::kFormatProDOS: case DiskImg::kFormatMacHFS: addFiles.fStripFolderNamesEnable = true; break; default: break; } } if (!addFiles.fStripFolderNamesEnable) { /* if we disabled it, we did so because it's mandatory */ addFiles.fStripFolderNames = true; } addFiles.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder); addFiles.DoModal(); if (addFiles.GetExitStatus() == IDOK) { fPreferences.SetPrefBool(kPrAddIncludeSubFolders, addFiles.fIncludeSubfolders != 0); if (addFiles.fStripFolderNamesEnable) { // only update the pref if they had the ability to change it fPreferences.SetPrefBool(kPrAddStripFolderNames, addFiles.fStripFolderNames != 0); } fPreferences.SetPrefBool(kPrAddOverwriteExisting, addFiles.fOverwriteExisting != 0); fPreferences.SetPrefLong(kPrAddTypePreservation, addFiles.fTypePreservation); if (addFiles.fConvEOLEnable) fPreferences.SetPrefLong(kPrAddConvEOL, addFiles.fConvEOL); CString saveFolder = addFiles.GetFileNames(); saveFolder = saveFolder.Left(addFiles.GetFileNameOffset()); fPreferences.SetPrefString(kPrAddFileFolder, saveFolder); /* * Set up a progress dialog and kick things off. */ bool result; fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionAdd, this); //fpContentList->Invalidate(); // don't allow redraws until done result = fpOpenArchive->BulkAdd(fpActionProgress, &addFiles); fpContentList->Reload(); fpActionProgress->Cleanup(this); fpActionProgress = nil; if (result) SuccessBeep(); } else { WMSG0("SFD bailed with Cancel\n"); } } void MainWindow::OnUpdateActionsAddFiles(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()); } /* * Figure out where they want to add files. * * If the volume directory of a disk is chosen, "*ppTargetSubdir" will * be set to nil. */ bool MainWindow::ChooseAddTarget(DiskImgLib::A2File** ppTargetSubdir, DiskImgLib::DiskFS** ppTargetDiskFS) { ASSERT(ppTargetSubdir != nil); ASSERT(ppTargetDiskFS != nil); *ppTargetSubdir = nil; *ppTargetDiskFS = nil; GenericEntry* pEntry = GetSelectedItem(fpContentList); if (pEntry != nil && (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory || pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir)) { /* * They've selected a single subdirectory. Add the files there. */ DiskEntry* pDiskEntry = (DiskEntry*) pEntry; *ppTargetSubdir = pDiskEntry->GetA2File(); *ppTargetDiskFS = (*ppTargetSubdir)->GetDiskFS(); } else { /* * Nothing selected, non-subdir selected, or multiple files * selected. Whatever the case, pop up the choose target dialog. * * This works on DOS 3.3 and Pascal disks, because the absence * of subdirectories means there's only one possible place to * put the files. We could short-circuit this code for anything * but ProDOS and HFS, but we have to be careful about embedded * sub-volumes. */ DiskArchive* pDiskArchive = (DiskArchive*) fpOpenArchive; WMSG0("Trying ChooseAddTarget\n"); ChooseAddTargetDialog targetDialog(this); targetDialog.fpDiskFS = pDiskArchive->GetDiskFS(); if (targetDialog.DoModal() != IDOK) return false; *ppTargetSubdir = targetDialog.fpChosenSubdir; *ppTargetDiskFS = targetDialog.fpChosenDiskFS; /* make sure the subdir is part of the diskfs */ ASSERT(*ppTargetSubdir == nil || (*ppTargetSubdir)->GetDiskFS() == *ppTargetDiskFS); } if (*ppTargetSubdir != nil && (*ppTargetSubdir)->IsVolumeDirectory()) *ppTargetSubdir = nil; return true; } /* * ========================================================================== * Add Disks * ========================================================================== */ /* * Add a disk to an archive. Not all archive formats support disk images. * * We open a single disk archive file as a DiskImg, get the format * figured out, then write it block-by-block into a file chosen by the user. * Standard open/save dialogs work fine here. */ void MainWindow::OnActionsAddDisks(void) { DIError dierr; DiskImg img; CString failed, errMsg; CString openFilters, saveFolder; AddFilesDialog addOpts; WMSG0("Add disks!\n"); failed.LoadString(IDS_FAILED); openFilters = kOpenDiskImage; openFilters += kOpenAll; openFilters += kOpenEnd; CFileDialog dlg(TRUE, L"dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); dlg.m_ofn.lpstrTitle = L"Add Disk Image"; /* file is always opened read-only */ dlg.m_ofn.Flags |= OFN_HIDEREADONLY; dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder); if (dlg.DoModal() != IDOK) goto bail; saveFolder = dlg.m_ofn.lpstrFile; saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); fPreferences.SetPrefString(kPrAddFileFolder, saveFolder); /* open the image file and analyze it */ dierr = img.OpenImage(dlg.GetPathName(), PathProposal::kLocalFssep, true); if (dierr != kDIErrNone) { errMsg.Format(L"Unable to open disk image: %hs.", DiskImgLib::DIStrError(dierr)); MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP); goto bail; } if (img.AnalyzeImage() != kDIErrNone) { errMsg.Format(L"The file '%ls' doesn't seem to hold a valid disk image.", dlg.GetPathName()); MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP); goto bail; } /* if requested (or necessary), verify the format */ if (/*img.GetFSFormat() == DiskImg::kFormatUnknown ||*/ img.GetSectorOrder() == DiskImg::kSectorOrderUnknown || fPreferences.GetPrefBool(kPrQueryImageFormat)) { ImageFormatDialog imf; imf.InitializeValues(&img); imf.fFileSource = dlg.GetPathName(); imf.SetQueryDisplayFormat(false); WMSG2(" On entry, sectord=%d format=%d\n", imf.fSectorOrder, imf.fFSFormat); if (imf.fFSFormat == DiskImg::kFormatUnknown) imf.fFSFormat = DiskImg::kFormatGenericProDOSOrd; if (imf.DoModal() != IDOK) { WMSG0("User bailed on IMF dialog\n"); goto bail; } WMSG2(" On exit, sectord=%d format=%d\n", imf.fSectorOrder, imf.fFSFormat); if (imf.fSectorOrder != img.GetSectorOrder() || imf.fFSFormat != img.GetFSFormat()) { WMSG0("Initial values overridden, forcing img format\n"); dierr = img.OverrideFormat(img.GetPhysicalFormat(), imf.fFSFormat, imf.fSectorOrder); if (dierr != kDIErrNone) { errMsg.Format(L"Unable to access disk image using selected" L" parameters. Error: %hs.", DiskImgLib::DIStrError(dierr)); MessageBox(errMsg, failed, MB_OK | MB_ICONSTOP); goto bail; } } } /* * We want to read from the image the way that a ProDOS application * would, which means forcing the FSFormat to generic ProDOS ordering. * This way, ProDOS disks load serially, and DOS 3.3 disks load with * their sectors swapped around. */ dierr = img.OverrideFormat(img.GetPhysicalFormat(), DiskImg::kFormatGenericProDOSOrd, img.GetSectorOrder()); if (dierr != kDIErrNone) { errMsg.Format(L"Internal error: couldn't switch to generic ProDOS: %hs.", DiskImgLib::DIStrError(dierr)); MessageBox(errMsg, failed, MB_OK | MB_ICONSTOP); goto bail; } /* * Set up an AddFilesDialog, but don't actually use it as a dialog. * Instead, we just configure the various options appropriately. * * To conform to multi-file-selection semantics, we need to drop a * null byte in right after the pathname. */ ASSERT(dlg.m_ofn.nFileOffset > 0); int len; len = wcslen(dlg.m_ofn.lpstrFile) + 2; dlg.m_ofn.lpstrFile[dlg.m_ofn.nFileOffset-1] = '\0'; addOpts.SetFileNames(dlg.m_ofn.lpstrFile, len, dlg.m_ofn.nFileOffset); addOpts.fStoragePrefix = ""; addOpts.fIncludeSubfolders = false; addOpts.fStripFolderNames = false; addOpts.fOverwriteExisting = false; addOpts.fTypePreservation = AddFilesDialog::kPreserveTypes; addOpts.fConvEOL = AddFilesDialog::kConvEOLNone; addOpts.fConvEOLEnable = false; // no EOL conversion on disk images! addOpts.fpDiskImg = &img; bool result; fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionAddDisk, this); //fpContentList->Invalidate(); // don't allow updates until done result = fpOpenArchive->AddDisk(fpActionProgress, &addOpts); fpContentList->Reload(); fpActionProgress->Cleanup(this); fpActionProgress = nil; if (result) SuccessBeep(); bail: return; } void MainWindow::OnUpdateActionsAddDisks(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && fpOpenArchive->GetCapability(GenericArchive::kCapCanAddDisk)); } /* * ========================================================================== * Create Subdirectory * ========================================================================== */ /* * Create a subdirectory inside another subdirectory (or volume directory). * * Simply asserting that an existing subdir be selected in the list does * away with all sorts of testing. Creating subdirs on DOS disks and NuFX * archives is impossible because neither has subdirs. Nested volumes are * selected for us by the user. */ void MainWindow::OnActionsCreateSubdir(void) { CreateSubdirDialog csDialog; ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); ASSERT(!fpOpenArchive->IsReadOnly()); GenericEntry* pEntry = GetSelectedItem(fpContentList); if (pEntry == nil) { // can happen for no selection or multi-selection; should not be here ASSERT(false); return; } if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory && pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir) { CString errMsg; errMsg = "Please select a subdirectory."; ShowFailureMsg(this, errMsg, IDS_MB_APP_NAME); return; } WMSG1("Creating subdir in '%ls'\n", pEntry->GetPathName()); csDialog.fBasePath = pEntry->GetPathName(); csDialog.fpArchive = fpOpenArchive; csDialog.fpParentEntry = pEntry; csDialog.fNewName = "New.Subdir"; if (csDialog.DoModal() != IDOK) return; WMSG1("Creating '%ls'\n", (LPCWSTR) csDialog.fNewName); fpOpenArchive->CreateSubdir(this, pEntry, csDialog.fNewName); fpContentList->Reload(); } void MainWindow::OnUpdateActionsCreateSubdir(CCmdUI* pCmdUI) { bool enable = fpContentList != nil && !fpOpenArchive->IsReadOnly() && fpContentList->GetSelectedCount() == 1 && fpOpenArchive->GetCapability(GenericArchive::kCapCanCreateSubdir); if (enable) { /* second-level check: make sure it's a subdir */ GenericEntry* pEntry = GetSelectedItem(fpContentList); if (pEntry == nil) { ASSERT(false); return; } if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory && pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir) { enable = false; } } pCmdUI->Enable(enable); } /* * ========================================================================== * Extract * ========================================================================== */ /* * Extract files. */ void MainWindow::OnActionsExtract(void) { ASSERT(fpContentList != nil); /* * Ask the user about various options. */ ExtractOptionsDialog extOpts(fpContentList->GetSelectedCount(), this); extOpts.fExtractPath = fPreferences.GetPrefString(kPrExtractFileFolder); extOpts.fConvEOL = fPreferences.GetPrefLong(kPrExtractConvEOL); extOpts.fConvHighASCII = fPreferences.GetPrefBool(kPrExtractConvHighASCII); extOpts.fIncludeDataForks = fPreferences.GetPrefBool(kPrExtractIncludeData); extOpts.fIncludeRsrcForks = fPreferences.GetPrefBool(kPrExtractIncludeRsrc); extOpts.fIncludeDiskImages = fPreferences.GetPrefBool(kPrExtractIncludeDisk); extOpts.fEnableReformat = fPreferences.GetPrefBool(kPrExtractEnableReformat); extOpts.fDiskTo2MG = fPreferences.GetPrefBool(kPrExtractDiskTo2MG); extOpts.fAddTypePreservation = fPreferences.GetPrefBool(kPrExtractAddTypePreservation); extOpts.fAddExtension = fPreferences.GetPrefBool(kPrExtractAddExtension); extOpts.fStripFolderNames = fPreferences.GetPrefBool(kPrExtractStripFolderNames); extOpts.fOverwriteExisting = fPreferences.GetPrefBool(kPrExtractOverwriteExisting); if (fpContentList->GetSelectedCount() > 0) extOpts.fFilesToExtract = ExtractOptionsDialog::kExtractSelection; else extOpts.fFilesToExtract = ExtractOptionsDialog::kExtractAll; if (extOpts.DoModal() != IDOK) return; if (extOpts.fExtractPath.Right(1) != "\\") extOpts.fExtractPath += "\\"; /* push preferences back out */ fPreferences.SetPrefString(kPrExtractFileFolder, extOpts.fExtractPath); fPreferences.SetPrefLong(kPrExtractConvEOL, extOpts.fConvEOL); fPreferences.SetPrefBool(kPrExtractConvHighASCII, extOpts.fConvHighASCII != 0); fPreferences.SetPrefBool(kPrExtractIncludeData, extOpts.fIncludeDataForks != 0); fPreferences.SetPrefBool(kPrExtractIncludeRsrc, extOpts.fIncludeRsrcForks != 0); fPreferences.SetPrefBool(kPrExtractIncludeDisk, extOpts.fIncludeDiskImages != 0); fPreferences.SetPrefBool(kPrExtractEnableReformat, extOpts.fEnableReformat != 0); fPreferences.SetPrefBool(kPrExtractDiskTo2MG, extOpts.fDiskTo2MG != 0); fPreferences.SetPrefBool(kPrExtractAddTypePreservation, extOpts.fAddTypePreservation != 0); fPreferences.SetPrefBool(kPrExtractAddExtension, extOpts.fAddExtension != 0); fPreferences.SetPrefBool(kPrExtractStripFolderNames, extOpts.fStripFolderNames != 0); fPreferences.SetPrefBool(kPrExtractOverwriteExisting, extOpts.fOverwriteExisting != 0); WMSG1("Requested extract path is '%ls'\n", (LPCWSTR) extOpts.fExtractPath); /* * Create a "selection set" of things to display. */ SelectionSet selSet; int threadMask = 0; if (extOpts.fIncludeDataForks) threadMask |= GenericEntry::kDataThread; if (extOpts.fIncludeRsrcForks) threadMask |= GenericEntry::kRsrcThread; if (extOpts.fIncludeDiskImages) threadMask |= GenericEntry::kDiskImageThread; if (extOpts.fFilesToExtract == ExtractOptionsDialog::kExtractSelection) { selSet.CreateFromSelection(fpContentList, threadMask); } else { selSet.CreateFromAll(fpContentList, threadMask); } //selSet.Dump(); if (selSet.GetNumEntries() == 0) { MessageBox(L"No files matched the selection criteria.", L"No match", MB_OK | MB_ICONEXCLAMATION); return; } /* * Set up the progress dialog then do the extraction. */ fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionExtract, this); DoBulkExtract(&selSet, &extOpts); fpActionProgress->Cleanup(this); fpActionProgress = nil; } void MainWindow::OnUpdateActionsExtract(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil); } /* * Handle a bulk extraction. * * IMPORTANT: since the pActionProgress dialog has the foreground, it's * vital that any MessageBox calls go through that. Otherwise the * progress dialog message handler won't get disabled by MessageBox and * we can end up permanently hiding the dialog. (Could also use * ::MessageBox or ::AfxMessageBox instead.) */ void MainWindow::DoBulkExtract(SelectionSet* pSelSet, const ExtractOptionsDialog* pExtOpts) { ReformatHolder* pHolder = nil; bool overwriteExisting, ovwrForAll; ASSERT(pSelSet != nil); ASSERT(fpActionProgress != nil); pSelSet->IterReset(); /* set up our "overwrite existing files" logic */ overwriteExisting = ovwrForAll = (pExtOpts->fOverwriteExisting != FALSE); while (true) { SelectionEntry* pSelEntry; bool result; /* make sure we service events */ PeekAndPump(); pSelEntry = pSelSet->IterNext(); if (pSelEntry == nil) { SuccessBeep(); break; // out of while (all done!) } GenericEntry* pEntry = pSelEntry->GetEntry(); if (pEntry->GetDamaged()) { WMSG1("Skipping '%ls' due to damage\n", pEntry->GetPathName()); continue; } /* * Extract all parts of the file -- including those we don't actually * intend to extract to disk -- and hold them in the ReformatHolder. * Some formats, e.g. GWP, need the resource fork to do formatting, * so it's important to have the resource fork available even if * we're just going to throw it away. * * The selection set should have screened out anything totally * inappropriate, e.g. files with nothing but a resource fork don't * make it into the set if we're not extracting resource forks. * * We only want to reformat files, not disk images, directories, * volume dirs, etc. We have a reformatter for ProDOS directories, * but (a) we don't explicitly extract subdirs, and (b) we'd really * like directories to be directories so we can extract files into * them. */ if (pExtOpts->ShouldTryReformat() && (pEntry->GetRecordKind() == GenericEntry::kRecordKindFile || pEntry->GetRecordKind() == GenericEntry::kRecordKindForkedFile)) { fpActionProgress->SetArcName(pEntry->GetDisplayName()); fpActionProgress->SetFileName(L"-"); SET_PROGRESS_BEGIN(); if (GetFileParts(pEntry, &pHolder) == 0) { /* * Use the prefs, but disable generic text conversion, so * that we default to "raw". That way we will use the text * conversion that the user has specified in the "extract" * dialog. * * We might want to just disable any "always"-level * reformatter, but that would require tweaking the reformat * code to return "raw" when nothing applies. */ ConfigureReformatFromPreferences(pHolder); pHolder->SetReformatAllowed(ReformatHolder::kReformatTextEOL_HA, false); pHolder->SetSourceAttributes( pEntry->GetFileType(), pEntry->GetAuxType(), ReformatterSourceFormat(pEntry->GetSourceFS()), pEntry->GetFileNameExtensionA()); pHolder->TestApplicability(); } } if (pExtOpts->fIncludeDataForks && pEntry->GetHasDataFork()) { result = ExtractEntry(pEntry, GenericEntry::kDataThread, pHolder, pExtOpts, &overwriteExisting, &ovwrForAll); if (!result) break; } if (pExtOpts->fIncludeRsrcForks && pEntry->GetHasRsrcFork()) { result = ExtractEntry(pEntry, GenericEntry::kRsrcThread, pHolder, pExtOpts, &overwriteExisting, &ovwrForAll); if (!result) break; } if (pExtOpts->fIncludeDiskImages && pEntry->GetHasDiskImage()) { result = ExtractEntry(pEntry, GenericEntry::kDiskImageThread, pHolder, pExtOpts, &overwriteExisting, &ovwrForAll); if (!result) break; } delete pHolder; pHolder = nil; } // if they cancelled, delete the "stray" delete pHolder; } /* * Extract a single entry. * * If "pHolder" is non-nil, it holds the data from the file, and can be * used for formatted or non-formatted output. If it's nil, we need to * extract the data ourselves. * * Returns "true" on success, "false" on failure. */ bool MainWindow::ExtractEntry(GenericEntry* pEntry, int thread, ReformatHolder* pHolder, const ExtractOptionsDialog* pExtOpts, bool* pOverwriteExisting, bool* pOvwrForAll) { /* * This first bit of setup is the same for every file. However, it's * pretty quick, and it's easier to pass "pExtOpts" in than all of * this stuff, so we just do it every time. */ GenericEntry::ConvertEOL convEOL; GenericEntry::ConvertHighASCII convHA; bool convTextByType = false; /* translate the EOL conversion mode into GenericEntry terms */ switch (pExtOpts->fConvEOL) { case ExtractOptionsDialog::kConvEOLNone: convEOL = GenericEntry::kConvertEOLOff; break; case ExtractOptionsDialog::kConvEOLType: convEOL = GenericEntry::kConvertEOLOff; convTextByType = true; break; case ExtractOptionsDialog::kConvEOLAuto: convEOL = GenericEntry::kConvertEOLAuto; break; case ExtractOptionsDialog::kConvEOLAll: convEOL = GenericEntry::kConvertEOLOn; break; default: ASSERT(false); convEOL = GenericEntry::kConvertEOLOff; break; } if (pExtOpts->fConvHighASCII) convHA = GenericEntry::kConvertHAAuto; else convHA = GenericEntry::kConvertHAOff; //WMSG2(" DBE initial text conversion: eol=%d ha=%d\n", // convEOL, convHA); ReformatHolder holder; CString outputPath; CString failed, errMsg; failed.LoadString(IDS_FAILED); bool writeFailed = false; bool extractAs2MG = false; char* reformatText = nil; MyDIBitmap* reformatDib = nil; ASSERT(pEntry != nil); /* * If we're interested in extracting disk images as 2MG files, * see if we want to handle this one that way. * * If they said "don't extract disk images", the images should * have been culled from the selection set earlier. */ if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDisk) { ASSERT(pExtOpts->fIncludeDiskImages); if (pExtOpts->fDiskTo2MG) { if (pEntry->GetUncompressedLen() > 0 && (pEntry->GetUncompressedLen() % TwoImgHeader::kBlockSize) == 0) { extractAs2MG = true; } else { WMSG2("Not extracting funky image '%ls' as 2MG (len=%ld)\n", pEntry->GetPathName(), pEntry->GetUncompressedLen()); } } } /* * Convert the archived pathname to something suitable for the * local machine (i.e. Win32). */ PathProposal pathProp; CString convName, convNameExtd, convNameExtdPlus; pathProp.Init(pEntry); pathProp.fThreadKind = thread; if (pExtOpts->fStripFolderNames) pathProp.fJunkPaths = true; pathProp.ArchiveToLocal(); convName = pathProp.fLocalPathName; /* run it through again, this time with optional type preservation */ if (pExtOpts->fAddTypePreservation) pathProp.fPreservation = true; pathProp.ArchiveToLocal(); convNameExtd = pathProp.fLocalPathName; /* and a 3rd time, also taking additional extensions into account */ if (pExtOpts->fAddExtension) pathProp.fAddExtension = true; pathProp.ArchiveToLocal(); convNameExtdPlus = pathProp.fLocalPathName; /* * Prepend the extraction dir to the local pathname. We also add * the sub-volume name (if any), which should already be a valid * Win32 directory name. We can't add it earlier because the fssep * char might be '\0'. */ ASSERT(pExtOpts->fExtractPath.Right(1) == "\\"); CString adjustedExtractPath(pExtOpts->fExtractPath); if (!pExtOpts->fStripFolderNames && pEntry->GetSubVolName() != nil) { adjustedExtractPath += pEntry->GetSubVolName(); adjustedExtractPath += "\\"; } outputPath = adjustedExtractPath; outputPath += convNameExtdPlus; ReformatOutput* pOutput = nil; /* * If requested, try to reformat this file. */ if (pHolder != nil) { ReformatHolder::ReformatPart part = ReformatHolder::kPartUnknown; ReformatHolder::ReformatID id; CString title; //int result; switch (thread) { case GenericEntry::kDataThread: part = ReformatHolder::kPartData; break; case GenericEntry::kRsrcThread: part = ReformatHolder::kPartRsrc; break; case GenericEntry::kDiskImageThread: part = ReformatHolder::kPartData; break; case GenericEntry::kCommentThread: default: assert(false); return false; } fpActionProgress->SetFileName(_T("(reformatting)")); id = pHolder->FindBest(part); { CWaitCursor waitc; pOutput = pHolder->Apply(part, id); } if (pOutput != nil) { /* use output pathname without preservation */ CString tmpPath; bool goodReformat = true; bool noChangePath = false; tmpPath = adjustedExtractPath; tmpPath += convName; CString lastFour = tmpPath.Right(4); /* * Tack on a file extension identifying the reformatted * contents. If the filename already has the correct * extension, don't tack it on again. */ switch (pOutput->GetOutputKind()) { case ReformatOutput::kOutputText: if (lastFour.CompareNoCase(L".txt") != 0) tmpPath += L".txt"; break; case ReformatOutput::kOutputRTF: if (lastFour.CompareNoCase(L".rtf") != 0) tmpPath += L".rtf"; break; case ReformatOutput::kOutputCSV: if (lastFour.CompareNoCase(L".csv") != 0) tmpPath += L".csv"; break; case ReformatOutput::kOutputBitmap: if (lastFour.CompareNoCase(L".bmp") != 0) tmpPath += L".bmp"; break; case ReformatOutput::kOutputRaw: noChangePath = true; break; default: // kOutputErrorMsg, kOutputUnknown goodReformat = false; break; } if (goodReformat) { if (!noChangePath) outputPath = tmpPath; } else { delete pOutput; pOutput = nil; } } } if (extractAs2MG) { /* * Reduce to base name and add 2IMG suffix. Would be nice to keep * the non-extended file type preservation stuff, but right now we * only expect unadorned sectors for that (and so does NuLib2). */ outputPath = adjustedExtractPath; outputPath += convName; outputPath += L".2mg"; } /* update the display in case we renamed it */ if (outputPath != fpActionProgress->GetFileName()) { WMSG2(" Renamed our output, from '%ls' to '%ls'\n", (LPCTSTR) fpActionProgress->GetFileName(), outputPath); fpActionProgress->SetFileName(outputPath); } /* * Update the progress meter output filename, and reset the thermometer. */ fpActionProgress->SetArcName(pathProp.fStoredPathName); fpActionProgress->SetFileName(outputPath); WMSG2("Extracting from '%ls' to '%ls'\n", pathProp.fStoredPathName, outputPath); SET_PROGRESS_BEGIN(); /* * Open the output file. * * Returns IDCANCEL on failures as well as user cancellation. */ FILE* fp = nil; int result; result = OpenOutputFile(&outputPath, pathProp, pEntry->GetModWhen(), pOverwriteExisting, pOvwrForAll, &fp); if (result == IDCANCEL) { // no messagebox for this one delete pOutput; return false; } /* update the display in case they renamed the file */ if (outputPath != fpActionProgress->GetFileName()) { WMSG2(" Detected rename, from '%ls' to '%ls'\n", (LPCWSTR) fpActionProgress->GetFileName(), outputPath); fpActionProgress->SetFileName(outputPath); } if (fp == nil) { /* looks like they elected to skip extraction of this file */ delete pOutput; return true; } //EventPause(500); // DEBUG DEBUG /* * Handle "extract as 2MG" by writing a 2MG header to the start * of the file before we hand off to the extraction function. * * NOTE: we're currently assuming that we're extracting an image * in ProDOS sector order. This is a valid assumption so long as * we're only pulling disk images out of ShrinkIt archives. * * We don't currently use the WriteFooter call here, because we're * not adding comments. */ if (extractAs2MG) { TwoImgHeader header; header.InitHeader(TwoImgHeader::kImageFormatProDOS, (long) pEntry->GetUncompressedLen(), (long) (pEntry->GetUncompressedLen() / TwoImgHeader::kBlockSize)); int err; ASSERT(ftell(fp) == 0); err = header.WriteHeader(fp); if (err != 0) { errMsg.Format(L"Unable to save 2MG file '%ls': %hs\n", outputPath, strerror(err)); fpActionProgress->MessageBox(errMsg, failed, MB_OK | MB_ICONERROR); goto open_file_fail; } ASSERT(ftell(fp) == 64); // size of 2MG header } /* * In some cases we want to override the automatic text detection. * * If we're in "auto" mode, force conversion on for DOS/RDOS text files. * This is important when "convHA" is off, because in high-ASCII mode * we might not recognize text files for what they are. We also * consider 0x00 to be binary, which screws up random-access text. * * We don't want to do text conversion on disk images or resource * forks, ever. Turn them off here. */ GenericEntry::ConvertEOL thisConv; thisConv = convEOL; if (thisConv == GenericEntry::kConvertEOLAuto) { if (DiskImg::UsesDOSFileStructure(pEntry->GetSourceFS()) && pEntry->GetFileType() == kFileTypeTXT) { WMSG0("Switching EOLAuto to EOLOn for DOS text file\n"); thisConv = GenericEntry::kConvertEOLOn; } } else if (convTextByType) { /* force it on or off when in conv-by-type mode */ if (pEntry->GetFileType() == kFileTypeTXT || pEntry->GetFileType() == kFileTypeSRC) { WMSG0("Enabling EOL conv for text file\n"); thisConv = GenericEntry::kConvertEOLOn; } else { ASSERT(thisConv == GenericEntry::kConvertEOLOff); } } if (thisConv != GenericEntry::kConvertEOLOff && (thread == GenericEntry::kRsrcThread || thread == GenericEntry::kDiskImageThread)) { WMSG0("Disabling EOL conv for resource fork or disk image\n"); thisConv = GenericEntry::kConvertEOLOff; } /* * Extract the contents to the file. * * In some cases, notably when the file size exceeds the limit of * the reformatter, we will be trying to reformat but won't have * loaded the original data. In such cases we fall through to the * normal extraction mode, because we threw out pOutput above when * the result was kOutputErrorMsg. * * (Could also be due to extraction failure, e.g. bad CRC.) */ if (pOutput != nil) { /* * We have the data in our buffer. Write it out. No need * to tweak the progress updater, which already shows 100%. * * There are four possibilities: * - Valid text/rtf/csv converted text. Write reformatted. * - Valid bitmap converted. Write bitmap. * - No reformatter found, type is "raw". Write raw. (Note * this may be zero bytes long for an empty file.) * - Error message encoded in result. Should not be here! */ if (pOutput->GetOutputKind() == ReformatOutput::kOutputText || pOutput->GetOutputKind() == ReformatOutput::kOutputRTF || pOutput->GetOutputKind() == ReformatOutput::kOutputCSV) { WMSG0(" Writing text, RTF, CSV, or raw\n"); ASSERT(pOutput->GetTextBuf() != nil); int err = 0; if (fwrite(pOutput->GetTextBuf(), pOutput->GetTextLen(), 1, fp) != 1) err = errno; if (err != 0) { errMsg.Format(L"Unable to save reformatted file '%ls': %hs\n", outputPath, strerror(err)); fpActionProgress->MessageBox(errMsg, failed, MB_OK | MB_ICONERROR); writeFailed = true; } else { SET_PROGRESS_UPDATE(100); } } else if (pOutput->GetOutputKind() == ReformatOutput::kOutputBitmap) { WMSG0(" Writing bitmap\n"); ASSERT(pOutput->GetDIB() != nil); int err = pOutput->GetDIB()->WriteToFile(fp); if (err != 0) { errMsg.Format(L"Unable to save bitmap '%ls': %hs\n", outputPath, strerror(err)); fpActionProgress->MessageBox(errMsg, failed, MB_OK | MB_ICONERROR); writeFailed = true; } else { SET_PROGRESS_UPDATE(100); } } else if (pOutput->GetOutputKind() == ReformatOutput::kOutputRaw) { /* * Send raw data through the text conversion configured in the * extract files dialog. Any file for which no dedicated * reformatter could be found ends up here. * * We could just send it through to the generic non-reformatter * case, but that would require reading the file twice. */ WMSG1(" Writing un-reformatted data (%ld bytes)\n", pOutput->GetTextLen()); ASSERT(pOutput->GetTextBuf() != nil); bool lastCR = false; GenericEntry::ConvertHighASCII thisConvHA = convHA; int err; err = GenericEntry::WriteConvert(fp, pOutput->GetTextBuf(), pOutput->GetTextLen(), &thisConv, &thisConvHA, &lastCR); if (err != 0) { errMsg.Format(L"Unable to write file '%ls': %hs\n", outputPath, strerror(err)); fpActionProgress->MessageBox(errMsg, failed, MB_OK | MB_ICONERROR); writeFailed = true; } else { SET_PROGRESS_UPDATE(100); } } else { /* something failed, and we don't have the file */ WMSG0("How'd we get here?\n"); ASSERT(false); } } else { /* * We don't have the data, probably because we aren't using file * reformatters. Use the GenericEntry extraction routine to copy * the data directly into the file. * * We also get here if the file has a length of zero. */ CString msg; int result; ASSERT(fpActionProgress != nil); WMSG3("Extracting '%ls', requesting thisConv=%d, convHA=%d\n", outputPath, thisConv, convHA); result = pEntry->ExtractThreadToFile(thread, fp, thisConv, convHA, &msg); if (result != IDOK) { if (result == IDCANCEL) { CString msg; msg.LoadString(IDS_OPERATION_CANCELLED); fpActionProgress->MessageBox(msg, L"CiderPress", MB_OK | MB_ICONEXCLAMATION); } else { WMSG2(" FAILED on '%ls': %ls\n", outputPath, msg); errMsg.Format(L"Unable to extract file '%ls': %ls\n", outputPath, msg); fpActionProgress->MessageBox(errMsg, failed, MB_OK | MB_ICONERROR); } writeFailed = true; } } open_file_fail: delete pOutput; fclose(fp); if (writeFailed) { // clean up ::DeleteFile(outputPath); return false; } /* * Fix the modification date. */ PathName datePath(outputPath); datePath.SetModWhen(pEntry->GetModWhen()); // datePath.SetAccess(pEntry->GetAccess()); return true; } /* * Open an output file. * * "outputPath" holds the name of the file to create. "origPath" is the * name as it was stored in the archive. "pOverwriteExisting" tells us * if we should just go ahead and overwrite the existing file, while * "pOvwrForAll" tells us if a "To All" button was hit previously. * * If the file exists, "*pOverwriteExisting" is false, and "*pOvwrForAll" * is false, then we will put up the "do you want to overwrite?" dialog. * One possible outcome of the dialog is renaming the output path. * * On success, "*pFp" will be non-nil, and IDOK will be returned. On * failure, IDCANCEL will be returned. The values in "*pOverwriteExisting" * and "*pOvwrForAll" may be updated, and "*pOutputPath" will change if * the user chose to rename the file. */ int MainWindow::OpenOutputFile(CString* pOutputPath, const PathProposal& pathProp, time_t arcFileModWhen, bool* pOverwriteExisting, bool* pOvwrForAll, FILE** pFp) { const int kUserCancel = -2; // must not conflict with errno values CString failed; CString msg; int err = 0; failed.LoadString(IDS_FAILED); *pFp = nil; did_rename: PathName path(*pOutputPath); if (path.Exists()) { if (*pOverwriteExisting) { do_overwrite: /* delete existing */ WMSG1(" Deleting existing '%ls'\n", (LPCWSTR) *pOutputPath); if (::_wunlink(*pOutputPath) != 0) { err = errno; WMSG2(" Failed deleting '%ls', err=%d\n", (LPCWSTR)*pOutputPath, err); if (err == ENOENT) { /* user might have removed it while dialog was up */ err = 0; } else { /* unable to delete, we'd better bail out */ goto bail; } } } else if (*pOvwrForAll) { /* never overwrite */ WMSG1(" Skipping '%ls'\n", (LPCWSTR) *pOutputPath); goto bail; } else { /* no firm policy, ask the user */ ConfirmOverwriteDialog confOvwr; PathName path(*pOutputPath); confOvwr.fExistingFile = *pOutputPath; confOvwr.fExistingFileModWhen = path.GetModWhen(); confOvwr.fNewFileSource = pathProp.fStoredPathName; confOvwr.fNewFileModWhen = arcFileModWhen; if (confOvwr.DoModal() == IDCANCEL) { err = kUserCancel; goto bail; } if (confOvwr.fResultRename) { *pOutputPath = confOvwr.fExistingFile; goto did_rename; } if (confOvwr.fResultApplyToAll) { *pOvwrForAll = confOvwr.fResultApplyToAll; *pOverwriteExisting = confOvwr.fResultOverwrite; } if (confOvwr.fResultOverwrite) goto do_overwrite; else goto bail; } } /* create the subdirectories, if necessary */ err = path.CreatePathIFN(); if (err != 0) goto bail; *pFp = _wfopen(*pOutputPath, L"wb"); if (*pFp == nil) err = errno ? errno : -1; /* fall through with error */ bail: /* if we failed, tell the user why */ if (err == ENOTDIR) { /* part of the output path exists, but isn't a directory */ msg.Format(L"Unable to create folders for '%ls': part of the path " L"already exists but is not a folder.\n", *pOutputPath); fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR); return IDCANCEL; } else if (err == EINVAL) { /* invalid argument; assume it's an invalid filename */ msg.Format(L"Unable to create file '%ls': invalid filename.\n", *pOutputPath); fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR); return IDCANCEL; } else if (err == kUserCancel) { /* user elected to cancel */ WMSG0("Cancelling due to user request\n"); return IDCANCEL; } else if (err != 0) { msg.Format(L"Unable to create file '%ls': %hs\n", *pOutputPath, strerror(err)); fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR); return IDCANCEL; } return IDOK; } /* * ========================================================================== * Test * ========================================================================== */ /* * Test files. */ void MainWindow::OnActionsTest(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); /* * Ask the user about various options. */ UseSelectionDialog selOpts(fpContentList->GetSelectedCount(), this); selOpts.Setup(IDS_TEST_TITLE, IDS_TEST_OK, IDS_TEST_SELECTED_COUNT, IDS_TEST_SELECTED_COUNTS_FMT, IDS_TEST_ALL_FILES); if (fpContentList->GetSelectedCount() > 0) selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; else selOpts.fFilesToAction = UseSelectionDialog::kActionAll; if (selOpts.DoModal() != IDOK) { WMSG0("Test cancelled\n"); return; } /* * Create a "selection set" of things to test. * * We don't currently test directories, because we don't currently * allow testing anything that has a directory (NuFX doesn't store * them explicitly). We could probably add them to the threadMask. */ SelectionSet selSet; int threadMask = GenericEntry::kAnyThread; if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { selSet.CreateFromSelection(fpContentList, threadMask); } else { selSet.CreateFromAll(fpContentList, threadMask); } //selSet.Dump(); if (selSet.GetNumEntries() == 0) { /* should be impossible */ MessageBox(L"No files matched the selection criteria.", L"No match", MB_OK|MB_ICONEXCLAMATION); return; } /* * Set up the progress window. */ bool result; fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionTest, this); result = fpOpenArchive->TestSelection(fpActionProgress, &selSet); fpActionProgress->Cleanup(this); fpActionProgress = nil; //if (result) // SuccessBeep(); } void MainWindow::OnUpdateActionsTest(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && fpContentList->GetItemCount() > 0 && fpOpenArchive->GetCapability(GenericArchive::kCapCanTest)); } /* * ========================================================================== * Delete * ========================================================================== */ /* * Delete archive entries. */ void MainWindow::OnActionsDelete(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); ASSERT(!fpOpenArchive->IsReadOnly()); /* * We handle deletions specially. If they have selected any * subdirectories, we recursively select the files in those subdirs * as well. We want to do it early so that the "#of files to delete" * display accurately reflects what we're about to do. */ fpContentList->SelectSubdirContents(); #if 0 /* * Ask the user about various options. */ UseSelectionDialog delOpts(fpContentList->GetSelectedCount(), this); delOpts.Setup(IDS_DEL_TITLE, IDS_DEL_OK, IDS_DEL_SELECTED_COUNT, IDS_DEL_SELECTED_COUNTS_FMT, IDS_DEL_ALL_FILES); if (fpContentList->GetSelectedCount() > 0) delOpts.fFilesToAction = UseSelectionDialog::kActionSelection; else delOpts.fFilesToAction = UseSelectionDialog::kActionAll; if (delOpts.DoModal() != IDOK) { WMSG0("Delete cancelled\n"); return; } #endif /* * Create a "selection set" of things to delete. * * We can't delete volume directories, so they're not included. */ SelectionSet selSet; int threadMask = GenericEntry::kAnyThread | GenericEntry::kAllowDirectory /*| GenericEntry::kAllowVolumeDir*/; #if 0 if (delOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { selSet.CreateFromSelection(fpContentList, threadMask); } else { CString appName; UINT response; appName.LoadString(IDS_MB_APP_NAME); response = MessageBox("Are you sure you want to delete everything?", appName, MB_OKCANCEL | MB_ICONEXCLAMATION); if (response == IDCANCEL) return; selSet.CreateFromAll(fpContentList, threadMask); } //selSet.Dump(); #endif selSet.CreateFromSelection(fpContentList, threadMask); if (selSet.GetNumEntries() == 0) { /* can happen if they selected volume dir only */ MessageBox(L"Nothing to delete.", L"No match", MB_OK | MB_ICONEXCLAMATION); return; } CString appName, msg; appName.LoadString(IDS_MB_APP_NAME); msg.Format(L"Delete %d file%ls?", selSet.GetNumEntries(), selSet.GetNumEntries() == 1 ? L"" : L"s"); if (MessageBox(msg, appName, MB_OKCANCEL | MB_ICONQUESTION) != IDOK) return; bool result; fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionDelete, this); //fpContentList->Invalidate(); // don't allow updates until done result = fpOpenArchive->DeleteSelection(fpActionProgress, &selSet); fpContentList->Reload(); fpActionProgress->Cleanup(this); fpActionProgress = nil; if (result) SuccessBeep(); } void MainWindow::OnUpdateActionsDelete(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && fpContentList->GetSelectedCount() > 0); } /* * ========================================================================== * Rename * ========================================================================== */ /* * Rename archive entries. Depending on the structure of the underlying * archive, we may only allow the user to alter the filename component. * Anything else would constitute moving the file around in the filesystem. */ void MainWindow::OnActionsRename(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); ASSERT(!fpOpenArchive->IsReadOnly()); /* * Create a "selection set" of entries to rename. We always go by * the selection, so there's no need to present a "all or some?" dialog. * * Renaming the volume dir is not done from here, so we don't include * it in the set. We could theoretically allow renaming of "damaged" * files, since most of the time the damage is in the file structure * not the directory, but the disk will be read-only anyway so there's * no point. */ SelectionSet selSet; int threadMask = GenericEntry::kAnyThread | GenericEntry::kAllowDirectory; selSet.CreateFromSelection(fpContentList, threadMask); //selSet.Dump(); if (selSet.GetNumEntries() == 0) { /* should be impossible */ MessageBox(L"No files matched the selection criteria.", L"No match", MB_OK | MB_ICONEXCLAMATION); return; } //fpContentList->Invalidate(); // this might be unnecessary fpOpenArchive->RenameSelection(this, &selSet); fpContentList->Reload(); // user interaction on each step, so skip the SuccessBeep } void MainWindow::OnUpdateActionsRename(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && fpContentList->GetSelectedCount() > 0); } /* * ========================================================================== * Edit Comment * ========================================================================== */ /* * Edit a comment, creating it if necessary. */ void MainWindow::OnActionsEditComment(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); ASSERT(!fpOpenArchive->IsReadOnly()); EditCommentDialog editDlg(this); CString oldComment; GenericEntry* pEntry = GetSelectedItem(fpContentList); if (pEntry == nil) { ASSERT(false); return; } if (!pEntry->GetHasComment()) { CString question, title; int result; question.LoadString(IDS_NO_COMMENT_ADD); title.LoadString(IDS_EDIT_COMMENT); result = MessageBox(question, title, MB_OKCANCEL | MB_ICONQUESTION); if (result == IDCANCEL) return; editDlg.fComment = ""; editDlg.fNewComment = true; } else { fpOpenArchive->GetComment(this, pEntry, &editDlg.fComment); } int result; result = editDlg.DoModal(); if (result == IDOK) { //fpContentList->Invalidate(); // probably unnecessary fpOpenArchive->SetComment(this, pEntry, editDlg.fComment); fpContentList->Reload(); } else if (result == EditCommentDialog::kDeleteCommentID) { //fpContentList->Invalidate(); // possibly unnecessary fpOpenArchive->DeleteComment(this, pEntry); fpContentList->Reload(); } } void MainWindow::OnUpdateActionsEditComment(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && fpContentList->GetSelectedCount() == 1 && fpOpenArchive->GetCapability(GenericArchive::kCapCanEditComment)); } /* * ========================================================================== * Edit Properties * ========================================================================== */ /* * Edit file properties. * * This causes a reload of the list, which isn't really necessary. We * do need to re-evaluate the sort order if one of the fields they modified * is the current sort key, but it would be nice if we could at least retain * the selection. Since we're not reloading the GenericArchive, we *can* * remember the selection. */ void MainWindow::OnActionsEditProps(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); EditPropsDialog propsDlg(this); CString oldComment; GenericEntry* pEntry = GetSelectedItem(fpContentList); if (pEntry == nil) { ASSERT(false); return; } propsDlg.InitProps(pEntry); if (fpOpenArchive->IsReadOnly()) propsDlg.fReadOnly = true; else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) propsDlg.fReadOnly = true; int result; result = propsDlg.DoModal(); if (result == IDOK && !propsDlg.fReadOnly) { (void) fpOpenArchive->SetProps(this, pEntry, &propsDlg.fProps); // only needed if underlying archive reloads fpContentList->Reload(true); } } void MainWindow::OnUpdateActionsEditProps(CCmdUI* pCmdUI) { // allow it in read-only mode, so we can view the props pCmdUI->Enable(fpContentList != nil && fpContentList->GetSelectedCount() == 1); } /* * ========================================================================== * Rename Volume * ========================================================================== */ /* * Change a volume name or volume number. */ void MainWindow::OnActionsRenameVolume(void) { RenameVolumeDialog rvDialog; ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); ASSERT(!fpOpenArchive->IsReadOnly()); /* only know how to deal with disk images */ if (fpOpenArchive->GetArchiveKind() != GenericArchive::kArchiveDiskImage) { ASSERT(false); return; } DiskImgLib::DiskFS* pDiskFS; pDiskFS = ((DiskArchive*) fpOpenArchive)->GetDiskFS(); ASSERT(pDiskFS != nil); rvDialog.fpArchive = (DiskArchive*) fpOpenArchive; if (rvDialog.DoModal() != IDOK) return; //WMSG1("Creating '%s'\n", rvDialog.fNewName); /* rename the chosen disk to the specified name */ bool result; result = fpOpenArchive->RenameVolume(this, rvDialog.fpChosenDiskFS, rvDialog.fNewName); if (!result) { WMSG0("RenameVolume FAILED\n"); /* keep going -- reload just in case something partially happened */ } /* * We need to do two things: reload the content list, because the * underlying DiskArchive got reloaded, and update the title bar. We * put the "volume ID" in the title, and we most likely just changed it. * * SetCPTitle invokes fpOpenArchive->GetDescription(), which pulls the * volume ID out of the primary DiskFS. */ fpContentList->Reload(); SetCPTitle(fOpenArchivePathName, fpOpenArchive); } void MainWindow::OnUpdateActionsRenameVolume(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && fpOpenArchive->GetCapability(GenericArchive::kCapCanRenameVolume)); } /* * ========================================================================== * Recompress * ========================================================================== */ /* * Recompress files. */ void MainWindow::OnActionsRecompress(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); /* * Ask the user about various options. */ RecompressOptionsDialog selOpts(fpContentList->GetSelectedCount(), this); selOpts.Setup(IDS_RECOMP_TITLE, IDS_RECOMP_OK, IDS_RECOMP_SELECTED_COUNT, IDS_RECOMP_SELECTED_COUNTS_FMT, IDS_RECOMP_ALL_FILES); if (fpContentList->GetSelectedCount() > 0) selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; else selOpts.fFilesToAction = UseSelectionDialog::kActionAll; selOpts.fCompressionType = fPreferences.GetPrefLong(kPrCompressionType); if (selOpts.DoModal() != IDOK) { WMSG0("Recompress cancelled\n"); return; } /* * Create a "selection set" of data forks, resource forks, and disk * images. If an entry has nothing but a comment, ignore it. */ SelectionSet selSet; int threadMask = GenericEntry::kDataThread | GenericEntry::kRsrcThread | GenericEntry::kDiskImageThread; if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { selSet.CreateFromSelection(fpContentList, threadMask); } else { selSet.CreateFromAll(fpContentList, threadMask); } //selSet.Dump(); if (selSet.GetNumEntries() == 0) { /* should be impossible */ MessageBox(L"No files matched the selection criteria.", L"No match", MB_OK|MB_ICONEXCLAMATION); return; } LONGLONG beforeUncomp, beforeComp; LONGLONG afterUncomp, afterComp; CalcTotalSize(&beforeUncomp, &beforeComp); /* * Set up the progress window. */ int result; fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionRecompress, this); //fpContentList->Invalidate(); // possibly unnecessary result = fpOpenArchive->RecompressSelection(fpActionProgress, &selSet, &selOpts); fpContentList->Reload(); fpActionProgress->Cleanup(this); fpActionProgress = nil; if (result) { CString msg, appName; CalcTotalSize(&afterUncomp, &afterComp); ASSERT(beforeUncomp == afterUncomp); appName.LoadString(IDS_MB_APP_NAME); msg.Format(L"Total uncompressed size of all files:\t%.1fK\r\n" L"Total size before recompress:\t\t%.1fK\r\n" L"Total size after recompress:\t\t%.1fK\r\n" L"Overall reduction:\t\t\t%.1fK", beforeUncomp / 1024.0, beforeComp / 1024.0, afterComp / 1024.0, (beforeComp - afterComp) / 1024.0); MessageBox(msg, appName, MB_OK|MB_ICONINFORMATION); } } void MainWindow::OnUpdateActionsRecompress(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && fpContentList->GetItemCount() > 0 && fpOpenArchive->GetCapability(GenericArchive::kCapCanRecompress)); } /* * Compute the total size of all files in the GenericArchive. */ void MainWindow::CalcTotalSize(LONGLONG* pUncomp, LONGLONG* pComp) const { GenericEntry* pEntry = fpOpenArchive->GetEntries(); LONGLONG uncomp = 0, comp = 0; while (pEntry != nil) { uncomp += pEntry->GetUncompressedLen(); comp += pEntry->GetCompressedLen(); pEntry = pEntry->GetNext(); } *pUncomp = uncomp; *pComp = comp; } /* * ========================================================================== * Convert to disk archive * ========================================================================== */ /* * Select files to convert. */ void MainWindow::OnActionsConvDisk(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); /* * Ask the user about various options. */ ConvDiskOptionsDialog selOpts(fpContentList->GetSelectedCount(), this); selOpts.Setup(IDS_CONVDISK_TITLE, IDS_CONVDISK_OK, IDS_CONVDISK_SELECTED_COUNT, IDS_CONVDISK_SELECTED_COUNTS_FMT, IDS_CONVDISK_ALL_FILES); if (fpContentList->GetSelectedCount() > 0) selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; else selOpts.fFilesToAction = UseSelectionDialog::kActionAll; //selOpts.fAllowLower = // fPreferences.GetPrefBool(kPrConvDiskAllowLower); //selOpts.fSparseAlloc = // fPreferences.GetPrefBool(kPrConvDiskAllocSparse); if (selOpts.DoModal() != IDOK) { WMSG0("ConvDisk cancelled\n"); return; } ASSERT(selOpts.fNumBlocks > 0); //fPreferences.SetPrefBool(kPrConvDiskAllowLower, // selOpts.fAllowLower != 0); //fPreferences.SetPrefBool(kPrConvDiskAllocSparse, // selOpts.fSparseAlloc != 0); /* * Create a "selection set" of data forks, resource forks, and * disk images. We don't want comment threads, but we can ignore * them later. */ SelectionSet selSet; int threadMask = GenericEntry::kAnyThread; if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { selSet.CreateFromSelection(fpContentList, threadMask); } else { selSet.CreateFromAll(fpContentList, threadMask); } //selSet.Dump(); if (selSet.GetNumEntries() == 0) { /* should be impossible */ MessageBox(L"No files matched the selection criteria.", L"No match", MB_OK|MB_ICONEXCLAMATION); return; } XferFileOptions xferOpts; //xferOpts.fAllowLowerCase = // fPreferences.GetPrefBool(kPrProDOSAllowLower) != 0; //xferOpts.fUseSparseBlocks = // fPreferences.GetPrefBool(kPrProDOSUseSparse) != 0; WMSG1("New volume name will be '%ls'\n", (LPCWSTR) selOpts.fVolName); /* * Create a new disk image. */ CString filename, saveFolder, errStr; CFileDialog dlg(FALSE, L"po", NULL, OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, L"Disk Images (*.po)|*.po||", this); dlg.m_ofn.lpstrTitle = L"New Disk Image (.PO)"; dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); if (dlg.DoModal() != IDOK) { WMSG0(" User cancelled xfer from image create dialog\n"); return; } saveFolder = dlg.m_ofn.lpstrFile; saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); filename = dlg.GetPathName(); WMSG1(" Will xfer to file '%ls'\n", filename); /* remove file if it already exists */ CString errMsg; errMsg = RemoveFile(filename); if (!errMsg.IsEmpty()) { ShowFailureMsg(this, errMsg, IDS_FAILED); return; } DiskArchive::NewOptions options; memset(&options, 0, sizeof(options)); options.base.format = DiskImg::kFormatProDOS; options.base.sectorOrder = DiskImg::kSectorOrderProDOS; options.prodos.volName = selOpts.fVolName; options.prodos.numBlocks = selOpts.fNumBlocks; xferOpts.fTarget = new DiskArchive; errStr = xferOpts.fTarget->New(filename, &options); if (!errStr.IsEmpty()) { ShowFailureMsg(this, errStr, IDS_FAILED); delete xferOpts.fTarget; return; } /* * Set up the progress window. */ GenericArchive::XferStatus result; fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionConvDisk, this); result = fpOpenArchive->XferSelection(fpActionProgress, &selSet, fpActionProgress, &xferOpts); fpActionProgress->Cleanup(this); fpActionProgress = nil; if (result == GenericArchive::kXferOK) SuccessBeep(); /* clean up */ delete xferOpts.fTarget; } void MainWindow::OnUpdateActionsConvDisk(CCmdUI* pCmdUI) { /* right now, only NufxArchive has the Xfer stuff implemented */ pCmdUI->Enable(fpContentList != nil && fpContentList->GetItemCount() > 0 && fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveNuFX); } /* * ========================================================================== * Convert to file archive * ========================================================================== */ /* * Select files to convert. */ void MainWindow::OnActionsConvFile(void) { ASSERT(fpContentList != nil); ASSERT(fpOpenArchive != nil); /* * Ask the user about various options. */ ConvFileOptionsDialog selOpts(fpContentList->GetSelectedCount(), this); selOpts.Setup(IDS_CONVFILE_TITLE, IDS_CONVFILE_OK, IDS_CONVFILE_SELECTED_COUNT, IDS_CONVFILE_SELECTED_COUNTS_FMT, IDS_CONVFILE_ALL_FILES); if (fpContentList->GetSelectedCount() > 0) selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; else selOpts.fFilesToAction = UseSelectionDialog::kActionAll; //selOpts.fConvDOSText = // fPreferences.GetPrefBool(kPrConvFileConvDOSText); //selOpts.fConvPascalText = // fPreferences.GetPrefBool(kPrConvFileConvPascalText); selOpts.fPreserveEmptyFolders = fPreferences.GetPrefBool(kPrConvFileEmptyFolders); if (selOpts.DoModal() != IDOK) { WMSG0("ConvFile cancelled\n"); return; } //fPreferences.SetPrefBool(kPrConvFileConvDOSText, // selOpts.fConvDOSText != 0); //fPreferences.SetPrefBool(kPrConvFileConvPascalText, // selOpts.fConvPascalText != 0); fPreferences.SetPrefBool(kPrConvFileEmptyFolders, selOpts.fPreserveEmptyFolders != 0); /* * Create a "selection set" of data forks, resource forks, and * directories. There are no comments or disk images on a disk image, * so we just request "any" thread. * * We only need to explicitly include directories if "preserve * empty folders" is set. */ SelectionSet selSet; int threadMask = GenericEntry::kAnyThread; if (selOpts.fPreserveEmptyFolders) threadMask |= GenericEntry::kAllowDirectory; if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { selSet.CreateFromSelection(fpContentList, threadMask); } else { selSet.CreateFromAll(fpContentList, threadMask); } //selSet.Dump(); if (selSet.GetNumEntries() == 0) { MessageBox(L"No files matched the selection criteria.", L"No match", MB_OK|MB_ICONEXCLAMATION); return; } XferFileOptions xferOpts; //xferOpts.fConvDOSText = (selOpts.fConvDOSText != 0); //xferOpts.fConvPascalText = (selOpts.fConvPascalText != 0); xferOpts.fPreserveEmptyFolders = (selOpts.fPreserveEmptyFolders != 0); /* * Create a new NuFX archive. */ CString filename, saveFolder, errStr; CFileDialog dlg(FALSE, L"shk", NULL, OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, L"ShrinkIt Archives (*.shk)|*.shk||", this); dlg.m_ofn.lpstrTitle = L"New Archive"; dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); if (dlg.DoModal() != IDOK) { WMSG0(" User cancelled xfer from archive create dialog\n"); return; } saveFolder = dlg.m_ofn.lpstrFile; saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); filename = dlg.GetPathName(); WMSG1(" Will xfer to file '%ls'\n", filename); /* remove file if it already exists */ CString errMsg; errMsg = RemoveFile(filename); if (!errMsg.IsEmpty()) { ShowFailureMsg(this, errMsg, IDS_FAILED); return; } xferOpts.fTarget = new NufxArchive; errStr = xferOpts.fTarget->New(filename, nil); if (!errStr.IsEmpty()) { ShowFailureMsg(this, errStr, IDS_FAILED); delete xferOpts.fTarget; return; } /* * Set up the progress window. */ GenericArchive::XferStatus result; fpActionProgress = new ActionProgressDialog; fpActionProgress->Create(ActionProgressDialog::kActionConvFile, this); result = fpOpenArchive->XferSelection(fpActionProgress, &selSet, fpActionProgress, &xferOpts); fpActionProgress->Cleanup(this); fpActionProgress = nil; if (result == GenericArchive::kXferOK) SuccessBeep(); /* clean up */ delete xferOpts.fTarget; } void MainWindow::OnUpdateActionsConvFile(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && fpContentList->GetItemCount() > 0 && fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage); } /* * ========================================================================== * Cassette WAV conversions * ========================================================================== */ /* * Convert BAS, INT, or BIN to a cassette-audio WAV file. */ void MainWindow::OnActionsConvToWav(void) { // do this someday WMSG0("Convert TO wav\n"); } void MainWindow::OnUpdateActionsConvToWav(CCmdUI* pCmdUI) { BOOL enable = false; if (fpContentList != nil && fpContentList->GetSelectedCount() == 1) { /* only BAS, INT, and BIN shorter than 64K */ GenericEntry* pEntry = GetSelectedItem(fpContentList); if ((pEntry->GetFileType() == kFileTypeBAS || pEntry->GetFileType() == kFileTypeINT || pEntry->GetFileType() == kFileTypeBIN) && pEntry->GetDataForkLen() < 65536 && pEntry->GetRecordKind() == GenericEntry::kRecordKindFile) { enable = true; } } pCmdUI->Enable(enable); } /* * Convert a WAV file with a digitized Apple II cassette tape into an * Apple II file, and add it to the current disk. */ void MainWindow::OnActionsConvFromWav(void) { CassetteDialog dlg; CString fileName, saveFolder; CFileDialog fileDlg(TRUE, L"wav", NULL, OFN_FILEMUSTEXIST|OFN_HIDEREADONLY, L"Sound Files (*.wav)|*.wav||", this); fileDlg.m_ofn.lpstrTitle = L"Open Sound File"; fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenWAVFolder); if (fileDlg.DoModal() != IDOK) goto bail; saveFolder = fileDlg.m_ofn.lpstrFile; saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset); fPreferences.SetPrefString(kPrOpenWAVFolder, saveFolder); fileName = fileDlg.GetPathName(); WMSG1("Opening WAV file '%ls'\n", fileName); dlg.fFileName = fileName; // pass in fpOpenArchive? dlg.DoModal(); if (dlg.IsDirty()) { assert(fpContentList != nil); fpContentList->Reload(); } bail: return; } void MainWindow::OnUpdateActionsConvFromWav(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()); } /* * Utility function used by cassette import. * * May modify contents of "pDetails". * * On failure, returns with an error message in "errMsg". */ /*static*/ bool MainWindow::SaveToArchive(GenericArchive::FileDetails* pDetails, const unsigned char* dataBufIn, long dataLen, const unsigned char* rsrcBufIn, long rsrcLen, CString& errMsg, CWnd* pDialog) { MainWindow* pMain = GET_MAIN_WINDOW(); GenericArchive* pArchive = pMain->GetOpenArchive(); DiskImgLib::A2File* pTargetSubdir = nil; XferFileOptions xferOpts; CString storagePrefix; unsigned char* dataBuf = nil; unsigned char* rsrcBuf = nil; ASSERT(pArchive != nil); ASSERT(errMsg.IsEmpty()); /* * Make a copy of the data for XferFile. */ if (dataLen >= 0) { if (dataLen == 0) dataBuf = new unsigned char[1]; else dataBuf = new unsigned char[dataLen]; if (dataBuf == nil) { errMsg.Format(L"Unable to allocate %ld bytes", dataLen); goto bail; } memcpy(dataBuf, dataBufIn, dataLen); } if (rsrcLen >= 0) { assert(false); } /* * Figure out where we want to put the files. For a disk archive * this can be complicated. * * The target DiskFS (which could be a sub-volume) gets tucked into * the xferOpts. */ if (pArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) { if (!pMain->ChooseAddTarget(&pTargetSubdir, &xferOpts.fpTargetFS)) goto bail; } else if (pArchive->GetArchiveKind() == GenericArchive::kArchiveNuFX) { // Always use ':' separator for SHK; this is a matter of // convenience, so they can specify a full path. //details.storageName.Replace(':', '_'); pDetails->fileSysInfo = ':'; } if (pTargetSubdir != nil) { storagePrefix = pTargetSubdir->GetPathName(); WMSG1("--- using storagePrefix '%ls'\n", storagePrefix); } if (!storagePrefix.IsEmpty()) { CString tmpStr, tmpFileName; tmpFileName = pDetails->storageName; tmpFileName.Replace(':', '_'); // strip any ':'s in the name pDetails->fileSysInfo = ':'; tmpStr = storagePrefix; tmpStr += ':'; tmpStr += tmpFileName; pDetails->storageName = tmpStr; } /* * Handle the transfer. * * On success, XferFile will null out our dataBuf and rsrcBuf pointers. */ pArchive->XferPrepare(&xferOpts); errMsg = pArchive->XferFile(pDetails, &dataBuf, dataLen, &rsrcBuf, rsrcLen); delete[] dataBuf; delete[] rsrcBuf; if (errMsg.IsEmpty()) pArchive->XferFinish(pDialog); else pArchive->XferAbort(pDialog); bail: return (errMsg.IsEmpty() != 0); } /* * ========================================================================== * Import BASIC programs from a text file * ========================================================================== */ /* * Import an Applesoft BASIC program from a text file. * * We currently allow the user to select a single file for import. Someday * we may want to allow multi-file import. */ void MainWindow::OnActionsImportBAS(void) { ImportBASDialog dlg; CString fileName, saveFolder; CFileDialog fileDlg(TRUE, L"txt", NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, L"Text files (*.txt)|*.txt||", this); fileDlg.m_ofn.lpstrTitle = L"Open Text File"; fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder); if (fileDlg.DoModal() != IDOK) goto bail; saveFolder = fileDlg.m_ofn.lpstrFile; saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset); fPreferences.SetPrefString(kPrAddFileFolder, saveFolder); fileName = fileDlg.GetPathName(); WMSG1("Opening TXT file '%ls'\n", fileName); dlg.fFileName = fileName; // pass in fpOpenArchive? dlg.DoModal(); if (dlg.IsDirty()) { assert(fpContentList != nil); fpContentList->Reload(); } bail: return; } void MainWindow::OnUpdateActionsImportBAS(CCmdUI* pCmdUI) { pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()); } /* * ========================================================================== * Multiple file handling * ========================================================================== */ /* * Extract every part of the file into "ReformatHolder". Does not try to * reformat anything, just extracts the parts. * * Returns IDOK on success, IDCANCEL if the user cancelled, or -1 on error. * On error, the reformatted text buffer gets the error message. */ int MainWindow::GetFileParts(const GenericEntry* pEntry, ReformatHolder** ppHolder) const { ReformatHolder* pHolder = new ReformatHolder; CString errMsg; if (pHolder == nil) return -1; if (pEntry->GetHasDataFork()) GetFilePart(pEntry, GenericEntry::kDataThread, pHolder); if (pEntry->GetHasRsrcFork()) GetFilePart(pEntry, GenericEntry::kRsrcThread, pHolder); if (pEntry->GetHasComment()) GetFilePart(pEntry, GenericEntry::kCommentThread, pHolder); if (pEntry->GetHasDiskImage()) GetFilePart(pEntry, GenericEntry::kDiskImageThread, pHolder); *ppHolder = pHolder; return 0; } /* * Load the requested part. */ void MainWindow::GetFilePart(const GenericEntry* pEntry, int whichThread, ReformatHolder* pHolder) const { CString errMsg; ReformatHolder::ReformatPart part; char* buf = nil; long len = 0; di_off_t threadLen; int result; switch (whichThread) { case GenericEntry::kDataThread: part = ReformatHolder::kPartData; threadLen = pEntry->GetDataForkLen(); break; case GenericEntry::kRsrcThread: part = ReformatHolder::kPartRsrc; threadLen = pEntry->GetRsrcForkLen(); break; case GenericEntry::kCommentThread: part = ReformatHolder::kPartCmmt; threadLen = -1; // no comment len getter; assume it's small break; case GenericEntry::kDiskImageThread: part = ReformatHolder::kPartData; // put disks into data thread threadLen = pEntry->GetDataForkLen(); break; default: ASSERT(false); return; } if (threadLen > fPreferences.GetPrefLong(kPrMaxViewFileSize)) { errMsg.Format( L"[File size (%I64d KBytes) exceeds file viewer maximum (%ld KBytes).]\n", ((LONGLONG) threadLen + 1023) / 1024, (fPreferences.GetPrefLong(kPrMaxViewFileSize) + 1023) / 1024); pHolder->SetErrorMsg(part, errMsg); goto bail; } result = pEntry->ExtractThreadToBuffer(whichThread, &buf, &len, &errMsg); if (result == IDOK) { /* on success, ETTB guarantees a buffer, even for zero-len file */ ASSERT(buf != nil); pHolder->SetSourceBuf(part, (unsigned char*) buf, len); } else if (result == IDCANCEL) { /* not expected */ errMsg = L"Cancelled!"; pHolder->SetErrorMsg(part, errMsg); ASSERT(buf == nil); } else { /* transfer error message to ReformatHolder buffer */ WMSG1("Got error message from ExtractThread: '%ls'\n", (LPCWSTR) errMsg); pHolder->SetErrorMsg(part, errMsg); ASSERT(buf == nil); } bail: return; }