mirror of
https://github.com/fadden/ciderpress.git
synced 2024-12-22 05:29:43 +00:00
d60a6dbcfa
Includes "printf format" annotation on debug log function.
2564 lines
84 KiB
C++
2564 lines
84 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 "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 <errno.h>
|
|
|
|
|
|
/*
|
|
* ==========================================================================
|
|
* 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 != NULL &&
|
|
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 != 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;
|
|
}
|
|
|
|
//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 = NULL;
|
|
|
|
// 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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* 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 != 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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Add files to an archive.
|
|
*/
|
|
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);
|
|
|
|
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 = NULL;
|
|
if (result)
|
|
SuccessBeep();
|
|
} else {
|
|
LOGI("SFD bailed with Cancel");
|
|
}
|
|
}
|
|
void
|
|
MainWindow::OnUpdateActionsAddFiles(CCmdUI* pCmdUI)
|
|
{
|
|
pCmdUI->Enable(fpContentList != NULL && !fpOpenArchive->IsReadOnly());
|
|
}
|
|
|
|
|
|
/*
|
|
* Figure out where they want to add files.
|
|
*
|
|
* If the volume directory of a disk is chosen, "*ppTargetSubdir" will
|
|
* be set to NULL.
|
|
*/
|
|
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;
|
|
|
|
LOGI("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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* 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;
|
|
|
|
LOGI("Add disks!");
|
|
|
|
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.",
|
|
(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.
|
|
*
|
|
* 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 = NULL;
|
|
|
|
if (result)
|
|
SuccessBeep();
|
|
|
|
bail:
|
|
return;
|
|
}
|
|
void
|
|
MainWindow::OnUpdateActionsAddDisks(CCmdUI* pCmdUI)
|
|
{
|
|
pCmdUI->Enable(fpContentList != NULL && !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 != 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'", pEntry->GetPathName());
|
|
|
|
csDialog.fBasePath = pEntry->GetPathName();
|
|
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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Extract files.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* 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 = 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", 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 = NULL;
|
|
}
|
|
|
|
// if they cancelled, delete the "stray"
|
|
delete pHolder;
|
|
}
|
|
|
|
/*
|
|
* Extract a single entry.
|
|
*
|
|
* If "pHolder" is non-NULL, it holds the data from the file, and can be
|
|
* used for formatted or non-formatted output. If it's NULL, 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;
|
|
|
|
//LOGI(" DBE initial text conversion: eol=%d ha=%d",
|
|
// convEOL, convHA);
|
|
|
|
|
|
ReformatHolder holder;
|
|
CString outputPath;
|
|
CString failed, errMsg;
|
|
failed.LoadString(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)",
|
|
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() != NULL) {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
msg.LoadString(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;
|
|
}
|
|
|
|
|
|
/*
|
|
* 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-NULL, 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 = 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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Test files.
|
|
*/
|
|
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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Delete archive entries.
|
|
*/
|
|
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;
|
|
|
|
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 = NULL;
|
|
|
|
if (result)
|
|
SuccessBeep();
|
|
}
|
|
void
|
|
MainWindow::OnUpdateActionsDelete(CCmdUI* pCmdUI)
|
|
{
|
|
pCmdUI->Enable(fpContentList != NULL && !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 != 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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Edit a comment, creating it if necessary.
|
|
*/
|
|
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;
|
|
|
|
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 != NULL && !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 != 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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Change a volume name or volume number.
|
|
*/
|
|
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) {
|
|
LOGI("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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Recompress files.
|
|
*/
|
|
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);
|
|
|
|
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 != NULL && !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 != NULL) {
|
|
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 != 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 to file archive
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Select files to convert.
|
|
*/
|
|
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();
|
|
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;
|
|
}
|
|
|
|
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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* Convert BAS, INT, or BIN to a cassette-audio WAV file.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* 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();
|
|
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());
|
|
}
|
|
|
|
|
|
/*
|
|
* 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 = NULL;
|
|
XferFileOptions xferOpts;
|
|
CString storagePrefix;
|
|
unsigned char* dataBuf = NULL;
|
|
unsigned char* rsrcBuf = NULL;
|
|
|
|
ASSERT(pArchive != NULL);
|
|
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 == NULL) {
|
|
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 != NULL) {
|
|
storagePrefix = pTargetSubdir->GetPathName();
|
|
LOGD(" using storagePrefix '%ls'", (LPCWSTR) 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();
|
|
LOGI("Opening TXT file '%ls'", (LPCWSTR) fileName);
|
|
|
|
dlg.fFileName = 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
|
|
* ==========================================================================
|
|
*/
|
|
|
|
/*
|
|
* 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 == 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;
|
|
}
|
|
|
|
/*
|
|
* Load the requested part.
|
|
*/
|
|
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).]\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;
|
|
}
|