shithub: ft²

ref: 007b8a42742ad0f220568db820b8d91a3c9ce2ec
dir: /src/ft2_diskop.c/

View raw version
// for finding memory leaks in debug mode with Visual Studio
#if defined _DEBUG && defined _MSC_VER
#include <crtdbg.h>
#endif

#define _FILE_OFFSET_BITS 64

#include <stdint.h>
#include <stdio.h>
#include <math.h>
#ifdef _WIN32
#define WIN32_MEAN_AND_LEAN
#include <shlwapi.h>
#include <windows.h>
#include <direct.h>
#include <shlobj.h> // SHGetFolderPathW()
#else
#include <sys/types.h>
#include <sys/stat.h>
#include <fts.h> // for fts_open() and stuff in recursiveDelete()
#include <unistd.h>
#include <dirent.h>
#endif
#include <wchar.h>
#include <sys/stat.h>
#include "ft2_header.h"
#include "ft2_unicode.h"
#include "ft2_config.h"
#include "ft2_mouse.h"
#include "ft2_gui.h"
#include "ft2_pattern_ed.h"
#include "ft2_sample_loader.h"
#include "ft2_sample_saver.h"
#include "ft2_diskop.h"
#include "ft2_wav_renderer.h"
#include "ft2_module_loader.h"
#include "ft2_module_saver.h"
#include "ft2_events.h"
#include "ft2_video.h"
#include "ft2_inst_ed.h"

// hide POSIX warnings
#ifdef _MSC_VER
#pragma warning(disable: 4996)
#endif

#define FILENAME_TEXT_X 170
#define FILESIZE_TEXT_X 295
#define DISKOP_MAX_DRIVE_BUTTONS 8

#ifdef _WIN32
#define PARENT_DIR_STR L".."
static HANDLE hFind;
#else
#define PARENT_DIR_STR ".."
static DIR *hFind;
#endif

// "look for file" flags
enum
{
	LFF_DONE = 0,
	LFF_SKIP = 1,
	LFF_OK = 2
};

typedef struct DirRec
{
	UNICHAR *nameU;
	bool isDir;
	int32_t filesize;
} DirRec;

static char FReq_SysReqText[196], *FReq_FileName, *FReq_NameTemp;
static char *modTmpFName, *insTmpFName, *smpTmpFName, *patTmpFName, *trkTmpFName;
static char *modTmpFNameUTF8; // for window title
static uint8_t FReq_Item;
static bool FReq_ShowAllFiles;
static int32_t FReq_EntrySelected = -1, FReq_FileCount, FReq_DirPos, lastMouseY;
static UNICHAR *FReq_CurPathU, *FReq_ModCurPathU, *FReq_InsCurPathU, *FReq_SmpCurPathU, *FReq_PatCurPathU, *FReq_TrkCurPathU;
static DirRec *FReq_Buffer;
static SDL_Thread *thread;

static void setDiskOpItem(uint8_t item);

int32_t getFileSize(UNICHAR *fileNameU) // returning -1 = filesize over 2GB
{
#ifdef _WIN32
	FILE *f;
#else
	struct stat st;
#endif
	int64_t fSize;

#ifdef _WIN32
	f = UNICHAR_FOPEN(fileNameU, "rb");
	if (f == NULL)
		return 0;

	_fseeki64(f, 0, SEEK_END);
	fSize = _ftelli64(f);
	fclose(f);
#else
	if (stat(fileNameU, &st) != 0)
		return 0;

	fSize = (int64_t)(st.st_size);
#endif
	if (fSize < 0)
		fSize = 0;

	if (fSize > INT32_MAX)
		return -1;
	
	return fSize & 0xFFFFFFFF;
}

uint8_t getDiskOpItem(void)
{
	return FReq_Item;
}

char *getCurrSongFilename(void) // for window title
{
	return modTmpFNameUTF8;
}

void updateCurrSongFilename(void) // for window title
{
	if (modTmpFNameUTF8 != NULL)
	{
		free(modTmpFNameUTF8);
		modTmpFNameUTF8 = NULL;
	}

	if (modTmpFName == NULL)
		return;

	modTmpFNameUTF8 = cp437ToUtf8(modTmpFName);
}

// drive buttons for Windows
#ifdef _WIN32
static char logicalDriveNames[26][3] = 
{
	"A:", "B:", "C:", "D:", "E:", "F:", "G:", "H:", "I:", "J:", "K:", "L:", "M:",
	"N:", "O:", "P:", "Q:", "R:", "S:", "T:", "U:", "V:", "W:", "X:", "Y:", "Z:"
};
static uint32_t numLogicalDrives;
static uint32_t driveIndexes[DISKOP_MAX_DRIVE_BUTTONS];
#endif

char *getDiskOpFilename(void)
{
	return FReq_FileName;
}

const UNICHAR *getDiskOpCurPath(void)
{
	return FReq_CurPathU;
}

const UNICHAR *getDiskOpModPath(void)
{
	return FReq_ModCurPathU;
}

const UNICHAR *getDiskOpSmpPath(void)
{
	return FReq_SmpCurPathU;
}

static void setupInitialPaths(void)
{
#ifdef _WIN32
	UNICHAR pathU[PATH_MAX + 2];
#endif

	// the UNICHAR paths are already zeroed out

#ifdef _WIN32
	if (config.modulesPath[0]  != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.modulesPath, -1, FReq_ModCurPathU, 80);
	if (config.instrPath[0]    != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.instrPath, -1, FReq_InsCurPathU, 80);
	if (config.samplesPath[0]  != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.samplesPath, -1, FReq_SmpCurPathU, 80);
	if (config.patternsPath[0] != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.patternsPath, -1, FReq_PatCurPathU, 80);
	if (config.tracksPath[0]   != '\0') MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, config.tracksPath, -1, FReq_TrkCurPathU, 80);
#else
	if (config.modulesPath[0]  != '\0') strncpy(FReq_ModCurPathU, config.modulesPath, 80);
	if (config.instrPath[0]    != '\0') strncpy(FReq_InsCurPathU, config.instrPath, 80);
	if (config.samplesPath[0]  != '\0') strncpy(FReq_SmpCurPathU, config.samplesPath, 80);
	if (config.patternsPath[0] != '\0') strncpy(FReq_PatCurPathU, config.patternsPath, 80);
	if (config.tracksPath[0]   != '\0') strncpy(FReq_TrkCurPathU, config.tracksPath, 80);
#endif

	// set initial path to user directory
#ifdef _WIN32
	if (SHGetFolderPathW(NULL, CSIDL_PROFILE, NULL, 0, pathU) >= 0)
		UNICHAR_CHDIR(pathU);
#else
	UNICHAR_CHDIR(getenv("HOME"));
#endif

	// if present in config, set custom "modules" path
	if (UNICHAR_CHDIR(FReq_ModCurPathU) != 0)
		UNICHAR_GETCWD(FReq_ModCurPathU, PATH_MAX);

	if (UNICHAR_CHDIR(FReq_InsCurPathU) != 0) UNICHAR_STRCPY(FReq_InsCurPathU, FReq_ModCurPathU);
	if (UNICHAR_CHDIR(FReq_SmpCurPathU) != 0) UNICHAR_STRCPY(FReq_SmpCurPathU, FReq_ModCurPathU);
	if (UNICHAR_CHDIR(FReq_PatCurPathU) != 0) UNICHAR_STRCPY(FReq_PatCurPathU, FReq_ModCurPathU);
	if (UNICHAR_CHDIR(FReq_TrkCurPathU) != 0) UNICHAR_STRCPY(FReq_TrkCurPathU, FReq_ModCurPathU);

	UNICHAR_CHDIR(FReq_ModCurPathU); // set back after testing
}

static void freeDirRecBuffer(void)
{
	if (FReq_Buffer != NULL)
	{
		for (int32_t i = 0; i < FReq_FileCount; i++)
		{
			if (FReq_Buffer[i].nameU != NULL)
				free(FReq_Buffer[i].nameU);
		}

		free(FReq_Buffer);
		FReq_Buffer = NULL;
	}

	FReq_FileCount = 0;
}

void freeDiskOp(void)
{
	if (editor.tmpFilenameU != NULL)
	{
		free(editor.tmpFilenameU);
		editor.tmpFilenameU = NULL;
	}

	if (editor.tmpInstrFilenameU != NULL)
	{
		free(editor.tmpInstrFilenameU);
		editor.tmpInstrFilenameU = NULL;
	}

	if (modTmpFName != NULL) { free(modTmpFName); modTmpFName = NULL; }
	if (insTmpFName != NULL) { free(insTmpFName); insTmpFName = NULL; }
	if (smpTmpFName != NULL) { free(smpTmpFName); smpTmpFName = NULL; }
	if (patTmpFName != NULL) { free(patTmpFName); patTmpFName = NULL; }
	if (trkTmpFName != NULL) { free(trkTmpFName); trkTmpFName = NULL; }
	if (FReq_NameTemp != NULL) { free(FReq_NameTemp); FReq_NameTemp = NULL; }
	if (FReq_ModCurPathU != NULL) { free(FReq_ModCurPathU); FReq_ModCurPathU = NULL; }
	if (FReq_InsCurPathU != NULL) { free(FReq_InsCurPathU); FReq_InsCurPathU = NULL; }
	if (FReq_SmpCurPathU != NULL) { free(FReq_SmpCurPathU); FReq_SmpCurPathU = NULL; }
	if (FReq_PatCurPathU != NULL) { free(FReq_PatCurPathU); FReq_PatCurPathU = NULL; }
	if (FReq_TrkCurPathU != NULL) { free(FReq_TrkCurPathU); FReq_TrkCurPathU = NULL; }
	if (modTmpFNameUTF8 != NULL) { free(modTmpFNameUTF8); modTmpFNameUTF8 = NULL; }

	freeDirRecBuffer();
}

