ciderpress/app/Actions.cpp
Andy McFadden da182e1c61 Change file extraction extension handling
When extracting files, you can ask CiderPress to add a file extension
by checking the "add file extension" box.  Whether or not this box
is checked, files that undergo a format conversion (e.g. AWP to RTF,
or SHR to BMP) have a file extension added to identify the file's
new format.

This turned out to be confusing and inconvenient at times, notably
when working with Merlin source files.  See issues #10 and #26 on
github for details.

CiderPress no longer adds file extensions to format-converted files
unless the "add file extension" box is checked.  (The "Configure for
easy access in Windows" button checks this box for you, so the
default behavior is still "safe".)

Also, fix a minor visual glitch in the extract dialog.

Also, update to version 4.0.3-a3.
2017-12-01 13:48:50 -08:00

2378 lines
80 KiB
C++

/*
* 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 "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 "../diskimg/TwoImg.h"
#include <errno.h>
/*
* ==========================================================================
* View
* ==========================================================================
*/
void MainWindow::OnActionsView(void)
{
HandleView();
}
void MainWindow::OnUpdateActionsView(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL &&
fpContentList->GetSelectedCount() > 0);
}
void MainWindow::HandleView(void)
{
ASSERT(fpContentList != NULL);
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;
}
ViewFilesDialog vfd(this);
vfd.SetSelectionSet(&selSet);
vfd.SetTextTypeFace(fPreferences.GetPrefString(kPrViewTextTypeFace));
vfd.SetTextPointSize(fPreferences.GetPrefLong(kPrViewTextPointSize));
vfd.SetNoWrapText(fPreferences.GetPrefBool(kPrNoWrapText));
vfd.DoModal();
// remember which font they used (sticky pref, not in registry)
fPreferences.SetPrefString(kPrViewTextTypeFace, vfd.GetTextTypeFace());
fPreferences.SetPrefLong(kPrViewTextPointSize, vfd.GetTextPointSize());
LOGI("Preferences: saving view font %d-point '%ls'",
fPreferences.GetPrefLong(kPrViewTextPointSize),
fPreferences.GetPrefString(kPrViewTextTypeFace));
}
/*
* ==========================================================================
* Open as disk image
* ==========================================================================
*/
void MainWindow::OnActionsOpenAsDisk(void)
{
ASSERT(fpContentList != NULL);
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 != NULL && fpContentList->GetSelectedCount() == 1) {
GenericEntry* pEntry = GetSelectedItem(fpContentList);
if (pEntry != NULL) {
if ((pEntry->GetHasDataFork() || pEntry->GetHasDiskImage()) &&
pEntry->GetUncompressedLen() > kMinLen)
{
allow = true;
}
}
}
pCmdUI->Enable(allow);
}
/*
* ==========================================================================
* Add Files
* ==========================================================================
*/
void MainWindow::OnActionsAddFiles(void)
{
LOGI("Add files!");
AddFilesDialog addFiles(this);
DiskImgLib::A2File* pTargetSubdir = NULL;
/*
* 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 != NULL) {
DiskImg::FSFormat format;
format = addFiles.fpTargetDiskFS->GetDiskImg()->GetFSFormat();
if (pTargetSubdir != NULL) {
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);
LRESULT addResult = addFiles.DoModal();
if (addResult == 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.GetDirectory();
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 = NULL;
if (result)
SuccessBeep();
} else {
LOGI("SFD bailed with Cancel");
}
}
void MainWindow::OnUpdateActionsAddFiles(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL && !fpOpenArchive->IsReadOnly());
}
bool MainWindow::ChooseAddTarget(DiskImgLib::A2File** ppTargetSubdir,
DiskImgLib::DiskFS** ppTargetDiskFS)
{
ASSERT(ppTargetSubdir != NULL);
ASSERT(ppTargetDiskFS != NULL);
*ppTargetSubdir = NULL;
*ppTargetDiskFS = NULL;
GenericEntry* pEntry = GetSelectedItem(fpContentList);
if (pEntry != NULL &&
(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;
LOGD("Trying ChooseAddTarget");
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 == NULL ||
(*ppTargetSubdir)->GetDiskFS() == *ppTargetDiskFS);
}
if (*ppTargetSubdir != NULL && (*ppTargetSubdir)->IsVolumeDirectory())
*ppTargetSubdir = NULL;
return true;
}
/*
* ==========================================================================
* Add Disks
* ==========================================================================
*/
void MainWindow::OnActionsAddDisks(void)
{
DIError dierr;
DiskImg img;
CString failed, errMsg;
CString openFilters, saveFolder;
CStringA pathNameA;
AddFilesDialog addOpts;
LOGI("Add disks!");
CheckedLoadString(&failed, 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 */
pathNameA = dlg.GetPathName();
dierr = img.OpenImage(pathNameA, 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.",
(LPCWSTR) 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);
LOGI(" On entry, sectord=%d format=%d",
imf.fSectorOrder, imf.fFSFormat);
if (imf.fFSFormat == DiskImg::kFormatUnknown)
imf.fFSFormat = DiskImg::kFormatGenericProDOSOrd;
if (imf.DoModal() != IDOK) {
LOGI("User bailed on IMF dialog");
goto bail;
}
LOGI(" On exit, sectord=%d format=%d",
imf.fSectorOrder, imf.fFSFormat);
if (imf.fSectorOrder != img.GetSectorOrder() ||
imf.fFSFormat != img.GetFSFormat())
{
LOGI("Initial values overridden, forcing img format");
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.
*/
ASSERT(dlg.m_ofn.nFileOffset > 0);
{
CString directory(dlg.m_ofn.lpstrFile, dlg.m_ofn.nFileOffset - 1);
CString file(dlg.m_ofn.lpstrFile + dlg.m_ofn.nFileOffset);
LOGD("Stuffing '%ls' '%ls'", (LPCWSTR) directory, (LPCWSTR) file);
addOpts.StuffSingleFilename(directory, file);
}
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 = NULL;
if (result)
SuccessBeep();
bail:
return;
}
void MainWindow::OnUpdateActionsAddDisks(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL && !fpOpenArchive->IsReadOnly() &&
fpOpenArchive->GetCapability(GenericArchive::kCapCanAddDisk));
}
/*
* ==========================================================================
* Create Subdirectory
* ==========================================================================
*/
void MainWindow::OnActionsCreateSubdir(void)
{
CreateSubdirDialog csDialog;
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
ASSERT(!fpOpenArchive->IsReadOnly());
GenericEntry* pEntry = GetSelectedItem(fpContentList);
if (pEntry == NULL) {
// 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;
}
LOGI("Creating subdir in '%ls'", (LPCWSTR) pEntry->GetPathNameUNI());
csDialog.fBasePath = pEntry->GetPathNameUNI();
csDialog.fpArchive = fpOpenArchive;
csDialog.fpParentEntry = pEntry;
csDialog.fNewName = "New.Subdir";
if (csDialog.DoModal() != IDOK)
return;
LOGI("Creating '%ls'", (LPCWSTR) csDialog.fNewName);
fpOpenArchive->CreateSubdir(this, pEntry, csDialog.fNewName);
fpContentList->Reload();
}
void MainWindow::OnUpdateActionsCreateSubdir(CCmdUI* pCmdUI)
{
bool enable = fpContentList != NULL && !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 == NULL) {
ASSERT(false);
return;
}
if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory &&
pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir)
{
enable = false;
}
}
pCmdUI->Enable(enable);
}
/*
* ==========================================================================
* Extract
* ==========================================================================
*/
void MainWindow::OnActionsExtract(void)
{
ASSERT(fpContentList != NULL);
/*
* 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);
LOGI("Requested extract path is '%ls'", (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 = NULL;
}
void MainWindow::OnUpdateActionsExtract(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL);
}
void MainWindow::DoBulkExtract(SelectionSet* pSelSet,
const ExtractOptionsDialog* pExtOpts)
{
/*
* 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.)
*/
ReformatHolder* pHolder = NULL;
bool overwriteExisting, ovwrForAll;
ASSERT(pSelSet != NULL);
ASSERT(fpActionProgress != NULL);
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 == NULL) {
SuccessBeep();
break; // out of while (all done!)
}
GenericEntry* pEntry = pSelEntry->GetEntry();
if (pEntry->GetDamaged()) {
LOGI("Skipping '%ls' due to damage", (LPCWSTR) pEntry->GetPathNameUNI());
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->GetFileNameExtensionMOR());
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 = NULL;
}
// if they cancelled, delete the "stray"
delete pHolder;
}
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;
//LOGI(" DBE initial text conversion: eol=%d ha=%d",
// convEOL, convHA);
ReformatHolder holder;
CString outputPath;
CString failed, errMsg;
CheckedLoadString(&failed, IDS_FAILED);
bool writeFailed = false;
bool extractAs2MG = false;
char* reformatText = NULL;
MyDIBitmap* reformatDib = NULL;
ASSERT(pEntry != NULL);
/*
* 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 {
LOGI("Not extracting funky image '%ls' as 2MG (len=%I64d)",
(LPCWSTR) pEntry->GetPathNameUNI(),
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().IsEmpty()) {
adjustedExtractPath += pEntry->GetSubVolName();
adjustedExtractPath += "\\";
}
outputPath = adjustedExtractPath;
outputPath += convNameExtdPlus;
ReformatOutput* pOutput = NULL;
/*
* If requested, try to reformat this file.
*/
if (pHolder != NULL) {
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 != NULL) {
/* 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;
}
/*
* Issue #26: if "add type extension" is not checked, don't add
* the format converter extension. This is useful for Merlin
* source files where you want to do the text conversion but still
* want the file to end with ".S".
*/
if (!pExtOpts->fAddExtension) {
noChangePath = true;
}
if (goodReformat) {
if (!noChangePath)
outputPath = tmpPath;
} else {
delete pOutput;
pOutput = NULL;
}
}
}
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()) {
LOGI(" Renamed our output, from '%ls' to '%ls'",
(LPCWSTR) fpActionProgress->GetFileName(), (LPCWSTR) outputPath);
fpActionProgress->SetFileName(outputPath);
}
/*
* Update the progress meter output filename, and reset the thermometer.
*/
fpActionProgress->SetArcName(pathProp.fStoredPathName);
fpActionProgress->SetFileName(outputPath);
LOGI("Extracting from '%ls' to '%ls'",
(LPCWSTR) pathProp.fStoredPathName, (LPCWSTR) outputPath);
SET_PROGRESS_BEGIN();
/*
* Open the output file.
*
* Returns IDCANCEL on failures as well as user cancellation.
*/
FILE* fp = NULL;
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()) {
LOGI(" Detected rename, from '%ls' to '%ls'",
(LPCWSTR) fpActionProgress->GetFileName(), (LPCWSTR) outputPath);
fpActionProgress->SetFileName(outputPath);
}
if (fp == NULL) {
/* 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",
(LPCWSTR) 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)
{
LOGI("Switching EOLAuto to EOLOn for DOS text file");
thisConv = GenericEntry::kConvertEOLOn;
}
} else if (convTextByType) {
/* force it on or off when in conv-by-type mode */
if (pEntry->GetFileType() == kFileTypeTXT ||
pEntry->GetFileType() == kFileTypeSRC)
{
LOGI("Enabling EOL conv for text file");
thisConv = GenericEntry::kConvertEOLOn;
} else {
ASSERT(thisConv == GenericEntry::kConvertEOLOff);
}
}
if (thisConv != GenericEntry::kConvertEOLOff &&
(thread == GenericEntry::kRsrcThread ||
thread == GenericEntry::kDiskImageThread))
{
LOGI("Disabling EOL conv for resource fork or disk image");
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 != NULL) {
/*
* 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)
{
LOGI(" Writing text, RTF, CSV, or raw");
ASSERT(pOutput->GetTextBuf() != NULL);
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",
(LPCWSTR) outputPath, strerror(err));
fpActionProgress->MessageBox(errMsg, failed,
MB_OK | MB_ICONERROR);
writeFailed = true;
} else {
SET_PROGRESS_UPDATE(100);
}
} else if (pOutput->GetOutputKind() == ReformatOutput::kOutputBitmap) {
LOGI(" Writing bitmap");
ASSERT(pOutput->GetDIB() != NULL);
int err = pOutput->GetDIB()->WriteToFile(fp);
if (err != 0) {
errMsg.Format(L"Unable to save bitmap '%ls': %hs\n",
(LPCWSTR) 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.
*/
LOGI(" Writing un-reformatted data (%ld bytes)",
pOutput->GetTextLen());
ASSERT(pOutput->GetTextBuf() != NULL);
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",
(LPCWSTR) 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 */
LOGI("How'd we get here?");
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 != NULL);
LOGI("Extracting '%ls', requesting thisConv=%d, convHA=%d",
(LPCWSTR) outputPath, thisConv, convHA);
result = pEntry->ExtractThreadToFile(thread, fp,
thisConv, convHA, &msg);
if (result != IDOK) {
if (result == IDCANCEL) {
CString msg;
CheckedLoadString(&msg, IDS_OPERATION_CANCELLED);
fpActionProgress->MessageBox(msg,
L"CiderPress", MB_OK | MB_ICONEXCLAMATION);
} else {
LOGI(" FAILED on '%ls': %ls",
(LPCWSTR) outputPath, (LPCWSTR) msg);
errMsg.Format(L"Unable to extract file '%ls': %ls\n",
(LPCWSTR) outputPath, (LPCWSTR) 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;
}
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;
CheckedLoadString(&failed, IDS_FAILED);
*pFp = NULL;
did_rename:
PathName path(*pOutputPath);
if (path.Exists()) {
if (*pOverwriteExisting) {
do_overwrite:
/* delete existing */
LOGI(" Deleting existing '%ls'", (LPCWSTR) *pOutputPath);
if (::_wunlink(*pOutputPath) != 0) {
err = errno;
LOGI(" Failed deleting '%ls', err=%d",
(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 */
LOGI(" Skipping '%ls'", (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 == NULL)
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",
(LPCWSTR) *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",
(LPCWSTR) *pOutputPath);
fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR);
return IDCANCEL;
} else if (err == kUserCancel) {
/* user elected to cancel */
LOGI("Cancelling due to user request");
return IDCANCEL;
} else if (err != 0) {
msg.Format(L"Unable to create file '%ls': %hs\n",
(LPCWSTR) *pOutputPath, strerror(err));
fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR);
return IDCANCEL;
}
return IDOK;
}
/*
* ==========================================================================
* Test
* ==========================================================================
*/
void MainWindow::OnActionsTest(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
/*
* 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) {
LOGI("Test cancelled");
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 = NULL;
//if (result)
// SuccessBeep();
}
void
MainWindow::OnUpdateActionsTest(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL && fpContentList->GetItemCount() > 0
&& fpOpenArchive->GetCapability(GenericArchive::kCapCanTest));
}
/*
* ==========================================================================
* Delete
* ==========================================================================
*/
void MainWindow::OnActionsDelete(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
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) {
LOGI("Delete cancelled");
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;
CheckedLoadString(&appName, 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 = NULL;
if (result)
SuccessBeep();
}
void MainWindow::OnUpdateActionsDelete(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL && !fpOpenArchive->IsReadOnly()
&& fpContentList->GetSelectedCount() > 0);
}
/*
* ==========================================================================
* Rename
* ==========================================================================
*/
void MainWindow::OnActionsRename(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
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 != NULL && !fpOpenArchive->IsReadOnly()
&& fpContentList->GetSelectedCount() > 0);
}
/*
* ==========================================================================
* Edit Comment
* ==========================================================================
*/
void MainWindow::OnActionsEditComment(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
ASSERT(!fpOpenArchive->IsReadOnly());
EditCommentDialog editDlg(this);
CString oldComment;
GenericEntry* pEntry = GetSelectedItem(fpContentList);
if (pEntry == NULL) {
ASSERT(false);
return;
}
if (!pEntry->GetHasComment()) {
CString question, title;
int result;
CheckedLoadString(&question, IDS_NO_COMMENT_ADD);
CheckedLoadString(&title, 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 != NULL && !fpOpenArchive->IsReadOnly() &&
fpContentList->GetSelectedCount() == 1 &&
fpOpenArchive->GetCapability(GenericArchive::kCapCanEditComment));
}
/*
* ==========================================================================
* Edit Properties
* ==========================================================================
*/
void MainWindow::OnActionsEditProps(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
EditPropsDialog propsDlg(this);
CString oldComment;
GenericEntry* pEntry = GetSelectedItem(fpContentList);
if (pEntry == NULL) {
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 != NULL &&
fpContentList->GetSelectedCount() == 1);
}
/*
* ==========================================================================
* Rename Volume
* ==========================================================================
*/
void MainWindow::OnActionsRenameVolume(void)
{
RenameVolumeDialog rvDialog;
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
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 != NULL);
rvDialog.fpArchive = (DiskArchive*) fpOpenArchive;
if (rvDialog.DoModal() != IDOK)
return;
//LOGI("Creating '%s'", rvDialog.fNewName);
/* rename the chosen disk to the specified name */
bool result;
result = fpOpenArchive->RenameVolume(this, rvDialog.fpChosenDiskFS,
rvDialog.fNewName);
if (!result) {
LOGW("RenameVolume FAILED");
/* 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 != NULL && !fpOpenArchive->IsReadOnly() &&
fpOpenArchive->GetCapability(GenericArchive::kCapCanRenameVolume));
}
/*
* ==========================================================================
* Recompress
* ==========================================================================
*/
void MainWindow::OnActionsRecompress(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
/*
* 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) {
LOGI("Recompress cancelled");
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 = NULL;
if (result) {
CString msg, appName;
CalcTotalSize(&afterUncomp, &afterComp);
ASSERT(beforeUncomp == afterUncomp);
CheckedLoadString(&appName, 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 != NULL && !fpOpenArchive->IsReadOnly() &&
fpContentList->GetItemCount() > 0 &&
fpOpenArchive->GetCapability(GenericArchive::kCapCanRecompress));
}
void MainWindow::CalcTotalSize(LONGLONG* pUncomp, LONGLONG* pComp) const
{
GenericEntry* pEntry = fpOpenArchive->GetEntries();
LONGLONG uncomp = 0, comp = 0;
while (pEntry != NULL) {
uncomp += pEntry->GetUncompressedLen();
comp += pEntry->GetCompressedLen();
pEntry = pEntry->GetNext();
}
*pUncomp = uncomp;
*pComp = comp;
}
/*
* ==========================================================================
* Convert to disk archive
* ==========================================================================
*/
void MainWindow::OnActionsConvDisk(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
/*
* 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) {
LOGI("ConvDisk cancelled");
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;
LOGI("New volume name will be '%ls'", (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) {
LOGI(" User cancelled xfer from image create dialog");
return;
}
saveFolder = dlg.m_ofn.lpstrFile;
saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset);
fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder);
filename = dlg.GetPathName();
LOGI(" Will xfer to file '%ls'", (LPCWSTR) 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 = NULL;
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 != NULL &&
fpContentList->GetItemCount() > 0 &&
fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveNuFX);
}
/*
* ==========================================================================
* Convert disk image to NuFX file archive
* ==========================================================================
*/
void MainWindow::OnActionsConvFile(void)
{
ASSERT(fpContentList != NULL);
ASSERT(fpOpenArchive != NULL);
/*
* 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) {
LOGI("ConvFile cancelled");
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) {
LOGI(" User cancelled xfer from archive create dialog");
return;
}
saveFolder = dlg.m_ofn.lpstrFile;
saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset);
fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder);
filename = dlg.GetPathName();
LOGD(" Will xfer to file '%ls'", (LPCWSTR) 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, NULL);
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 = NULL;
if (result == GenericArchive::kXferOK)
SuccessBeep();
/* clean up */
delete xferOpts.fTarget;
}
void MainWindow::OnUpdateActionsConvFile(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL &&
fpContentList->GetItemCount() > 0 &&
fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage);
}
/*
* ==========================================================================
* Cassette WAV conversions
* ==========================================================================
*/
void MainWindow::OnActionsConvToWav(void)
{
// do this someday
LOGI("Convert TO wav");
}
void MainWindow::OnUpdateActionsConvToWav(CCmdUI* pCmdUI)
{
BOOL enable = false;
if (fpContentList != NULL && 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);
}
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();
LOGI("Opening WAV file '%ls'", (LPCWSTR) fileName);
dlg.fFileName = fileName;
// pass in fpOpenArchive?
dlg.DoModal();
if (dlg.IsDirty()) {
assert(fpContentList != NULL);
fpContentList->Reload();
}
bail:
return;
}
void MainWindow::OnUpdateActionsConvFromWav(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL && !fpOpenArchive->IsReadOnly());
}
/*static*/ bool MainWindow::SaveToArchive(GenericArchive::LocalFileDetails* pDetails,
const uint8_t* dataBufIn, long dataLen,
const uint8_t* rsrcBufIn, long rsrcLen,
CString* pErrMsg, CWnd* pDialog)
{
MainWindow* pMain = GET_MAIN_WINDOW();
GenericArchive* pArchive = pMain->GetOpenArchive();
DiskImgLib::A2File* pTargetSubdir = NULL;
XferFileOptions xferOpts;
CString storagePrefix;
uint8_t* dataBuf = NULL;
uint8_t* rsrcBuf = NULL;
ASSERT(pArchive != NULL);
ASSERT(pErrMsg->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 == NULL) {
pErrMsg->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->SetFssep(':');
}
if (pTargetSubdir != NULL) {
storagePrefix = pTargetSubdir->GetPathName();
LOGD(" using storagePrefix '%ls'", (LPCWSTR) storagePrefix);
}
if (!storagePrefix.IsEmpty()) {
CString tmpStr, tmpFileName;
tmpFileName = pDetails->GetStrippedLocalPathName();
tmpFileName.Replace(':', '_'); // strip any ':'s in the name
pDetails->SetFssep(':');
tmpStr = storagePrefix;
tmpStr += ':';
tmpStr += tmpFileName;
pDetails->SetStrippedLocalPathName(tmpStr);
}
/*
* Handle the transfer.
*
* On success, XferFile will null out our dataBuf and rsrcBuf pointers.
*/
pArchive->XferPrepare(&xferOpts);
*pErrMsg = pArchive->XferFile(pDetails, &dataBuf, dataLen,
&rsrcBuf, rsrcLen);
delete[] dataBuf;
delete[] rsrcBuf;
if (pErrMsg->IsEmpty())
pArchive->XferFinish(pDialog);
else
pArchive->XferAbort(pDialog);
bail:
return (pErrMsg->IsEmpty() != 0);
}
/*
* ==========================================================================
* Import BASIC programs from a text file
* ==========================================================================
*/
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();
LOGI("Opening TXT file '%ls'", (LPCWSTR) fileName);
dlg.SetFileName(fileName);
// pass in fpOpenArchive?
dlg.DoModal();
if (dlg.IsDirty()) {
assert(fpContentList != NULL);
fpContentList->Reload();
}
bail:
return;
}
void MainWindow::OnUpdateActionsImportBAS(CCmdUI* pCmdUI)
{
pCmdUI->Enable(fpContentList != NULL && !fpOpenArchive->IsReadOnly());
}
/*
* ==========================================================================
* Multiple file handling
* ==========================================================================
*/
int MainWindow::GetFileParts(const GenericEntry* pEntry,
ReformatHolder** ppHolder) const
{
ReformatHolder* pHolder = new ReformatHolder;
CString errMsg;
if (pHolder == NULL)
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;
}
void MainWindow::GetFilePart(const GenericEntry* pEntry, int whichThread,
ReformatHolder* pHolder) const
{
CString errMsg;
ReformatHolder::ReformatPart part;
char* buf = NULL;
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). "
L"The limit can be adjusted in the file viewer preferences.]\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 != NULL);
pHolder->SetSourceBuf(part, (unsigned char*) buf, len);
} else if (result == IDCANCEL) {
/* not expected */
errMsg = L"Cancelled!";
pHolder->SetErrorMsg(part, errMsg);
ASSERT(buf == NULL);
} else {
/* transfer error message to ReformatHolder buffer */
LOGI("Got error message from ExtractThread: '%ls'",
(LPCWSTR) errMsg);
pHolder->SetErrorMsg(part, errMsg);
ASSERT(buf == NULL);
}
bail:
return;
}