/*
 * PatternEditorDialogs.cpp
 * ------------------------
 * Purpose: Code for various dialogs that are used in the pattern editor.
 * Notes  : (currently none)
 * Authors: Olivier Lapicque
 *          OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "Mptrack.h"
#include "Mainfrm.h"
#include "InputHandler.h"
#include "Moddoc.h"
#include "View_pat.h"
#include "PatternEditorDialogs.h"
#include "TempoSwingDialog.h"
#include "../soundlib/mod_specifications.h"
#include "../common/mptStringBuffer.h"


OPENMPT_NAMESPACE_BEGIN


static constexpr EffectCommand ExtendedCommands[] = {CMD_OFFSET, CMD_PATTERNBREAK, CMD_POSITIONJUMP, CMD_TEMPO, CMD_FINETUNE, CMD_FINETUNE_SMOOTH};

// For a given pattern cell, check if it contains a command supported by the X-Param mechanism.
// If so, calculate the multipler for this cell and the value of all the other cells belonging to this param.
void getXParam(ModCommand::COMMAND command, PATTERNINDEX nPat, ROWINDEX nRow, CHANNELINDEX nChannel, const CSoundFile &sndFile, UINT &xparam, UINT &multiplier)
{
	UINT xp = 0, mult = 1;
	int cmdRow = static_cast<int>(nRow);
	const auto &pattern = sndFile.Patterns[nPat];

	if(command == CMD_XPARAM)
	{
		// current command is a parameter extension command
		cmdRow--;

		// Try to find previous command parameter to be extended
		while(cmdRow >= 0)
		{
			const ModCommand &m = *pattern.GetpModCommand(cmdRow, nChannel);
			if(mpt::contains(ExtendedCommands, m.command))
				break;
			if(m.command != CMD_XPARAM)
			{
				cmdRow = -1;
				break;
			}
			cmdRow--;
		}
	} else if(!mpt::contains(ExtendedCommands, command))
	{
		// If current row do not own any satisfying command parameter to extend, set return state
		cmdRow = -1;
	}

	if(cmdRow >= 0)
	{
		// An 'extendable' command parameter has been found
		const ModCommand &m = *pattern.GetpModCommand(cmdRow, nChannel);

		// Find extension resolution (8 to 24 bits)
		uint32 n = 1;
		while(n < 4 && cmdRow + n < pattern.GetNumRows())
		{
			if(pattern.GetpModCommand(cmdRow + n, nChannel)->command != CMD_XPARAM)
				break;
			n++;
		}

		// Parameter extension found (above 8 bits non-standard parameters)
		if(n > 1)
		{
			// Limit offset command to 24 bits, other commands to 16 bits
			n = m.command == CMD_OFFSET ? n : (n > 2 ? 2 : n);

			// Compute extended value WITHOUT current row parameter value : this parameter
			// is being currently edited (this is why this function is being called) so we
			// only need to compute a multiplier so that we can add its contribution while
			// its value is changed by user
			for(uint32 j = 0; j < n; j++)
			{
				const ModCommand &mx = *pattern.GetpModCommand(cmdRow + j, nChannel);

				uint32 k = 8 * (n - j - 1);
				if(cmdRow + j == nRow)
					mult = 1 << k;
				else
					xp += (mx.param << k);
			}
		} else if(m.command == CMD_OFFSET || m.command == CMD_FINETUNE || m.command == CMD_FINETUNE_SMOOTH)
		{
			// No parameter extension to perform (8 bits standard parameter),
			// just care about offset command special case (16 bits, fake)
			mult <<= 8;
		}

		const auto modDoc = sndFile.GetpModDoc();
		if(m.command == CMD_OFFSET && m.volcmd == VOLCMD_OFFSET && modDoc != nullptr)
		{
			SAMPLEINDEX smp = modDoc->GetSampleIndex(m);
			if(m.vol == 0 && smp != 0)
			{
				xp = Util::muldivr_unsigned(sndFile.GetSample(smp).nLength, pattern.GetpModCommand(nRow, nChannel)->param  * mult + xp, 256u << (8u * (std::max(uint32(2), n) - 1u)));
				mult = 0;
			} else if(m.vol > 0 && smp != 0)
			{
				xp += sndFile.GetSample(smp).cues[m.vol - 1];
			}
		}
	}

	// Return x-parameter
	multiplier = mult;
	xparam = xp;
}


/////////////////////////////////////////////////////////////////////////////////////////////
// CPatternPropertiesDlg

BEGIN_MESSAGE_MAP(CPatternPropertiesDlg, CDialog)
	ON_COMMAND(IDC_BUTTON_HALF,		&CPatternPropertiesDlg::OnHalfRowNumber)
	ON_COMMAND(IDC_BUTTON_DOUBLE,	&CPatternPropertiesDlg::OnDoubleRowNumber)
	ON_COMMAND(IDC_CHECK1,			&CPatternPropertiesDlg::OnOverrideSignature)
	ON_COMMAND(IDC_BUTTON1,			&CPatternPropertiesDlg::OnTempoSwing)
END_MESSAGE_MAP()

BOOL CPatternPropertiesDlg::OnInitDialog()
{
	CComboBox *combo;
	CDialog::OnInitDialog();
	combo = (CComboBox *)GetDlgItem(IDC_COMBO1);
	const CSoundFile &sndFile = modDoc.GetSoundFile();

	if(m_nPattern < sndFile.Patterns.Size() && combo)
	{
		CString s;
		const CPattern &pattern = sndFile.Patterns[m_nPattern];
		ROWINDEX nrows = pattern.GetNumRows();

		const CModSpecifications &specs = sndFile.GetModSpecifications();
		combo->SetRedraw(FALSE);
		for(UINT irow = specs.patternRowsMin; irow <= specs.patternRowsMax; irow++)
		{
			combo->AddString(mpt::cfmt::dec(irow));
		}
		combo->SetCurSel(nrows - specs.patternRowsMin);
		combo->SetRedraw(TRUE);

		CheckRadioButton(IDC_RADIO1, IDC_RADIO2, IDC_RADIO2);

		s = MPT_CFORMAT("Pattern #{}: {} row{} ({}K)")(
			m_nPattern,
			pattern.GetNumRows(),
			(pattern.GetNumRows() == 1) ? CString(_T("")) : CString(_T("s")),
			static_cast<int>((pattern.GetNumRows() * sndFile.GetNumChannels() * sizeof(ModCommand)) / 1024));
		SetDlgItemText(IDC_TEXT1, s);

		// Window title
		const CString patternName = mpt::ToCString(sndFile.GetCharsetInternal(), pattern.GetName());
		s = MPT_CFORMAT("Pattern Properties for Pattern #{}")(m_nPattern);
		if(!patternName.IsEmpty())
		{
			s += _T(" (");
			s += patternName;
			s += _T(")");
		}
		SetWindowText(s);

		// Pattern time signature
		const bool bOverride = pattern.GetOverrideSignature();
		ROWINDEX nRPB = pattern.GetRowsPerBeat(), nRPM = pattern.GetRowsPerMeasure();
		if(nRPB == 0 || !bOverride)
			nRPB = sndFile.m_nDefaultRowsPerBeat;
		if(nRPM == 0 || !bOverride)
			nRPM = sndFile.m_nDefaultRowsPerMeasure;

		m_tempoSwing = pattern.HasTempoSwing() ? pattern.GetTempoSwing() : sndFile.m_tempoSwing;

		GetDlgItem(IDC_CHECK1)->EnableWindow(sndFile.GetModSpecifications().hasPatternSignatures ? TRUE : FALSE);
		CheckDlgButton(IDC_CHECK1, bOverride ? BST_CHECKED : BST_UNCHECKED);
		SetDlgItemInt(IDC_ROWSPERBEAT, nRPB, FALSE);
		SetDlgItemInt(IDC_ROWSPERMEASURE, nRPM, FALSE);
		OnOverrideSignature();
	}
	return TRUE;
}


void CPatternPropertiesDlg::OnHalfRowNumber()
{
	const CSoundFile &sndFile = modDoc.GetSoundFile();

	UINT nRows = GetDlgItemInt(IDC_COMBO1, NULL, FALSE);
	nRows /= 2;
	if(nRows < sndFile.GetModSpecifications().patternRowsMin)
		nRows = sndFile.GetModSpecifications().patternRowsMin;
	SetDlgItemInt(IDC_COMBO1, nRows, FALSE);
}


void CPatternPropertiesDlg::OnDoubleRowNumber()
{
	const CSoundFile &sndFile = modDoc.GetSoundFile();

	UINT nRows = GetDlgItemInt(IDC_COMBO1, NULL, FALSE);
	nRows *= 2;
	if(nRows > sndFile.GetModSpecifications().patternRowsMax)
		nRows = sndFile.GetModSpecifications().patternRowsMax;
	SetDlgItemInt(IDC_COMBO1, nRows, FALSE);
}


void CPatternPropertiesDlg::OnOverrideSignature()
{
	GetDlgItem(IDC_ROWSPERBEAT)->EnableWindow(IsDlgButtonChecked(IDC_CHECK1));
	GetDlgItem(IDC_ROWSPERMEASURE)->EnableWindow(IsDlgButtonChecked(IDC_CHECK1));
	GetDlgItem(IDC_BUTTON1)->EnableWindow(IsDlgButtonChecked(IDC_CHECK1) && modDoc.GetSoundFile().m_nTempoMode == TempoMode::Modern);
}


void CPatternPropertiesDlg::OnTempoSwing()
{
	CPattern &pat = modDoc.GetSoundFile().Patterns[m_nPattern];
	const ROWINDEX oldRPB = pat.GetRowsPerBeat();
	const ROWINDEX oldRPM = pat.GetRowsPerMeasure();

	// 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);
	pat.SetSignature(newRPB, newRPM);

	m_tempoSwing.resize(newRPB, TempoSwing::Unity);
	CTempoSwingDlg dlg(this, m_tempoSwing, modDoc.GetSoundFile(), m_nPattern);
	if(dlg.DoModal() == IDOK)
	{
		m_tempoSwing = dlg.m_tempoSwing;
	}
	pat.SetSignature(oldRPB, oldRPM);
}


void CPatternPropertiesDlg::OnOK()
{
	CSoundFile &sndFile = modDoc.GetSoundFile();
	CPattern &pattern = sndFile.Patterns[m_nPattern];
	// Update pattern signature if necessary
	if(sndFile.GetModSpecifications().hasPatternSignatures)
	{
		if(IsDlgButtonChecked(IDC_CHECK1))
		{
			// Enable signature
			const ROWINDEX newRPB = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERBEAT, NULL, FALSE)), MAX_ROWS_PER_BEAT);
			const ROWINDEX newRPM = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERMEASURE, NULL, FALSE)), MAX_ROWS_PER_BEAT);

			if(newRPB != pattern.GetRowsPerBeat() || newRPM != pattern.GetRowsPerMeasure() || m_tempoSwing != pattern.GetTempoSwing())
			{
				if(!pattern.SetSignature(newRPB, newRPM))
				{
					Reporting::Error("Invalid time signature!", "Pattern Properties");
					GetDlgItem(IDC_ROWSPERBEAT)->SetFocus();
					return;
				}
				m_tempoSwing.resize(newRPB, TempoSwing::Unity);
				pattern.SetTempoSwing(m_tempoSwing);
				modDoc.SetModified();
			}
		} else
		{
			// Disable signature
			if(pattern.GetOverrideSignature() || pattern.HasTempoSwing())
			{
				pattern.RemoveSignature();
				pattern.RemoveTempoSwing();
				modDoc.SetModified();
			}
		}
	}


	const ROWINDEX newSize = (ROWINDEX)GetDlgItemInt(IDC_COMBO1, NULL, FALSE);

	// Check if any pattern data would be removed.
	bool resize = (newSize != sndFile.Patterns[m_nPattern].GetNumRows());
	bool resizeAtEnd = IsDlgButtonChecked(IDC_RADIO2) != BST_UNCHECKED;
	if(newSize < sndFile.Patterns[m_nPattern].GetNumRows())
	{
		ROWINDEX firstRow = resizeAtEnd ? newSize : 0;
		ROWINDEX lastRow = resizeAtEnd ? sndFile.Patterns[m_nPattern].GetNumRows() : sndFile.Patterns[m_nPattern].GetNumRows() - newSize;
		for(ROWINDEX row = firstRow; row < lastRow; row++)
		{
			if(!sndFile.Patterns[m_nPattern].IsEmptyRow(row))
			{
				resize = (Reporting::Confirm(MPT_AFORMAT("Data at the {} of the pattern will be lost.\nDo you want to continue?")(resizeAtEnd ? "end" : "start"), "Shrink Pattern") == cnfYes);
				break;
			}
		}
	}

	if(resize)
	{
		modDoc.BeginWaitCursor();
		modDoc.GetPatternUndo().PrepareUndo(m_nPattern, 0, 0, sndFile.Patterns[m_nPattern].GetNumChannels(), sndFile.Patterns[m_nPattern].GetNumRows(), "Resize");
		if(sndFile.Patterns[m_nPattern].Resize(newSize, true, resizeAtEnd))
		{
			modDoc.SetModified();
		}
		modDoc.EndWaitCursor();
	}
	CDialog::OnOK();
}


////////////////////////////////////////////////////////////////////////////////////////////
// CEditCommand

BEGIN_MESSAGE_MAP(CEditCommand, CDialog)
	ON_WM_ACTIVATE()
	ON_WM_CLOSE()

	ON_CBN_SELCHANGE(IDC_COMBO1,	&CEditCommand::OnNoteChanged)
	ON_CBN_SELCHANGE(IDC_COMBO2,	&CEditCommand::OnNoteChanged)
	ON_CBN_SELCHANGE(IDC_COMBO3,	&CEditCommand::OnVolCmdChanged)
	ON_CBN_SELCHANGE(IDC_COMBO4,	&CEditCommand::OnCommandChanged)
	ON_CBN_SELCHANGE(IDC_COMBO5,	&CEditCommand::OnPlugParamChanged)
	ON_WM_HSCROLL()
END_MESSAGE_MAP()


void CEditCommand::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CSplitKeyboadSettings)
	DDX_Control(pDX, IDC_COMBO1,	cbnNote);
	DDX_Control(pDX, IDC_COMBO2,	cbnInstr);
	DDX_Control(pDX, IDC_COMBO3,	cbnVolCmd);
	DDX_Control(pDX, IDC_COMBO4,	cbnCommand);
	DDX_Control(pDX, IDC_COMBO5,	cbnPlugParam);
	DDX_Control(pDX, IDC_SLIDER1,	sldVolParam);
	DDX_Control(pDX, IDC_SLIDER2,	sldParam);
	//}}AFX_DATA_MAP
}


CEditCommand::CEditCommand(CSoundFile &sndFile)
    : sndFile(sndFile), effectInfo(sndFile)
{
	CDialog::Create(IDD_PATTERN_EDITCOMMAND);
}


BOOL CEditCommand::PreTranslateMessage(MSG *pMsg)
{
	if((pMsg) && (pMsg->message == WM_KEYDOWN))
	{
		if((pMsg->wParam == VK_ESCAPE) || (pMsg->wParam == VK_RETURN) || (pMsg->wParam == VK_APPS))
		{
			OnClose();
			return TRUE;
		}
	}
	return CDialog::PreTranslateMessage(pMsg);
}


bool CEditCommand::ShowEditWindow(PATTERNINDEX pat, const PatternCursor &cursor, CWnd *parent)
{
	editPattern = pat;
	const ROWINDEX row = editRow = cursor.GetRow();
	const CHANNELINDEX chn = editChannel = cursor.GetChannel();

	if(!sndFile.Patterns.IsValidPat(pat)
	   || !sndFile.Patterns[pat].IsValidRow(row)
	   || chn >= sndFile.GetNumChannels())
	{
		ShowWindow(SW_HIDE);
		return false;
	}

	m = sndFile.Patterns[pat].GetpModCommand(row, chn);
	modified = false;

	InitAll();

	switch(cursor.GetColumnType())
	{
	case PatternCursor::noteColumn:
		cbnNote.SetFocus();
		break;
	case PatternCursor::instrColumn:
		cbnInstr.SetFocus();
		break;
	case PatternCursor::volumeColumn:
		if(m->IsPcNote())
			cbnPlugParam.SetFocus();
		else
			cbnVolCmd.SetFocus();
		break;
	case PatternCursor::effectColumn:
		if(m->IsPcNote())
			sldParam.SetFocus();
		else
			cbnCommand.SetFocus();
		break;
	case PatternCursor::paramColumn:
		sldParam.SetFocus();
		break;
	}

	// Update Window Title
	SetWindowText(MPT_CFORMAT("Note Properties - Row {}, Channel {}")(row, chn + 1));

	CRect rectParent, rectWnd;
	parent->GetWindowRect(&rectParent);
	GetClientRect(&rectWnd);
	SetWindowPos(CMainFrame::GetMainFrame(),
		rectParent.left + (rectParent.Width() - rectWnd.right) / 2,
		rectParent.top + (rectParent.Height() - rectWnd.bottom) / 2,
		-1, -1, SWP_NOSIZE | SWP_NOACTIVATE);
	ShowWindow(SW_RESTORE);
	return true;
}


void CEditCommand::InitNote()
{
	// Note
	cbnNote.SetRedraw(FALSE);
	if(oldSpecs != &sndFile.GetModSpecifications())
	{
		cbnNote.ResetContent();
		cbnNote.SetItemData(cbnNote.AddString(_T("No Note")), 0);
		AppendNotesToControlEx(cbnNote, sndFile, m->instr);
		oldSpecs = &sndFile.GetModSpecifications();
	}

	if(m->IsNote())
	{
		// Normal note / no note
		const ModCommand::NOTE noteStart = sndFile.GetModSpecifications().noteMin;
		cbnNote.SetCurSel(m->note - (noteStart - 1));
	} else if(m->note == NOTE_NONE)
	{
		cbnNote.SetCurSel(0);
	} else
	{
		// Special notes
		for(int i = cbnNote.GetCount() - 1; i >= 0; --i)
		{
			if(cbnNote.GetItemData(i) == m->note)
			{
				cbnNote.SetCurSel(i);
				break;
			}
		}
	}
	cbnNote.SetRedraw(TRUE);

	// Instrument
	cbnInstr.SetRedraw(FALSE);
	cbnInstr.ResetContent();

	if(m->IsPcNote())
	{
		// control plugin param note
		cbnInstr.SetItemData(cbnInstr.AddString(_T("No Effect")), 0);
		AddPluginNamesToCombobox(cbnInstr, sndFile.m_MixPlugins, false);
	} else
	{
		// instrument / sample
		cbnInstr.SetItemData(cbnInstr.AddString(_T("No Instrument")), 0);
		const uint32 nmax = sndFile.GetNumInstruments() ? sndFile.GetNumInstruments() : sndFile.GetNumSamples();
		for(uint32 i = 1; i <= nmax; i++)
		{
			CString s = mpt::cfmt::val(i) + _T(": ");
			// instrument / sample
			if(sndFile.GetNumInstruments())
			{
				if(sndFile.Instruments[i])
					s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.Instruments[i]->name);
			} else
				s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[i]);
			cbnInstr.SetItemData(cbnInstr.AddString(s), i);
		}
	}
	cbnInstr.SetCurSel(m->instr);
	cbnInstr.SetRedraw(TRUE);
}


void CEditCommand::InitVolume()
{
	cbnVolCmd.SetRedraw(FALSE);
	cbnVolCmd.ResetContent();
	if(sndFile.GetType() == MOD_TYPE_MOD || m->IsPcNote())
	{
		cbnVolCmd.EnableWindow(FALSE);
		sldVolParam.EnableWindow(FALSE);
	} else
	{
		// Normal volume column effect
		cbnVolCmd.EnableWindow(TRUE);
		sldVolParam.EnableWindow(TRUE);
		uint32 count = effectInfo.GetNumVolCmds();
		cbnVolCmd.SetItemData(cbnVolCmd.AddString(_T(" None")), (DWORD_PTR)-1);
		cbnVolCmd.SetCurSel(0);
		UINT fxndx = effectInfo.GetIndexFromVolCmd(m->volcmd);
		for(uint32 i = 0; i < count; i++)
		{
			CString s;
			if(effectInfo.GetVolCmdInfo(i, &s))
			{
				int k = cbnVolCmd.AddString(s);
				cbnVolCmd.SetItemData(k, i);
				if(i == fxndx)
					cbnVolCmd.SetCurSel(k);
			}
		}
		UpdateVolCmdRange();
	}
	cbnVolCmd.SetRedraw(TRUE);
}


void CEditCommand::InitEffect()
{
	if(m->IsPcNote())
	{
		cbnCommand.ShowWindow(SW_HIDE);
		return;
	}
	cbnCommand.ShowWindow(SW_SHOW);
	xParam = 0;
	xMultiplier = 1;
	getXParam(m->command, editPattern, editRow, editChannel, sndFile, xParam, xMultiplier);

	cbnCommand.SetRedraw(FALSE);
	cbnCommand.ResetContent();
	uint32 numfx = effectInfo.GetNumEffects();
	uint32 fxndx = effectInfo.GetIndexFromEffect(m->command, m->param);
	cbnCommand.SetItemData(cbnCommand.AddString(_T(" None")), (DWORD_PTR)-1);
	if(m->command == CMD_NONE)
		cbnCommand.SetCurSel(0);

	CString s;
	for(uint32 i = 0; i < numfx; i++)
	{
		if(effectInfo.GetEffectInfo(i, &s, true))
		{
			int k = cbnCommand.AddString(s);
			cbnCommand.SetItemData(k, i);
			if(i == fxndx)
				cbnCommand.SetCurSel(k);
		}
	}
	UpdateEffectRange(false);
	cbnCommand.SetRedraw(TRUE);
	cbnCommand.Invalidate();
}


void CEditCommand::InitPlugParam()
{
	if(!m->IsPcNote())
	{
		cbnPlugParam.ShowWindow(SW_HIDE);
		return;
	}
	cbnPlugParam.ShowWindow(SW_SHOW);

	cbnPlugParam.SetRedraw(FALSE);
	cbnPlugParam.ResetContent();

	if(m->instr > 0 && m->instr <= MAX_MIXPLUGINS)
	{
		AddPluginParameternamesToCombobox(cbnPlugParam, sndFile.m_MixPlugins[m->instr - 1]);
		cbnPlugParam.SetCurSel(m->GetValueVolCol());
	}
	UpdateEffectRange(false);

	cbnPlugParam.SetRedraw(TRUE);
	cbnPlugParam.Invalidate();
}


void CEditCommand::UpdateVolCmdRange()
{
	ModCommand::VOL rangeMin = 0, rangeMax = 0;
	LONG fxndx = effectInfo.GetIndexFromVolCmd(m->volcmd);
	bool ok = effectInfo.GetVolCmdInfo(fxndx, NULL, &rangeMin, &rangeMax);
	if(ok && rangeMax > rangeMin)
	{
		sldVolParam.EnableWindow(TRUE);
		sldVolParam.SetRange(rangeMin, rangeMax);
		Limit(m->vol, rangeMin, rangeMax);
		sldVolParam.SetPos(m->vol);
	} else
	{
		// Why does this not update the display at all?
		sldVolParam.SetRange(0, 0);
		sldVolParam.SetPos(0);
		sldVolParam.EnableWindow(FALSE);
	}
	UpdateVolCmdValue();
}


void CEditCommand::UpdateEffectRange(bool set)
{
	DWORD pos;
	bool enable = true;

	if(m->IsPcNote())
	{
		// plugin param control note
		sldParam.SetRange(0, ModCommand::maxColumnValue);
		pos = m->GetValueEffectCol();
	} else
	{
		// process as effect
		ModCommand::PARAM rangeMin = 0, rangeMax = 0;
		LONG fxndx = effectInfo.GetIndexFromEffect(m->command, m->param);
		enable = ((fxndx >= 0) && (effectInfo.GetEffectInfo(fxndx, NULL, false, &rangeMin, &rangeMax)));

		pos = effectInfo.MapValueToPos(fxndx, m->param);
		if(pos > rangeMax)
			pos = rangeMin | (pos & 0x0F);
		Limit(pos, rangeMin, rangeMax);

		sldParam.SetRange(rangeMin, rangeMax);
	}

	if(enable)
	{
		sldParam.EnableWindow(TRUE);
		sldParam.SetPageSize(1);
		sldParam.SetPos(pos);
	} else
	{
		// Why does this not update the display at all?
		sldParam.SetRange(0, 0);
		sldParam.SetPos(0);
		sldParam.EnableWindow(FALSE);
	}
	UpdateEffectValue(set);
}


void CEditCommand::OnNoteChanged()
{
	const bool wasParamControl = m->IsPcNote();
	ModCommand::NOTE newNote = m->note;
	ModCommand::INSTR newInstr = m->instr;

	int n = cbnNote.GetCurSel();
	if(n >= 0)
		newNote = static_cast<ModCommand::NOTE>(cbnNote.GetItemData(n));

	n = cbnInstr.GetCurSel();
	if(n >= 0)
		newInstr = static_cast<ModCommand::INSTR>(cbnInstr.GetItemData(n));

	if(m->note != newNote || m->instr != newInstr)
	{
		PrepareUndo("Note Entry");
		CModDoc *modDoc = sndFile.GetpModDoc();
		m->note = newNote;
		m->instr = newInstr;

		modDoc->UpdateAllViews(nullptr, RowHint(editRow), nullptr);

		if(wasParamControl != m->IsPcNote())
		{
			InitAll();
		} else if(!m->IsPcNote()
		          && m->instr <= sndFile.GetNumInstruments()
		          && newInstr <= sndFile.GetNumInstruments()
		          && sndFile.Instruments[m->instr] != nullptr
		          && sndFile.Instruments[newInstr] != nullptr
		          && sndFile.Instruments[newInstr]->pTuning != sndFile.Instruments[m->instr]->pTuning)
		{
			//Checking whether note names should be recreated.
			InitNote();
		} else if(m->IsPcNote())
		{
			// Update parameter list
			InitPlugParam();
		}
	}
}


void CEditCommand::OnVolCmdChanged()
{
	ModCommand::VOLCMD newVolCmd = m->volcmd;
	ModCommand::VOL newVol = m->vol;

	int n = cbnVolCmd.GetCurSel();
	if(n >= 0)
	{
		newVolCmd = effectInfo.GetVolCmdFromIndex(static_cast<UINT>(cbnVolCmd.GetItemData(n)));
	}

	newVol = static_cast<ModCommand::VOL>(sldVolParam.GetPos());

	const bool volCmdChanged = m->volcmd != newVolCmd;
	if(volCmdChanged || m->vol != newVol)
	{
		PrepareUndo("Volume Entry");
		CModDoc *modDoc = sndFile.GetpModDoc();
		m->volcmd = newVolCmd;
		m->vol = newVol;

		modDoc->UpdateAllViews(nullptr, RowHint(editRow), nullptr);

		if(volCmdChanged)
			UpdateVolCmdRange();
		else
			UpdateVolCmdValue();
	}
}


void CEditCommand::OnCommandChanged()
{
	ModCommand::COMMAND newCommand = m->command;
	ModCommand::PARAM newParam = m->param;

	int n = cbnCommand.GetCurSel();
	if(n >= 0)
	{
		int ndx = static_cast<int>(cbnCommand.GetItemData(n));
		newCommand = static_cast<ModCommand::COMMAND>((ndx >= 0) ? effectInfo.GetEffectFromIndex(ndx, newParam) : CMD_NONE);
	}

	if(m->command != newCommand || m->param != newParam)
	{
		PrepareUndo("Effect Entry");

		m->command = newCommand;
		if(newCommand != CMD_NONE)
		{
			m->param = newParam;
		}

		xParam = 0;
		xMultiplier = 1;
		if(newCommand == CMD_XPARAM || mpt::contains(ExtendedCommands, newCommand))
		{
			getXParam(newCommand, editPattern, editRow, editChannel, sndFile, xParam, xMultiplier);
		}

		UpdateEffectRange(true);

		sndFile.GetpModDoc()->UpdateAllViews(nullptr, RowHint(editRow), nullptr);
	}
}


void CEditCommand::OnPlugParamChanged()
{
	uint16 newPlugParam = m->GetValueVolCol();

	int n = cbnPlugParam.GetCurSel();
	if(n >= 0)
	{
		newPlugParam = static_cast<uint16>(cbnPlugParam.GetItemData(n));
	}

	if(m->GetValueVolCol() != newPlugParam)
	{
		PrepareUndo("Effect Entry");
		m->SetValueVolCol(newPlugParam);
		sndFile.GetpModDoc()->UpdateAllViews(nullptr, RowHint(editRow), nullptr);
	}
}


void CEditCommand::UpdateVolCmdValue()
{
	CString s;
	if(m->IsPcNote())
	{
		// plugin param control note
		uint16 plugParam = static_cast<uint16>(sldVolParam.GetPos());
		s.Format(_T("Value: %u"), plugParam);
	} else
	{
		// process as effect
		effectInfo.GetVolCmdParamInfo(*m, &s);
	}
	SetDlgItemText(IDC_TEXT2, s);
}


void CEditCommand::UpdateEffectValue(bool set)
{
	CString s;

	uint16 newPlugParam = 0;
	ModCommand::PARAM newParam = 0;

	if(m->IsPcNote())
	{
		// plugin param control note
		newPlugParam = static_cast<uint16>(sldParam.GetPos());
		s.Format(_T("Value: %u"), newPlugParam);
	} else
	{
		// process as effect
		LONG fxndx = effectInfo.GetIndexFromEffect(m->command, m->param);
		if(fxndx >= 0)
		{
			newParam = static_cast<ModCommand::PARAM>(effectInfo.MapPosToValue(fxndx, sldParam.GetPos()));
			effectInfo.GetEffectNameEx(s, *m, newParam * xMultiplier + xParam, editChannel);
		}
	}
	SetDlgItemText(IDC_TEXT1, s);

	if(set)
	{
		if((!m->IsPcNote() && m->param != newParam)
		   || (m->IsPcNote() && m->GetValueVolCol() != newPlugParam))
		{
			PrepareUndo("Effect Entry");
			CModDoc *modDoc = sndFile.GetpModDoc();
			if(m->IsPcNote())
			{
				m->SetValueEffectCol(newPlugParam);
			} else
			{
				m->param = newParam;
			}

			modDoc->UpdateAllViews(nullptr, RowHint(editRow), nullptr);
		}
	}
}


void CEditCommand::PrepareUndo(const char *description)
{
	CModDoc *modDoc = sndFile.GetpModDoc();
	if(!modified)
	{
		// Let's create just one undo step.
		modDoc->GetPatternUndo().PrepareUndo(editPattern, editChannel, editRow, 1, 1, description);
		modified = true;
	}
	modDoc->SetModified();
}


void CEditCommand::OnHScroll(UINT, UINT, CScrollBar *bar)
{
	if(bar == static_cast<CWnd *>(&sldVolParam))
	{
		OnVolCmdChanged();
	} else if(bar == static_cast<CWnd *>(&sldParam))
	{
		UpdateEffectValue(true);
	}
}


void CEditCommand::OnActivate(UINT nState, CWnd *pWndOther, BOOL bMinimized)
{
	CDialog::OnActivate(nState, pWndOther, bMinimized);
	if(nState == WA_INACTIVE)
		ShowWindow(SW_HIDE);
}


////////////////////////////////////////////////////////////////////////////////////////////
// Chord Editor

BEGIN_MESSAGE_MAP(CChordEditor, ResizableDialog)
	ON_MESSAGE(WM_MOD_KBDNOTIFY, &CChordEditor::OnKeyboardNotify)
	ON_CBN_SELCHANGE(IDC_COMBO1, &CChordEditor::OnChordChanged)
	ON_CBN_SELCHANGE(IDC_COMBO2, &CChordEditor::OnBaseNoteChanged)
	ON_CBN_SELCHANGE(IDC_COMBO3, &CChordEditor::OnNote1Changed)
	ON_CBN_SELCHANGE(IDC_COMBO4, &CChordEditor::OnNote2Changed)
	ON_CBN_SELCHANGE(IDC_COMBO5, &CChordEditor::OnNote3Changed)
END_MESSAGE_MAP()


void CChordEditor::DoDataExchange(CDataExchange *pDX)
{
	ResizableDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CChordEditor)
	DDX_Control(pDX, IDC_KEYBOARD1, m_Keyboard);
	DDX_Control(pDX, IDC_COMBO1,    m_CbnShortcut);
	DDX_Control(pDX, IDC_COMBO2,    m_CbnBaseNote);
	DDX_Control(pDX, IDC_COMBO3,    m_CbnNote[0]);
	DDX_Control(pDX, IDC_COMBO4,    m_CbnNote[1]);
	DDX_Control(pDX, IDC_COMBO5,    m_CbnNote[2]);
	static_assert(mpt::array_size<decltype(m_CbnNote)>::size == 3);
	//}}AFX_DATA_MAP
}


CChordEditor::CChordEditor(CWnd *parent)
    : ResizableDialog(IDD_CHORDEDIT, parent)
{
	m_chords = TrackerSettings::GetChords();
}

BOOL CChordEditor::OnInitDialog()
{
	ResizableDialog::OnInitDialog();
	m_Keyboard.Init(this, (CHORD_MAX - CHORD_MIN) / 12, true);

	m_CbnShortcut.SetRedraw(FALSE);
	m_CbnBaseNote.SetRedraw(FALSE);
	for(auto &combo : m_CbnNote)
		combo.SetRedraw(FALSE);

	// Shortcut key combo box
	AppendNotesToControl(m_CbnShortcut, NOTE_MIN, NOTE_MIN + static_cast<int>(kcVPEndChords) - static_cast<int>(kcVPStartChords));

	m_CbnShortcut.SetCurSel(0);
	// Base Note combo box
	m_CbnBaseNote.SetItemData(m_CbnBaseNote.AddString(_T("Relative")), MPTChord::relativeMode);
	AppendNotesToControl(m_CbnBaseNote, NOTE_MIN, NOTE_MIN + 3 * 12 - 1);

	// Chord Note combo boxes
	CString s;
	for(int note = CHORD_MIN - 1; note < CHORD_MAX; note++)
	{
		int noteVal = note;
		if(note == CHORD_MIN - 1)
		{
			s = _T("--");
			noteVal = MPTChord::noNote;
		} else
		{
			s = mpt::ToCString(CSoundFile::GetDefaultNoteName(mpt::wrapping_modulo(note, 12)));
			const int octave = mpt::wrapping_divide(note, 12);
			if(octave > 0)
				s.AppendFormat(_T(" (+%d)"), octave);
			else if(octave < 0)
				s.AppendFormat(_T(" (%d)"), octave);
		}
		for(auto &combo : m_CbnNote)
			combo.SetItemData(combo.AddString(s), noteVal);
	}

	m_CbnShortcut.SetRedraw(TRUE);
	m_CbnBaseNote.SetRedraw(TRUE);
	for(auto &combo : m_CbnNote)
		combo.SetRedraw(TRUE);

	// Update Dialog
	OnChordChanged();
	return TRUE;
}


void CChordEditor::OnOK()
{
	TrackerSettings::GetChords() = m_chords;
	ResizableDialog::OnOK();
}


MPTChord &CChordEditor::GetChord()
{
	int chord = m_CbnShortcut.GetCurSel();
	if(chord >= 0)
		chord = static_cast<int>(m_CbnShortcut.GetItemData(chord)) - NOTE_MIN;
	if(chord < 0 || chord >= static_cast<int>(m_chords.size()))
		chord = 0;
	return m_chords[chord];
}


LRESULT CChordEditor::OnKeyboardNotify(WPARAM cmd, LPARAM nKey)
{
	const bool outside = static_cast<int>(nKey) == -1;
	if(cmd == KBDNOTIFY_LBUTTONUP && outside)
	{
		// Stopped dragging ouside of keyboard area
		m_mouseDownKey = m_dragKey = MPTChord::noNote;
		return 0;
	} else if (cmd == KBDNOTIFY_MOUSEMOVE || outside)
	{
		return 0;
	}

	MPTChord &chord = GetChord();
	const MPTChord::NoteType key = static_cast<MPTChord::NoteType>(nKey) + CHORD_MIN;
	bool update = false;

	if(cmd == KBDNOTIFY_LBUTTONDOWN && m_mouseDownKey == MPTChord::noNote)
	{
		// Initial mouse down
		m_mouseDownKey = key;
		m_dragKey = MPTChord::noNote;
		return 0;
	}
	if(cmd == KBDNOTIFY_LBUTTONDOWN && m_dragKey == MPTChord::noNote && key != m_mouseDownKey)
	{
		// Start dragging
		m_dragKey = m_mouseDownKey;
	}

	// Remove dragged note or toggle
	bool noteIsSet = false;
	for(auto &note : chord.notes)
	{
		if((m_dragKey != MPTChord::noNote && note == m_dragKey)
		   || (m_dragKey == MPTChord::noNote && note == m_mouseDownKey))
		{
			note = MPTChord::noNote;
			noteIsSet = update = true;
			break;
		}
	}

	// Move or toggle note
	if(cmd != KBDNOTIFY_LBUTTONUP || m_dragKey != MPTChord::noNote || !noteIsSet)
	{
		for(auto &note : chord.notes)
		{
			if(note == MPTChord::noNote)
			{
				note = key;
				update = true;
				break;
			}
		}
	}

	if(cmd == KBDNOTIFY_LBUTTONUP)
		m_mouseDownKey = m_dragKey = MPTChord::noNote;
	else
		m_dragKey = key;

	if(update)
	{
		std::sort(chord.notes.begin(), chord.notes.end(), [](MPTChord::NoteType left, MPTChord::NoteType right)
		{
			return (left == MPTChord::noNote)  ? false : (left < right);
		});
		OnChordChanged();
	}
	return 0;
}


void CChordEditor::OnChordChanged()
{
	const MPTChord &chord = GetChord();
	if(chord.key != MPTChord::relativeMode)
		m_CbnBaseNote.SetCurSel(chord.key + 1);
	else
		m_CbnBaseNote.SetCurSel(0);
	for(int i = 0; i < MPTChord::notesPerChord - 1; i++)
	{
		int note = chord.notes[i];
		if(note == MPTChord::noNote)
			note = 0;
		else
			note += 1 - CHORD_MIN;
		m_CbnNote[i].SetCurSel(note);
	}
	UpdateKeyboard();
}


void CChordEditor::UpdateKeyboard()
{
	MPTChord &chord = GetChord();
	const int baseNote = (chord.key == MPTChord::relativeMode) ? 0 : (chord.key % 12);
	for(int i = CHORD_MIN; i < CHORD_MAX; i++)
	{
		uint8 b = CKeyboardControl::KEYFLAG_NORMAL;
		for(const auto note : chord.notes)
		{
			if(i == note)
				b |= CKeyboardControl::KEYFLAG_REDDOT;
		}
		if(i == baseNote)
			b |= CKeyboardControl::KEYFLAG_BRIGHTDOT;
		m_Keyboard.SetFlags(i - CHORD_MIN, b);
	}
	m_Keyboard.InvalidateRect(nullptr, FALSE);
}


void CChordEditor::OnBaseNoteChanged()
{
	MPTChord &chord = GetChord();
	int basenote = static_cast<int>(m_CbnBaseNote.GetItemData(m_CbnBaseNote.GetCurSel()));
	if(basenote != MPTChord::relativeMode)
		basenote -= NOTE_MIN;
	chord.key = (uint8)basenote;
	UpdateKeyboard();
}


void CChordEditor::OnNoteChanged(int noteIndex)
{
	MPTChord &chord = GetChord();
	int note = m_CbnNote[noteIndex].GetCurSel();
	if(note < 0)
		return;
	chord.notes[noteIndex] = static_cast<int8>(m_CbnNote[noteIndex].GetItemData(note));
	UpdateKeyboard();
}


////////////////////////////////////////////////////////////////////////////////////////////
// Keyboard Split Settings (pattern editor)

BEGIN_MESSAGE_MAP(CSplitKeyboardSettings, CDialog)
	ON_CBN_SELCHANGE(IDC_COMBO_OCTAVEMODIFIER, &CSplitKeyboardSettings::OnOctaveModifierChanged)
END_MESSAGE_MAP()


void CSplitKeyboardSettings::DoDataExchange(CDataExchange *pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CSplitKeyboadSettings)
	DDX_Control(pDX, IDC_COMBO_SPLITINSTRUMENT, m_CbnSplitInstrument);
	DDX_Control(pDX, IDC_COMBO_SPLITNOTE,       m_CbnSplitNote);
	DDX_Control(pDX, IDC_COMBO_OCTAVEMODIFIER,  m_CbnOctaveModifier);
	DDX_Control(pDX, IDC_COMBO_SPLITVOLUME,     m_CbnSplitVolume);
	//}}AFX_DATA_MAP
}


BOOL CSplitKeyboardSettings::OnInitDialog()
{
	if(sndFile.GetpModDoc() == nullptr)
		return TRUE;

	CDialog::OnInitDialog();

	CString s;

	// Split Notes
	AppendNotesToControl(m_CbnSplitNote, sndFile.GetModSpecifications().noteMin, sndFile.GetModSpecifications().noteMax);
	m_CbnSplitNote.SetCurSel(m_Settings.splitNote - (sndFile.GetModSpecifications().noteMin - NOTE_MIN));

	// Octave modifier
	m_CbnOctaveModifier.SetRedraw(FALSE);
	m_CbnSplitVolume.InitStorage(SplitKeyboardSettings::splitOctaveRange * 2 + 1, 9);
	for(int i = -SplitKeyboardSettings::splitOctaveRange; i < SplitKeyboardSettings::splitOctaveRange + 1; i++)
	{
		s.Format(i < 0 ? _T("Octave -%d") : i > 0 ? _T("Octave +%d") : _T("No Change"), std::abs(i));
		int n = m_CbnOctaveModifier.AddString(s);
		m_CbnOctaveModifier.SetItemData(n, i);
	}
	m_CbnOctaveModifier.SetRedraw(TRUE);
	m_CbnOctaveModifier.SetCurSel(m_Settings.octaveModifier + SplitKeyboardSettings::splitOctaveRange);
	CheckDlgButton(IDC_PATTERN_OCTAVELINK, (m_Settings.octaveLink && m_Settings.octaveModifier != 0) ? BST_CHECKED : BST_UNCHECKED);

	// Volume
	m_CbnSplitVolume.SetRedraw(FALSE);
	m_CbnSplitVolume.InitStorage(65, 4);
	m_CbnSplitVolume.AddString(_T("No Change"));
	m_CbnSplitVolume.SetItemData(0, 0);
	for(int i = 1; i <= 64; i++)
	{
		s.Format(_T("%d"), i);
		int n = m_CbnSplitVolume.AddString(s);
		m_CbnSplitVolume.SetItemData(n, i);
	}
	m_CbnSplitVolume.SetRedraw(TRUE);
	m_CbnSplitVolume.SetCurSel(m_Settings.splitVolume);

	// Instruments
	m_CbnSplitInstrument.SetRedraw(FALSE);
	m_CbnSplitInstrument.InitStorage(1 + (sndFile.GetNumInstruments() ? sndFile.GetNumInstruments() : sndFile.GetNumSamples()), 16);
	m_CbnSplitInstrument.SetItemData(m_CbnSplitInstrument.AddString(_T("No Change")), 0);

	if(sndFile.GetNumInstruments())
	{
		for(INSTRUMENTINDEX nIns = 1; nIns <= sndFile.GetNumInstruments(); nIns++)
		{
			if(sndFile.Instruments[nIns] == nullptr)
				continue;

			CString displayName = sndFile.GetpModDoc()->GetPatternViewInstrumentName(nIns);
			int n = m_CbnSplitInstrument.AddString(displayName);
			m_CbnSplitInstrument.SetItemData(n, nIns);
		}
	} else
	{
		for(SAMPLEINDEX nSmp = 1; nSmp <= sndFile.GetNumSamples(); nSmp++)
		{
			if(sndFile.GetSample(nSmp).HasSampleData())
			{
				s.Format(_T("%02d: "), nSmp);
				s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[nSmp]);
				int n = m_CbnSplitInstrument.AddString(s);
				m_CbnSplitInstrument.SetItemData(n, nSmp);
			}
		}
	}
	m_CbnSplitInstrument.SetRedraw(TRUE);
	m_CbnSplitInstrument.SetCurSel(m_Settings.splitInstrument);

	return TRUE;
}


void CSplitKeyboardSettings::OnOK()
{
	CDialog::OnOK();

	m_Settings.splitNote = static_cast<ModCommand::NOTE>(m_CbnSplitNote.GetItemData(m_CbnSplitNote.GetCurSel()) - 1);
	m_Settings.octaveModifier = m_CbnOctaveModifier.GetCurSel() - SplitKeyboardSettings::splitOctaveRange;
	m_Settings.octaveLink = (IsDlgButtonChecked(IDC_PATTERN_OCTAVELINK) != BST_UNCHECKED);
	m_Settings.splitVolume = static_cast<ModCommand::VOL>(m_CbnSplitVolume.GetCurSel());
	m_Settings.splitInstrument = static_cast<ModCommand::INSTR>(m_CbnSplitInstrument.GetItemData(m_CbnSplitInstrument.GetCurSel()));
}


void CSplitKeyboardSettings::OnCancel()
{
	CDialog::OnCancel();
}


void CSplitKeyboardSettings::OnOctaveModifierChanged()
{
	CheckDlgButton(IDC_PATTERN_OCTAVELINK, (m_CbnOctaveModifier.GetCurSel() != 9) ? BST_CHECKED : BST_UNCHECKED);
}


/////////////////////////////////////////////////////////////////////////
// Show channel properties from pattern editor

BEGIN_MESSAGE_MAP(QuickChannelProperties, CDialog)
	ON_WM_HSCROLL()		// Sliders
	ON_WM_ACTIVATE()	// Catch Window focus change
	ON_EN_UPDATE(IDC_EDIT1,	&QuickChannelProperties::OnVolChanged)
	ON_EN_UPDATE(IDC_EDIT2,	&QuickChannelProperties::OnPanChanged)
	ON_EN_UPDATE(IDC_EDIT3,	&QuickChannelProperties::OnNameChanged)
	ON_COMMAND(IDC_CHECK1,	&QuickChannelProperties::OnMuteChanged)
	ON_COMMAND(IDC_CHECK2,	&QuickChannelProperties::OnSurroundChanged)
	ON_COMMAND(IDC_BUTTON1,	&QuickChannelProperties::OnPrevChannel)
	ON_COMMAND(IDC_BUTTON2,	&QuickChannelProperties::OnNextChannel)
	ON_COMMAND(IDC_BUTTON3,	&QuickChannelProperties::OnChangeColor)
	ON_COMMAND(IDC_BUTTON4,	&QuickChannelProperties::OnChangeColor)
	ON_COMMAND(IDC_BUTTON5, &QuickChannelProperties::OnPickPrevColor)
	ON_COMMAND(IDC_BUTTON6, &QuickChannelProperties::OnPickNextColor)
	ON_MESSAGE(WM_MOD_KEYCOMMAND,	&QuickChannelProperties::OnCustomKeyMsg)
	ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFF, &QuickChannelProperties::OnToolTipText)
END_MESSAGE_MAP()


void QuickChannelProperties::DoDataExchange(CDataExchange *pDX)
{
	DDX_Control(pDX, IDC_SLIDER1, m_volSlider);
	DDX_Control(pDX, IDC_SLIDER2, m_panSlider);
	DDX_Control(pDX, IDC_SPIN1,   m_volSpin);
	DDX_Control(pDX, IDC_SPIN2,   m_panSpin);
	DDX_Control(pDX, IDC_EDIT3,   m_nameEdit);
}


QuickChannelProperties::~QuickChannelProperties()
{
	DestroyWindow();
}


void QuickChannelProperties::OnActivate(UINT nState, CWnd *, BOOL)
{
	if(nState == WA_INACTIVE && !m_settingColor)
	{
		// Hide window when changing focus to another window.
		m_visible = false;
		ShowWindow(SW_HIDE);
	}
}


// Show channel properties for a given channel at a given screen position.
void QuickChannelProperties::Show(CModDoc *modDoc, CHANNELINDEX chn, CPoint position)
{
	if(!m_hWnd)
	{
		Create(IDD_CHANNELSETTINGS, nullptr);
		EnableToolTips();
		m_colorBtn.SubclassDlgItem(IDC_BUTTON4, this);
		m_colorBtnPrev.SubclassDlgItem(IDC_BUTTON5, this);
		m_colorBtnNext.SubclassDlgItem(IDC_BUTTON6, this);

		m_volSlider.SetRange(0, 64);
		m_volSlider.SetTicFreq(8);
		m_volSpin.SetRange(0, 64);

		m_panSlider.SetRange(0, 64);
		m_panSlider.SetTicFreq(8);
		m_panSpin.SetRange(0, 256);

		m_nameEdit.SetFocus();
	}
	m_document = modDoc;
	m_channel = chn;

	SetParent(nullptr);

	// Center window around point where user clicked.
	CRect rect, screenRect;
	GetWindowRect(rect);
	::GetWindowRect(::GetDesktopWindow(), &screenRect);
	rect.MoveToXY(
	    Clamp(static_cast<int>(position.x) - rect.Width() / 2, 0, static_cast<int>(screenRect.right) - rect.Width()),
	    Clamp(static_cast<int>(position.y) - rect.Height() / 2, 0, static_cast<int>(screenRect.bottom) - rect.Height()));
	MoveWindow(rect);

	SetWindowText(MPT_TFORMAT("Settings for Channel {}")(chn + 1).c_str());

	UpdateDisplay();

	const BOOL enablePan = (m_document->GetModType() & (MOD_TYPE_XM | MOD_TYPE_MOD)) ? FALSE : TRUE;
	const BOOL itOnly = (m_document->GetModType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) ? TRUE : FALSE;

	// Volume controls
	m_volSlider.EnableWindow(itOnly);
	m_volSpin.EnableWindow(itOnly);
	::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT1), itOnly);

	// Pan controls
	m_panSlider.EnableWindow(enablePan);
	m_panSpin.EnableWindow(enablePan);
	::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT2), enablePan);
	::EnableWindow(::GetDlgItem(m_hWnd, IDC_CHECK2), itOnly);

	// Channel name
	m_nameEdit.EnableWindow((m_document->GetModType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)) ? TRUE : FALSE);

	ShowWindow(SW_SHOW);
	m_visible = true;
}


void QuickChannelProperties::UpdateDisplay()
{
	// Set up channel properties
	m_visible = false;
	const ModChannelSettings &settings = m_document->GetSoundFile().ChnSettings[m_channel];
	SetDlgItemInt(IDC_EDIT1, settings.nVolume, FALSE);
	SetDlgItemInt(IDC_EDIT2, settings.nPan, FALSE);
	m_volSlider.SetPos(settings.nVolume);
	m_panSlider.SetPos(settings.nPan / 4u);
	CheckDlgButton(IDC_CHECK1, (settings.dwFlags[CHN_MUTE]) ? TRUE : FALSE);
	CheckDlgButton(IDC_CHECK2, (settings.dwFlags[CHN_SURROUND]) ? TRUE : FALSE);

	TCHAR description[16];
	wsprintf(description, _T("Channel %d:"), m_channel + 1);
	SetDlgItemText(IDC_STATIC_CHANNEL_NAME, description);
	m_nameEdit.LimitText(MAX_CHANNELNAME - 1);
	m_nameEdit.SetWindowText(mpt::ToCString(m_document->GetSoundFile().GetCharsetInternal(), settings.szName));

	const bool isFirst = (m_channel <= 0), isLast = (m_channel >= m_document->GetNumChannels() - 1);

	m_colorBtn.SetColor(settings.color);
	m_colorBtnPrev.EnableWindow(isFirst ? FALSE : TRUE);
	if(!isFirst)
		m_colorBtnPrev.SetColor(m_document->GetSoundFile().ChnSettings[m_channel - 1].color);
	m_colorBtnNext.EnableWindow(isLast ? FALSE : TRUE);
	if(!isLast)
		m_colorBtnNext.SetColor(m_document->GetSoundFile().ChnSettings[m_channel + 1].color);

	m_settingsChanged = false;
	m_visible = true;

	::EnableWindow(::GetDlgItem(m_hWnd, IDC_BUTTON1), isFirst ? FALSE : TRUE);
	::EnableWindow(::GetDlgItem(m_hWnd, IDC_BUTTON2), isLast ? FALSE : TRUE);
}

void QuickChannelProperties::PrepareUndo()
{
	if(!m_settingsChanged)
	{
		// Backup old channel settings through pattern undo.
		m_settingsChanged = true;
		m_document->GetPatternUndo().PrepareChannelUndo(m_channel, 1, "Channel Settings");
	}
}


void QuickChannelProperties::OnVolChanged()
{
	if(!m_visible)
	{
		return;
	}

	uint16 volume = static_cast<uint16>(GetDlgItemInt(IDC_EDIT1));
	if(volume >= 0 && volume <= 64)
	{
		PrepareUndo();
		m_document->SetChannelGlobalVolume(m_channel, volume);
		m_volSlider.SetPos(volume);
		m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
	}
}


void QuickChannelProperties::OnPanChanged()
{
	if(!m_visible)
	{
		return;
	}

	uint16 panning = static_cast<uint16>(GetDlgItemInt(IDC_EDIT2));
	if(panning >= 0 && panning <= 256)
	{
		PrepareUndo();
		m_document->SetChannelDefaultPan(m_channel, panning);
		m_panSlider.SetPos(panning / 4u);
		m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
		// Surround is forced off when changing pan, so uncheck the checkbox.
		CheckDlgButton(IDC_CHECK2, BST_UNCHECKED);
	}
}


void QuickChannelProperties::OnHScroll(UINT, UINT, CScrollBar *bar)
{
	if(!m_visible)
	{
		return;
	}

	bool update = false;

	// Volume slider
	if(bar == reinterpret_cast<CScrollBar *>(&m_volSlider))
	{
		uint16 pos = static_cast<uint16>(m_volSlider.GetPos());
		PrepareUndo();
		if(m_document->SetChannelGlobalVolume(m_channel, pos))
		{
			SetDlgItemInt(IDC_EDIT1, pos);
			update = true;
		}
	}
	// Pan slider
	if(bar == reinterpret_cast<CScrollBar *>(&m_panSlider))
	{
		uint16 pos = static_cast<uint16>(m_panSlider.GetPos());
		PrepareUndo();
		if(m_document->SetChannelDefaultPan(m_channel, pos * 4u))
		{
			SetDlgItemInt(IDC_EDIT2, pos * 4u);
			CheckDlgButton(IDC_CHECK2, BST_UNCHECKED);
			update = true;
		}
	}

	if(update)
	{
		m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
	}
}


void QuickChannelProperties::OnMuteChanged()
{
	if(!m_visible)
	{
		return;
	}

	m_document->MuteChannel(m_channel, IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED);
	m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
}


void QuickChannelProperties::OnSurroundChanged()
{
	if(!m_visible)
	{
		return;
	}

	PrepareUndo();
	m_document->SurroundChannel(m_channel, IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED);
	m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
	UpdateDisplay();
}


void QuickChannelProperties::OnNameChanged()
{
	if(!m_visible)
	{
		return;
	}

	ModChannelSettings &settings = m_document->GetSoundFile().ChnSettings[m_channel];
	CString newNameTmp;
	m_nameEdit.GetWindowText(newNameTmp);
	std::string newName = mpt::ToCharset(m_document->GetSoundFile().GetCharsetInternal(), newNameTmp);

	if(newName != settings.szName)
	{
		PrepareUndo();
		settings.szName = newName;
		m_document->SetModified();
		m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
	}
}


void QuickChannelProperties::OnChangeColor()
{
	m_settingColor = true;
	if(auto color = m_colorBtn.PickColor(m_document->GetSoundFile(), m_channel); color.has_value())
	{
		PrepareUndo();
		m_document->GetSoundFile().ChnSettings[m_channel].color = *color;
		if(m_document->SupportsChannelColors())
			m_document->SetModified();
		m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
	}
	m_settingColor = false;
}


void QuickChannelProperties::OnPickPrevColor()
{
	if(m_channel > 0)
		PickColorFromChannel(m_channel - 1);
}


void QuickChannelProperties::OnPickNextColor()
{
	if(m_channel < m_document->GetNumChannels() - 1)
		PickColorFromChannel(m_channel + 1);
}


void QuickChannelProperties::PickColorFromChannel(CHANNELINDEX channel)
{
	auto &channels = m_document->GetSoundFile().ChnSettings;
	if(channels[channel].color != channels[m_channel].color)
	{
		PrepareUndo();
		channels[m_channel].color = channels[channel].color;
		m_colorBtn.SetColor(channels[m_channel].color);
		if(m_document->SupportsChannelColors())
			m_document->SetModified();
		m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this);
	}
}


void QuickChannelProperties::OnPrevChannel()
{
	if(m_channel > 0)
	{
		m_channel--;
		UpdateDisplay();
	}
}


void QuickChannelProperties::OnNextChannel()
{
	if(m_channel < m_document->GetNumChannels() - 1)
	{
		m_channel++;
		UpdateDisplay();
	}
}


BOOL QuickChannelProperties::PreTranslateMessage(MSG *pMsg)
{
	if(pMsg)
	{
		//We handle keypresses before Windows has a chance to handle them (for alt etc..)
		if((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) ||
			(pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN))
		{
			CInputHandler *ih = CMainFrame::GetInputHandler();

			//Translate message manually
			UINT nChar = static_cast<UINT>(pMsg->wParam);
			UINT nRepCnt = LOWORD(pMsg->lParam);
			UINT nFlags = HIWORD(pMsg->lParam);
			KeyEventType kT = ih->GetKeyEventType(nFlags);

			if(ih->KeyEvent(kCtxChannelSettings, nChar, nRepCnt, nFlags, kT, this) != kcNull)
			{
				return TRUE;  // Mapped to a command, no need to pass message on.
			}
		}
	}

	return CDialog::PreTranslateMessage(pMsg);
}


LRESULT QuickChannelProperties::OnCustomKeyMsg(WPARAM wParam, LPARAM)
{
	switch(wParam)
	{
	case kcChnSettingsPrev:
		OnPrevChannel();
		return wParam;
	case kcChnSettingsNext:
		OnNextChannel();
		return wParam;
	case kcChnColorFromPrev:
		OnPickPrevColor();
		return wParam;
	case kcChnColorFromNext:
		OnPickNextColor();
		return wParam;
	case kcChnSettingsClose:
		OnActivate(WA_INACTIVE, nullptr, FALSE);
		return wParam;
	}

	return kcNull;
}


BOOL QuickChannelProperties::OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult)
{
	auto pTTT = reinterpret_cast<TOOLTIPTEXT *>(pNMHDR);
	UINT_PTR id = pNMHDR->idFrom;
	if(pTTT->uFlags & TTF_IDISHWND)
	{
		// idFrom is actually the HWND of the tool
		id = static_cast<UINT_PTR>(::GetDlgCtrlID(reinterpret_cast<HWND>(id)));
	}

	mpt::tstring text;
	CommandID cmd = kcNull;
	switch (id)
	{
	case IDC_EDIT1:
	case IDC_SLIDER1:
		text = CModDoc::LinearToDecibels(m_document->GetSoundFile().ChnSettings[m_channel].nVolume, 64.0);
		break;
	case IDC_EDIT2:
	case IDC_SLIDER2:
		text = CModDoc::PanningToString(m_document->GetSoundFile().ChnSettings[m_channel].nPan, 128);
		break;
	case IDC_BUTTON1:
		text = _T("Previous Channel");
		cmd = kcChnSettingsPrev;
		break;
	case IDC_BUTTON2:
		text = _T("Next Channel");
		cmd = kcChnSettingsNext;
		break;
	case IDC_BUTTON5:
		text = _T("Take color from previous channel");
		cmd = kcChnColorFromPrev;
		break;
	case IDC_BUTTON6:
		text = _T("Take color from next channel");
		cmd = kcChnColorFromNext;
		break;
	default:
		return FALSE;
	}
	
	if(cmd != kcNull)
	{
		auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0);
		if(!keyText.IsEmpty())
			text += MPT_TFORMAT(" ({})")(keyText);
	}

	mpt::String::WriteWinBuf(pTTT->szText) = text;
	*pResult = 0;

	// bring the tooltip window above other popup windows
	::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOOWNERZORDER);

	return TRUE;  // message was handled
}


OPENMPT_NAMESPACE_END