bool setupDiskOp(void)
{
	modTmpFName = (char *)calloc(PATH_MAX + 1, sizeof (char));
	insTmpFName = (char *)calloc(PATH_MAX + 1, sizeof (char));
	smpTmpFName = (char *)calloc(PATH_MAX + 1, sizeof (char));
	patTmpFName = (char *)calloc(PATH_MAX + 1, sizeof (char));
	trkTmpFName = (char *)calloc(PATH_MAX + 1, sizeof (char));
	FReq_NameTemp = (char *)calloc(PATH_MAX + 1, sizeof (char));
	FReq_ModCurPathU = (UNICHAR *)calloc(PATH_MAX + 2, sizeof (UNICHAR));
	FReq_InsCurPathU = (UNICHAR *)calloc(PATH_MAX + 2, sizeof (UNICHAR));
	FReq_SmpCurPathU = (UNICHAR *)calloc(PATH_MAX + 2, sizeof (UNICHAR));
	FReq_PatCurPathU = (UNICHAR *)calloc(PATH_MAX + 2, sizeof (UNICHAR));
	FReq_TrkCurPathU = (UNICHAR *)calloc(PATH_MAX + 2, sizeof (UNICHAR));

	if (modTmpFName == NULL || insTmpFName == NULL || smpTmpFName == NULL || patTmpFName == NULL ||
		trkTmpFName == NULL || FReq_NameTemp == NULL || FReq_ModCurPathU == NULL ||
		FReq_InsCurPathU == NULL || FReq_SmpCurPathU == NULL || FReq_PatCurPathU == NULL ||
		FReq_TrkCurPathU == NULL)
	{
		// allocated memory is free'd lateron
		showErrorMsgBox("Not enough memory!");
		return false;
	}

	strcpy(modTmpFName, "untitled.xm");
	strcpy(insTmpFName, "untitled.xi");
	strcpy(smpTmpFName, "untitled.wav");
	strcpy(patTmpFName, "untitled.xp");
	strcpy(trkTmpFName, "untitled.xt");

	setupInitialPaths();
	setDiskOpItem(0);

	updateCurrSongFilename(); // for window title
	updateWindowTitle(true);

	return true;
}

int32_t getExtOffset(char *s, int32_t stringLen) // get byte offset of file extension (last '.')
{
	if (s == NULL || stringLen < 1)
		return -1;

	for (int32_t i = stringLen - 1; i >= 0; i--)
	{
		if (i != 0 && s[i] == '.')
			return i;
	}

	return -1;
}

static void removeQuestionmarksFromString(char *s)
{
	int32_t len;

	if (s == NULL || *s == '\0')
		return;

	len = (int32_t)strlen(s);
	for (int32_t i = 0; i < len; i++)
	{
		if (s[i] == '?')
			s[i] = ' ' ;
	}
}

#ifdef _WIN32 // WINDOWS SPECIFIC FILE OPERATION ROUTINES

bool fileExistsAnsi(char *str)
{
	int32_t retVal;
	UNICHAR *strU;

	strU = cp437ToUnichar(str);
	if (strU == NULL)
		return false;

	retVal = PathFileExistsW(strU);
	free(strU);

	return retVal;
}

static bool deleteDirRecursive(UNICHAR *strU)
{
	SHFILEOPSTRUCTW shfo;

	memset(&shfo, 0, sizeof (shfo));
	shfo.wFunc = FO_DELETE;
	shfo.fFlags = FOF_SILENT | FOF_NOERRORUI | FOF_NOCONFIRMATION;
	shfo.pFrom = strU;

	return (SHFileOperationW(&shfo) == 0);
}

static bool makeDirAnsi(char *str)
{
	int32_t retVal;
	UNICHAR *strU;

	strU = cp437ToUnichar(str);
	if (strU == NULL)
		return false;

	retVal = _wmkdir(strU);
	free(strU);

	return (retVal == 0);
}

static bool renameAnsi(UNICHAR *oldNameU, char *newName)
{
	int32_t retVal;
	UNICHAR *newNameU;

	newNameU = cp437ToUnichar(newName);
	if (newNameU == NULL)
		return false;

	retVal = UNICHAR_RENAME(oldNameU, newNameU);
	free(newNameU);

	return (retVal == 0);
}

static void setupDiskOpDrives(void) // Windows only
{
	uint16_t i;
	uint32_t drivesBitmask;

	fillRect(134, 29, 31, 111, PAL_DESKTOP);

	numLogicalDrives = 0;

	// get number of drives and drive names
	drivesBitmask = GetLogicalDrives();
	for (i = 0; i < 8*sizeof (uint32_t); i++)
	{
		if ((drivesBitmask & (1 << i)) != 0)
		{
			driveIndexes[numLogicalDrives++] = i;
			if (numLogicalDrives == DISKOP_MAX_DRIVE_BUTTONS)
				break;
		}
	}

	// hide all buttons
	for (i = 0; i < DISKOP_MAX_DRIVE_BUTTONS; i++)
		hidePushButton(PB_DISKOP_DRIVE1 + i);

	// set button names and show buttons
	for (i = 0; i < numLogicalDrives; i++)
	{
		pushButtons[PB_DISKOP_DRIVE1 + i].caption = logicalDriveNames[driveIndexes[i]];
		showPushButton(PB_DISKOP_DRIVE1 + i);
	}
}

static void openDrive(char *str) // Windows only
{
	if (mouse.mode == MOUSE_MODE_DELETE)
	{
		okBox(8, "System complaint", "Very funny.");
		return;
	}

	if (str == NULL || *str == '\0')
	{
		okBox(0, "System message", "Couldn't open drive!");
		return;
	}

	if (chdir(str) != 0)
		okBox(0, "System message", "Couldn't open drive! Please make sure there's a disk in it.");
	else
		editor.diskOpReadDir = true;
}

#else // NON-WINDOWS SPECIFIC FILE OPERATION ROUTINES

bool fileExistsAnsi(char *str)
{
	int32_t retVal;
	UNICHAR *strU;

	strU = cp437ToUnichar(str);
	if (strU == NULL)
		return false;

	retVal = access(strU, F_OK);
	free(strU);

	return (retVal != -1);
}

static bool deleteDirRecursive(UNICHAR *strU)
{
	int32_t ret;
	FTS *ftsp;
	FTSENT *curr;
	char *files[] = { (char *)(strU), NULL };

	ftsp = NULL;

	ftsp = fts_open(files, FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV, NULL);
	if (!ftsp)
		return false;

	ret = true;
	while ((curr = fts_read(ftsp)))
	{
		switch (curr->fts_info)
		{
			default:
			case FTS_NS:
			case FTS_DNR:
			case FTS_ERR:
				ret = false;
			break;

			case FTS_D:
			case FTS_DC:
			case FTS_DOT:
			case FTS_NSOK:
				break;

			case FTS_DP:
			case FTS_F:
			case FTS_SL:
			case FTS_SLNONE:
			case FTS_DEFAULT:
			{
				if (remove(curr->fts_accpath) < 0)
					ret = false;
			}
			break;
		}
	}

	if (ftsp != NULL)
		fts_close(ftsp);

	return ret;
}

