/*
 * dlg_misc.cpp
 * ------------
 * Purpose: Implementation of various OpenMPT dialogs.
 * Notes  : (currently none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "Mptrack.h"
#include "Moddoc.h"
#include "Mainfrm.h"
#include "dlg_misc.h"
#include "Dlsbank.h"
#include "Childfrm.h"
#include "../soundlib/plugins/PlugInterface.h"
#include "ChannelManagerDlg.h"
#include "TempoSwingDialog.h"
#include "../soundlib/mod_specifications.h"
#include "../common/version.h"
#include "../common/mptStringBuffer.h"


OPENMPT_NAMESPACE_BEGIN


///////////////////////////////////////////////////////////////////////
// CModTypeDlg


BEGIN_MESSAGE_MAP(CModTypeDlg, CDialog)
	//{{AFX_MSG_MAP(CModTypeDlg)
	ON_CBN_SELCHANGE(IDC_COMBO1,			&CModTypeDlg::UpdateDialog)
	ON_CBN_SELCHANGE(IDC_COMBO_TEMPOMODE,	&CModTypeDlg::OnTempoModeChanged)
	ON_COMMAND(IDC_CHECK_PT1X,				&CModTypeDlg::OnPTModeChanged)
	ON_COMMAND(IDC_BUTTON1,					&CModTypeDlg::OnTempoSwing)
	ON_COMMAND(IDC_BUTTON2,					&CModTypeDlg::OnLegacyPlaybackSettings)
	ON_COMMAND(IDC_BUTTON3,					&CModTypeDlg::OnDefaultBehaviour)

	ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CModTypeDlg::OnToolTipNotify)

	//}}AFX_MSG_MAP

END_MESSAGE_MAP()


void CModTypeDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CModTypeDlg)
	DDX_Control(pDX, IDC_COMBO1,		m_TypeBox);
	DDX_Control(pDX, IDC_COMBO2,		m_ChannelsBox);
	DDX_Control(pDX, IDC_COMBO_TEMPOMODE,	m_TempoModeBox);
	DDX_Control(pDX, IDC_COMBO_MIXLEVELS,	m_PlugMixBox);

	DDX_Control(pDX, IDC_CHECK1,		m_CheckBox1);
	DDX_Control(pDX, IDC_CHECK2,		m_CheckBox2);
	DDX_Control(pDX, IDC_CHECK3,		m_CheckBox3);
	DDX_Control(pDX, IDC_CHECK4,		m_CheckBox4);
	DDX_Control(pDX, IDC_CHECK5,		m_CheckBox5);
	DDX_Control(pDX, IDC_CHECK_PT1X,	m_CheckBoxPT1x);
	DDX_Control(pDX, IDC_CHECK_AMIGALIMITS,	m_CheckBoxAmigaLimits);

	//}}AFX_DATA_MAP
}


BOOL CModTypeDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
	m_nType = sndFile.GetType();
	m_nChannels = sndFile.GetNumChannels();
	m_tempoSwing = sndFile.m_tempoSwing;
	m_playBehaviour = sndFile.m_playBehaviour;
	initialized = false;

	// Mod types

	m_TypeBox.SetItemData(m_TypeBox.AddString(_T("ProTracker MOD")), MOD_TYPE_MOD);
	m_TypeBox.SetItemData(m_TypeBox.AddString(_T("Scream Tracker S3M")), MOD_TYPE_S3M);
	m_TypeBox.SetItemData(m_TypeBox.AddString(_T("FastTracker XM")), MOD_TYPE_XM);
	m_TypeBox.SetItemData(m_TypeBox.AddString(_T("Impulse Tracker IT")), MOD_TYPE_IT);
	m_TypeBox.SetItemData(m_TypeBox.AddString(_T("OpenMPT MPTM")), MOD_TYPE_MPT);
	switch(m_nType)
	{
	case MOD_TYPE_S3M:	m_TypeBox.SetCurSel(1); break;
	case MOD_TYPE_XM:	m_TypeBox.SetCurSel(2); break;
	case MOD_TYPE_IT:	m_TypeBox.SetCurSel(3); break;
	case MOD_TYPE_MPT:	m_TypeBox.SetCurSel(4); break;
	default:			m_TypeBox.SetCurSel(0); break;
	}

	// Time signature information

	SetDlgItemInt(IDC_ROWSPERBEAT, sndFile.m_nDefaultRowsPerBeat);
	SetDlgItemInt(IDC_ROWSPERMEASURE, sndFile.m_nDefaultRowsPerMeasure);

	// Version information

	if(sndFile.m_dwCreatedWithVersion) SetDlgItemText(IDC_EDIT_CREATEDWITH, _T("OpenMPT ") + FormatVersionNumber(sndFile.m_dwCreatedWithVersion));
	SetDlgItemText(IDC_EDIT_SAVEDWITH, mpt::ToCString(sndFile.m_modFormat.madeWithTracker.empty() ? sndFile.m_modFormat.formatName : sndFile.m_modFormat.madeWithTracker));

	const int iconSize = Util::ScalePixels(32, m_hWnd);
	m_warnIcon = (HICON)::LoadImage(NULL, IDI_EXCLAMATION, IMAGE_ICON, iconSize, iconSize, LR_SHARED);

	UpdateDialog();

	initialized = true;
	EnableToolTips(TRUE);
	return TRUE;
}


CString CModTypeDlg::FormatVersionNumber(Version version)
{
	return mpt::ToCString(version.ToUString() + (version.IsTestVersion() ? U_(" (test build)") : U_("")));
}


void CModTypeDlg::UpdateChannelCBox()
{
	const MODTYPE type = static_cast<MODTYPE>(m_TypeBox.GetItemData(m_TypeBox.GetCurSel()));
	CHANNELINDEX currChanSel = static_cast<CHANNELINDEX>(m_ChannelsBox.GetItemData(m_ChannelsBox.GetCurSel()));
	const CHANNELINDEX minChans = CSoundFile::GetModSpecifications(type).channelsMin;
	const CHANNELINDEX maxChans = CSoundFile::GetModSpecifications(type).channelsMax;
	
	if(m_ChannelsBox.GetCount() < 1 
		|| m_ChannelsBox.GetItemData(0) != minChans
		|| m_ChannelsBox.GetItemData(m_ChannelsBox.GetCount() - 1) != maxChans)
	{
		// Update channel list if number of supported channels has changed.
		if(m_ChannelsBox.GetCount() < 1) currChanSel = m_nChannels;
		m_ChannelsBox.ResetContent();

		CString s;
		for(CHANNELINDEX i = minChans; i <= maxChans; i++)
		{
			s.Format(_T("%u Channel%s"), i, (i != 1) ? _T("s") : _T(""));
			m_ChannelsBox.SetItemData(m_ChannelsBox.AddString(s), i);
		}

		Limit(currChanSel, minChans, maxChans);
		m_ChannelsBox.SetCurSel(currChanSel - minChans);
	}
}


void CModTypeDlg::UpdateDialog()
{
	m_nType = static_cast<MODTYPE>(m_TypeBox.GetItemData(m_TypeBox.GetCurSel()));

	UpdateChannelCBox();

	m_CheckBox1.SetCheck(sndFile.m_SongFlags[SONG_LINEARSLIDES] ? BST_CHECKED : BST_UNCHECKED);
	m_CheckBox2.SetCheck(sndFile.m_SongFlags[SONG_FASTVOLSLIDES] ? BST_CHECKED : BST_UNCHECKED);
	m_CheckBox3.SetCheck(sndFile.m_SongFlags[SONG_ITOLDEFFECTS] ? BST_CHECKED : BST_UNCHECKED);
	m_CheckBox4.SetCheck(sndFile.m_SongFlags[SONG_ITCOMPATGXX] ? BST_CHECKED : BST_UNCHECKED);
	m_CheckBox5.SetCheck(sndFile.m_SongFlags[SONG_EXFILTERRANGE] ? BST_CHECKED : BST_UNCHECKED);
	m_CheckBoxPT1x.SetCheck(sndFile.m_SongFlags[SONG_PT_MODE] ? BST_CHECKED : BST_UNCHECKED);
	m_CheckBoxAmigaLimits.SetCheck(sndFile.m_SongFlags[SONG_AMIGALIMITS] ? BST_CHECKED : BST_UNCHECKED);

	const FlagSet<SongFlags> allowedFlags(sndFile.GetModSpecifications(m_nType).songFlags);
	m_CheckBox1.EnableWindow(allowedFlags[SONG_LINEARSLIDES]);
	m_CheckBox2.EnableWindow(allowedFlags[SONG_FASTVOLSLIDES]);
	m_CheckBox3.EnableWindow(allowedFlags[SONG_ITOLDEFFECTS]);
	m_CheckBox4.EnableWindow(allowedFlags[SONG_ITCOMPATGXX]);
	m_CheckBox5.EnableWindow(allowedFlags[SONG_EXFILTERRANGE]);
	m_CheckBoxPT1x.EnableWindow(allowedFlags[SONG_PT_MODE]);
	m_CheckBoxAmigaLimits.EnableWindow(allowedFlags[SONG_AMIGALIMITS]);

	// These two checkboxes are mutually exclusive and share the same screen space
	m_CheckBoxPT1x.ShowWindow(m_nType == MOD_TYPE_MOD ? SW_SHOW : SW_HIDE);
	m_CheckBox5.ShowWindow(m_nType != MOD_TYPE_MOD ? SW_SHOW : SW_HIDE);
	if(allowedFlags[SONG_PT_MODE]) OnPTModeChanged();

	// Tempo modes
	const TempoMode oldTempoMode = initialized ? static_cast<TempoMode>(m_TempoModeBox.GetItemData(m_TempoModeBox.GetCurSel())) : sndFile.m_nTempoMode;
	m_TempoModeBox.ResetContent();

	m_TempoModeBox.SetItemData(m_TempoModeBox.AddString(_T("Classic")), static_cast<DWORD_PTR>(TempoMode::Classic));
	if(m_nType == MOD_TYPE_MPT || (sndFile.GetType() != MOD_TYPE_MPT && sndFile.m_nTempoMode == TempoMode::Alternative))
		m_TempoModeBox.SetItemData(m_TempoModeBox.AddString(_T("Alternative")), static_cast<DWORD_PTR>(TempoMode::Alternative));
	if(m_nType == MOD_TYPE_MPT || (sndFile.GetType() != MOD_TYPE_MPT && sndFile.m_nTempoMode == TempoMode::Modern))
		m_TempoModeBox.SetItemData(m_TempoModeBox.AddString(_T("Modern (accurate)")), static_cast<DWORD_PTR>(TempoMode::Modern));
	m_TempoModeBox.SetCurSel(0);
	for(int i = m_TempoModeBox.GetCount(); i > 0; i--)
	{
		if(static_cast<TempoMode>(m_TempoModeBox.GetItemData(i)) == oldTempoMode)
		{
			m_TempoModeBox.SetCurSel(i);
			break;
		}
	}
	OnTempoModeChanged();

	// Mix levels
	const MixLevels oldMixLevels = initialized ? static_cast<MixLevels>(m_PlugMixBox.GetItemData(m_PlugMixBox.GetCurSel())) : sndFile.GetMixLevels();
	m_PlugMixBox.ResetContent();
	if(m_nType == MOD_TYPE_MPT || sndFile.GetMixLevels() == MixLevels::v1_17RC3) // In XM/IT, this is only shown for backwards compatibility with existing tunes
		m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("OpenMPT 1.17RC3")), static_cast<DWORD_PTR>(MixLevels::v1_17RC3));
	if(sndFile.GetMixLevels() == MixLevels::v1_17RC2) // Only shown for backwards compatibility with existing tunes
		m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("OpenMPT 1.17RC2")), static_cast<DWORD_PTR>(MixLevels::v1_17RC2));
	if(sndFile.GetMixLevels() == MixLevels::v1_17RC1) // Ditto
		m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("OpenMPT 1.17RC1")), static_cast<DWORD_PTR>(MixLevels::v1_17RC1));
	if(sndFile.GetMixLevels() == MixLevels::Original) // Ditto
		m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("Original (MPT 1.16)")), static_cast<DWORD_PTR>(MixLevels::Original));
	int compatMixMode = m_PlugMixBox.AddString(_T("Compatible"));
	m_PlugMixBox.SetItemData(compatMixMode, static_cast<DWORD_PTR>(MixLevels::Compatible));
	if(m_nType == MOD_TYPE_XM)
		m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("Compatible (FT2 Pan Law)")), static_cast<DWORD_PTR>(MixLevels::CompatibleFT2));

	// Default to compatible mix mode
	m_PlugMixBox.SetCurSel(compatMixMode);
	int mixCount = m_PlugMixBox.GetCount();
	for(int i = 0; i < mixCount; i++)
	{
		if(static_cast<MixLevels>(m_PlugMixBox.GetItemData(i)) == oldMixLevels)
		{
			m_PlugMixBox.SetCurSel(i);
			break;
		}
	}

	const bool XMorITorMPT = (m_nType & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT));
	const bool isMPTM = (m_nType == MOD_TYPE_MPT);

	// Mixmode Box
	GetDlgItem(IDC_TEXT_MIXMODE)->EnableWindow(XMorITorMPT);
	m_PlugMixBox.EnableWindow(XMorITorMPT);
	
	// Tempo mode box
	m_TempoModeBox.EnableWindow(XMorITorMPT);
	GetDlgItem(IDC_ROWSPERBEAT)->EnableWindow(XMorITorMPT);
	GetDlgItem(IDC_ROWSPERMEASURE)->EnableWindow(XMorITorMPT);
	GetDlgItem(IDC_TEXT_ROWSPERBEAT)->EnableWindow(XMorITorMPT);
	GetDlgItem(IDC_TEXT_ROWSPERMEASURE)->EnableWindow(XMorITorMPT);
	GetDlgItem(IDC_TEXT_TEMPOMODE)->EnableWindow(XMorITorMPT);
	GetDlgItem(IDC_FRAME_TEMPOMODE)->EnableWindow(XMorITorMPT);

	// Compatibility settings
	const PlayBehaviourSet defaultBehaviour = CSoundFile::GetDefaultPlaybackBehaviour(m_nType);
	const PlayBehaviourSet supportedBehaviour = CSoundFile::GetSupportedPlaybackBehaviour(m_nType);
	bool enableSetDefaults = false, showWarning = false;
	if(m_nType & (MOD_TYPE_MPT | MOD_TYPE_IT | MOD_TYPE_XM))
	{
		for(size_t i = 0; i < m_playBehaviour.size(); i++)
		{
			// Some flags are not really important for "default" behaviour.
			if(defaultBehaviour[i] != m_playBehaviour[i]
				&& i != MSF_COMPATIBLE_PLAY
				&& i != kFT2VolumeRamping)
			{
				enableSetDefaults = true;
				if(!isMPTM)
				{
					showWarning = true;
					break;
				}
			}
			if(isMPTM && m_playBehaviour[i] && !supportedBehaviour[i])
			{

				enableSetDefaults = true;
				showWarning = true;
				break;
			}
		}
	}
	static_cast<CStatic *>(GetDlgItem(IDC_STATIC1))->SetIcon(showWarning ? m_warnIcon : nullptr);
	GetDlgItem(IDC_STATIC2)->SetWindowText(showWarning
		? _T("Playback settings have been set to legacy compatibility mode. Click \"Set Defaults\" to use the recommended settings instead.")
		: _T("Compatibility settings are currently optimal. It is advised to not edit them."));
	GetDlgItem(IDC_BUTTON3)->EnableWindow(enableSetDefaults ? TRUE : FALSE);
}


void CModTypeDlg::OnPTModeChanged()
{
	// PT1/2 mode enforces Amiga limits
	const bool ptMode = IsDlgButtonChecked(IDC_CHECK_PT1X) != BST_UNCHECKED;
	m_CheckBoxAmigaLimits.EnableWindow(!ptMode);
	if(ptMode) m_CheckBoxAmigaLimits.SetCheck(BST_CHECKED);
}


void CModTypeDlg::OnTempoModeChanged()
{
	GetDlgItem(IDC_BUTTON1)->EnableWindow(static_cast<TempoMode>(m_TempoModeBox.GetItemData(m_TempoModeBox.GetCurSel())) == TempoMode::Modern);
}


void CModTypeDlg::OnTempoSwing()
{
	const ROWINDEX oldRPB = sndFile.m_nDefaultRowsPerBeat;
	const ROWINDEX oldRPM = sndFile.m_nDefaultRowsPerMeasure;
	const TempoMode oldMode = sndFile.m_nTempoMode;

	// Temporarily apply new tempo signature for preview
	const ROWINDEX newRPB = std::clamp(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERBEAT)), ROWINDEX(1), MAX_ROWS_PER_BEAT);
	const ROWINDEX newRPM = std::clamp(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERMEASURE)), newRPB, MAX_ROWS_PER_BEAT);
	sndFile.m_nDefaultRowsPerBeat = newRPB;
	sndFile.m_nDefaultRowsPerMeasure = newRPM;
	sndFile.m_nTempoMode = TempoMode::Modern;

	m_tempoSwing.resize(newRPB, TempoSwing::Unity);
	CTempoSwingDlg dlg(this, m_tempoSwing, sndFile);
	if(dlg.DoModal() == IDOK)
	{
		m_tempoSwing = dlg.m_tempoSwing;
	}
	sndFile.m_nDefaultRowsPerBeat = oldRPB;
	sndFile.m_nDefaultRowsPerMeasure = oldRPM;
	sndFile.m_nTempoMode = oldMode;
}


void CModTypeDlg::OnLegacyPlaybackSettings()
{
	CLegacyPlaybackSettingsDlg dlg(this, m_playBehaviour, m_nType);
	if(dlg.DoModal() == IDOK)
	{
		m_playBehaviour = dlg.GetPlayBehaviour();
	}
	UpdateDialog();
}


void CModTypeDlg::OnDefaultBehaviour()
{
	m_playBehaviour = CSoundFile::GetDefaultPlaybackBehaviour(m_nType);
	UpdateDialog();
}


bool CModTypeDlg::VerifyData()
{
	const int newRPB = GetDlgItemInt(IDC_ROWSPERBEAT);
	const int newRPM = GetDlgItemInt(IDC_ROWSPERMEASURE);
	if(newRPB > newRPM)
	{
		Reporting::Warning("Error: Rows per measure must be greater than or equal to rows per beat.");
		GetDlgItem(IDC_ROWSPERMEASURE)->SetFocus();
		return false;
	}
	if(newRPB == 0 && static_cast<TempoMode>(m_TempoModeBox.GetItemData(m_TempoModeBox.GetCurSel())) == TempoMode::Modern)
	{
		Reporting::Warning("Error: Rows per beat must be greater than 0 in modern tempo mode.");
		GetDlgItem(IDC_ROWSPERBEAT)->SetFocus();
		return false;
	}

	int sel = static_cast<int>(m_ChannelsBox.GetItemData(m_ChannelsBox.GetCurSel()));
	MODTYPE type = static_cast<MODTYPE>(m_TypeBox.GetItemData(m_TypeBox.GetCurSel()));

	CHANNELINDEX maxChans = CSoundFile::GetModSpecifications(type).channelsMax;

	if(sel > maxChans)
	{
		CString error;
		error.Format(_T("Error: Maximum number of channels for this module type is %u."), maxChans);
		Reporting::Warning(error);
		return false;
	}

	if(maxChans < sndFile.GetNumChannels())
	{
		if(Reporting::Confirm("New module type supports less channels than currently used, and reducing channel number is required. Continue?") != cnfYes)
			return false;
	}

	return true;
}


void CModTypeDlg::OnOK()
{
	if (!VerifyData())
		return;

	int sel = m_TypeBox.GetCurSel();
	if (sel >= 0)
	{
		m_nType = static_cast<MODTYPE>(m_TypeBox.GetItemData(sel));
	}
	const auto &newModSpecs = sndFile.GetModSpecifications(m_nType);

	sndFile.m_SongFlags.set(SONG_LINEARSLIDES, m_CheckBox1.GetCheck() != BST_UNCHECKED);
	sndFile.m_SongFlags.set(SONG_FASTVOLSLIDES, m_CheckBox2.GetCheck() != BST_UNCHECKED);
	sndFile.m_SongFlags.set(SONG_ITOLDEFFECTS, m_CheckBox3.GetCheck() != BST_UNCHECKED);
	sndFile.m_SongFlags.set(SONG_ITCOMPATGXX, m_CheckBox4.GetCheck() != BST_UNCHECKED);
	sndFile.m_SongFlags.set(SONG_EXFILTERRANGE, m_CheckBox5.GetCheck() != BST_UNCHECKED);
	sndFile.m_SongFlags.set(SONG_PT_MODE, m_CheckBoxPT1x.GetCheck() != BST_UNCHECKED);
	sndFile.m_SongFlags.set(SONG_AMIGALIMITS, m_CheckBoxAmigaLimits.GetCheck() != BST_UNCHECKED);

	sel = m_ChannelsBox.GetCurSel();
	if (sel >= 0)
	{
		m_nChannels = static_cast<CHANNELINDEX>(m_ChannelsBox.GetItemData(sel));
	}
	
	sndFile.m_nDefaultRowsPerBeat    = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERBEAT)), MAX_ROWS_PER_BEAT);
	sndFile.m_nDefaultRowsPerMeasure = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERMEASURE)), MAX_ROWS_PER_BEAT);

	sel = m_TempoModeBox.GetCurSel();
	if(sel >= 0)
	{
		const auto oldMode = sndFile.m_nTempoMode;
		sndFile.m_nTempoMode = static_cast<TempoMode>(m_TempoModeBox.GetItemData(sel));
		if(oldMode == TempoMode::Modern && sndFile.m_nTempoMode != TempoMode::Modern)
		{
			double newTempo = sndFile.m_nDefaultTempo.ToDouble() * (sndFile.m_nDefaultSpeed * sndFile.m_nDefaultRowsPerBeat) / ((sndFile.m_nTempoMode == TempoMode::Classic) ? 24 : 60);
			if(!newModSpecs.hasFractionalTempo)
				newTempo = std::round(newTempo);
			sndFile.m_nDefaultTempo = Clamp(TEMPO(newTempo), newModSpecs.GetTempoMin(), newModSpecs.GetTempoMax());
		}
	}
	if(sndFile.m_nTempoMode == TempoMode::Modern)
	{
		sndFile.m_tempoSwing = m_tempoSwing;
		if(!sndFile.m_tempoSwing.empty())
			sndFile.m_tempoSwing.resize(sndFile.m_nDefaultRowsPerBeat);
	} else
	{
		sndFile.m_tempoSwing.clear();
	}

	sel = m_PlugMixBox.GetCurSel();
	if(sel >= 0)
	{
		sndFile.SetMixLevels(static_cast<MixLevels>(m_PlugMixBox.GetItemData(sel)));
	}

	PlayBehaviourSet allowedFlags = CSoundFile::GetSupportedPlaybackBehaviour(m_nType);
	for(size_t i = 0; i < kMaxPlayBehaviours; i++)
	{
		// Only set those flags which are supported by the new format or were already enabled previously
		sndFile.m_playBehaviour.set(i, m_playBehaviour[i] && (allowedFlags[i] || (sndFile.m_playBehaviour[i] && sndFile.GetType() == m_nType)));
	}

	DestroyIcon(m_warnIcon);
	CDialog::OnOK();
}


BOOL CModTypeDlg::OnToolTipNotify(UINT, NMHDR *pNMHDR, LRESULT *)
{
	TOOLTIPTEXT *pTTT = (TOOLTIPTEXT*)pNMHDR;
	UINT_PTR nID = pNMHDR->idFrom;
	if(pTTT->uFlags & TTF_IDISHWND)
	{
		// idFrom is actually the HWND of the tool
		nID = ::GetDlgCtrlID((HWND)nID);
	}

	mpt::tstring text;
	switch(nID)
	{
	case IDC_CHECK1:
		text = _T("Note slides always slide the same amount, not depending on the sample frequency.");
		break;
	case IDC_CHECK2:
		text = _T("Old Scream Tracker 3 volume slide behaviour (not recommended).");
		break;
	case IDC_CHECK3:
		text = _T("Play some effects like in early versions of Impulse Tracker (not recommended).");
		break;
	case IDC_CHECK4:
		text = _T("Gxx and Exx/Fxx won't share effect memory. Gxx resets instrument envelopes.");
		break;
	case IDC_CHECK5:
		text = _T("The resonant filter's frequency range is increased from about 5kHz to 10kHz.");
		break;
	case IDC_CHECK_PT1X:
		text = _T("Enforce Amiga frequency limits, ProTracker offset bug emulation.");
		break;
	case IDC_COMBO_MIXLEVELS:
		text = _T("Mixing method of sample and instrument plugin levels.");
		break;
	case IDC_BUTTON1:
		if(!GetDlgItem(IDC_BUTTON1)->IsWindowEnabled())
		{
			text = _T("Tempo swing is only available in modern tempo mode.");
		} else
		{
			text = _T("Swing setting: ");
			if(m_tempoSwing.empty())
			{
				text += _T("Default");
			} else
			{
				for(size_t i = 0; i < m_tempoSwing.size(); i++)
				{
					if(i > 0)
						text += _T(" / ");
					text += MPT_TFORMAT("{}%")(Util::muldivr(m_tempoSwing[i], 100, TempoSwing::Unity));
				}
			}
		}
	}

	mpt::String::WriteWinBuf(pTTT->szText) = text;
	return TRUE;
}


//////////////////////////////////////////////////////////////////////////////
// Legacy Playback Settings dialog

BEGIN_MESSAGE_MAP(CLegacyPlaybackSettingsDlg, ResizableDialog)
	ON_COMMAND(IDC_BUTTON1,      &CLegacyPlaybackSettingsDlg::OnSelectDefaults)
	ON_EN_UPDATE(IDC_EDIT1,      &CLegacyPlaybackSettingsDlg::OnFilterStringChanged)
	ON_CLBN_CHKCHANGE(IDC_LIST1, &CLegacyPlaybackSettingsDlg::UpdateSelectDefaults)
END_MESSAGE_MAP()


void CLegacyPlaybackSettingsDlg::DoDataExchange(CDataExchange* pDX)
{
	ResizableDialog::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_LIST1,	m_CheckList);
}


BOOL CLegacyPlaybackSettingsDlg::OnInitDialog()
{
	ResizableDialog::OnInitDialog();
	OnFilterStringChanged();
	UpdateSelectDefaults();
	return TRUE;
}


void CLegacyPlaybackSettingsDlg::OnSelectDefaults()
{
	const int count = m_CheckList.GetCount();
	m_playBehaviour = CSoundFile::GetDefaultPlaybackBehaviour(m_modType);
	for(int i = 0; i < count; i++)
	{
		m_CheckList.SetCheck(i, m_playBehaviour[m_CheckList.GetItemData(i)] ? BST_CHECKED : BST_UNCHECKED);
	}
}


void CLegacyPlaybackSettingsDlg::UpdateSelectDefaults()
{
	const int count = m_CheckList.GetCount();
	for(int i = 0; i < count; i++)
	{
		m_playBehaviour.set(m_CheckList.GetItemData(i),  m_CheckList.GetCheck(i) != BST_UNCHECKED);
	}
	const auto defaults = CSoundFile::GetDefaultPlaybackBehaviour(m_modType);
	GetDlgItem(IDC_BUTTON1)->EnableWindow(m_playBehaviour != defaults ? TRUE : FALSE);
}


void CLegacyPlaybackSettingsDlg::OnFilterStringChanged()
{
	CString s;
	GetDlgItemText(IDC_EDIT1, s);
	const bool filterActive = !s.IsEmpty();
	s.MakeLower();

	m_CheckList.SetRedraw(FALSE);
	m_CheckList.ResetContent();

	const auto allowedFlags = CSoundFile::GetSupportedPlaybackBehaviour(m_modType);
	for(size_t i = 0; i < kMaxPlayBehaviours; i++)
	{
		const TCHAR *desc = _T("");
		switch(i)
		{
		case MSF_COMPATIBLE_PLAY: continue;

		case kMPTOldSwingBehaviour: desc = _T("OpenMPT 1.17 compatible random variation behaviour for instruments"); break;
		case kMIDICCBugEmulation: desc = _T("Plugin volume MIDI CC bug emulation"); break;
		case kOldMIDIPitchBends: desc = _T("Old Pitch Wheel behaviour for instrument plugins"); break;
		case kFT2VolumeRamping: desc = _T("Use smooth Fasttracker 2 volume ramping"); break;
		case kMODVBlankTiming: desc = _T("VBlank timing: F20 and above sets speed instead of tempo"); break;

		case kSlidesAtSpeed1: desc = _T("Execute regular portamento slides at speed 1"); break;
		case kPeriodsAreHertz: desc = _T("Compute note frequency in Hertz rather than periods"); break;
		case kTempoClamp: desc = _T("Clamp tempo to 32-255 range"); break;
		case kPerChannelGlobalVolSlide: desc = _T("Global volume slide memory is per-channel"); break;
		case kPanOverride: desc = _T("Panning commands override surround and random pan variation"); break;

		case kITInstrWithoutNote: desc = _T("Retrigger instrument envelopes on instrument change"); break;
		case kITVolColFinePortamento: desc = _T("Volume column portamento never does fine portamento"); break;
		case kITArpeggio: desc = _T("IT arpeggio algorithm"); break;
		case kITOutOfRangeDelay: desc = _T("Out-of-range delay commands queue new instrument"); break;
		case kITPortaMemoryShare: desc = _T("Gxx shares memory with Exx and Fxx"); break;
		case kITPatternLoopTargetReset: desc = _T("After finishing a pattern loop, set the pattern loop target to the next row"); break;
		case kITFT2PatternLoop: desc = _T("Nested pattern loop behaviour"); break;
		case kITPingPongNoReset: desc = _T("Do not reset ping pong direction with instrument numbers"); break;
		case kITEnvelopeReset: desc = _T("IT envelope reset behaviour"); break;
		case kITClearOldNoteAfterCut: desc = _T("Forget the previous note after cutting it"); break;
		case kITVibratoTremoloPanbrello: desc = _T("More IT-like Vibrato, Tremolo and Panbrello handling"); break;
		case kITTremor: desc = _T("Ixx behaves like in IT"); break;
		case kITRetrigger: desc = _T("Qxx behaves like in IT"); break;
		case kITMultiSampleBehaviour: desc = _T("Properly update C-5 frequency when changing note in multisampled instrument"); break;
		case kITPortaTargetReached: desc = _T("Clear portamento target after it has been reached"); break;
		case kITPatternLoopBreak: desc = _T("Do not reset loop count on pattern break"); break;
		case kITOffset: desc = _T("Offset after sample end is treated like in IT"); break;
		case kITSwingBehaviour: desc = _T("Volume and panning random variation work more like in IT"); break;
		case kITNNAReset: desc = _T("NNA is reset on every note change, not every instrument change"); break;
		case kITSCxStopsSample: desc = _T("SCx really stops the sample and does not just mute it"); break;
		case kITEnvelopePositionHandling: desc = _T("IT-style envelope position advance + enable/disable behaviour"); break;
		case kITPortamentoInstrument: desc = _T("More compatible instrument change + portamento"); break;
		case kITPingPongMode: desc = _T("Do not repeat last sample point in ping pong loop, like IT's software mixer"); break;
		case kITRealNoteMapping: desc = _T("Use triggered note rather than translated note for PPS and DNA note check"); break;
		case kITHighOffsetNoRetrig: desc = _T("SAx does not apply an offset effect to a note next to it"); break;
		case kITFilterBehaviour: desc = _T("User IT's filter coefficients (unless extended filter range is used) and behaviour"); break;
		case kITNoSurroundPan: desc = _T("Panning modulation is disabled on surround channels"); break;
		case kITShortSampleRetrig: desc = _T("Do not retrigger already stopped channels"); break;
		case kITPortaNoNote: desc = _T("Do not apply any portamento if no previous note is playing"); break;
		case kITFT2DontResetNoteOffOnPorta:
			if(m_modType == MOD_TYPE_XM)
				desc = _T("Reset note-off on portamento if there is an instrument number");
			else
				desc = _T("Reset note-off on portamento if there is an instrument number in Compatible Gxx mode");
			break;
		case kITVolColMemory: desc = _T("Volume column effects share their memory with the effect column"); break;
		case kITPortamentoSwapResetsPos: desc = _T("Portamento with sample swap plays the new sample from the beginning"); break;
		case kITEmptyNoteMapSlot: desc = _T("Ignore instrument note map entries with no note completely"); break;
		case kITFirstTickHandling: desc = _T("IT-style first tick handling"); break;
		case kITSampleAndHoldPanbrello: desc = _T("IT-style sample&hold panbrello waveform"); break;
		case kITClearPortaTarget: desc = _T("New notes reset portamento target in IT"); break;
		case kITPanbrelloHold: desc = _T("Do not reset panbrello effect until next note or panning effect"); break;
		case kITPanningReset: desc = _T("Sample and instrument panning is only applied on note change, not instrument change"); break;
		case kITPatternLoopWithJumpsOld: desc = _T("Bxx on the same row as SBx terminates the loop in IT"); break;
		case kITInstrWithNoteOff: desc = _T("Instrument number with note-off recalls default volume"); break;
		case kFT2Arpeggio: desc = _T("FT2 arpeggio algorithm"); break;
		case kFT2Retrigger: desc = _T("Rxx behaves like in FT2"); break;
		case kFT2VolColVibrato: desc = _T("Vibrato speed in volume column does not actually execute the vibrato effect"); break;
		case kFT2PortaNoNote: desc = _T("Do not play portamento-ed note if no previous note is playing"); break;
		case kFT2KeyOff: desc = _T("FT2-style Kxx handling"); break;
		case kFT2PanSlide: desc = _T("Volume-column pan slides are finer"); break;
		case kFT2ST3OffsetOutOfRange: desc = _T("Offset past sample end stops the note"); break;
		case kFT2RestrictXCommand: desc = _T("Do not allow ModPlug extensions to X command"); break;
		case kFT2RetrigWithNoteDelay: desc = _T("Retrigger envelopes if there is a note delay with no note"); break;
		case kFT2SetPanEnvPos: desc = _T("Lxx only sets the pan envelope position if the volume envelope's sustain flag is set"); break;
		case kFT2PortaIgnoreInstr: desc = _T("Portamento with instrument number applies volume settings of new sample, but not the new sample itself"); break;
		case kFT2VolColMemory: desc = _T("No volume column memory"); break;
		case kFT2LoopE60Restart: desc = _T("Next pattern starts on the same row as the last E60 command"); break;
		case kFT2ProcessSilentChannels: desc = _T("Keep processing faded channels for later portamento pickup"); break;
		case kFT2ReloadSampleSettings: desc = _T("Reload sample settings even if a note-off is placed next to an instrument number"); break;
		case kFT2PortaDelay: desc = _T("Portamento with note delay next to it is ignored"); break;
		case kFT2Transpose: desc = _T("Ignore out-of-range transposed notes"); break;
		case kFT2PatternLoopWithJumps: desc = _T("Bxx or Dxx on the same row as E6x terminates the loop"); break;
		case kFT2PortaTargetNoReset: desc = _T("Portamento target is not reset with new notes"); break;
		case kFT2EnvelopeEscape: desc = _T("Sustain point at end of envelope loop stops the loop after release"); break;
		case kFT2Tremor: desc = _T("Txx behaves like in FT2"); break;
		case kFT2OutOfRangeDelay: desc = _T("Do not trigger notes with out-of-range note delay"); break;
		case kFT2Periods: desc = _T("Use FT2's broken period handling"); break;
		case kFT2PanWithDelayedNoteOff: desc = _T("Panning command with delayed note-off is ignored"); break;
		case kFT2VolColDelay: desc = _T("FT2-style volume column handling if there is a note delay"); break;
		case kFT2FinetunePrecision: desc = _T("Round sample finetune to multiples of 8"); break;
		case kFT2NoteOffFlags: desc = _T("Fade instrument on note-off when there is no volume envelope; instrument numbers reset note-off status"); break;
		case kITMultiSampleInstrumentNumber: desc = _T("Lone instrument number after portamento within multi-sampled instrument sets the target sample's settings"); break;
		case kRowDelayWithNoteDelay: desc = _T("Note delays next to a row delay are repeated on every row repetition"); break;
		case kFT2MODTremoloRampWaveform: desc = _T("Emulate FT2/ProTracker tremolo ramp down / triangle waveform"); break;
		case kFT2PortaUpDownMemory: desc = _T("Portamento Up and Down have separate effect memory"); break;
		case kST3NoMutedChannels: desc = _T("Do not process any effects on muted S3M channels"); break;
		case kST3EffectMemory: desc = _T("Most effects share the same memory"); break;
		case kST3PortaSampleChange: desc = _T("Portamento with instrument number applies volume settings of new sample, but not the new sample itself (GUS)"); break;
		case kST3VibratoMemory: desc = _T("Do not remember vibrato type in effect memory"); break;
		case kST3LimitPeriod: desc = _T("ModPlug Tracker frequency limits"); break;
		case KST3PortaAfterArpeggio: desc = _T("Portamento immediately following an arpeggio effect continues at the last arpeggiated note"); break;
		case kMODOneShotLoops: desc = _T("ProTracker one-shot loops"); break;
		case kMODIgnorePanning: desc = _T("Ignore panning commands"); break;
		case kMODSampleSwap: desc = _T("Enable on-the-fly sample swapping"); break;
		case kMODOutOfRangeNoteDelay: desc = _T("Out-of-range note delay is played on next row"); break;
		case kMODTempoOnSecondTick: desc = _T("Tempo changes are handled on second tick instead of first"); break;
		case kFT2PanSustainRelease: desc = _T("If the sustain point of the panning envelope is reached before key-off, it is never released"); break;
		case kLegacyReleaseNode: desc = _T("Old volume envelope release node scaling behaviour"); break;
		case kOPLBeatingOscillators: desc = _T("Beating OPL oscillators"); break;
		case kST3OffsetWithoutInstrument: desc = _T("Notes without instrument use the previous note's sample offset"); break;
		case kReleaseNodePastSustainBug: desc = _T("Broken release node after sustain end behaviour"); break;
		case kFT2NoteDelayWithoutInstr: desc = _T("Delayed instrument-less notes should not recall volume and panning"); break;
		case kOPLFlexibleNoteOff: desc = _T("Full control over OPL notes after note-off"); break;
		case kITInstrWithNoteOffOldEffects: desc = _T("Instrument number with note-off retriggers envelopes with Old Effects enabled"); break;
		case kMIDIVolumeOnNoteOffBug: desc = _T("Reset VST volume on note-off"); break;
		case kITDoNotOverrideChannelPan: desc = _T("Instruments / samples with forced panning do not override channel panning for following instruments / samples"); break;
		case kITPatternLoopWithJumps: desc = _T("Bxx right of SBx terminates the loop in IT"); break;
		case kITDCTBehaviour: desc = _T("Duplicate Sample Check requires same instrument, Duplicate Note Check uses pattern notes for comparison"); break;
		case kOPLwithNNA: desc = _T("New Note Action / Duplicate Note Action set to Note Off and Note Fade affect OPL notes like samples"); break;
		case kST3RetrigAfterNoteCut: desc = _T("Notes cannot be retriggered after they have been cut"); break;
		case kST3SampleSwap: desc = _T("Enable on-the-fly sample swapping (SoundBlaster driver)"); break;
		case kOPLRealRetrig: desc = _T("Retrigger (Qxy) affects OPL notes"); break;
		case kOPLNoResetAtEnvelopeEnd: desc = _T("Do not reset OPL channel status at end of envelopes"); break;
		case kOPLNoteStopWith0Hz: desc = _T("OPL key-off sets note frequency to 0 Hz"); break;
		case kOPLNoteOffOnNoteChange: desc = _T("Send OPL key-off when triggering notes"); break;
		case kFT2PortaResetDirection: desc = _T("Tone Portamento direction resets after reaching portamento target from below"); break;
		case kApplyUpperPeriodLimit: desc = _T("Apply lower frequency limit"); break;
		case kApplyOffsetWithoutNote: desc = _T("Offset commands work without a note next to them"); break;
		case kITPitchPanSeparation: desc = _T("Pitch / Pan Separation can be overridden by panning commands"); break;
		case kImprecisePingPongLoops: desc = _T("Use old imprecise ping-pong loop end calculation"); break;

		default: MPT_ASSERT_NOTREACHED();
		}

		if(filterActive && CString{desc}.MakeLower().Find(s) < 0)
			continue;

		if(m_playBehaviour[i] || allowedFlags[i])
		{
			int item = m_CheckList.AddString(desc);
			m_CheckList.SetItemData(item, i);
			int check = m_playBehaviour[i] ? BST_CHECKED : BST_UNCHECKED;
			if(!allowedFlags[i])
				check = BST_INDETERMINATE;  // Is checked but not supported by format -> grey out
			m_CheckList.SetCheck(item, check);
		}
	}
	m_CheckList.SetRedraw(TRUE);
}


///////////////////////////////////////////////////////////
// CRemoveChannelsDlg

void CRemoveChannelsDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_REMCHANSLIST,		m_RemChansList);
}


BEGIN_MESSAGE_MAP(CRemoveChannelsDlg, CDialog)
	ON_LBN_SELCHANGE(IDC_REMCHANSLIST,		&CRemoveChannelsDlg::OnChannelChanged)
END_MESSAGE_MAP()



BOOL CRemoveChannelsDlg::OnInitDialog()
{
	CString s;
	CDialog::OnInitDialog();
	const CHANNELINDEX numChannels = sndFile.GetNumChannels();
	for(CHANNELINDEX n = 0; n < numChannels; n++)
	{
		s = MPT_CFORMAT("Channel {}")(n + 1);
		if(sndFile.ChnSettings[n].szName[0] >= 0x20)
		{
			s += _T(": ");
			s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.ChnSettings[n].szName);
		}
		m_RemChansList.SetItemData(m_RemChansList.AddString(s), n);
		if (!m_bKeepMask[n]) m_RemChansList.SetSel(n);
	}

	if (m_nRemove > 0)
		s = MPT_CFORMAT("Select {} channel{} to remove:")(m_nRemove, (m_nRemove != 1) ? CString(_T("s")) : CString(_T("")));
	else
		s = MPT_CFORMAT("Select channels to remove (the minimum number of remaining channels is {})")(sndFile.GetModSpecifications().channelsMin);
	
	SetDlgItemText(IDC_QUESTION1, s);
	if(GetDlgItem(IDCANCEL)) GetDlgItem(IDCANCEL)->ShowWindow(m_ShowCancel);

	OnChannelChanged();
	return TRUE;
}


void CRemoveChannelsDlg::OnOK()
{
	int selCount = m_RemChansList.GetSelCount();
	std::vector<int> selected(selCount);
	m_RemChansList.GetSelItems(selCount, selected.data());

	m_bKeepMask.assign(sndFile.GetNumChannels(), true);
	for (const auto sel : selected)
	{
		m_bKeepMask[sel] = false;
	}
	if ((static_cast<CHANNELINDEX>(selCount) == m_nRemove && selCount > 0)
		|| (m_nRemove == 0 && (sndFile.GetNumChannels() >= selCount + sndFile.GetModSpecifications().channelsMin)))
		CDialog::OnOK();
	else
		CDialog::OnCancel();
}


void CRemoveChannelsDlg::OnChannelChanged()
{
	const UINT selCount = m_RemChansList.GetSelCount();
	GetDlgItem(IDOK)->EnableWindow(((selCount == m_nRemove && selCount > 0)  || (m_nRemove == 0 && (sndFile.GetNumChannels() >= selCount + sndFile.GetModSpecifications().channelsMin) && selCount > 0)) ? TRUE : FALSE);
}


InfoDialog::InfoDialog(CWnd *parent)
	: ResizableDialog(IDD_INFO_BOX, parent)
{ }

BOOL InfoDialog::OnInitDialog()
{
	ResizableDialog::OnInitDialog();
	SetWindowText(m_caption.c_str());
	SetDlgItemText(IDC_EDIT1, m_content.c_str());
	return TRUE;
}

void InfoDialog::SetContent(mpt::winstring content)
{
	m_content = std::move(content);
}

void InfoDialog::SetCaption(mpt::winstring caption)
{
	m_caption = std::move(caption);
}

////////////////////////////////////////////////////////////////////////////////
// Sound Bank Information

CSoundBankProperties::CSoundBankProperties(const CDLSBank &bank, CWnd *parent)
	: InfoDialog(parent)
{
	const SOUNDBANKINFO &bi = bank.GetBankInfo();
	std::string info;
	info.reserve(128 + bi.szBankName.size() + bi.szDescription.size() + bi.szCopyRight.size() + bi.szEngineer.size() + bi.szSoftware.size() + bi.szComments.size());
	info = "Type:\t" + std::string((bank.GetBankType() & SOUNDBANK_TYPE_SF2) ? "Sound Font (SF2)" : "Downloadable Sound (DLS)");
	if (bi.szBankName.size())
		info += "\r\nName:\t" + bi.szBankName;
	if (bi.szDescription.size())
		info += "\r\n\t" + bi.szDescription;
	if (bi.szCopyRight.size())
		info += "\r\nCopyright:\t" + bi.szCopyRight;
	if (bi.szEngineer.size())
		info += "\r\nAuthor:\t" + bi.szEngineer;
	if (bi.szSoftware.size())
		info += "\r\nSoftware:\t" + bi.szSoftware;
	if (bi.szComments.size())
		info += "\r\n\r\nComments:\r\n" + bi.szComments;
	SetCaption((bank.GetFileName().AsNative() + _T(" - Sound Bank Information")));
	SetContent(mpt::ToWin(mpt::Charset::Locale, info));
}


////////////////////////////////////////////////////////////////////////////////////////////
// Keyboard Control

static constexpr uint8 whitetab[7] = {0,2,4,5,7,9,11};
static constexpr uint8 blacktab[7] = {0xff,1,3,0xff,6,8,10};

BEGIN_MESSAGE_MAP(CKeyboardControl, CWnd)
	ON_WM_DESTROY()
	ON_WM_PAINT()
	ON_WM_MOUSEMOVE()
	ON_WM_LBUTTONDOWN()
	ON_WM_LBUTTONUP()
END_MESSAGE_MAP()


void CKeyboardControl::Init(CWnd *parent, int octaves, bool cursorNotify)
{
	m_parent = parent;
	m_nOctaves = std::max(1, octaves);
	m_cursorNotify = cursorNotify;
	MemsetZero(KeyFlags);
	MemsetZero(m_sampleNum);
	
	// Point size to pixels
	int fontSize = -MulDiv(60, Util::GetDPIy(m_hWnd), 720);
	m_font.CreateFont(fontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_RASTER_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, FIXED_PITCH | FF_DONTCARE, _T("MS Shell Dlg"));
}


void CKeyboardControl::OnDestroy()
{
	m_font.DeleteObject();
}


void CKeyboardControl::DrawKey(CPaintDC &dc, const CRect rect, int key, bool black) const
{
	const bool selected = (key == m_nSelection);
	COLORREF color = black ? RGB(20, 20, 20) : RGB(255, 255, 255);
	if(m_mouseDown && selected)
		color = black ? RGB(104, 104, 104) : RGB(212, 212, 212);
	else if(selected)
		color = black ? RGB(130, 130, 130) : RGB(228, 228, 228);
	dc.SetDCBrushColor(color);
	dc.Rectangle(&rect);

	if(static_cast<size_t>(key) < std::size(KeyFlags) && KeyFlags[key] != KEYFLAG_NORMAL)
	{
		const int margin = black ? 0 : 2;
		CRect ellipseRect(rect.left + margin, rect.bottom - rect.Width() + margin, rect.right - margin, rect.bottom - margin);
		dc.SetDCBrushColor((KeyFlags[key] & KEYFLAG_BRIGHTDOT) ? RGB(255, 192, 192) : RGB(255, 0, 0));
		dc.Ellipse(ellipseRect);
		if(m_sampleNum[key] != 0)
		{
			dc.SetTextColor((KeyFlags[key] & KEYFLAG_BRIGHTDOT) ? RGB(0, 0, 0) : RGB(255, 255, 255));
			TCHAR s[16];
			wsprintf(s, _T("%u"), m_sampleNum[key]);
			dc.DrawText(s, -1, ellipseRect, DT_CENTER | DT_SINGLELINE | DT_VCENTER);
		}

		if(KeyFlags[key] == (KEYFLAG_REDDOT | KEYFLAG_BRIGHTDOT))
		{
			// Both flags set: Draw second dot
			ellipseRect.MoveToY(ellipseRect.top - ellipseRect.Height() - 2);
			dc.SetDCBrushColor(RGB(255, 0, 0));
			dc.Ellipse(ellipseRect);
		}
	}
}


void CKeyboardControl::OnPaint()
{
	CRect rcClient, rect;
	CPaintDC dc(this);

	dc.SetBkMode(TRANSPARENT);
	GetClientRect(&rcClient);
	rect = rcClient;
	auto oldBrush = dc.SelectObject(GetStockObject(DC_BRUSH));
	auto oldPen = dc.SelectObject(GetStockObject(DC_PEN));
	auto oldFont = dc.SelectObject(&m_font);

	// Rectangle outline
	dc.SetDCPenColor(RGB(50, 50, 50));

	// White notes
	for(int note = 0; note < m_nOctaves * 7; note++)
	{
		rect.right = ((note + 1) * rcClient.Width()) / (m_nOctaves * 7);
		int val = (note / 7) * 12 + whitetab[note % 7];

		DrawKey(dc, rect, val, false);
		
		rect.left = rect.right - 1;
	}

	// Black notes
	rect = rcClient;
	rect.bottom -= rcClient.Height() / 3;
	for(int note = 0; note < m_nOctaves * 7; note++)
	{
		switch(note % 7)
		{
		case 1:
		case 2:
		case 4:
		case 5:
		case 6:
		{
			rect.left = (note * rcClient.Width()) / (m_nOctaves * 7);
			rect.right = rect.left;
			int delta = rcClient.Width() / (m_nOctaves * 7 * 3);
			rect.left -= delta;
			rect.right += delta;
			int val = (note / 7) * 12 + blacktab[note % 7];

			DrawKey(dc, rect, val, true);
			break;
		}
		}
	}

	dc.SelectObject(oldBrush);
	dc.SelectObject(oldPen);
	dc.SelectObject(oldFont);
}


void CKeyboardControl::OnMouseMove(UINT flags, CPoint point)
{
	CRect rcClient, rect;
	GetClientRect(&rcClient);
	rect = rcClient;
	int xmin = rcClient.right;
	int xmax = rcClient.left;
	int sel = -1;
	// White notes
	for(int note = 0; note < m_nOctaves * 7; note++)
	{
		int val = (note / 7) * 12 + whitetab[note % 7];
		rect.right = ((note + 1) * rcClient.Width()) / (m_nOctaves * 7);
		if (val == m_nSelection)
		{
			if (rect.left < xmin) xmin = rect.left;
			if (rect.right > xmax) xmax = rect.right;
		}
		if (rect.PtInRect(point))
		{
			sel = val;
			if (rect.left < xmin) xmin = rect.left;
			if (rect.right > xmax) xmax = rect.right;
		}
		rect.left = rect.right - 1;
	}
	// Black notes
	rect = rcClient;
	rect.bottom -= rcClient.Height() / 3;
	for(int note = 0; note < m_nOctaves * 7; note++)
	{
		switch(note % 7)
		{
		case 1:
		case 2:
		case 4:
		case 5:
		case 6:
		{
			int val = (note / 7) * 12 + blacktab[note % 7];
			rect.left = (note * rcClient.Width()) / (m_nOctaves * 7);
			rect.right = rect.left;
			int delta = rcClient.Width() / (m_nOctaves * 7 * 3);
			rect.left -= delta;
			rect.right += delta;
			if(val == m_nSelection)
			{
				if(rect.left < xmin)
					xmin = rect.left;
				if(rect.right > xmax)
					xmax = rect.right;
			}
			if(rect.PtInRect(point))
			{
				sel = val;
				if(rect.left < xmin)
					xmin = rect.left;
				if(rect.right > xmax)
					xmax = rect.right;
			}
			break;
		}
		}
	}
	// Check for selection change
	if(sel != m_nSelection)
	{
		m_nSelection = sel;
		rcClient.left = xmin;
		rcClient.right = xmax;
		InvalidateRect(&rcClient, FALSE);
		if(m_cursorNotify && m_parent)
		{
			m_parent->PostMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_MOUSEMOVE, m_nSelection);
			if(flags & MK_LBUTTON)
				m_parent->SendMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_LBUTTONDOWN, m_nSelection);
		}
	}
	if(sel >= 0)
	{
		if(!m_mouseCapture)
		{
			m_mouseCapture = true;
			SetCapture();
		}
	} else
	{
		if(m_mouseCapture)
		{
			m_mouseCapture = false;
			ReleaseCapture();
		}
	}
}


void CKeyboardControl::OnLButtonDown(UINT, CPoint)
{
	m_mouseDown = true;
	InvalidateRect(nullptr, FALSE);
	if(m_parent)
		m_parent->SendMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_LBUTTONDOWN, m_nSelection);
}


void CKeyboardControl::OnLButtonUp(UINT, CPoint)
{
	m_mouseDown = false;
	InvalidateRect(nullptr, FALSE);
	if(m_parent)
		m_parent->SendMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_LBUTTONUP, m_nSelection);
}


////////////////////////////////////////////////////////////////////////////////
//
// Sample Map
//

BEGIN_MESSAGE_MAP(CSampleMapDlg, CDialog)
	ON_MESSAGE(WM_MOD_KBDNOTIFY,	&CSampleMapDlg::OnKeyboardNotify)
	ON_WM_HSCROLL()
	ON_COMMAND(IDC_CHECK1,			&CSampleMapDlg::OnUpdateSamples)
	ON_CBN_SELCHANGE(IDC_COMBO1,	&CSampleMapDlg::OnUpdateKeyboard)
END_MESSAGE_MAP()

void CSampleMapDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CSampleMapDlg)
	DDX_Control(pDX, IDC_KEYBOARD1,		m_Keyboard);
	DDX_Control(pDX, IDC_COMBO1,		m_CbnSample);
	DDX_Control(pDX, IDC_SLIDER1,		m_SbOctave);
	//}}AFX_DATA_MAP
}


BOOL CSampleMapDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
	ModInstrument *pIns = sndFile.Instruments[m_nInstrument];
	if(pIns)
	{
		for(UINT i = 0; i < NOTE_MAX; i++)
		{
			KeyboardMap[i] = pIns->Keyboard[i];
		}
	}
	m_Keyboard.Init(this, 3, TRUE);
	m_SbOctave.SetRange(0, 7);
	m_SbOctave.SetPos(4);
	OnUpdateSamples();
	OnUpdateOctave();
	return TRUE;
}


void CSampleMapDlg::OnHScroll(UINT nCode, UINT nPos, CScrollBar *pBar)
{
	CDialog::OnHScroll(nCode, nPos, pBar);
	OnUpdateKeyboard();
	OnUpdateOctave();
}


void CSampleMapDlg::OnUpdateSamples()
{
	UINT oldPos = 0;
	UINT newPos = 0;

	if(m_nInstrument >= MAX_INSTRUMENTS)
		return;
	if(m_CbnSample.GetCount() > 0)
		oldPos = static_cast<UINT>(m_CbnSample.GetItemData(m_CbnSample.GetCurSel()));
	m_CbnSample.SetRedraw(FALSE);
	m_CbnSample.ResetContent();
	const bool showAll = (IsDlgButtonChecked(IDC_CHECK1) != FALSE) || (*std::max_element(std::begin(KeyboardMap), std::end(KeyboardMap)) == 0);

	UINT insertPos = m_CbnSample.AddString(_T("0: No sample"));
	m_CbnSample.SetItemData(insertPos, 0);

	for(SAMPLEINDEX i = 1; i <= sndFile.GetNumSamples(); i++)
	{
		bool isUsed = showAll || mpt::contains(KeyboardMap, i);
		if(isUsed)
		{
			CString sampleName;
			sampleName.Format(_T("%d: %s"), i, mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.GetSampleName(i)).GetString());
			insertPos = m_CbnSample.AddString(sampleName);

			m_CbnSample.SetItemData(insertPos, i);
			if(i == oldPos)
				newPos = insertPos;
		}
	}
	m_CbnSample.SetRedraw(TRUE);
	m_CbnSample.SetCurSel(newPos);
	OnUpdateKeyboard();
}


void CSampleMapDlg::OnUpdateOctave()
{
	TCHAR s[64];
	const UINT baseOctave = m_SbOctave.GetPos() & 7;
	wsprintf(s, _T("Octaves %u-%u"), baseOctave, baseOctave + 2);
	SetDlgItemText(IDC_TEXT1, s);
}



void CSampleMapDlg::OnUpdateKeyboard()
{
	SAMPLEINDEX nSample = static_cast<SAMPLEINDEX>(m_CbnSample.GetItemData(m_CbnSample.GetCurSel()));
	const UINT baseOctave = m_SbOctave.GetPos() & 7;
	bool redraw = false;
	for(UINT iNote = 0; iNote < 3 * 12; iNote++)
	{
		uint8 oldFlags = m_Keyboard.GetFlags(iNote);
		SAMPLEINDEX oldSmp = m_Keyboard.GetSample(iNote);
		UINT ndx = baseOctave * 12 + iNote;
		uint8 newFlags = CKeyboardControl::KEYFLAG_NORMAL;
		if(KeyboardMap[ndx] == nSample)
			newFlags = CKeyboardControl::KEYFLAG_REDDOT;
		else if(KeyboardMap[ndx] != 0)
			newFlags = CKeyboardControl::KEYFLAG_BRIGHTDOT;
		if(newFlags != oldFlags || oldSmp != KeyboardMap[ndx])
		{
			m_Keyboard.SetFlags(iNote, newFlags);
			m_Keyboard.SetSample(iNote, KeyboardMap[ndx]);
			redraw = true;
		}
	}
	if(redraw)
		m_Keyboard.InvalidateRect(NULL, FALSE);
}


LRESULT CSampleMapDlg::OnKeyboardNotify(WPARAM wParam, LPARAM lParam)
{
	TCHAR s[32] = _T("--");

	if((lParam >= 0) && (lParam < 3 * 12))
	{
		const SAMPLEINDEX sample = static_cast<SAMPLEINDEX>(m_CbnSample.GetItemData(m_CbnSample.GetCurSel()));
		const uint32 baseOctave = m_SbOctave.GetPos() & 7;

		const CString temp = mpt::ToCString(sndFile.GetNoteName(static_cast<ModCommand::NOTE>(lParam + 1 + 12 * baseOctave), m_nInstrument));
		if(temp.GetLength() >= mpt::saturate_cast<int>(std::size(s)))
			wsprintf(s, _T("%s"), _T("..."));
		else
			wsprintf(s, _T("%s"), temp.GetString());

		ModInstrument *pIns = sndFile.Instruments[m_nInstrument];
		if((wParam == KBDNOTIFY_LBUTTONDOWN) && (sample < MAX_SAMPLES) && (pIns))
		{
			const uint32 note = static_cast<uint32>(baseOctave * 12 + lParam);

			if(mouseAction == mouseUnknown)
			{
				// Mouse down -> decide if we are going to set or remove notes
				mouseAction = mouseSet;
				if(KeyboardMap[note] == sample)
				{
					mouseAction = (KeyboardMap[note] == pIns->Keyboard[note]) ? mouseZero : mouseUnset;
				}
			}

			switch(mouseAction)
			{
			case mouseSet:
				KeyboardMap[note] = sample;
				break;
			case mouseUnset:
				KeyboardMap[note] = pIns->Keyboard[note];
				break;
			case mouseZero:
				if(KeyboardMap[note] == sample)
				{
					KeyboardMap[note] = 0;
				}
				break;
			}
			OnUpdateKeyboard();
		}
	}
	if(wParam == KBDNOTIFY_LBUTTONUP)
	{
		mouseAction = mouseUnknown;
	}
	SetDlgItemText(IDC_TEXT2, s);
	return 0;
}


void CSampleMapDlg::OnOK()
{
	ModInstrument *pIns = sndFile.Instruments[m_nInstrument];
	if(pIns)
	{
		bool modified = false;
		for(UINT i = 0; i < NOTE_MAX; i++)
		{
			if(KeyboardMap[i] != pIns->Keyboard[i])
			{
				pIns->Keyboard[i] = KeyboardMap[i];
				modified = true;
			}
		}
		if(modified)
		{
			CDialog::OnOK();
			return;
		}
	}
	CDialog::OnCancel();
}


////////////////////////////////////////////////////////////////////////////////////////////
// Edit history dialog

BEGIN_MESSAGE_MAP(CEditHistoryDlg, ResizableDialog)
	ON_COMMAND(IDC_BTN_CLEAR,	&CEditHistoryDlg::OnClearHistory)
END_MESSAGE_MAP()


BOOL CEditHistoryDlg::OnInitDialog()
{
	ResizableDialog::OnInitDialog();

	CString s;
	uint64 totalTime = 0;
	const auto &editHistory = m_modDoc.GetSoundFile().GetFileHistory();
	const bool isEmpty = editHistory.empty();

	for(const auto &entry : editHistory)
	{
		totalTime += entry.openTime;

		// Date
		CString sDate;
		if(entry.HasValidDate())
		{
			TCHAR szDate[32];
			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &entry.loadDate);
			sDate = szDate;
		} else
		{
			sDate = _T("<unknown date>");
		}
		// Time + stuff
		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
		s += MPT_CFORMAT("Loaded {}, open for {}h {}m {}s\r\n")(
			sDate, mpt::cfmt::dec(duration / 3600), mpt::cfmt::dec0<2>((duration / 60) % 60), mpt::cfmt::dec0<2>(duration % 60));
	}
	if(isEmpty)
	{
		s = _T("No information available about the previous edit history of this module.");
	}
	SetDlgItemText(IDC_EDIT_HISTORY, s);

	// Total edit time
	s.Empty();
	if(totalTime)
	{
		totalTime = mpt::saturate_round<uint64>(totalTime / HISTORY_TIMER_PRECISION);

		s.Format(_T("Total edit time: %lluh %02llum %02llus (%zu session%s)"), totalTime / 3600, (totalTime / 60) % 60, totalTime % 60, editHistory.size(), (editHistory.size() != 1) ? _T("s") : _T(""));
		SetDlgItemText(IDC_TOTAL_EDIT_TIME, s);
		// Window title
		s.Format(_T("Edit History for %s"), m_modDoc.GetTitle().GetString());
		SetWindowText(s);
	}
	// Enable or disable Clear button
	GetDlgItem(IDC_BTN_CLEAR)->EnableWindow(isEmpty ? FALSE : TRUE);

	return TRUE;
}


void CEditHistoryDlg::OnClearHistory()
{
	if(!m_modDoc.GetSoundFile().GetFileHistory().empty())
	{
		m_modDoc.GetSoundFile().GetFileHistory().clear();
		m_modDoc.SetModified();
		OnInitDialog();
	}
}


/////////////////////////////////////////////////////////////////////////
// Generic input dialog

void CInputDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	if(m_minValueInt == m_maxValueInt && m_minValueDbl == m_maxValueDbl)
	{
		// Only need this for freeform text
		DDX_Control(pDX, IDC_EDIT1, m_edit);
	}
	DDX_Control(pDX, IDC_SPIN1, m_spin);
}


BOOL CInputDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
	SetDlgItemText(IDC_PROMPT, m_description);

	// Get all current control sizes and positions
	CRect windowRect, labelRect, inputRect, okRect, cancelRect;
	GetWindowRect(windowRect);
	GetDlgItem(IDC_PROMPT)->GetWindowRect(labelRect);
	GetDlgItem(IDC_EDIT1)->GetWindowRect(inputRect);
	GetDlgItem(IDOK)->GetWindowRect(okRect);
	GetDlgItem(IDCANCEL)->GetWindowRect(cancelRect);
	ScreenToClient(labelRect);
	ScreenToClient(inputRect);
	ScreenToClient(okRect);
	ScreenToClient(cancelRect);

	// Find out how big our label shall be
	HDC dc = ::GetDC(m_hWnd);
	CRect textRect(0,0,0,0);
	DrawText(dc, m_description, m_description.GetLength(), textRect, DT_CALCRECT);
	LPtoDP(dc, &textRect.BottomRight(), 1);
	::ReleaseDC(m_hWnd, dc);
	if(textRect.right < 320) textRect.right = 320;
	const int windowWidth = windowRect.Width() - labelRect.Width() + textRect.right;
	const int windowHeight = windowRect.Height() - labelRect.Height() + textRect.bottom;

	// Resize and move all controls
	GetDlgItem(IDC_PROMPT)->SetWindowPos(nullptr, 0, 0, textRect.right, textRect.bottom, SWP_NOMOVE | SWP_NOZORDER);
	GetDlgItem(IDC_EDIT1)->SetWindowPos(nullptr, inputRect.left, labelRect.top + textRect.bottom + (inputRect.top - labelRect.bottom), textRect.right, inputRect.Height(), SWP_NOZORDER);
	GetDlgItem(IDOK)->SetWindowPos(nullptr, windowWidth - (windowRect.Width() - okRect.left), windowHeight - (windowRect.Height() - okRect.top), 0, 0, SWP_NOSIZE | SWP_NOZORDER);
	GetDlgItem(IDCANCEL)->SetWindowPos(nullptr, windowWidth - (windowRect.Width() - cancelRect.left), windowHeight - (windowRect.Height() - cancelRect.top), 0, 0, SWP_NOSIZE | SWP_NOZORDER);
	SetWindowPos(nullptr, 0, 0, windowWidth, windowHeight, SWP_NOMOVE | SWP_NOZORDER);

	if(m_minValueInt != m_maxValueInt)
	{
		// Numeric (int)
		m_spin.SetRange32(m_minValueInt, m_maxValueInt);
		m_edit.SubclassDlgItem(IDC_EDIT1, this);
		m_edit.ModifyStyle(0, ES_NUMBER);
		m_edit.AllowNegative(m_minValueInt < 0);
		m_edit.AllowFractions(false);
		SetDlgItemInt(IDC_EDIT1, resultAsInt);
		m_spin.SetBuddy(&m_edit);
	} else if(m_minValueDbl != m_maxValueDbl)
	{
		// Numeric (double)
		m_spin.SetRange32(static_cast<int32>(m_minValueDbl), static_cast<int32>(m_maxValueDbl));
		m_edit.SubclassDlgItem(IDC_EDIT1, this);
		m_edit.ModifyStyle(0, ES_NUMBER);
		m_edit.AllowNegative(m_minValueDbl < 0);
		m_edit.AllowFractions(true);
		m_edit.SetDecimalValue(resultAsDouble);
		m_spin.SetBuddy(&m_edit);
	} else
	{
		// Text
		m_spin.ShowWindow(SW_HIDE);
		if(m_maxLength > 0)
			Edit_LimitText(m_edit, m_maxLength);
		SetDlgItemText(IDC_EDIT1, resultAsString);
	}

	return TRUE;
}


void CInputDlg::OnOK()
{
	CDialog::OnOK();
	GetDlgItemText(IDC_EDIT1, resultAsString);
	resultAsInt = static_cast<int32>(GetDlgItemInt(IDC_EDIT1));
	Limit(resultAsInt, m_minValueInt, m_maxValueInt);
	m_edit.GetDecimalValue(resultAsDouble);
	Limit(resultAsDouble, m_minValueDbl, m_maxValueDbl);
}


///////////////////////////////////////////////////////////////////////////////////////
// Messagebox with 'don't show again'-option.

class CMsgBoxHidable : public CDialog
{
public:
	CMsgBoxHidable(const TCHAR *strMsg, bool checkStatus = true, CWnd* pParent = NULL);
	enum { IDD = IDD_MSGBOX_HIDABLE };

	const TCHAR *m_StrMsg;
	int m_nCheckStatus;
protected:
	void DoDataExchange(CDataExchange* pDX) override;   // DDX/DDV support
	BOOL OnInitDialog() override;
};


struct MsgBoxHidableMessage
{
	const TCHAR *message;
	uint32 mask;
	bool defaultDontShowAgainStatus; // true for don't show again, false for show again.
};

static constexpr MsgBoxHidableMessage HidableMessages[] =
{
	{ _T("Note: First two bytes of oneshot samples are silenced for ProTracker compatibility."), 1, true },
	{ _T("Hint: To create IT-files without MPT-specific extensions included, try compatibility export from File-menu."), 1 << 1, true },
	{ _T("Press OK to apply signed/unsigned conversion\n (note: this often significantly increases volume level)"), 1 << 2, false },
	{ _T("Hint: To create XM-files without MPT-specific extensions included, try compatibility export from File-menu."), 1 << 3, true },
	{ _T("Warning: The exported file will not contain any of MPT's file format hacks."), 1 << 4, true },
};

static_assert(mpt::array_size<decltype(HidableMessages)>::size == enMsgBoxHidableMessage_count);

// Messagebox with 'don't show this again'-checkbox. Uses parameter 'enMsg'
// to get the needed information from message array, and updates the variable that
// controls the show/don't show-flags.
void MsgBoxHidable(enMsgBoxHidableMessage enMsg)
{
	// Check whether the message should be shown.
	if((TrackerSettings::Instance().gnMsgBoxVisiblityFlags & HidableMessages[enMsg].mask) == 0)
		return;

	// Show dialog.
	CMsgBoxHidable dlg(HidableMessages[enMsg].message, HidableMessages[enMsg].defaultDontShowAgainStatus);
	dlg.DoModal();

	// Update visibility flags.
	const uint32 mask = HidableMessages[enMsg].mask;
	if(dlg.m_nCheckStatus == BST_CHECKED)
		TrackerSettings::Instance().gnMsgBoxVisiblityFlags &= ~mask;
	else
		TrackerSettings::Instance().gnMsgBoxVisiblityFlags |= mask;
}


CMsgBoxHidable::CMsgBoxHidable(const  TCHAR *strMsg, bool checkStatus, CWnd* pParent)
	: CDialog(CMsgBoxHidable::IDD, pParent)
	, m_StrMsg(strMsg)
	, m_nCheckStatus((checkStatus) ? BST_CHECKED : BST_UNCHECKED)
{}

BOOL CMsgBoxHidable::OnInitDialog()
{
	CDialog::OnInitDialog();
	SetDlgItemText(IDC_MESSAGETEXT, m_StrMsg);
	SetWindowText(AfxGetAppName());
	return TRUE;
}

void CMsgBoxHidable::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	DDX_Check(pDX, IDC_DONTSHOWAGAIN, m_nCheckStatus);
}


/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////


void AppendNotesToControl(CComboBox& combobox, ModCommand::NOTE noteStart, ModCommand::NOTE noteEnd)
{
	const ModCommand::NOTE upperLimit = std::min(ModCommand::NOTE(NOTE_MAX), noteEnd);
	for(ModCommand::NOTE note = noteStart; note <= upperLimit; note++)
		combobox.SetItemData(combobox.AddString(mpt::ToCString(CSoundFile::GetNoteName(note, CSoundFile::GetDefaultNoteNames()))), note);
}


void AppendNotesToControlEx(CComboBox& combobox, const CSoundFile &sndFile, INSTRUMENTINDEX nInstr, ModCommand::NOTE noteStart, ModCommand::NOTE noteEnd)
{
	bool addSpecial = noteStart == noteEnd;
	if(noteStart == noteEnd)
	{
		noteStart = sndFile.GetModSpecifications().noteMin;
		noteEnd = sndFile.GetModSpecifications().noteMax;
	}
	for(ModCommand::NOTE note = noteStart; note <= noteEnd; note++)
	{
		combobox.SetItemData(combobox.AddString(mpt::ToCString(sndFile.GetNoteName(note, nInstr))), note);
	}
	if(addSpecial)
	{
		for(ModCommand::NOTE note = NOTE_MIN_SPECIAL - 1; note++ < NOTE_MAX_SPECIAL;)
		{
			if(sndFile.GetModSpecifications().HasNote(note))
				combobox.SetItemData(combobox.AddString(szSpecialNoteNamesMPT[note - NOTE_MIN_SPECIAL]), note);
		}
	}
}


OPENMPT_NAMESPACE_END