static bool makeDirAnsi(char *str)
{
	int32_t retVal;
	UNICHAR *strU;

	strU = cp437ToUnichar(str);
	if (strU == NULL)
		return false;

	retVal = mkdir(str, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
	free(strU);

	return (retVal == 0);
}

static bool renameAnsi(UNICHAR *oldNameU, char *newName)
{
	int32_t retVal;
	UNICHAR *newNameU;

	newNameU = cp437ToUnichar(newName);
	if (newNameU == NULL)
		return false;

	retVal = UNICHAR_RENAME(oldNameU, newNameU);
	free(newNameU);

	return (retVal == 0);
}
#endif

static void openDirectory(UNICHAR *strU)
{
	if (strU == NULL || UNICHAR_STRLEN(strU) == 0)
	{
		okBox(0, "System message", "Couldn't open directory! No permission or in use?");
		return;
	}

	if (UNICHAR_CHDIR(strU) != 0)
		okBox(0, "System message", "Couldn't open directory! No permission or in use?");
	else
		editor.diskOpReadDir = true;
}

bool diskOpGoParent(void)
{
	if (chdir("..") == 0)
	{
		editor.diskOpReadDir = true;
		FReq_EntrySelected = -1;

		return true;
	}

	return false;
}

static char *getFilenameFromPath(char *p)
{
	int32_t i, len;

	if (p == NULL || p[0] == '\0')
		return p;

	len = (int32_t)strlen(p);
	if (len < 2 || p[len-1] == DIR_DELIMITER)
		return p;

	// search for last directory delimiter
	for (i = len - 1; i >= 0; i--)
	{
		if (p[i] == DIR_DELIMITER)
			break;
	}

	if (i != 0)
		p += i+1; // we found a directory delimiter - skip to the last one

	return p;
}

void sanitizeFilename(const char *src)
{
	// some of these are legal on GNU/Linux and macOS, but whatever...
	const char illegalChars[] = "\\/:*?\"<>|";
	char *ptr;

	if (src == NULL || src[0] == '\0')
		return;

	// convert illegal characters to space (for making a filename the OS will accept)
	while ((ptr = (char *)strpbrk(src, illegalChars)) != NULL)
		*ptr = ' ';
}

void diskOpSetFilename(uint8_t type, UNICHAR *pathU)
{
	char *ansiPath, *filename;

	ansiPath = unicharToCp437(pathU, true);
	if (ansiPath == NULL)
		return;

	filename = getFilenameFromPath(ansiPath);
	if (strlen(filename) > PATH_MAX-1)
	{
		free(ansiPath);
		return; // filename is too long, don't bother to copy it over
	}

	sanitizeFilename(filename);

	switch (type)
	{
		default:
		case DISKOP_ITEM_MODULE:
		{
			strcpy(modTmpFName, filename);

			updateCurrSongFilename(); // for window title
			updateWindowTitle(true);
		}
		break;

		case DISKOP_ITEM_INSTR:
			strcpy(insTmpFName, filename);
		break;

		case DISKOP_ITEM_SAMPLE:
			strcpy(smpTmpFName, filename);
		break;

		case DISKOP_ITEM_PATTERN:
			strcpy(patTmpFName, filename);
		break;

		case DISKOP_ITEM_TRACK:
			strcpy(trkTmpFName, filename);
		break;
	}

	free(ansiPath);

	if (editor.ui.diskOpShown)
		drawTextBox(TB_DISKOP_FILENAME);
}

static void openFile(UNICHAR *filenameU, bool songModifiedCheck)
{
	int32_t filesize;
	FILE *f;

	// first check if we can actually open the requested file
	f = UNICHAR_FOPEN(filenameU, "rb");
	if (f == NULL)
	{
		okBox(0, "System message", "Couldn't open file/directory! No permission or in use?");
		return;
	}
	fclose(f);

	filesize = getFileSize(filenameU);
	if (filesize == -1) // >2GB
	{
		okBox(0, "System message", "The file is too big and can't be loaded (over 2GB).");
		return;
	}

	if (filesize >= 128L*1024*1024) // 128MB
	{
		if (okBox(2, "System request", "Are you sure you want to load such a big file?") != 1)
			return;
	}

	// file is readable, handle file...
	switch (FReq_Item)
	{
		default:
		case DISKOP_ITEM_MODULE:
		{
			if (songModifiedCheck && song.isModified)
			{
				// remove file selection
				FReq_EntrySelected = -1;
				diskOp_DrawDirectory();

				if (okBox(2, "System request", "You have unsaved changes in your song. Load new song and lose all changes?") != 1)
					return;
			}

			editor.loadMusicEvent = EVENT_LOADMUSIC_DISKOP;
			loadMusic(filenameU);
		}
		break;

		case DISKOP_ITEM_INSTR:
			loadInstr(filenameU);
		break;

		case DISKOP_ITEM_SAMPLE:
			loadSample(filenameU, editor.curSmp, false);
		break;

		case DISKOP_ITEM_PATTERN:
			loadPattern(filenameU);
		break;

		case DISKOP_ITEM_TRACK:
			loadTrack(filenameU);
		break;
	}
}

static void removeFilenameExt(char *name)
{
	int32_t extOffset, len;

	if (name == NULL || *name == '\0')
		return;

	len = (int32_t)strlen(name);

	extOffset = getExtOffset(name, len);
	if (extOffset != -1)
		name[extOffset] = '\0';
}

void changeFilenameExt(char *name, char *ext, int32_t nameMaxLen)
{
	int32_t len, extLen;

	 if (name == NULL || name[0] == '\0' || ext == NULL)
		 return;

	 removeFilenameExt(name);

	 len    = (int32_t)strlen(name);
	 extLen = (int32_t)strlen(ext);

	 if (len + extLen > nameMaxLen-1)
		 name[(nameMaxLen-1) - extLen] = '\0';

	 strcat(name, ext);

	 if (editor.ui.diskOpShown)
		diskOp_DrawDirectory();
}

void diskOpChangeFilenameExt(char *ext)
{
	changeFilenameExt(FReq_FileName, ext, PATH_MAX);
	if (editor.ui.diskOpShown)
		diskOp_DrawDirectory();
}

void trimEntryName(char *name, bool isDir)
{
	char extBuffer[24];
	int32_t j, extOffset, extLen;

	j = (int32_t)strlen(name);
	extOffset = getExtOffset(name, j);
	extLen = (int32_t)strlen(&name[extOffset]);
	j--;

	if (isDir)
	{
		// directory
		while (textWidth(name) > 160-8 && j >= 2)
		{
			name[j-2] = '.';
			name[j-1] = '.';
			name[j-0] = '\0';
			j--;
		}

		return;
	}

	if (extOffset != -1 && extLen <= 4)
	{
		// has extension
		sprintf(extBuffer, ".. %s", &name[extOffset]); // "testtestte... .xm"

		extLen = (int32_t)strlen(extBuffer);
		while (textWidth(name) >= FILESIZE_TEXT_X-FILENAME_TEXT_X && j >= extLen+1)
		{
			memcpy(&name[j - extLen], extBuffer, extLen + 1);
			j--;
		}
	}
	else
	{
		// no extension
		while (textWidth(name) >= FILESIZE_TEXT_X-FILENAME_TEXT_X && j >= 2)
		{
			name[j-2] = '.';
			name[j-1] = '.';
			name[j-0] = '\0';
			j--;
		}
	}
}

static void createOverwriteText(char *name)
{
	char nameTmp[128];
	uint32_t nameLen;

	// read entry name to a small buffer
	nameLen = (uint32_t)strlen(name);
	memcpy(nameTmp, name, (nameLen >= sizeof (nameTmp)) ? sizeof (nameTmp) : (nameLen + 1));
	nameTmp[sizeof (nameTmp) - 1] = '\0';

	trimEntryName(nameTmp, false);

	sprintf(FReq_SysReqText, "Overwrite file \"%s\"?", nameTmp);
}

static void diskOpSave(bool checkOverwrite)
{
	UNICHAR *fileNameU;

	if (FReq_FileName[0] == '\0')
	{
		okBox(0, "System message", "Filename can't be empty!");
		return;
	}

	// test if the very first character has a dot...
	if (FReq_FileName[0] == '.')
	{
		okBox(0, "System message", "The very first character in the filename can't be '.' (dot)!");
		return;
	}

	// test for illegal file name
	if (FReq_FileName[0] == '\0' || strpbrk(FReq_FileName, "\\/:*?\"<>|") != NULL)
	{
		okBox(0, "System message", "The filename can't contain the following characters: \\ / : * ? \" < > |");
		return;
	}

	switch (FReq_Item)
	{
		default:
		case DISKOP_ITEM_MODULE:
		{
			switch (editor.moduleSaveMode)
			{
				         case MOD_SAVE_MODE_MOD: diskOpChangeFilenameExt(".mod"); break;
				default: case MOD_SAVE_MODE_XM:  diskOpChangeFilenameExt(".xm");  break;
				         case MOD_SAVE_MODE_WAV: diskOpChangeFilenameExt(".wav"); break;
			}

			// enter WAV renderer if needed
			if (editor.moduleSaveMode == MOD_SAVE_MODE_WAV)
			{
				exitDiskOpScreen();
				showWavRenderer();
				return;
			}

			if (checkOverwrite && fileExistsAnsi(FReq_FileName))
			{
				createOverwriteText(FReq_FileName);
				if (okBox(2, "System request", FReq_SysReqText) != 1)
					return;
			}

			fileNameU = cp437ToUnichar(FReq_FileName);
			if (fileNameU == NULL)
			{
				okBox(0, "System message", "General I/O error during saving! Is the file in use?");
				return;
			}

			saveMusic(fileNameU);
			free(fileNameU);
			// sets editor.diskOpReadDir after thread is done
		}
		break;

		case DISKOP_ITEM_INSTR:
		{
			diskOpChangeFilenameExt(".xi");

			if (checkOverwrite && fileExistsAnsi(FReq_FileName))
			{
				createOverwriteText(FReq_FileName);
				if (okBox(2, "System request", FReq_SysReqText) != 1)
					return;
			}

			fileNameU = cp437ToUnichar(FReq_FileName);
			if (fileNameU == NULL)
			{
				okBox(0, "System message", "General I/O error during saving! Is the file in use?");
				return;
			}

			saveInstr(fileNameU, editor.curInstr);
			free(fileNameU);
			// editor.diskOpReadDir is set after thread is done
		}
		break;

		case DISKOP_ITEM_SAMPLE:
		{
			switch (editor.sampleSaveMode)
			{
				         case SMP_SAVE_MODE_RAW: diskOpChangeFilenameExt(".raw"); break;
				         case SMP_SAVE_MODE_IFF: diskOpChangeFilenameExt(".iff"); break;
				default: case SMP_SAVE_MODE_WAV: diskOpChangeFilenameExt(".wav"); break;
			}

			if (checkOverwrite && fileExistsAnsi(FReq_FileName))
			{
				createOverwriteText(FReq_FileName);
				if (okBox(2, "System request", FReq_SysReqText) != 1)
					return;
			}

			fileNameU = cp437ToUnichar(FReq_FileName);
			if (fileNameU == NULL)
			{
				okBox(0, "System message", "General I/O error during saving! Is the file in use?");
				return;
			}

			saveSample(fileNameU, SAVE_NORMAL);
			free(fileNameU);
			// editor.diskOpReadDir is set after thread is done
		}
		break;

		case DISKOP_ITEM_PATTERN:
		{
			diskOpChangeFilenameExt(".xp");

			if (checkOverwrite && fileExistsAnsi(FReq_FileName))
			{
				createOverwriteText(FReq_FileName);
				if (okBox(2, "System request", FReq_SysReqText) != 1)
					return;
			}

			fileNameU = cp437ToUnichar(FReq_FileName);
			if (fileNameU == NULL)
			{
				okBox(0, "System message", "General I/O error during saving! Is the file in use?");
				return;
			}

			editor.diskOpReadDir = savePattern(fileNameU);
			free(fileNameU);
		}
		break;

		case DISKOP_ITEM_TRACK:
		{
			diskOpChangeFilenameExt(".xt");

			if (checkOverwrite && fileExistsAnsi(FReq_FileName))
			{
				createOverwriteText(FReq_FileName);
				if (okBox(2, "System request", FReq_SysReqText) != 1)
					return;
			}

			fileNameU = cp437ToUnichar(FReq_FileName);
			if (fileNameU == NULL)
			{
				okBox(0, "System message", "General I/O error during saving! Is the file in use?");
				return;
			}

			editor.diskOpReadDir = saveTrack(fileNameU);
			free(fileNameU);
		}
		break;
	}
}

void pbDiskOpSave(void)
{
	diskOpSave(config.cfg_OverwriteWarning ? true : false); // check if about to overwrite
}

static void fileListPressed(int32_t index)
{
	char *nameTmp;
	int8_t mode;
	int32_t result, entryIndex;
	DirRec *dirEntry;

	entryIndex = FReq_DirPos + index;
	if (entryIndex >= FReq_FileCount || FReq_FileCount == 0)
		return; // illegal entry

	mode = mouse.mode;

	// set normal mouse cursor
	if (mouse.mode != MOUSE_MODE_NORMAL)
		setMouseMode(MOUSE_MODE_NORMAL);

	// remove file selection
	FReq_EntrySelected = -1;
	diskOp_DrawDirectory();

	dirEntry = &FReq_Buffer[entryIndex];
	switch (mode)
	{
		// open file/folder
		default:
		case MOUSE_MODE_NORMAL:
		{
			if (dirEntry->isDir)
				openDirectory(dirEntry->nameU);
			else
				openFile(dirEntry->nameU, true);
		}
		break;

		// delete file/folder
		case MOUSE_MODE_DELETE:
		{
			if (!dirEntry->isDir || UNICHAR_STRCMP(dirEntry->nameU, PARENT_DIR_STR)) // don't handle ".." dir
			{
				nameTmp = unicharToCp437(dirEntry->nameU, true);
				if (nameTmp == NULL)
					break;

				trimEntryName(nameTmp, dirEntry->isDir);

				if (dirEntry->isDir)
					sprintf(FReq_SysReqText, "Delete directory \"%s\"?", nameTmp);
				else
					sprintf(FReq_SysReqText, "Delete file \"%s\"?", nameTmp);

				free(nameTmp);

				if (okBox(2, "System request", FReq_SysReqText) == 1)
				{
					if (dirEntry->isDir)
					{
						result = deleteDirRecursive(dirEntry->nameU);
						if (!result)
							okBox(0, "System message", "Couldn't delete folder: Access denied!");
						else
							editor.diskOpReadDir = true;
					}
					else
					{
						result = (UNICHAR_REMOVE(dirEntry->nameU) == 0);
						if (!result)
							okBox(0, "System message", "Couldn't delete file: Access denied!");
						else
							editor.diskOpReadDir = true;
					}
				}
			}
		}
		break;

		// rename file/folder
		case MOUSE_MODE_RENAME:
		{
			if (dirEntry->isDir || UNICHAR_STRCMP(dirEntry->nameU, PARENT_DIR_STR)) // don't handle ".." dir
			{
				nameTmp = unicharToCp437(dirEntry->nameU, true);
				if (nameTmp == NULL)
					break;

				strncpy(FReq_NameTemp, nameTmp, PATH_MAX - 1);
				free(nameTmp);

				// in case of UTF8 -> CP437 encoding failure, there can be question marks. Remove them...
				removeQuestionmarksFromString(FReq_NameTemp);

				if (inputBox(1, dirEntry->isDir ? "Enter new directory name:" : "Enter new filename:", FReq_NameTemp, PATH_MAX - 1) == 1)
				{
					if ((FReq_NameTemp == NULL) || (FReq_NameTemp[0] == '\0'))
					{
						okBox(0, "System message", "New name can't be empty!");
						break;
					}

					if (!renameAnsi(dirEntry->nameU, FReq_NameTemp))
					{
						if (dirEntry->isDir)
							okBox(0, "System message", "Couldn't rename directory: Access denied, or dir already exists!");
						else
							okBox(0, "System message", "Couldn't rename file: Access denied, or file already exists!");
					}
					else
					{
						editor.diskOpReadDir = true;
					}
				}
			}
		}
		break;
	}
}

bool testDiskOpMouseDown(bool mouseHeldDlown)
{
	int32_t tmpEntry, max;

	if (!editor.ui.diskOpShown || FReq_FileCount == 0)
		return false;

	max = FReq_FileCount - FReq_DirPos;
	if (max > DISKOP_ENTRY_NUM) // needed kludge when mouse-scrolling
		max = DISKOP_ENTRY_NUM;

	if (!mouseHeldDlown)
	{
		FReq_EntrySelected = -1;

		if (mouse.x >= 169 && mouse.x <= 331 && mouse.y >= 4 && mouse.y <= 168)
		{
			tmpEntry = (mouse.y - 4) / (FONT1_CHAR_H + 1);
			if (tmpEntry >= 0 && tmpEntry < max)
			{
				FReq_EntrySelected = tmpEntry;
				diskOp_DrawDirectory();
			}

			mouse.lastUsedObjectType = OBJECT_DISKOPLIST;
			return true;
		}

		return false;
	}

	// handle scrolling if outside of disk op. list area
	if (mouse.y < 4)
	{
		scrollBarScrollUp(SB_DISKOP_LIST, 1);
		FReq_EntrySelected = -1;
	}
	else if (mouse.y > 168)
	{
		scrollBarScrollDown(SB_DISKOP_LIST, 1);
		FReq_EntrySelected = -1;
	}

	if (mouse.y == lastMouseY)
		return true;

	lastMouseY = mouse.y;

	tmpEntry = (mouse.y - 4) / (FONT1_CHAR_H + 1);
	if (mouse.x < 169 || mouse.x > 331 || mouse.y < 4 || tmpEntry < 0 || tmpEntry >= max)
	{
		FReq_EntrySelected = -1;
		diskOp_DrawDirectory();

		return true;
	}

	if (tmpEntry != FReq_EntrySelected)
	{
		FReq_EntrySelected = tmpEntry;
		diskOp_DrawDirectory();
	}

	return true;
}

void testDiskOpMouseRelease(void)
{
	if (editor.ui.diskOpShown && FReq_EntrySelected != -1)
	{
		if (mouse.x >= 169 && mouse.x <= 329 && mouse.y >= 4 && mouse.y <= 168)
			fileListPressed((mouse.y - 4) / (FONT1_CHAR_H + 1));

		FReq_EntrySelected = -1;
		diskOp_DrawDirectory();
	}
}

static uint8_t handleEntrySkip(UNICHAR *nameU, bool isDir)
{
	char *name, *extPtr;
	int32_t nameLen, extOffset, extLen;

	// skip if illegal name or filesize >32-bit
	if (nameU == NULL)
		return true;

	name = unicharToCp437(nameU, false);
	if (name == NULL)
		return true;
	
	if (name[0] == '\0')
		goto skipEntry;

	nameLen = (int32_t)strlen(name);

	// skip ".name" dirs/files
	if (nameLen >= 2 && name[0] == '.' && name[1] != '.')
		goto skipEntry;

	if (isDir)
	{
		// skip '.' directory
		if (nameLen == 1 && name[0] == '.')
			goto skipEntry;

		// macOS/Linux: skip '..' directory if we're in root
#ifndef _WIN32
		if (nameLen == 2 && name[0] == '.' && name[1] == '.')
		{
			if (FReq_CurPathU[0] == '/' && FReq_CurPathU[1] == '\0')
				goto skipEntry;
		}
#endif
	}
	else if (!FReq_ShowAllFiles)
	{
		extOffset = getExtOffset(name, nameLen);
		if (extOffset == -1)
			goto skipEntry;

		extLen = (int32_t)strlen(&name[extOffset]);
		if (extLen < 3 || extLen > 5)
			goto skipEntry; // no possibly known extensions to filter out

		extPtr = &name[extOffset];

		// decide what entries to keep based on file extension
		switch (FReq_Item)
		{
			default:
			case DISKOP_ITEM_MODULE:
			{
				if (extLen == 3)
				{
					if (_stricmp(".xm", extPtr) && _stricmp(".ft", extPtr))
						goto skipEntry; // skip, none of the extensions above
				}
				else if (extLen == 4)
				{
					if (_stricmp(".nst", extPtr) && _stricmp(".mod", extPtr) &&
						_stricmp(".s3m", extPtr) && _stricmp(".stm", extPtr) &&
						_stricmp(".fst", extPtr) && _stricmp(".wav", extPtr))
					{
						goto skipEntry; // skip, none of the extensions above
					}
				}
				else
				{
					goto skipEntry;
				}
			}
			break;

			case DISKOP_ITEM_INSTR:
			{
				if (extLen == 3)
				{
					if (_stricmp(".xi", extPtr))
						goto skipEntry; // skip, none of the extensions above
				}
				else if (extLen == 4)
				{
					if (_stricmp(".iff", extPtr) && _stricmp(".raw", extPtr) &&
						_stricmp(".wav", extPtr) && _stricmp(".snd", extPtr) &&
						_stricmp(".smp", extPtr) && _stricmp(".sam", extPtr) &&
						_stricmp(".aif", extPtr) && _stricmp(".pat", extPtr))
					{
						goto skipEntry; // skip, none of the extensions above
					}
				}
				else if (extLen == 5)
				{
					if (_stricmp(".aiff", extPtr))
						goto skipEntry; // skip, not the extension above
				}
				else
				{
					goto skipEntry;
				}
			}
			break;

			case DISKOP_ITEM_SAMPLE:
			{
				if (extLen == 4)
				{
					if (_stricmp(".iff", extPtr) && _stricmp(".raw", extPtr) &&
						_stricmp(".wav", extPtr) && _stricmp(".snd", extPtr) &&
						_stricmp(".smp", extPtr) && _stricmp(".sam", extPtr) &&
						_stricmp(".aif", extPtr))
					{
						goto skipEntry; // skip, none of the extensions above
					}
				}
				else if (extLen == 5)
				{
					if (_stricmp(".aiff", extPtr))
						goto skipEntry; // skip, not the extension above
				}
				else
				{
					goto skipEntry;
				}
			}
			break;

			case DISKOP_ITEM_PATTERN:
			{
				if (extLen == 3)
				{
					if (_stricmp(".xp", extPtr))
						goto skipEntry; // skip, not the extension above
				}
				else
				{
					goto skipEntry;
				}
			}
			break;

			case DISKOP_ITEM_TRACK:
			{
				if (extLen == 3)
				{
					if (_stricmp(".xt", extPtr))
						goto skipEntry;  // skip, not the extension above
				}
				else
				{
					goto skipEntry;
				}
			}
			break;
		}
	}

	free(name);
	return false; // "Show All Files" mode is enabled, don't skip entry

skipEntry:
	free(name);
	return true;
}

static int8_t findFirst(DirRec *searchRec)
{
#ifdef _WIN32
	WIN32_FIND_DATAW fData;
#else
	struct dirent *fData;
	struct stat st;
	int64_t fSize;
#endif

#if defined(__sun) || defined(sun)
	struct stat s;
#endif

	searchRec->nameU = NULL; // this one must be initialized

#ifdef _WIN32
	hFind = FindFirstFileW(L"*", &fData);
	if (hFind == NULL || hFind == INVALID_HANDLE_VALUE)
		return LFF_DONE;

	searchRec->nameU = UNICHAR_STRDUP(fData.cFileName);
	if (searchRec->nameU == NULL)
		return LFF_SKIP;

	searchRec->filesize = (fData.nFileSizeHigh > 0) ? -1 : fData.nFileSizeLow;
	searchRec->isDir = (fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false;
#else
	hFind = opendir(".");
	if (hFind == NULL)
		return LFF_DONE;

	fData = readdir(hFind);
	if (fData == NULL)
		return LFF_DONE;

	searchRec->nameU = UNICHAR_STRDUP(fData->d_name);
	if (searchRec->nameU == NULL)
		return LFF_SKIP;

	searchRec->filesize = 0;

#if defined(__sun) || defined(sun)
	stat(fData->d_name, &s);
	searchRec->isDir = (s.st_mode != S_IFDIR) ? true : false;
#else
	searchRec->isDir = (fData->d_type == DT_DIR) ? true : false;
#endif

#if defined(__sun) || defined(sun)
	if (s.st_mode == S_IFLNK)
#else
	if (fData->d_type == DT_UNKNOWN || fData->d_type == DT_LNK)
#endif
	{
		if (stat(fData->d_name, &st) == 0)
		{
			fSize = (int64_t)st.st_size;
			searchRec->filesize = (fSize > INT32_MAX) ? -1 : (fSize & 0xFFFFFFFF);

			if ((st.st_mode & S_IFMT) == S_IFDIR)
				searchRec->isDir = true;
		}
	}
	else if (!searchRec->isDir)
	{
		if (stat(fData->d_name, &st) == 0)
		{
			fSize = (int64_t)st.st_size;
			searchRec->filesize = (fSize > INT32_MAX) ? -1 : (fSize & 0xFFFFFFFF);
		}
	}
#endif

	if (handleEntrySkip(searchRec->nameU, searchRec->isDir))
	{
		// skip file
		free(searchRec->nameU);
		searchRec->nameU = NULL;

		return LFF_SKIP;
	}

	return LFF_OK;
}

static int8_t findNext(DirRec *searchRec)
{
#ifdef _WIN32
	WIN32_FIND_DATAW fData;
#else
	struct dirent *fData;
	struct stat st;
	int64_t fSize;
#endif

#if defined(__sun) || defined(sun)
	struct stat s;
#endif

	searchRec->nameU = NULL; // important

#ifdef _WIN32
	if (hFind == NULL || FindNextFileW(hFind, &fData) == 0)
		return LFF_DONE;

	searchRec->nameU = UNICHAR_STRDUP(fData.cFileName);
	if (searchRec->nameU == NULL)
		return LFF_SKIP;

	searchRec->filesize = (fData.nFileSizeHigh > 0) ? -1 : fData.nFileSizeLow;
	searchRec->isDir = (fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false;
#else
	if (hFind == NULL || (fData = readdir(hFind)) == NULL)
		return LFF_DONE;

	searchRec->nameU = UNICHAR_STRDUP(fData->d_name);
	if (searchRec->nameU == NULL)
		return LFF_SKIP;

	searchRec->filesize = 0;

#if defined(__sun) || defined(sun)
	stat(fData->d_name, &s);
	searchRec->isDir = (s.st_mode != S_IFDIR) ? true : false;
#else
	searchRec->isDir = (fData->d_type == DT_DIR) ? true : false;
#endif

#if defined(__sun) || defined(sun)
	if (s.st_mode == S_IFLNK)
#else
	if (fData->d_type == DT_UNKNOWN || fData->d_type == DT_LNK)
#endif
	{
		if (stat(fData->d_name, &st) == 0)
		{
			fSize = (int64_t)st.st_size;
			searchRec->filesize = (fSize > INT32_MAX) ? -1 : (fSize & 0xFFFFFFFF);

			if ((st.st_mode & S_IFMT) == S_IFDIR)
				searchRec->isDir = true;
		}
	}
	else if (!searchRec->isDir)
	{
		if (stat(fData->d_name, &st) == 0)
		{
			fSize = (int64_t)st.st_size;
			searchRec->filesize = (fSize > INT32_MAX) ? -1 : (fSize & 0xFFFFFFFF);
		}
	}
#endif

	if (handleEntrySkip(searchRec->nameU, searchRec->isDir))
	{
		// skip file
		free(searchRec->nameU);
		searchRec->nameU = NULL;

		return LFF_SKIP;
	}

	return LFF_OK;
}

static void findClose(void)
{
	if (hFind != NULL)
	{
#ifdef _WIN32
		FindClose(hFind);
#else
		closedir(hFind);
#endif
		hFind = NULL;
	}
}

static bool swapBufferEntry(int32_t a, int32_t b) // used for sorting
{
	DirRec tmpBuffer;

	if (a >= FReq_FileCount || b >= FReq_FileCount)
		return false;

	tmpBuffer = FReq_Buffer[a];
	FReq_Buffer[a] = FReq_Buffer[b];
	FReq_Buffer[b] = tmpBuffer;

	return true;
}

static char *ach(int32_t rad) // used for sortDirectory()
{
	char *p, *name;
	int32_t i, nameLen, extLen;
	DirRec *dirEntry;

	dirEntry = &FReq_Buffer[rad];

	name = unicharToCp437(dirEntry->nameU, true);
	if (name == NULL)
		return NULL;

	nameLen = (int32_t)strlen(name);
	if (nameLen == 0)
	{
		free(name);
		return NULL;
	}

	p = (char *)malloc(nameLen + 2);
	if (p == NULL)
	{
		free(name);
		return NULL;
	}

	if (dirEntry->isDir)
	{
		// directory

		if (nameLen == 2 && name[0] == '.' && name[1] == '.')
			p[0] = 0x01; // make ".." directory first priority
		else
			p[0] = 0x02; // make second priority

		strcpy(&p[1], name);

		free(name);
		return p;
	}
	else
	{
		// file

		i = getExtOffset(name, nameLen);
		if (config.cfg_SortPriority == 1 || i == -1)
		{
			// sort by filename
			strcpy(p, name);
			free(name);
			return p;
		}
		else
		{
			// sort by filename extension
			extLen = nameLen - i;
			if (extLen <= 1)
			{
				strcpy(p, name);
				free(name);
				return p;
			}

			// FILENAME.EXT -> EXT.FILENAME (for sorting)
			memcpy(p, &name[i+1], extLen - 1);
			memcpy(&p[extLen-1], name, i);
			p[nameLen-1] = '\0';

			free(name);
			return p;
		}
	}
}

static void sortDirectory(void)
{
	bool didSwap;
	char *p1, *p2;
	uint32_t offset, limit, i;

	if (FReq_FileCount < 2)
		return; // no need to sort

	offset = FReq_FileCount / 2;
	while (offset > 0)
	{
		limit = FReq_FileCount - offset;
		do
		{
			didSwap = false;
			for (i = 0; i < limit; i++)
			{
				p1 = ach(i);
				p2 = ach(offset + i);

				if (p1 == NULL || p2 == NULL)
				{
					if (p1 != NULL) free(p1);
					if (p2 != NULL) free(p2);
					okBox(0, "System message", "Not enough memory!");
					return;
				}

				if (_stricmp(p1, p2) > 0)
				{
					if (!swapBufferEntry(i, offset + i))
					{
						free(p1);
						free(p2);
						return;
					}

					didSwap = true;
				}

				free(p1);
				free(p2);
			}
		}
		while (didSwap);

		offset /= 2;
	}
}

static uint8_t numDigits32(uint32_t x)
{
	if (x >= 1000000000) return 10;
	if (x >=  100000000) return  9;
	if (x >=   10000000) return  8;
	if (x >=    1000000) return  7;
	if (x >=     100000) return  6;
	if (x >=      10000) return  5;
	if (x >=       1000) return  4;
	if (x >=        100) return  3;
	if (x >=         10) return  2;
	
	return 1;
}

static void printFormattedFilesize(uint16_t x, uint16_t y, uint32_t bufEntry)
{
	char sizeStrBuffer[16];
	int32_t filesize, printFilesize;

	filesize = FReq_Buffer[bufEntry].filesize;
	if (filesize == -1)
	{
		x += 6;
		textOut(x, y, PAL_BLCKTXT, ">2GB");
		return;
	}

	assert(filesize >= 0);

	if (filesize >= 1024*1024*10) // >= 10MB?
	{
forceMB:
		printFilesize = (int32_t)ceil(filesize / (1024.0*1024.0));
		x += (4 - numDigits32(printFilesize)) * (FONT1_CHAR_W - 1);
		sprintf(sizeStrBuffer, "%dM", printFilesize);
	}
	else if (filesize >= 1024*10) // >= 10kB?
	{
		printFilesize = (int32_t)ceil(filesize / 1024.0);
		if (printFilesize > 9999)
			goto forceMB; // ceil visual overflow kludge

		x += (4 - numDigits32(printFilesize)) * (FONT1_CHAR_W - 1);
		sprintf(sizeStrBuffer, "%dk", printFilesize);
	}
	else // bytes
	{
		printFilesize = filesize;
		x += (5 - numDigits32(printFilesize)) * (FONT1_CHAR_W - 1);
		sprintf(sizeStrBuffer, "%d", printFilesize);
	}

	textOut(x, y, PAL_BLCKTXT, sizeStrBuffer);
}

static void displayCurrPath(void)
{
	char *p, *delimiter, *asciiPath;
	int32_t j;
	uint32_t pathLen;

	fillRect(4, 145, 162, 10, PAL_DESKTOP);

	pathLen = (uint32_t)UNICHAR_STRLEN(FReq_CurPathU);
	if (pathLen == 0)
		return;

	asciiPath = unicharToCp437(FReq_CurPathU, true);
	if (asciiPath == NULL)
	{
		okBox(0, "System message", "Not enough memory!");
		return;
	}

	p = asciiPath;
	if (textWidth(p) <= 162)
	{
		// path fits, print it all
		textOut(4, 145, PAL_FORGRND, p);
	}
	else
	{
		// path doesn't fit, print drive + ".." + last directory

		memset(FReq_NameTemp, 0, PATH_MAX + 1);
#ifdef _WIN32
		strncpy(FReq_NameTemp, p, 3);
		strcat(FReq_NameTemp, ".\001"); // special character in font
#else
		strcpy(FReq_NameTemp, "/");
		strcat(FReq_NameTemp, "..");
#endif

		delimiter = strrchr(p, DIR_DELIMITER);
		if (delimiter != NULL)
		{
#ifdef _WIN32
			strcat(FReq_NameTemp, delimiter + 1);
#else
			strcat(FReq_NameTemp, delimiter);
#endif
		}

		j = (int32_t)strlen(FReq_NameTemp);
		if (j > 6)
		{
			j--;

			p = FReq_NameTemp;
			while (j >= 6 && textWidth(p) >= 162)
			{
				p[j - 2] = '.';
				p[j - 1] = '.';
				p[j - 0] = '\0';
				j--;
			}
		}

		textOutClipX(4, 145, PAL_FORGRND, FReq_NameTemp, 165);
	}

	free(asciiPath);
}

void diskOp_DrawDirectory(void)
{
	char *readName;
	uint16_t y;
	int32_t bufEntry;

	clearRect(FILENAME_TEXT_X - 1, 4, 162, 164);
	drawTextBox(TB_DISKOP_FILENAME);

	if (FReq_EntrySelected != -1)
	{
		y = 4 + (uint16_t)((FONT1_CHAR_H + 1) * FReq_EntrySelected);
		fillRect(FILENAME_TEXT_X - 1, y, 162, FONT1_CHAR_H, PAL_PATTEXT);
	}

	displayCurrPath();
#ifdef _WIN32
	setupDiskOpDrives();
#endif

	if (FReq_FileCount == 0)
		return;

	for (uint16_t i = 0; i < DISKOP_ENTRY_NUM; i++)
	{
		bufEntry = FReq_DirPos + i;
		if (bufEntry >= FReq_FileCount)
			break;

		if (FReq_Buffer[bufEntry].nameU == NULL)
			continue;

		// convert unichar name to codepage 437
		readName = unicharToCp437(FReq_Buffer[bufEntry].nameU, true);
		if (readName == NULL)
			continue;

		y = 4 + (i * (FONT1_CHAR_H + 1));

		// shrink entry name and add ".." if it doesn't fit on screen
		trimEntryName(readName, FReq_Buffer[bufEntry].isDir);

		if (FReq_Buffer[bufEntry].isDir)
		{
			// directory
			charOut(FILENAME_TEXT_X, y, PAL_BLCKTXT, DIR_DELIMITER);
			textOut(FILENAME_TEXT_X + FONT1_CHAR_W, y, PAL_BLCKTXT, readName);
		}
		else
		{
			// filename
			textOut(FILENAME_TEXT_X, y, PAL_BLCKTXT, readName);
		}

		free(readName);

		if (!FReq_Buffer[bufEntry].isDir)
			printFormattedFilesize(FILESIZE_TEXT_X, y, bufEntry);
	}

	setScrollBarPos(SB_DISKOP_LIST, FReq_DirPos, true);
	setScrollBarEnd(SB_DISKOP_LIST, FReq_FileCount);
}

static DirRec *bufferCreateEmptyDir(void) // special case: creates a dir entry with a ".." directory
{
	DirRec *dirEntry;

	dirEntry = (DirRec *)malloc(sizeof (DirRec));
	if (dirEntry == NULL)
		return NULL;

	dirEntry->nameU = UNICHAR_STRDUP(PARENT_DIR_STR);
	if (dirEntry->nameU == NULL)
	{
		free(dirEntry);
		return NULL;
	}

	dirEntry->isDir = true;
	dirEntry->filesize = 0;

	return dirEntry;
}

static int32_t SDLCALL diskOp_ReadDirectoryThread(void *ptr)
{
	uint8_t lastFindFileFlag;
	DirRec tmpBuffer, *newPtr;

	(void)ptr;

	FReq_DirPos = 0;

	// free old buffer
	freeDirRecBuffer();

	UNICHAR_GETCWD(FReq_CurPathU, PATH_MAX);

	// read first file
	lastFindFileFlag = findFirst(&tmpBuffer);
	if (lastFindFileFlag != LFF_DONE && lastFindFileFlag != LFF_SKIP)
	{
		FReq_Buffer = (DirRec *)malloc(sizeof (DirRec) * (FReq_FileCount + 1));
		if (FReq_Buffer == NULL)
		{
			findClose();

			okBoxThreadSafe(0, "System message", "Not enough memory!");

			FReq_Buffer = bufferCreateEmptyDir();
			if (FReq_Buffer != NULL)
				FReq_FileCount = 1;
			else
				okBoxThreadSafe(0, "System message", "Not enough memory!");

			setMouseBusy(false);
			return false;
		}

		memcpy(&FReq_Buffer[FReq_FileCount], &tmpBuffer, sizeof (DirRec));
		FReq_FileCount++;
	}

	// read remaining files
	while (lastFindFileFlag != LFF_DONE)
	{
		lastFindFileFlag = findNext(&tmpBuffer);
		if (lastFindFileFlag != LFF_DONE && lastFindFileFlag != LFF_SKIP)
		{
			newPtr = (DirRec *)realloc(FReq_Buffer, sizeof (DirRec) * (FReq_FileCount + 1));
			if (newPtr == NULL)
			{
				freeDirRecBuffer();
				okBoxThreadSafe(0, "System message", "Not enough memory!");
				break;
			}

			FReq_Buffer = newPtr;

			memcpy(&FReq_Buffer[FReq_FileCount], &tmpBuffer, sizeof (DirRec));
			FReq_FileCount++;
		}
	}

	findClose();

	if (FReq_FileCount > 0)
	{
		sortDirectory();
	}
	else
	{
		// access denied or out of memory - create parent directory link
		FReq_Buffer = bufferCreateEmptyDir();
		if (FReq_Buffer != NULL)
			FReq_FileCount = 1;
		else
			okBoxThreadSafe(0, "System message", "Not enough memory!");
	}

	editor.diskOpReadDone = true;

	setMouseBusy(false);

	return true;
}

void startDiskOpFillThread(void)
{
	editor.diskOpReadDone = false;

	mouseAnimOn();
	thread = SDL_CreateThread(diskOp_ReadDirectoryThread, NULL, NULL);
	if (thread == NULL)
	{
		editor.diskOpReadDone = true;
		okBox(0, "System message", "Couldn't create thread!");
		return;
	}

	SDL_DetachThread(thread);
}

static void drawSaveAsElements(void)
{
	switch (FReq_Item)
	{
		default:
		case DISKOP_ITEM_MODULE:
		{
			textOutShadow(19, 101, PAL_FORGRND, PAL_DSKTOP2, "MOD");
			textOutShadow(19, 115, PAL_FORGRND, PAL_DSKTOP2, "XM");
			textOutShadow(19, 129, PAL_FORGRND, PAL_DSKTOP2, "WAV");
		}
		break;

		case DISKOP_ITEM_INSTR:
			textOutShadow(19, 101, PAL_FORGRND, PAL_DSKTOP2, "XI");
		break;

		case DISKOP_ITEM_SAMPLE:
		{
			textOutShadow(19, 101, PAL_FORGRND, PAL_DSKTOP2, "RAW");
			textOutShadow(19, 115, PAL_FORGRND, PAL_DSKTOP2, "IFF");
			textOutShadow(19, 129, PAL_FORGRND, PAL_DSKTOP2, "WAV");
		}
		break;

		case DISKOP_ITEM_PATTERN:
			textOutShadow(19, 101, PAL_FORGRND, PAL_DSKTOP2, "XP");
		break;

		case DISKOP_ITEM_TRACK:
			textOutShadow(19, 101, PAL_FORGRND, PAL_DSKTOP2, "XT");
		break;
	}
}

static void setDiskOpItemRadioButtons(void)
{
	uncheckRadioButtonGroup(RB_GROUP_DISKOP_MOD_SAVEAS);
	uncheckRadioButtonGroup(RB_GROUP_DISKOP_INS_SAVEAS);
	uncheckRadioButtonGroup(RB_GROUP_DISKOP_SMP_SAVEAS);
	uncheckRadioButtonGroup(RB_GROUP_DISKOP_PAT_SAVEAS);
	uncheckRadioButtonGroup(RB_GROUP_DISKOP_TRK_SAVEAS);

	hideRadioButtonGroup(RB_GROUP_DISKOP_MOD_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_INS_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_SMP_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_PAT_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_TRK_SAVEAS);

	if (editor.moduleSaveMode > 3)
		editor.moduleSaveMode = 3;

	if (editor.sampleSaveMode > 3)
		editor.sampleSaveMode = 3;

	radioButtons[RB_DISKOP_MOD_SAVEAS_MOD + editor.moduleSaveMode].state = RADIOBUTTON_CHECKED;
	radioButtons[RB_DISKOP_SMP_SAVEAS_RAW + editor.sampleSaveMode].state = RADIOBUTTON_CHECKED;

	if (FReq_Item == DISKOP_ITEM_INSTR)   radioButtons[RB_DISKOP_INS_SAVEAS_XI].state = RADIOBUTTON_CHECKED;
	if (FReq_Item == DISKOP_ITEM_PATTERN) radioButtons[RB_DISKOP_PAT_SAVEAS_XP].state = RADIOBUTTON_CHECKED;
	if (FReq_Item == DISKOP_ITEM_TRACK)   radioButtons[RB_DISKOP_TRK_SAVEAS_XT].state = RADIOBUTTON_CHECKED;

	if (editor.ui.diskOpShown)
	{
		switch (FReq_Item)
		{
			default: case DISKOP_ITEM_MODULE:  showRadioButtonGroup(RB_GROUP_DISKOP_MOD_SAVEAS); break;
			         case DISKOP_ITEM_INSTR:   showRadioButtonGroup(RB_GROUP_DISKOP_INS_SAVEAS); break;
			         case DISKOP_ITEM_SAMPLE:  showRadioButtonGroup(RB_GROUP_DISKOP_SMP_SAVEAS); break;
			         case DISKOP_ITEM_PATTERN: showRadioButtonGroup(RB_GROUP_DISKOP_PAT_SAVEAS); break;
			         case DISKOP_ITEM_TRACK:   showRadioButtonGroup(RB_GROUP_DISKOP_TRK_SAVEAS); break;
		}
	}
}

static void setDiskOpItem(uint8_t item)
{
	int32_t pathLen;

	hideRadioButtonGroup(RB_GROUP_DISKOP_MOD_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_INS_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_SMP_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_PAT_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_TRK_SAVEAS);

	if (item > 4)
		item = 4;

	FReq_Item = item;
	switch (FReq_Item)
	{
		default: case DISKOP_ITEM_MODULE:  FReq_FileName = modTmpFName; FReq_CurPathU = FReq_ModCurPathU; break;
		         case DISKOP_ITEM_INSTR:   FReq_FileName = insTmpFName; FReq_CurPathU = FReq_InsCurPathU; break;
		         case DISKOP_ITEM_SAMPLE:  FReq_FileName = smpTmpFName; FReq_CurPathU = FReq_SmpCurPathU; break;
		         case DISKOP_ITEM_PATTERN: FReq_FileName = patTmpFName; FReq_CurPathU = FReq_PatCurPathU; break;
		         case DISKOP_ITEM_TRACK:   FReq_FileName = trkTmpFName; FReq_CurPathU = FReq_TrkCurPathU; break;
	}

	pathLen = (int32_t)UNICHAR_STRLEN(FReq_CurPathU);
	if (pathLen == 0)
	{
		memset(FReq_CurPathU, 0, (PATH_MAX + 2) * sizeof (UNICHAR));
		UNICHAR_STRCPY(FReq_CurPathU, FReq_ModCurPathU);
	}
	else
	{
		UNICHAR_CHDIR(FReq_CurPathU);
	}

	textBoxes[TB_DISKOP_FILENAME].textPtr = FReq_FileName;

	FReq_ShowAllFiles = false;

	if (editor.ui.diskOpShown)
	{
		editor.diskOpReadDir = true;

		fillRect(4, 101, 40, 38, PAL_DESKTOP);
		drawSaveAsElements();
		setDiskOpItemRadioButtons();

		diskOp_DrawDirectory();
		drawTextBox(TB_DISKOP_FILENAME);
	}
	else
	{
		editor.diskOpReadOnOpen = true;
	}
}

static void drawDiskOpScreen(void)
{
	drawFramework(0,     0,  67,  86, FRAMEWORK_TYPE1);
	drawFramework(67,    0,  64, 142, FRAMEWORK_TYPE1);
	drawFramework(131,   0,  37, 142, FRAMEWORK_TYPE1);
	drawFramework(0,    86,  67,  56, FRAMEWORK_TYPE1);
	drawFramework(0,   142, 168,  31, FRAMEWORK_TYPE1);
	drawFramework(168,   0, 164,   3, FRAMEWORK_TYPE1);
	drawFramework(168, 170, 164,   3, FRAMEWORK_TYPE1);
	drawFramework(332,   0,  24, 173, FRAMEWORK_TYPE1);
	drawFramework(30,  157, 136,  14, FRAMEWORK_TYPE2);

	clearRect(168, 2, 164, 168);

	showPushButton(PB_DISKOP_SAVE);
	showPushButton(PB_DISKOP_DELETE);
	showPushButton(PB_DISKOP_RENAME);
	showPushButton(PB_DISKOP_MAKEDIR);
	showPushButton(PB_DISKOP_REFRESH);
	showPushButton(PB_DISKOP_EXIT);
	showPushButton(PB_DISKOP_PARENT);
	showPushButton(PB_DISKOP_ROOT);
	showPushButton(PB_DISKOP_SHOW_ALL);
	showPushButton(PB_DISKOP_SET_PATH);
	showPushButton(PB_DISKOP_LIST_UP);
	showPushButton(PB_DISKOP_LIST_DOWN);

	showScrollBar(SB_DISKOP_LIST);
	showTextBox(TB_DISKOP_FILENAME);

	textBoxes[TB_DISKOP_FILENAME].textPtr = FReq_FileName;

	if (FReq_Item > 4)
		FReq_Item = 4;

	uncheckRadioButtonGroup(RB_GROUP_DISKOP_ITEM);
	radioButtons[RB_DISKOP_MODULE + FReq_Item].state = RADIOBUTTON_CHECKED;
	showRadioButtonGroup(RB_GROUP_DISKOP_ITEM);

	// item selector
	textOutShadow(5,   3, PAL_FORGRND, PAL_DSKTOP2, "Item:");
	textOutShadow(19, 17, PAL_FORGRND, PAL_DSKTOP2, "Module");
	textOutShadow(19, 31, PAL_FORGRND, PAL_DSKTOP2, "Instr.");
	textOutShadow(19, 45, PAL_FORGRND, PAL_DSKTOP2, "Sample");
	textOutShadow(19, 59, PAL_FORGRND, PAL_DSKTOP2, "Pattern");
	textOutShadow(19, 73, PAL_FORGRND, PAL_DSKTOP2, "Track");

	// file format
	textOutShadow(5,  89, PAL_FORGRND, PAL_DSKTOP2, "Save as:");
	drawSaveAsElements();
	setDiskOpItemRadioButtons();

	// filename
	textOutShadow(4, 159, PAL_FORGRND, PAL_DSKTOP2, "File:");

	diskOp_DrawDirectory();
}

void showDiskOpScreen(void)
{
	if (editor.ui.extended)
		exitPatternEditorExtended();

	hideTopScreen();
	editor.ui.diskOpShown = true;
	editor.ui.scopesShown = false;

	showTopRightMainScreen();
	drawDiskOpScreen();

	// a flag that says if we need to update the filelist after disk op. was shown
	if (editor.diskOpReadOnOpen)
	{
		editor.diskOpReadOnOpen = false;
		startDiskOpFillThread();
	}
}

void hideDiskOpScreen(void)
{
#ifdef _WIN32
	for (uint16_t i = 0; i < DISKOP_MAX_DRIVE_BUTTONS; i++)
		hidePushButton(PB_DISKOP_DRIVE1 + i);
#endif

	hidePushButton(PB_DISKOP_SAVE);
	hidePushButton(PB_DISKOP_DELETE);
	hidePushButton(PB_DISKOP_RENAME);
	hidePushButton(PB_DISKOP_MAKEDIR);
	hidePushButton(PB_DISKOP_REFRESH);
	hidePushButton(PB_DISKOP_EXIT);
	hidePushButton(PB_DISKOP_PARENT);
	hidePushButton(PB_DISKOP_ROOT);
	hidePushButton(PB_DISKOP_SHOW_ALL);
	hidePushButton(PB_DISKOP_SET_PATH);
	hidePushButton(PB_DISKOP_LIST_UP);
	hidePushButton(PB_DISKOP_LIST_DOWN);

	hideScrollBar(SB_DISKOP_LIST);
	hideTextBox(TB_DISKOP_FILENAME);
	hideRadioButtonGroup(RB_GROUP_DISKOP_ITEM);
	hideRadioButtonGroup(RB_GROUP_DISKOP_MOD_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_INS_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_SMP_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_PAT_SAVEAS);
	hideRadioButtonGroup(RB_GROUP_DISKOP_TRK_SAVEAS);

	editor.ui.diskOpShown = false;
}

void exitDiskOpScreen(void)
{
	hideDiskOpScreen();
	editor.ui.oldTopLeftScreen = 0; // disk op. ignores previously opened top screens
	showTopScreen(true);
}

void toggleDiskOpScreen(void)
{
	if (editor.ui.diskOpShown)
		exitDiskOpScreen();
	else
		showDiskOpScreen();
}

void sbDiskOpSetPos(uint32_t pos)
{
	if ((int32_t)pos != FReq_DirPos && FReq_FileCount > DISKOP_ENTRY_NUM)
	{
		FReq_DirPos = (int32_t)pos;
		diskOp_DrawDirectory();
	}
}

void pbDiskOpListUp(void)
{
	if (FReq_DirPos > 0 && FReq_FileCount > DISKOP_ENTRY_NUM)
		scrollBarScrollUp(SB_DISKOP_LIST, 1);
}

void pbDiskOpListDown(void)
{
	if (FReq_DirPos < FReq_FileCount-DISKOP_ENTRY_NUM && FReq_FileCount > DISKOP_ENTRY_NUM)
		scrollBarScrollDown(SB_DISKOP_LIST, 1);
}

void pbDiskOpParent(void)
{
	diskOpGoParent();
}

void pbDiskOpRoot(void)
{
#ifdef _WIN32
	openDirectory(L"\\");
#else
	openDirectory("/");
#endif
}

void pbDiskOpShowAll(void)
{
	FReq_ShowAllFiles = true;
	editor.diskOpReadDir = true; // refresh dir
}

#ifdef _WIN32
void pbDiskOpDrive1(void) { openDrive(logicalDriveNames[driveIndexes[0]]); }
void pbDiskOpDrive2(void) { openDrive(logicalDriveNames[driveIndexes[1]]); }
void pbDiskOpDrive3(void) { openDrive(logicalDriveNames[driveIndexes[2]]); }
void pbDiskOpDrive4(void) { openDrive(logicalDriveNames[driveIndexes[3]]); }
void pbDiskOpDrive5(void) { openDrive(logicalDriveNames[driveIndexes[4]]); }
void pbDiskOpDrive6(void) { openDrive(logicalDriveNames[driveIndexes[5]]); }
void pbDiskOpDrive7(void) { openDrive(logicalDriveNames[driveIndexes[6]]); }
void pbDiskOpDrive8(void) { openDrive(logicalDriveNames[driveIndexes[7]]); }
#endif

void pbDiskOpDelete(void)
{
	setMouseMode(MOUSE_MODE_DELETE);
}

void pbDiskOpRename(void)
{
	setMouseMode(MOUSE_MODE_RENAME);
}

void pbDiskOpMakeDir(void)
{
	FReq_NameTemp[0] = '\0';
	if (inputBox(1, "Enter directory name:", FReq_NameTemp, PATH_MAX - 1) == 1)
	{
		if (FReq_NameTemp[0] == '\0')
		{
			okBox(0, "System message", "Name can't be empty!");
			return;
		}

		if (makeDirAnsi(FReq_NameTemp))
			editor.diskOpReadDir = true;
		else
			okBox(0, "System message", "Couldn't create directory: Access denied, or a dir with the same name already exists!");
	}
}

void pbDiskOpRefresh(void)
{
	editor.diskOpReadDir = true; // refresh dir
#ifdef _WIN32
	setupDiskOpDrives();
#endif
}

void pbDiskOpSetPath(void)
{
	FReq_NameTemp[0] = '\0';
	if (inputBox(1, "Enter new directory path:", FReq_NameTemp, PATH_MAX - 1) == 1)
	{
		if (FReq_NameTemp[0] == '\0')
		{
			okBox(0, "System message", "Name can't be empty!");
			return;
		}

		if (chdir(FReq_NameTemp) == 0)
			editor.diskOpReadDir = true;
		else
			okBox(0, "System message", "Couldn't set directory path!");
	}
}

void pbDiskOpExit(void)
{
	exitDiskOpScreen();
}

void rbDiskOpModule(void)
{
	checkRadioButton(RB_DISKOP_MODULE);
	setDiskOpItem(DISKOP_ITEM_MODULE);
}

void rbDiskOpInstr(void)
{
	checkRadioButton(RB_DISKOP_INSTR);
	setDiskOpItem(DISKOP_ITEM_INSTR);
}

void rbDiskOpSample(void)
{
	checkRadioButton(RB_DISKOP_SAMPLE);
	setDiskOpItem(DISKOP_ITEM_SAMPLE);
}

void rbDiskOpPattern(void)
{
	checkRadioButton(RB_DISKOP_PATTERN);
	setDiskOpItem(DISKOP_ITEM_PATTERN);
}

void rbDiskOpTrack(void)
{
	checkRadioButton(RB_DISKOP_TRACK);
	setDiskOpItem(DISKOP_ITEM_TRACK);
}

void rbDiskOpModSaveMod(void)
{
	editor.moduleSaveMode = MOD_SAVE_MODE_MOD;
	checkRadioButton(RB_DISKOP_MOD_SAVEAS_MOD);
	diskOpChangeFilenameExt(".mod");

	updateCurrSongFilename(); // for window title
	updateWindowTitle(true);
}

void rbDiskOpModSaveXm(void)
{
	editor.moduleSaveMode = MOD_SAVE_MODE_XM;
	checkRadioButton(RB_DISKOP_MOD_SAVEAS_XM);
	diskOpChangeFilenameExt(".xm");

	updateCurrSongFilename(); // for window title
	updateWindowTitle(true);
}

void rbDiskOpModSaveWav(void)
{
	editor.moduleSaveMode = MOD_SAVE_MODE_WAV;
	checkRadioButton(RB_DISKOP_MOD_SAVEAS_WAV);
	diskOpChangeFilenameExt(".wav");

	updateCurrSongFilename(); // for window title
	updateWindowTitle(true);
}

void rbDiskOpSmpSaveRaw(void)
{
	editor.sampleSaveMode = SMP_SAVE_MODE_RAW;
	checkRadioButton(RB_DISKOP_SMP_SAVEAS_RAW);
	diskOpChangeFilenameExt(".raw");
}

void rbDiskOpSmpSaveIff(void)
{
	editor.sampleSaveMode = SMP_SAVE_MODE_IFF;
	checkRadioButton(RB_DISKOP_SMP_SAVEAS_IFF);
	diskOpChangeFilenameExt(".iff");
}

void rbDiskOpSmpSaveWav(void)
{
	editor.sampleSaveMode = SMP_SAVE_MODE_WAV;
	checkRadioButton(RB_DISKOP_SMP_SAVEAS_WAV);
	diskOpChangeFilenameExt(".wav");
}