/*
 * view_ins.cpp
 * ------------
 * Purpose: Instrument tab, lower panel.
 * 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 "ImageLists.h"
#include "Childfrm.h"
#include "Moddoc.h"
#include "Globals.h"
#include "Ctrl_ins.h"
#include "View_ins.h"
#include "Dlsbank.h"
#include "ChannelManagerDlg.h"
#include "ScaleEnvPointsDlg.h"
#include "../soundlib/MIDIEvents.h"
#include "../soundlib/mod_specifications.h"
#include "../common/mptStringBuffer.h"
#include "FileDialog.h"


OPENMPT_NAMESPACE_BEGIN

namespace
{
	const int ENV_POINT_SIZE = 4;
	const float ENV_MIN_ZOOM = 2.0f;
	const float ENV_MAX_ZOOM = 256.0f;
}


// Non-client toolbar
#define ENV_LEFTBAR_CY			Util::ScalePixels(29, m_hWnd)
#define ENV_LEFTBAR_CXSEP		Util::ScalePixels(14, m_hWnd)
#define ENV_LEFTBAR_CXSPC		Util::ScalePixels(3, m_hWnd)
#define ENV_LEFTBAR_CXBTN		Util::ScalePixels(24, m_hWnd)
#define ENV_LEFTBAR_CYBTN		Util::ScalePixels(22, m_hWnd)


static constexpr UINT cLeftBarButtons[ENV_LEFTBAR_BUTTONS] =
{
	ID_ENVSEL_VOLUME,
	ID_ENVSEL_PANNING,
	ID_ENVSEL_PITCH,
		ID_SEPARATOR,
	ID_ENVELOPE_VOLUME,
	ID_ENVELOPE_PANNING,
	ID_ENVELOPE_PITCH,
	ID_ENVELOPE_FILTER,
		ID_SEPARATOR,
	ID_ENVELOPE_SETLOOP,
	ID_ENVELOPE_SUSTAIN,
	ID_ENVELOPE_CARRY,
		ID_SEPARATOR,
	ID_INSTRUMENT_SAMPLEMAP,
		ID_SEPARATOR,
	ID_ENVELOPE_VIEWGRID,
		ID_SEPARATOR,
	ID_ENVELOPE_ZOOM_IN,
	ID_ENVELOPE_ZOOM_OUT,
		ID_SEPARATOR,
	ID_ENVELOPE_LOAD,
	ID_ENVELOPE_SAVE,
};


IMPLEMENT_SERIAL(CViewInstrument, CModScrollView, 0)

BEGIN_MESSAGE_MAP(CViewInstrument, CModScrollView)
	//{{AFX_MSG_MAP(CViewInstrument)
#if !defined(MPT_BUILD_RETRO)
	ON_MESSAGE(WM_DPICHANGED, &CViewInstrument::OnDPIChanged)
#endif
	ON_WM_ERASEBKGND()
	ON_WM_SETFOCUS()
	ON_WM_SIZE()
	ON_WM_NCCALCSIZE()
	ON_WM_NCPAINT()
	ON_WM_NCHITTEST()
	ON_WM_MOUSEMOVE()
	ON_WM_NCMOUSEMOVE()
	ON_WM_LBUTTONDOWN()
	ON_WM_LBUTTONUP()
	ON_WM_LBUTTONDBLCLK()
	ON_WM_RBUTTONDOWN()
	ON_WM_MBUTTONDOWN()
	ON_WM_XBUTTONUP()
	ON_WM_NCLBUTTONDOWN()
	ON_WM_NCLBUTTONUP()
	ON_WM_NCLBUTTONDBLCLK()
	ON_WM_DROPFILES()
	ON_COMMAND(ID_PREVINSTRUMENT,			&CViewInstrument::OnPrevInstrument)
	ON_COMMAND(ID_NEXTINSTRUMENT,			&CViewInstrument::OnNextInstrument)
	ON_COMMAND(ID_ENVELOPE_SETLOOP,			&CViewInstrument::OnEnvLoopChanged)
	ON_COMMAND(ID_ENVELOPE_SUSTAIN,			&CViewInstrument::OnEnvSustainChanged)
	ON_COMMAND(ID_ENVELOPE_CARRY,			&CViewInstrument::OnEnvCarryChanged)
	ON_COMMAND(ID_ENVELOPE_INSERTPOINT,		&CViewInstrument::OnEnvInsertPoint)
	ON_COMMAND(ID_ENVELOPE_REMOVEPOINT,		&CViewInstrument::OnEnvRemovePoint)
	ON_COMMAND(ID_ENVELOPE_VOLUME,			&CViewInstrument::OnEnvVolChanged)
	ON_COMMAND(ID_ENVELOPE_PANNING,			&CViewInstrument::OnEnvPanChanged)
	ON_COMMAND(ID_ENVELOPE_PITCH,			&CViewInstrument::OnEnvPitchChanged)
	ON_COMMAND(ID_ENVELOPE_FILTER,			&CViewInstrument::OnEnvFilterChanged)
	ON_COMMAND(ID_ENVELOPE_VIEWGRID,		&CViewInstrument::OnEnvToggleGrid)
	ON_COMMAND(ID_ENVELOPE_ZOOM_IN,			&CViewInstrument::OnEnvZoomIn)
	ON_COMMAND(ID_ENVELOPE_ZOOM_OUT,		&CViewInstrument::OnEnvZoomOut)
	ON_COMMAND(ID_ENVELOPE_LOAD,			&CViewInstrument::OnEnvLoad)
	ON_COMMAND(ID_ENVELOPE_SAVE,			&CViewInstrument::OnEnvSave)
	ON_COMMAND(ID_ENVSEL_VOLUME,			&CViewInstrument::OnSelectVolumeEnv)
	ON_COMMAND(ID_ENVSEL_PANNING,			&CViewInstrument::OnSelectPanningEnv)
	ON_COMMAND(ID_ENVSEL_PITCH,				&CViewInstrument::OnSelectPitchEnv)
	ON_COMMAND(ID_EDIT_COPY,				&CViewInstrument::OnEditCopy)
	ON_COMMAND(ID_EDIT_PASTE,				&CViewInstrument::OnEditPaste)
	ON_COMMAND(ID_EDIT_UNDO,				&CViewInstrument::OnEditUndo)
	ON_COMMAND(ID_EDIT_REDO,				&CViewInstrument::OnEditRedo)
	ON_COMMAND(ID_INSTRUMENT_SAMPLEMAP,		&CViewInstrument::OnEditSampleMap)
	ON_COMMAND(ID_ENVELOPE_TOGGLERELEASENODE, &CViewInstrument::OnEnvToggleReleasNode)
	ON_COMMAND(ID_ENVELOPE_SCALEPOINTS, &CViewInstrument::OnEnvelopeScalePoints)

	ON_MESSAGE(WM_MOD_MIDIMSG,				&CViewInstrument::OnMidiMsg)
	ON_MESSAGE(WM_MOD_KEYCOMMAND,			&CViewInstrument::OnCustomKeyMsg)

	ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO,		&CViewInstrument::OnUpdateUndo)
	ON_UPDATE_COMMAND_UI(ID_EDIT_REDO,		&CViewInstrument::OnUpdateRedo)

	//}}AFX_MSG_MAP
	ON_WM_MOUSEWHEEL()
END_MESSAGE_MAP()


///////////////////////////////////////////////////////////////
// CViewInstrument operations

CViewInstrument::CViewInstrument()
{
	EnableActiveAccessibility();
	m_rcClient.bottom = 2;
	m_dwNotifyPos.fill(uint32(Notification::PosInvalid));
	MemsetZero(m_NcButtonState);

	m_bmpEnvBar.Create(&CMainFrame::GetMainFrame()->m_EnvelopeIcons);

	m_baPlayingNote.reset();
}


void CViewInstrument::OnInitialUpdate()
{
	CModScrollView::OnInitialUpdate();
	ModifyStyleEx(0, WS_EX_ACCEPTFILES);
	m_zoom = (ENV_POINT_SIZE * m_nDPIx) / 96.0f;
	m_envPointSize = Util::ScalePixels(ENV_POINT_SIZE, m_hWnd);
	UpdateScrollSize();
	UpdateNcButtonState();
	EnableToolTips();
}


void CViewInstrument::UpdateScrollSize()
{
	CModDoc *pModDoc = GetDocument();
	GetClientRect(&m_rcClient);
	if(m_rcClient.bottom < 2)
		m_rcClient.bottom = 2;
	if(pModDoc)
	{
		SIZE sizeTotal, sizePage, sizeLine;
		uint32 maxTick = EnvGetTick(EnvGetLastPoint());

		sizeTotal.cx = mpt::saturate_round<int>((maxTick + 2) * m_zoom);
		sizeTotal.cy = 1;
		sizeLine.cx = mpt::saturate_round<int>(m_zoom);
		sizeLine.cy = 2;
		sizePage.cx = sizeLine.cx * 4;
		sizePage.cy = sizeLine.cy;
		SetScrollSizes(MM_TEXT, sizeTotal, sizePage, sizeLine);
	}
}


LRESULT CViewInstrument::OnDPIChanged(WPARAM wParam, LPARAM lParam)
{
	LRESULT res = CModScrollView::OnDPIChanged(wParam, lParam);
	m_envPointSize = Util::ScalePixels(4, m_hWnd);
	return res;
}


void CViewInstrument::PrepareUndo(const char *description)
{
	GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, description, m_nEnv);
}


// Set instrument (and moddoc) as modified.
// updateAll: Update all views including this one. Otherwise, only update update other views.
void CViewInstrument::SetModified(InstrumentHint hint, bool updateAll)
{
	CModDoc *pModDoc = GetDocument();
	pModDoc->SetModified();
	pModDoc->UpdateAllViews(nullptr, hint.SetData(m_nInstrument), updateAll ? nullptr : this);
	CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this);
}


BOOL CViewInstrument::SetCurrentInstrument(INSTRUMENTINDEX nIns, EnvelopeType nEnv)
{
	CModDoc *pModDoc = GetDocument();
	Notification::Type type;

	if((!pModDoc) || (nIns < 1) || (nIns >= MAX_INSTRUMENTS))
		return FALSE;
	m_nEnv = nEnv;
	m_nInstrument = nIns;
	switch(m_nEnv)
	{
	case ENV_PANNING:	type = Notification::PanEnv; break;
	case ENV_PITCH:		type = Notification::PitchEnv; break;
	default:			m_nEnv = ENV_VOLUME; type = Notification::VolEnv; break;
	}
	pModDoc->SetNotifications(type, m_nInstrument);
	pModDoc->SetFollowWnd(m_hWnd);
	UpdateScrollSize();
	UpdateNcButtonState();
	InvalidateRect(NULL, FALSE);
	return TRUE;
}


void CViewInstrument::OnSetFocus(CWnd *pOldWnd)
{
	CScrollView::OnSetFocus(pOldWnd);
	SetCurrentInstrument(m_nInstrument, m_nEnv);
}


LRESULT CViewInstrument::OnModViewMsg(WPARAM wParam, LPARAM lParam)
{
	switch(wParam)
	{
	case VIEWMSG_SETCURRENTINSTRUMENT:
		SetCurrentInstrument(lParam & 0xFFFF, m_nEnv);
		break;

	case VIEWMSG_LOADSTATE:
		if(lParam)
		{
			INSTRUMENTVIEWSTATE *pState = (INSTRUMENTVIEWSTATE *)lParam;
			if(pState->initialized)
			{
				m_zoom = pState->zoom;
				SetCurrentInstrument(m_nInstrument, pState->nEnv);
				m_bGrid = pState->bGrid;
			}
		}
		break;

	case VIEWMSG_SAVESTATE:
		if(lParam)
		{
			INSTRUMENTVIEWSTATE *pState = (INSTRUMENTVIEWSTATE *)lParam;
			pState->initialized = true;
			pState->zoom = m_zoom;
			pState->nEnv = m_nEnv;
			pState->bGrid = m_bGrid;
		}
		break;

	default:
		return CModScrollView::OnModViewMsg(wParam, lParam);
	}
	return 0;
}


uint32 CViewInstrument::EnvGetTick(int nPoint) const
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return 0;
	if((nPoint >= 0) && (nPoint < (int)envelope->size()))
		return envelope->at(nPoint).tick;
	else
		return 0;
}


uint32 CViewInstrument::EnvGetValue(int nPoint) const
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return 0;
	if(nPoint >= 0 && nPoint < (int)envelope->size())
		return envelope->at(nPoint).value;
	else
		return 0;
}


bool CViewInstrument::EnvSetValue(int nPoint, int32 nTick, int32 nValue, bool moveTail)
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr || nPoint < 0)
		return false;

	if(nPoint == 0)
	{
		nTick = 0;
		moveTail = false;
	}
	int tickDiff = 0;

	bool ok = false;
	if(nPoint < (int)envelope->size())
	{
		if(nTick != int32_min)
		{
			nTick = std::max(0, nTick);
			tickDiff = envelope->at(nPoint).tick;
			int mintick = (nPoint > 0) ? envelope->at(nPoint - 1).tick : 0;
			int maxtick;
			if(nPoint + 1 >= (int)envelope->size() || moveTail)
				maxtick = std::numeric_limits<decltype(maxtick)>::max();
			else
				maxtick = envelope->at(nPoint + 1).tick;

			// Can't have multiple points on same tick
			if(nPoint > 0 && mintick < maxtick - 1)
			{
				mintick++;
				if(nPoint + 1 < (int)envelope->size())
					maxtick--;
			}
			if(nTick < mintick)
				nTick = mintick;
			if(nTick > maxtick)
				nTick = maxtick;
			if(nTick != envelope->at(nPoint).tick)
			{
				envelope->at(nPoint).tick = static_cast<EnvelopeNode::tick_t>(nTick);
				ok = true;
			}
		}
		const int maxVal = (GetDocument()->GetModType() != MOD_TYPE_XM || m_nEnv != ENV_PANNING) ? 64 : 63;
		if(nValue != int32_min)
		{
			Limit(nValue, 0, maxVal);
			if(nValue != envelope->at(nPoint).value)
			{
				envelope->at(nPoint).value = static_cast<EnvelopeNode::value_t>(nValue);
				ok = true;
			}
		}
	}

	if(ok && moveTail)
	{
		// Move all points after modified point as well.
		tickDiff = envelope->at(nPoint).tick - tickDiff;
		for(auto it = envelope->begin() + nPoint + 1; it != envelope->end(); it++)
		{
			it->tick = static_cast<EnvelopeNode::tick_t>(std::max(0, (int)it->tick + tickDiff));
		}
	}

	return ok;
}


uint32 CViewInstrument::EnvGetNumPoints() const
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return 0;
	return envelope->size();
}


uint32 CViewInstrument::EnvGetLastPoint() const
{
	uint32 nPoints = EnvGetNumPoints();
	if(nPoints > 0)
		return nPoints - 1;
	return 0;
}


// Return if an envelope flag is set.
bool CViewInstrument::EnvGetFlag(const EnvelopeFlags dwFlag) const
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv != nullptr)
		return pEnv->dwFlags[dwFlag];
	return false;
}


uint32 CViewInstrument::EnvGetLoopStart() const
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return 0;
	return envelope->nLoopStart;
}


uint32 CViewInstrument::EnvGetLoopEnd() const
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return 0;
	return envelope->nLoopEnd;
}


uint32 CViewInstrument::EnvGetSustainStart() const
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return 0;
	return envelope->nSustainStart;
}


uint32 CViewInstrument::EnvGetSustainEnd() const
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return 0;
	return envelope->nSustainEnd;
}


bool CViewInstrument::EnvGetVolEnv() const
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns)
		return pIns->VolEnv.dwFlags[ENV_ENABLED] != 0;
	return false;
}


bool CViewInstrument::EnvGetPanEnv() const
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns)
		return pIns->PanEnv.dwFlags[ENV_ENABLED] != 0;
	return false;
}


bool CViewInstrument::EnvGetPitchEnv() const
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns)
		return ((pIns->PitchEnv.dwFlags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED);
	return false;
}


bool CViewInstrument::EnvGetFilterEnv() const
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns)
		return ((pIns->PitchEnv.dwFlags & (ENV_ENABLED | ENV_FILTER)) == (ENV_ENABLED | ENV_FILTER));
	return false;
}


bool CViewInstrument::EnvSetLoopStart(int nPoint)
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return false;
	if(nPoint < 0 || nPoint > (int)EnvGetLastPoint())
		return false;

	if(nPoint != envelope->nLoopStart)
	{
		envelope->nLoopStart = static_cast<decltype(envelope->nLoopStart)>(nPoint);
		if(envelope->nLoopEnd < nPoint)
			envelope->nLoopEnd = static_cast<decltype(envelope->nLoopEnd)>(nPoint);
		return true;
	} else
	{
		return false;
	}
}


bool CViewInstrument::EnvSetLoopEnd(int nPoint)
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return false;
	if(nPoint < 0 || nPoint > (int)EnvGetLastPoint())
		return false;

	if(nPoint != envelope->nLoopEnd)
	{
		envelope->nLoopEnd = static_cast<decltype(envelope->nLoopEnd)>(nPoint);
		if(envelope->nLoopStart > nPoint)
			envelope->nLoopStart = static_cast<decltype(envelope->nLoopStart)>(nPoint);
		return true;
	} else
	{
		return false;
	}
}


bool CViewInstrument::EnvSetSustainStart(int nPoint)
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return false;
	if(nPoint < 0 || nPoint > (int)EnvGetLastPoint())
		return false;

	// We won't do any security checks here as GetEnvelopePtr() does that for us.
	CSoundFile &sndFile = GetDocument()->GetSoundFile();

	if(nPoint != envelope->nSustainStart)
	{
		envelope->nSustainStart = static_cast<decltype(envelope->nSustainStart)>(nPoint);
		if((envelope->nSustainEnd < nPoint) || (sndFile.GetType() & MOD_TYPE_XM))
			envelope->nSustainEnd = static_cast<decltype(envelope->nSustainEnd)>(nPoint);
		return true;
	} else
	{
		return false;
	}
}


bool CViewInstrument::EnvSetSustainEnd(int nPoint)
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return false;
	if(nPoint < 0 || nPoint > (int)EnvGetLastPoint())
		return false;

	// We won't do any security checks here as GetEnvelopePtr() does that for us.
	CSoundFile &sndFile = GetDocument()->GetSoundFile();

	if(nPoint != envelope->nSustainEnd)
	{
		envelope->nSustainEnd = static_cast<decltype(envelope->nSustainEnd)>(nPoint);
		if((envelope->nSustainStart > nPoint) || (sndFile.GetType() & MOD_TYPE_XM))
			envelope->nSustainStart = static_cast<decltype(envelope->nSustainStart)>(nPoint);
		return true;
	} else
	{
		return false;
	}
}


bool CViewInstrument::EnvToggleReleaseNode(int nPoint)
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return false;
	if(nPoint < 0 || nPoint >= (int)EnvGetNumPoints())
		return false;

	// Don't allow release nodes in IT/XM. GetDocument()/... nullptr check is done in GetEnvelopePtr, so no need to check twice.
	if(!GetDocument()->GetSoundFile().GetModSpecifications().hasReleaseNode)
	{
		if(envelope->nReleaseNode != ENV_RELEASE_NODE_UNSET)
		{
			envelope->nReleaseNode = ENV_RELEASE_NODE_UNSET;
			return true;
		}
		return false;
	}

	if(envelope->nReleaseNode == nPoint)
	{
		envelope->nReleaseNode = ENV_RELEASE_NODE_UNSET;
	} else
	{
		envelope->nReleaseNode = static_cast<decltype(envelope->nReleaseNode)>(nPoint);
	}
	return true;
}

// Enable or disable a flag of the current envelope
bool CViewInstrument::EnvSetFlag(EnvelopeFlags flag, bool enable)
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr || envelope->empty())
		return false;

	bool modified = envelope->dwFlags[flag] != enable;
	PrepareUndo("Toggle Envelope Flag");
	envelope->dwFlags.set(flag, enable);
	return modified;
}


bool CViewInstrument::EnvToggleEnv(EnvelopeType envelope, CSoundFile &sndFile, ModInstrument &ins, bool enable, EnvelopeNode::value_t defaultValue, EnvelopeFlags extraFlags)
{
	InstrumentEnvelope &env = ins.GetEnvelope(envelope);

	const FlagSet<EnvelopeFlags> flags = (ENV_ENABLED | extraFlags);

	env.dwFlags.set(flags, enable);
	if(enable && env.empty())
	{
		env.reserve(2);
		env.push_back(EnvelopeNode(0, defaultValue));
		env.push_back(EnvelopeNode(10, defaultValue));
		InvalidateRect(NULL, FALSE);
	}

	CriticalSection cs;

	// Update mixing flags...
	for(auto &chn : sndFile.m_PlayState.Chn)
	{
		if(chn.pModInstrument == &ins)
		{
			chn.GetEnvelope(envelope).flags.set(flags, enable);
		}
	}

	return true;
}


bool CViewInstrument::EnvSetVolEnv(bool enable)
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns == nullptr)
		return false;
	return EnvToggleEnv(ENV_VOLUME, GetDocument()->GetSoundFile(), *pIns, enable, 64);
}


bool CViewInstrument::EnvSetPanEnv(bool enable)
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns == nullptr)
		return false;
	return EnvToggleEnv(ENV_PANNING, GetDocument()->GetSoundFile(), *pIns, enable, 32);
}


bool CViewInstrument::EnvSetPitchEnv(bool enable)
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns == nullptr)
		return false;

	pIns->PitchEnv.dwFlags.reset(ENV_FILTER);
	return EnvToggleEnv(ENV_PITCH, GetDocument()->GetSoundFile(), *pIns, enable, 32);
}


bool CViewInstrument::EnvSetFilterEnv(bool enable)
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns == nullptr)
		return false;

	return EnvToggleEnv(ENV_PITCH, GetDocument()->GetSoundFile(), *pIns, enable, 64, ENV_FILTER);
}


uint32 CViewInstrument::DragItemToEnvPoint() const
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !m_nDragItem)
		return 0;

	switch(m_nDragItem)
	{
	case ENV_DRAGLOOPSTART: return pEnv->nLoopStart;
	case ENV_DRAGLOOPEND: return pEnv->nLoopEnd;
	case ENV_DRAGSUSTAINSTART: return pEnv->nSustainStart;
	case ENV_DRAGSUSTAINEND: return pEnv->nSustainEnd;
	default: return m_nDragItem - 1;
	}
}


int CViewInstrument::TickToScreen(int tick) const
{
	return static_cast<int>((tick * m_zoom) - m_nScrollPosX + m_envPointSize);
}

int CViewInstrument::PointToScreen(int nPoint) const
{
	return TickToScreen(EnvGetTick(nPoint));
}


int CViewInstrument::ScreenToTick(int x) const
{
	int offset = m_nScrollPosX + x;
	if(offset < m_envPointSize)
		return 0;
	return mpt::saturate_round<int>((offset - m_envPointSize) / m_zoom);
}


int CViewInstrument::ScreenToValue(int y) const
{
	if(m_rcClient.bottom < 2)
		return ENVELOPE_MIN;
	int n = ENVELOPE_MAX - Util::muldivr(y, ENVELOPE_MAX, m_rcClient.bottom - 1);
	if(n < ENVELOPE_MIN)
		return ENVELOPE_MIN;
	if(n > ENVELOPE_MAX)
		return ENVELOPE_MAX;
	return n;
}


int CViewInstrument::ScreenToPoint(int x0, int y0) const
{
	int nPoint = -1;
	int64 ydist = int64_max, xdist = int64_max;
	int numPoints = EnvGetNumPoints();
	for(int i = 0; i < numPoints; i++)
	{
		int dx = x0 - PointToScreen(i);
		int64 dx2 = Util::mul32to64(dx, dx);
		if(dx2 <= xdist)
		{
			int dy = y0 - ValueToScreen(EnvGetValue(i));
			int64 dy2 = Util::mul32to64(dy, dy);
			if(dx2 < xdist || (dx2 == xdist && dy2 < ydist))
			{
				nPoint = i;
				xdist = dx2;
				ydist = dy2;
			}
		}
	}
	return nPoint;
}


bool CViewInstrument::GetNcButtonRect(UINT button, CRect &rect) const
{
	rect.left = 4;
	rect.top = 3;
	rect.bottom = rect.top + ENV_LEFTBAR_CYBTN;
	if(button >= ENV_LEFTBAR_BUTTONS)
		return false;
	for(UINT i = 0; i < button; i++)
	{
		if(cLeftBarButtons[i] == ID_SEPARATOR)
			rect.left += ENV_LEFTBAR_CXSEP;
		else
			rect.left += ENV_LEFTBAR_CXBTN + ENV_LEFTBAR_CXSPC;
	}
	if(cLeftBarButtons[button] == ID_SEPARATOR)
	{
		rect.left += ENV_LEFTBAR_CXSEP / 2 - 2;
		rect.right = rect.left + 2;
		return false;
	} else
	{
		rect.right = rect.left + ENV_LEFTBAR_CXBTN;
	}
	return true;
}


UINT CViewInstrument::GetNcButtonAtPoint(CPoint point, CRect *outRect) const
{
	CRect rect, rcWnd;
	UINT button = uint32_max;
	GetWindowRect(&rcWnd);
	for(UINT i = 0; i < ENV_LEFTBAR_BUTTONS; i++)
	{
		if(!(m_NcButtonState[i] & NCBTNS_DISABLED) && GetNcButtonRect(i, rect))
		{
			rect.OffsetRect(rcWnd.left, rcWnd.top);
			if(rect.PtInRect(point))
			{
				button = i;
				break;
			}
		}
	}
	if(outRect)
		*outRect = rect;
	return button;
}


void CViewInstrument::UpdateNcButtonState()
{
	CModDoc *pModDoc = GetDocument();
	if(!pModDoc)
		return;
	CSoundFile &sndFile = pModDoc->GetSoundFile();

	CDC *pDC = NULL;
	for (UINT i=0; i<ENV_LEFTBAR_BUTTONS; i++) if (cLeftBarButtons[i] != ID_SEPARATOR)
	{
		DWORD dwStyle = 0;

		switch(cLeftBarButtons[i])
		{
		case ID_ENVSEL_VOLUME:		if (m_nEnv == ENV_VOLUME) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVSEL_PANNING:		if (m_nEnv == ENV_PANNING) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVSEL_PITCH:		if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED;
									else if (m_nEnv == ENV_PITCH) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_SETLOOP:	if (EnvGetLoop()) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_SUSTAIN:	if (EnvGetSustain()) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_CARRY:		if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED;
									else if (EnvGetCarry()) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_VOLUME:	if (EnvGetVolEnv()) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_PANNING:	if (EnvGetPanEnv()) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_PITCH:		if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED; else
									if (EnvGetPitchEnv()) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_FILTER:	if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED; else
									if (EnvGetFilterEnv()) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_VIEWGRID:	if (m_bGrid) dwStyle |= NCBTNS_CHECKED; break;
		case ID_ENVELOPE_ZOOM_IN:	if (m_zoom >= ENV_MAX_ZOOM) dwStyle |= NCBTNS_DISABLED; break;
		case ID_ENVELOPE_ZOOM_OUT:	if (m_zoom <= ENV_MIN_ZOOM) dwStyle |= NCBTNS_DISABLED; break;
		case ID_ENVELOPE_LOAD:
		case ID_ENVELOPE_SAVE:		if (GetInstrumentPtr() == nullptr) dwStyle |= NCBTNS_DISABLED; break;
		}
		if (m_nBtnMouseOver == i)
		{
			dwStyle |= NCBTNS_MOUSEOVER;
			if (m_dwStatus & INSSTATUS_NCLBTNDOWN) dwStyle |= NCBTNS_PUSHED;
		}
		if (dwStyle != m_NcButtonState[i])
		{
			m_NcButtonState[i] = dwStyle;
			if (!pDC) pDC = GetWindowDC();
			DrawNcButton(pDC, i);
		}
	}
	if (pDC) ReleaseDC(pDC);
}


////////////////////////////////////////////////////////////////////
// CViewInstrument drawing

void CViewInstrument::UpdateView(UpdateHint hint, CObject *pObj)
{
	if(pObj == this)
	{
		return;
	}
	const InstrumentHint instrHint = hint.ToType<InstrumentHint>();
	FlagSet<HintType> hintType = instrHint.GetType();
	const INSTRUMENTINDEX updateIns = instrHint.GetInstrument();
	if(hintType[HINT_MPTOPTIONS | HINT_MODTYPE]
		|| (hintType[HINT_ENVELOPE] && (m_nInstrument == updateIns || updateIns == 0)))
	{
		UpdateScrollSize();
		UpdateNcButtonState();
		InvalidateRect(NULL, FALSE);
	}
}


void CViewInstrument::DrawGrid(CDC *pDC, uint32 speed)
{
	bool windowResized = false;

	if(m_dcGrid.m_hDC)
	{
		m_dcGrid.SelectObject(m_pbmpOldGrid);
		m_dcGrid.DeleteDC();
		m_bmpGrid.DeleteObject();
		windowResized = true;
	}

	if(windowResized || m_bGridForceRedraw || (m_nScrollPosX != m_GridScrollPos) || (speed != (UINT)m_GridSpeed) && speed > 0)
	{
		m_GridSpeed = speed;
		m_GridScrollPos = m_nScrollPosX;
		m_bGridForceRedraw = false;

		// create a memory based dc for drawing the grid
		m_dcGrid.CreateCompatibleDC(pDC);
		m_bmpGrid.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height());
		m_pbmpOldGrid = *m_dcGrid.SelectObject(&m_bmpGrid);

		// Do draw
		const int width = m_rcClient.Width();
		int rowsPerBeat = 1, rowsPerMeasure = 1;
		const CModDoc *modDoc = GetDocument();
		if(modDoc != nullptr)
		{
			rowsPerBeat = modDoc->GetSoundFile().m_nDefaultRowsPerBeat;
			rowsPerMeasure = modDoc->GetSoundFile().m_nDefaultRowsPerMeasure;
		}

		// Paint it black!
		m_dcGrid.FillSolidRect(&m_rcClient, TrackerSettings::Instance().rgbCustomColors[MODCOLOR_BACKENV]);

		const uint32 startTick = (ScreenToTick(0) / speed) * speed;
		const uint32 endTick = (ScreenToTick(width) / speed) * speed;

		auto oldPen = m_dcGrid.SelectStockObject(DC_PEN);
		for(uint32 tick = startTick, row = startTick / speed; tick <= endTick; tick += speed, row++)
		{
			if(rowsPerMeasure > 0 && row % rowsPerMeasure == 0)
				m_dcGrid.SetDCPenColor(RGB(0x80, 0x80, 0x80));
			else if(rowsPerBeat > 0 && row % rowsPerBeat == 0)
				m_dcGrid.SetDCPenColor(RGB(0x55, 0x55, 0x55));
			else
				m_dcGrid.SetDCPenColor(RGB(0x33, 0x33, 0x33));

			int x = TickToScreen(tick);
			m_dcGrid.MoveTo(x, 0);
			m_dcGrid.LineTo(x, m_rcClient.bottom);
		}
		if(oldPen)
			m_dcGrid.SelectObject(oldPen);
	}

	pDC->BitBlt(m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), &m_dcGrid, 0, 0, SRCCOPY);
}


void CViewInstrument::OnDraw(CDC *pDC)
{
	CModDoc *pModDoc = GetDocument();
	if((!pModDoc) || (!pDC))
		return;

	// to avoid flicker, establish a memory dc, draw to it
	// and then BitBlt it to the destination "pDC"

	//check for window resize
	if(m_dcMemMain.GetSafeHdc() && m_rcOldClient != m_rcClient)
	{
		m_dcMemMain.SelectObject(oldBitmap);
		m_dcMemMain.DeleteDC();
		m_bmpMemMain.DeleteObject();
	}

	if(!m_dcMemMain.m_hDC)
	{
		m_dcMemMain.CreateCompatibleDC(pDC);
		if(!m_dcMemMain.m_hDC)
			return;
	}
	if(!m_bmpMemMain.m_hObject)
	{
		m_bmpMemMain.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height());
	}
	m_rcOldClient = m_rcClient;
	oldBitmap = *m_dcMemMain.SelectObject(&m_bmpMemMain);

	auto stockBrush = CBrush::FromHandle(GetStockBrush(DC_BRUSH));
	if(m_bGrid)
	{
		DrawGrid(&m_dcMemMain, pModDoc->GetSoundFile().m_PlayState.m_nMusicSpeed);
	} else
	{
		// Paint it black!
		m_dcMemMain.FillSolidRect(&m_rcClient, TrackerSettings::Instance().rgbCustomColors[MODCOLOR_BACKENV]);
	}

	auto oldPen = m_dcMemMain.SelectObject(CMainFrame::penDarkGray);

	// Middle line (half volume or pitch / panning center)
	const int ymed = (m_rcClient.bottom - 1) / 2;
	m_dcMemMain.MoveTo(0, ymed);
	m_dcMemMain.LineTo(m_rcClient.right, ymed);

	// Drawing Loop Start/End
	if(EnvGetLoop())
	{
		m_dcMemMain.SelectObject(m_nDragItem == ENV_DRAGLOOPSTART ? CMainFrame::penGray99 : CMainFrame::penDarkGray);
		int x1 = PointToScreen(EnvGetLoopStart()) - m_envPointSize / 2;
		m_dcMemMain.MoveTo(x1, 0);
		m_dcMemMain.LineTo(x1, m_rcClient.bottom);
		m_dcMemMain.SelectObject(m_nDragItem == ENV_DRAGLOOPEND ? CMainFrame::penGray99 : CMainFrame::penDarkGray);
		int x2 = PointToScreen(EnvGetLoopEnd()) + m_envPointSize / 2;
		m_dcMemMain.MoveTo(x2, 0);
		m_dcMemMain.LineTo(x2, m_rcClient.bottom);
	}
	// Drawing Sustain Start/End
	if(EnvGetSustain())
	{
		m_dcMemMain.SelectObject(CMainFrame::penHalfDarkGray);
		int nspace = m_rcClient.bottom / 4;
		int n1 = EnvGetSustainStart();
		int x1 = PointToScreen(n1) - m_envPointSize / 2;
		int y1 = ValueToScreen(EnvGetValue(n1));
		m_dcMemMain.MoveTo(x1, y1 - nspace);
		m_dcMemMain.LineTo(x1, y1 + nspace);
		int n2 = EnvGetSustainEnd();
		int x2 = PointToScreen(n2) + m_envPointSize / 2;
		int y2 = ValueToScreen(EnvGetValue(n2));
		m_dcMemMain.MoveTo(x2, y2 - nspace);
		m_dcMemMain.LineTo(x2, y2 + nspace);
	}
	uint32 maxpoint = EnvGetNumPoints();
	// Drawing Envelope
	if(maxpoint)
	{
		maxpoint--;
		m_dcMemMain.SelectObject(GetStockObject(DC_PEN));
		m_dcMemMain.SetDCPenColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_ENVELOPES]);
		uint32 releaseNode = EnvGetReleaseNode();
		RECT rect;
		for(uint32 i = 0; i <= maxpoint; i++)
		{
			int x = PointToScreen(i);
			int y = ValueToScreen(EnvGetValue(i));
			rect.left = x - m_envPointSize + 1;
			rect.top = y - m_envPointSize + 1;
			rect.right = x + m_envPointSize;
			rect.bottom = y + m_envPointSize;
			if(i)
				m_dcMemMain.LineTo(x, y);
			else
				m_dcMemMain.MoveTo(x, y);

			if(i == releaseNode)
			{
				m_dcMemMain.SetDCBrushColor(RGB(0xFF, 0x00, 0x00));
				m_dcMemMain.FrameRect(&rect, stockBrush);
				m_dcMemMain.SetDCPenColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_ENVELOPE_RELEASE]);
			} else if(i == m_nDragItem - 1)
			{
				// currently selected env point
				m_dcMemMain.SetDCBrushColor(RGB(0xFF, 0xFF, 0x00));
				m_dcMemMain.FrameRect(&rect, stockBrush);
			} else
			{
				m_dcMemMain.SetDCBrushColor(RGB(0xFF, 0xFF, 0xFF));
				m_dcMemMain.FrameRect(&rect, stockBrush);
			}
		}
	}
	DrawPositionMarks();
	if(oldPen)
		m_dcMemMain.SelectObject(oldPen);

	pDC->BitBlt(m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), &m_dcMemMain, 0, 0, SRCCOPY);
}


uint8 CViewInstrument::EnvGetReleaseNode()
{
	InstrumentEnvelope *envelope = GetEnvelopePtr();
	if(envelope == nullptr)
		return ENV_RELEASE_NODE_UNSET;
	return envelope->nReleaseNode;
}


bool CViewInstrument::EnvRemovePoint(uint32 nPoint)
{
	CModDoc *pModDoc = GetDocument();
	if((pModDoc) && (nPoint <= EnvGetLastPoint()))
	{
		ModInstrument *pIns = pModDoc->GetSoundFile().Instruments[m_nInstrument];
		if(pIns)
		{
			InstrumentEnvelope *envelope = GetEnvelopePtr();
			if(envelope == nullptr || envelope->empty())
				return false;

			PrepareUndo("Remove Envelope Point");
			envelope->erase(envelope->begin() + nPoint);
			if (nPoint >= envelope->size()) nPoint = envelope->size() - 1;
			if (envelope->nLoopStart > nPoint) envelope->nLoopStart--;
			if (envelope->nLoopEnd > nPoint) envelope->nLoopEnd--;
			if (envelope->nSustainStart > nPoint) envelope->nSustainStart--;
			if (envelope->nSustainEnd > nPoint) envelope->nSustainEnd--;
			if (envelope->nReleaseNode>nPoint && envelope->nReleaseNode != ENV_RELEASE_NODE_UNSET) envelope->nReleaseNode--;
			envelope->at(0).tick = 0;

			if(envelope->size() <= 1)
			{
				// if only one node is left, just disable the envelope completely
				*envelope = InstrumentEnvelope();
			}

			SetModified(InstrumentHint().Envelope(), true);
			return true;
		}
	}
	return false;
}


// Insert point. Returns 0 if error occurred, else point ID + 1.
uint32 CViewInstrument::EnvInsertPoint(int nTick, int nValue)
{
	CModDoc *pModDoc = GetDocument();
	if(pModDoc && nTick >= 0)
	{
		InstrumentEnvelope *envelope = GetEnvelopePtr();
		if(envelope != nullptr && envelope->size() < pModDoc->GetSoundFile().GetModSpecifications().envelopePointsMax)
		{
			nValue = Clamp(nValue, ENVELOPE_MIN, ENVELOPE_MAX);

			if(std::binary_search(envelope->cbegin(), envelope->cend(), EnvelopeNode(static_cast<EnvelopeNode::tick_t>(nTick), 0),
				[] (const EnvelopeNode &l, const EnvelopeNode &r) { return l.tick < r.tick; }))
			{
				// Don't want to insert a node at the same position as another node.
				return 0;
			}

			uint8 defaultValue;
			switch(m_nEnv)
			{
			case ENV_VOLUME:
				defaultValue = ENVELOPE_MAX;
				break;
			case ENV_PANNING:
				defaultValue = ENVELOPE_MID;
				break;
			case ENV_PITCH:
				defaultValue = envelope->dwFlags[ENV_FILTER] ? ENVELOPE_MAX : ENVELOPE_MID;
				break;
			default:
				return 0;
			}

			PrepareUndo("Insert Envelope Point");
			if(envelope->empty())
			{
				envelope->reserve(2);
				envelope->push_back(EnvelopeNode(0, defaultValue));
				envelope->dwFlags.set(ENV_ENABLED);
				if(nTick == 0)
				{
					// Can't insert two points on the same tick!
					nTick = 16;
				}
			}
			uint32 i = 0;
			for(i = 0; i < envelope->size(); i++) if(nTick <= envelope->at(i).tick) break;
			envelope->insert(envelope->begin() + i, EnvelopeNode(mpt::saturate_cast<EnvelopeNode::tick_t>(nTick), static_cast<EnvelopeNode::value_t>(nValue)));
			if(envelope->nLoopStart >= i) envelope->nLoopStart++;
			if(envelope->nLoopEnd >= i) envelope->nLoopEnd++;
			if(envelope->nSustainStart >= i) envelope->nSustainStart++;
			if(envelope->nSustainEnd >= i) envelope->nSustainEnd++;
			if(envelope->nReleaseNode >= i && envelope->nReleaseNode != ENV_RELEASE_NODE_UNSET) envelope->nReleaseNode++;

			SetModified(InstrumentHint().Envelope(), true);
			return i + 1;
		}
	}
	return 0;
}



void CViewInstrument::DrawPositionMarks()
{
	CRect rect;
	for(auto pos : m_dwNotifyPos) if (pos != Notification::PosInvalid)
	{
		rect.top = -2;
		rect.left = TickToScreen(pos);
		rect.right = rect.left + 1;
		rect.bottom = m_rcClient.bottom + 1;
		InvertRect(m_dcMemMain.m_hDC, &rect);
	}
}


LRESULT CViewInstrument::OnPlayerNotify(Notification *pnotify)
{
	Notification::Type type;
	CModDoc *pModDoc = GetDocument();
	if((!pnotify) || (!pModDoc))
		return 0;
	switch(m_nEnv)
	{
	case ENV_PANNING:	type = Notification::PanEnv; break;
	case ENV_PITCH:		type = Notification::PitchEnv; break;
	default:			type = Notification::VolEnv; break;
	}
	if(pnotify->type[Notification::Stop])
	{
		bool invalidate = false;
		for(auto &pos : m_dwNotifyPos)
		{
			if(pos != (uint32)Notification::PosInvalid)
			{
				pos = (uint32)Notification::PosInvalid;
				invalidate = true;
			}
		}
		if(invalidate)
		{
			InvalidateEnvelope();
		}
		m_baPlayingNote.reset();
	} else if(pnotify->type[type] && pnotify->item == m_nInstrument)
	{
		bool update = false;
		for(CHANNELINDEX i = 0; i < MAX_CHANNELS; i++)
		{
			uint32 newpos = (uint32)pnotify->pos[i];
			if(m_dwNotifyPos[i] != newpos)
			{
				update = true;
				break;
			}
		}
		if(update)
		{
			HDC hdc = ::GetDC(m_hWnd);
			DrawPositionMarks();
			for(CHANNELINDEX j = 0; j < MAX_CHANNELS; j++)
			{
				uint32 newpos = (uint32)pnotify->pos[j];
				m_dwNotifyPos[j] = newpos;
			}
			DrawPositionMarks();
			BitBlt(hdc, m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), m_dcMemMain.GetSafeHdc(), 0, 0, SRCCOPY);
			::ReleaseDC(m_hWnd, hdc);
		}
	}
	return 0;
}


void CViewInstrument::DrawNcButton(CDC *pDC, UINT nBtn)
{
	CRect rect;
	COLORREF crHi = GetSysColor(COLOR_3DHILIGHT);
	COLORREF crDk = GetSysColor(COLOR_3DSHADOW);
	COLORREF crFc = GetSysColor(COLOR_3DFACE);
	COLORREF c1, c2;

	if(GetNcButtonRect(nBtn, rect))
	{
		DWORD dwStyle = m_NcButtonState[nBtn];
		COLORREF c3, c4;
		int xofs = 0, yofs = 0, nImage = 0;

		c1 = c2 = c3 = c4 = crFc;
		if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS))
		{
			c1 = c3 = crHi;
			c2 = crDk;
			c4 = RGB(0, 0, 0);
		}
		if(dwStyle & (NCBTNS_PUSHED | NCBTNS_CHECKED))
		{
			c1 = crDk;
			c2 = crHi;
			if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS))
			{
				c4 = crHi;
				c3 = (dwStyle & NCBTNS_PUSHED) ? RGB(0, 0, 0) : crDk;
			}
			xofs = yofs = 1;
		} else if((dwStyle & NCBTNS_MOUSEOVER) && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS))
		{
			c1 = crHi;
			c2 = crDk;
		}
		switch(cLeftBarButtons[nBtn])
		{
		case ID_ENVSEL_VOLUME:		nImage = IIMAGE_VOLENV; break;
		case ID_ENVSEL_PANNING:		nImage = IIMAGE_PANENV; break;
		case ID_ENVSEL_PITCH:		nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOPITCHENV : IIMAGE_PITCHENV; break;
		case ID_ENVELOPE_SETLOOP:	nImage = IIMAGE_LOOP; break;
		case ID_ENVELOPE_SUSTAIN:	nImage = IIMAGE_SUSTAIN; break;
		case ID_ENVELOPE_CARRY:		nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOCARRY : IIMAGE_CARRY; break;
		case ID_ENVELOPE_VOLUME:	nImage = IIMAGE_VOLSWITCH; break;
		case ID_ENVELOPE_PANNING:	nImage = IIMAGE_PANSWITCH; break;
		case ID_ENVELOPE_PITCH:		nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOPITCHSWITCH : IIMAGE_PITCHSWITCH; break;
		case ID_ENVELOPE_FILTER:	nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOFILTERSWITCH : IIMAGE_FILTERSWITCH; break;
		case ID_INSTRUMENT_SAMPLEMAP: nImage = IIMAGE_SAMPLEMAP; break;
		case ID_ENVELOPE_VIEWGRID:	nImage = IIMAGE_GRID; break;
		case ID_ENVELOPE_ZOOM_IN:	nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOZOOMIN : IIMAGE_ZOOMIN; break;
		case ID_ENVELOPE_ZOOM_OUT:	nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOZOOMOUT : IIMAGE_ZOOMOUT; break;
		case ID_ENVELOPE_LOAD:		nImage = IIMAGE_LOAD; break;
		case ID_ENVELOPE_SAVE:		nImage = IIMAGE_SAVE; break;
		}
		pDC->Draw3dRect(rect.left - 1, rect.top - 1, ENV_LEFTBAR_CXBTN + 2, ENV_LEFTBAR_CYBTN + 2, c3, c4);
		pDC->Draw3dRect(rect.left, rect.top, ENV_LEFTBAR_CXBTN, ENV_LEFTBAR_CYBTN, c1, c2);
		rect.DeflateRect(1, 1);
		pDC->FillSolidRect(&rect, crFc);
		rect.left += xofs;
		rect.top += yofs;
		if(dwStyle & NCBTNS_CHECKED)
			m_bmpEnvBar.Draw(pDC, IIMAGE_CHECKED, rect.TopLeft(), ILD_NORMAL);
		m_bmpEnvBar.Draw(pDC, nImage, rect.TopLeft(), ILD_NORMAL);
	} else
	{
		c1 = c2 = crFc;
		if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)
		{
			c1 = crDk;
			c2 = crHi;
		}
		pDC->Draw3dRect(rect.left, rect.top, 2, ENV_LEFTBAR_CYBTN, c1, c2);
	}
}


void CViewInstrument::OnNcPaint()
{
	RECT rect;

	CModScrollView::OnNcPaint();
	GetWindowRect(&rect);
	// Assumes there is no other non-client items
	rect.bottom = ENV_LEFTBAR_CY;
	rect.right -= rect.left;
	rect.left = 0;
	rect.top = 0;
	if((rect.left < rect.right) && (rect.top < rect.bottom))
	{
		CDC *pDC = GetWindowDC();
		{
			// Shadow
			auto shadowRect = rect;
			shadowRect.top = shadowRect.bottom - 1;
			pDC->FillSolidRect(&shadowRect, GetSysColor(COLOR_BTNSHADOW));
		}
		rect.bottom--;
		if(rect.top < rect.bottom)
			pDC->FillSolidRect(&rect, GetSysColor(COLOR_BTNFACE));
		if(rect.top + 2 < rect.bottom)
		{
			for(UINT i = 0; i < ENV_LEFTBAR_BUTTONS; i++)
			{
				DrawNcButton(pDC, i);
			}
		}
		ReleaseDC(pDC);
	}
}


////////////////////////////////////////////////////////////////////
// CViewInstrument messages


void CViewInstrument::OnSize(UINT nType, int cx, int cy)
{
	CModScrollView::OnSize(nType, cx, cy);
	if(((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0))
	{
		UpdateScrollSize();
	}
}


void CViewInstrument::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS *lpncsp)
{
	CModScrollView::OnNcCalcSize(bCalcValidRects, lpncsp);
	if(lpncsp)
	{
		lpncsp->rgrc[0].top += ENV_LEFTBAR_CY;
		if(lpncsp->rgrc[0].bottom < lpncsp->rgrc[0].top)
			lpncsp->rgrc[0].top = lpncsp->rgrc[0].bottom;
	}
}


void CViewInstrument::OnNcMouseMove(UINT nHitTest, CPoint point)
{
	const auto button = GetNcButtonAtPoint(point);
	if(button != m_nBtnMouseOver)
	{
		CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
		if(pMainFrm)
		{
			CString strText;
			if(button < ENV_LEFTBAR_BUTTONS && cLeftBarButtons[button] != ID_SEPARATOR)
			{
				strText = LoadResourceString(cLeftBarButtons[button]);
			}
			pMainFrm->SetHelpText(strText);
		}
		m_nBtnMouseOver = button;
		UpdateNcButtonState();
	}
	CModScrollView::OnNcMouseMove(nHitTest, point);
}


void CViewInstrument::OnNcLButtonDown(UINT uFlags, CPoint point)
{
	if(m_nBtnMouseOver < ENV_LEFTBAR_BUTTONS)
	{
		m_dwStatus |= INSSTATUS_NCLBTNDOWN;
		if(cLeftBarButtons[m_nBtnMouseOver] != ID_SEPARATOR)
		{
			PostMessage(WM_COMMAND, cLeftBarButtons[m_nBtnMouseOver]);
			UpdateNcButtonState();
		}
	}
	CModScrollView::OnNcLButtonDown(uFlags, point);
}


void CViewInstrument::OnNcLButtonUp(UINT uFlags, CPoint point)
{
	if(m_dwStatus & INSSTATUS_NCLBTNDOWN)
	{
		m_dwStatus &= ~INSSTATUS_NCLBTNDOWN;
		UpdateNcButtonState();
	}
	CModScrollView::OnNcLButtonUp(uFlags, point);
}


void CViewInstrument::OnNcLButtonDblClk(UINT uFlags, CPoint point)
{
	OnNcLButtonDown(uFlags, point);
}


LRESULT CViewInstrument::OnNcHitTest(CPoint point)
{
	CRect rect;
	GetWindowRect(&rect);
	rect.bottom = rect.top + ENV_LEFTBAR_CY;
	if(rect.PtInRect(point))
	{
		return HTBORDER;
	}
	return CModScrollView::OnNcHitTest(point);
}


void CViewInstrument::OnMouseMove(UINT, CPoint pt)
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns == nullptr)
		return;

	bool splitCursor = false;

	if((m_nBtnMouseOver < ENV_LEFTBAR_BUTTONS) || (m_dwStatus & INSSTATUS_NCLBTNDOWN))
	{
		m_dwStatus &= ~INSSTATUS_NCLBTNDOWN;
		m_nBtnMouseOver = 0xFFFF;
		UpdateNcButtonState();
		CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
		if(pMainFrm)
			pMainFrm->SetHelpText(_T(""));
	}
	int nTick = ScreenToTick(pt.x);
	int nVal = Clamp(ScreenToValue(pt.y), ENVELOPE_MIN, ENVELOPE_MAX);
	if(nTick < 0)
		nTick = 0;
	UpdateIndicator(nTick, nVal);

	if((m_dwStatus & INSSTATUS_DRAGGING) && (m_nDragItem))
	{
		if(!m_mouseMoveModified)
		{
			PrepareUndo("Move Envelope Point");
			m_mouseMoveModified = true;
		}
		bool changed = false;
		if(pt.x >= m_rcClient.right - 2)
			nTick++;
		if(IsDragItemEnvPoint())
		{
			// Ctrl pressed -> move tail of envelope
			changed = EnvSetValue(m_nDragItem - 1, nTick, nVal, CMainFrame::GetInputHandler()->CtrlPressed());
		} else
		{
			int nPoint = ScreenToPoint(pt.x, pt.y);
			if (nPoint >= 0) switch(m_nDragItem)
			{
			case ENV_DRAGLOOPSTART:
				changed = EnvSetLoopStart(nPoint);
				splitCursor = true;
				break;
			case ENV_DRAGLOOPEND:
				changed = EnvSetLoopEnd(nPoint);
				splitCursor = true;
				break;
			case ENV_DRAGSUSTAINSTART:
				changed = EnvSetSustainStart(nPoint);
				splitCursor = true;
				break;
			case ENV_DRAGSUSTAINEND:
				changed = EnvSetSustainEnd(nPoint);
				splitCursor = true;
				break;
			}
		}
		if(changed)
		{
			if(pt.x <= 0)
			{
				UpdateScrollSize();
				OnScrollBy(CSize(pt.x - (int)m_zoom, 0), TRUE);
			}
			if(pt.x >= m_rcClient.right - 1)
			{
				UpdateScrollSize();
				OnScrollBy(CSize((int)m_zoom + pt.x - m_rcClient.right, 0), TRUE);
			}
			SetModified(InstrumentHint().Envelope(), true);
			UpdateWindow();  //rewbs: TODO - optimisation here so we don't redraw whole view.
		}
	} else
	{
		CRect rect;
		if(EnvGetSustain())
		{
			int nspace = m_rcClient.bottom / 4;
			rect.top = ValueToScreen(EnvGetValue(EnvGetSustainStart())) - nspace;
			rect.bottom = rect.top + nspace * 2;
			rect.right = PointToScreen(EnvGetSustainStart()) + 1;
			rect.left = rect.right - m_envPointSize * 2;
			if(rect.PtInRect(pt))
			{
				splitCursor = true;  // ENV_DRAGSUSTAINSTART;
			} else
			{
				rect.top = ValueToScreen(EnvGetValue(EnvGetSustainEnd())) - nspace;
				rect.bottom = rect.top + nspace * 2;
				rect.left = PointToScreen(EnvGetSustainEnd()) - 1;
				rect.right = rect.left + m_envPointSize * 2;
				if(rect.PtInRect(pt))
					splitCursor = true;  // ENV_DRAGSUSTAINEND;
			}
		}
		if(EnvGetLoop())
		{
			rect.top = m_rcClient.top;
			rect.bottom = m_rcClient.bottom;
			rect.right = PointToScreen(EnvGetLoopStart()) + 1;
			rect.left = rect.right - m_envPointSize * 2;
			if(rect.PtInRect(pt))
			{
				splitCursor = true;  // ENV_DRAGLOOPSTART;
			} else
			{
				rect.left = PointToScreen(EnvGetLoopEnd()) - 1;
				rect.right = rect.left + m_envPointSize * 2;
				if(rect.PtInRect(pt))
					splitCursor = true;  // ENV_DRAGLOOPEND;
			}
		}
	}
	// Update the mouse cursor
	if(splitCursor)
	{
		if(!(m_dwStatus & INSSTATUS_SPLITCURSOR))
		{
			m_dwStatus |= INSSTATUS_SPLITCURSOR;
			if(!(m_dwStatus & INSSTATUS_DRAGGING))
				SetCapture();
			SetCursor(CMainFrame::curVSplit);
		}
	} else
	{
		if(m_dwStatus & INSSTATUS_SPLITCURSOR)
		{
			m_dwStatus &= ~INSSTATUS_SPLITCURSOR;
			SetCursor(CMainFrame::curArrow);
			if(!(m_dwStatus & INSSTATUS_DRAGGING))
				ReleaseCapture();
		}
	}
}



void CViewInstrument::UpdateIndicator()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !m_nDragItem)
		return;

	uint32 point = DragItemToEnvPoint();
	if(point < pEnv->size())
	{
		UpdateIndicator(pEnv->at(point).tick, pEnv->at(point).value);
	}
}


void CViewInstrument::UpdateIndicator(int tick, int val)
{
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns == nullptr)
		return;

	CString s;
	s.Format(TrackerSettings::Instance().cursorPositionInHex ? _T("Tick %X, [%s]") : _T("Tick %d, [%s]"), tick, EnvValueToString(tick, val).GetString());
	CModScrollView::UpdateIndicator(s);
	CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this);
}


CString CViewInstrument::EnvValueToString(int tick, int val) const
{
	const InstrumentEnvelope *env = GetEnvelopePtr();
	const bool hasReleaseNode = env->nReleaseNode != ENV_RELEASE_NODE_UNSET;
	EnvelopeNode releaseNode;
	if(hasReleaseNode)
	{
		releaseNode = env->at(env->nReleaseNode);
	}

	CString s;
	if(!hasReleaseNode || tick <= releaseNode.tick + 1)
	{
		// ticks before release node (or no release node)
		const int displayVal = (m_nEnv != ENV_VOLUME && !(m_nEnv == ENV_PITCH && env->dwFlags[ENV_FILTER])) ? val - 32 : val;
		if(m_nEnv != ENV_PANNING)
			s.Format(_T("%d"), displayVal);
		else  // panning envelope: display right/center/left chars
			s.Format(_T("%d %c"), std::abs(displayVal), displayVal > 0 ? _T('R') : (displayVal < 0 ? _T('L') : _T('C')));
	} else
	{
		// ticks after release node
		int displayVal = (val - releaseNode.value) * 2;
		displayVal = (m_nEnv != ENV_VOLUME) ? displayVal - 32 : displayVal;
		s.Format(_T("Rel%c%d"), displayVal > 0 ? _T('+') : _T('-'), std::abs(displayVal));
	}
	return s;
}


void CViewInstrument::OnLButtonDown(UINT, CPoint pt)
{
	m_mouseMoveModified = false;
	if(!(m_dwStatus & INSSTATUS_DRAGGING))
	{
		CRect rect;
		// Look if dragging a point
		uint32 maxpoint = EnvGetLastPoint();
		uint32 oldDragItem = m_nDragItem;
		m_nDragItem = 0;
		const int hitboxSize = static_cast<int>((6 * m_nDPIx) / 96.0f);
		for(uint32 i = 0; i <= maxpoint; i++)
		{
			int x = PointToScreen(i);
			int y = ValueToScreen(EnvGetValue(i));
			rect.SetRect(x - hitboxSize, y - hitboxSize, x + hitboxSize + 1, y + hitboxSize + 1);
			if(rect.PtInRect(pt))
			{
				m_nDragItem = i + 1;
				break;
			}
		}
		if((!m_nDragItem) && (EnvGetSustain()))
		{
			int nspace = m_rcClient.bottom / 4;
			rect.top = ValueToScreen(EnvGetValue(EnvGetSustainStart())) - nspace;
			rect.bottom = rect.top + nspace * 2;
			rect.right = PointToScreen(EnvGetSustainStart()) + 1;
			rect.left = rect.right - m_envPointSize * 2;
			if(rect.PtInRect(pt))
			{
				m_nDragItem = ENV_DRAGSUSTAINSTART;
			} else
			{
				rect.top = ValueToScreen(EnvGetValue(EnvGetSustainEnd())) - nspace;
				rect.bottom = rect.top + nspace * 2;
				rect.left = PointToScreen(EnvGetSustainEnd()) - 1;
				rect.right = rect.left + m_envPointSize * 2;
				if(rect.PtInRect(pt))
					m_nDragItem = ENV_DRAGSUSTAINEND;
			}
		}
		if((!m_nDragItem) && (EnvGetLoop()))
		{
			rect.top = m_rcClient.top;
			rect.bottom = m_rcClient.bottom;
			rect.right = PointToScreen(EnvGetLoopStart()) + 1;
			rect.left = rect.right - m_envPointSize * 2;
			if(rect.PtInRect(pt))
			{
				m_nDragItem = ENV_DRAGLOOPSTART;
			} else
			{
				rect.left = PointToScreen(EnvGetLoopEnd()) - 1;
				rect.right = rect.left + m_envPointSize * 2;
				if(rect.PtInRect(pt))
					m_nDragItem = ENV_DRAGLOOPEND;
			}
		}

		if(m_nDragItem)
		{
			SetCapture();
			m_dwStatus |= INSSTATUS_DRAGGING;
			// refresh active node colour
			InvalidateRect(NULL, FALSE);
		} else
		{
			// Shift-Click: Insert envelope point here
			if(CMainFrame::GetInputHandler()->ShiftPressed())
			{
				if(InsertAtPoint(pt) == 0 && oldDragItem != 0)
				{
					InvalidateRect(NULL, FALSE);
				}
			} else if(oldDragItem)
			{
				InvalidateRect(NULL, FALSE);
			}
		}
	}
}


void CViewInstrument::OnLButtonUp(UINT, CPoint)
{
	m_mouseMoveModified = false;
	if(m_dwStatus & INSSTATUS_SPLITCURSOR)
	{
		m_dwStatus &= ~INSSTATUS_SPLITCURSOR;
		SetCursor(CMainFrame::curArrow);
	}
	if(m_dwStatus & INSSTATUS_DRAGGING)
	{
		m_dwStatus &= ~INSSTATUS_DRAGGING;
		ReleaseCapture();
	}
}


void CViewInstrument::OnRButtonDown(UINT flags, CPoint pt)
{
	const CModDoc *pModDoc = GetDocument();
	if(!pModDoc)
		return;
	const CSoundFile &sndFile = GetDocument()->GetSoundFile();

	if(m_dwStatus & INSSTATUS_DRAGGING)
		return;

	// Ctrl + Right-Click = Delete point
	if(flags & MK_CONTROL)
	{
		OnMButtonDown(flags, pt);
		return;
	}

	CMenu Menu;
	if((pModDoc) && (Menu.LoadMenu(IDR_ENVELOPES)))
	{
		CMenu *pSubMenu = Menu.GetSubMenu(0);
		if(pSubMenu != nullptr)
		{
			m_nDragItem = ScreenToPoint(pt.x, pt.y) + 1;
			const uint32 maxPoint = (sndFile.GetType() == MOD_TYPE_XM) ? 11 : 24;
			const uint32 lastpoint = EnvGetLastPoint();
			const bool forceRelease = !sndFile.GetModSpecifications().hasReleaseNode && (EnvGetReleaseNode() != ENV_RELEASE_NODE_UNSET);
			pSubMenu->EnableMenuItem(ID_ENVELOPE_INSERTPOINT, (lastpoint < maxPoint) ? MF_ENABLED : MF_GRAYED);
			pSubMenu->EnableMenuItem(ID_ENVELOPE_REMOVEPOINT, ((m_nDragItem) && (lastpoint > 0)) ? MF_ENABLED : MF_GRAYED);
			pSubMenu->EnableMenuItem(ID_ENVELOPE_CARRY, (sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) ? MF_ENABLED : MF_GRAYED);
			pSubMenu->EnableMenuItem(ID_ENVELOPE_TOGGLERELEASENODE, ((sndFile.GetModSpecifications().hasReleaseNode && m_nEnv == ENV_VOLUME) || forceRelease) ? MF_ENABLED : MF_GRAYED);
			pSubMenu->CheckMenuItem(ID_ENVELOPE_SETLOOP, (EnvGetLoop()) ? MF_CHECKED : MF_UNCHECKED);
			pSubMenu->CheckMenuItem(ID_ENVELOPE_SUSTAIN, (EnvGetSustain()) ? MF_CHECKED : MF_UNCHECKED);
			pSubMenu->CheckMenuItem(ID_ENVELOPE_CARRY, (EnvGetCarry()) ? MF_CHECKED : MF_UNCHECKED);
			pSubMenu->CheckMenuItem(ID_ENVELOPE_TOGGLERELEASENODE, (EnvGetReleaseNode() == m_nDragItem - 1) ? MF_CHECKED : MF_UNCHECKED);
			m_ptMenu = pt;
			ClientToScreen(&pt);
			pSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this);
		}
	}
}

void CViewInstrument::OnMButtonDown(UINT, CPoint pt)
{
	// Middle mouse button: Remove envelope point
	int point = ScreenToPoint(pt.x, pt.y);
	if(point >= 0)
	{
		EnvRemovePoint(point);
		m_nDragItem = point + 1;
	}
}


void CViewInstrument::OnPrevInstrument()
{
	SendCtrlMessage(CTRLMSG_INS_PREVINSTRUMENT);
}


void CViewInstrument::OnNextInstrument()
{
	SendCtrlMessage(CTRLMSG_INS_NEXTINSTRUMENT);
}


void CViewInstrument::OnEditSampleMap()
{
	SendCtrlMessage(CTRLMSG_INS_SAMPLEMAP);
}


void CViewInstrument::OnSelectVolumeEnv()
{
	if(m_nEnv != ENV_VOLUME)
		SetCurrentInstrument(m_nInstrument, ENV_VOLUME);
}


void CViewInstrument::OnSelectPanningEnv()
{
	if(m_nEnv != ENV_PANNING)
		SetCurrentInstrument(m_nInstrument, ENV_PANNING);
}


void CViewInstrument::OnSelectPitchEnv()
{
	if(m_nEnv != ENV_PITCH)
		SetCurrentInstrument(m_nInstrument, ENV_PITCH);
}


void CViewInstrument::OnEnvLoopChanged()
{
	CModDoc *pModDoc = GetDocument();
	PrepareUndo("Toggle Envelope Loop");
	if((pModDoc) && (EnvSetLoop(!EnvGetLoop())))
	{
		InstrumentEnvelope *pEnv = GetEnvelopePtr();
		if(EnvGetLoop() && pEnv != nullptr && pEnv->nLoopEnd == 0)
		{
			// Enabled loop => set loop points if no loop has been specified yet.
			pEnv->nLoopStart = 0;
			pEnv->nLoopEnd = mpt::saturate_cast<decltype(pEnv->nLoopEnd)>(pEnv->size() - 1);
		}
		SetModified(InstrumentHint().Envelope(), true);
	}
}


void CViewInstrument::OnEnvSustainChanged()
{
	CModDoc *pModDoc = GetDocument();
	PrepareUndo("Toggle Envelope Sustain");
	if((pModDoc) && (EnvSetSustain(!EnvGetSustain())))
	{
		InstrumentEnvelope *pEnv = GetEnvelopePtr();
		if(EnvGetSustain() && pEnv != nullptr && pEnv->nSustainStart == pEnv->nSustainEnd && IsDragItemEnvPoint())
		{
			// Enabled sustain loop => set sustain loop points if no sustain loop has been specified yet.
			pEnv->nSustainStart = pEnv->nSustainEnd = mpt::saturate_cast<decltype(pEnv->nSustainEnd)>(m_nDragItem - 1);
		}
		SetModified(InstrumentHint().Envelope(), true);
	}
}


void CViewInstrument::OnEnvCarryChanged()
{
	CModDoc *pModDoc = GetDocument();
	PrepareUndo("Toggle Envelope Carry");
	if((pModDoc) && (EnvSetCarry(!EnvGetCarry())))
	{
		SetModified(InstrumentHint().Envelope(), false);
		UpdateNcButtonState();
	}
}

void CViewInstrument::OnEnvToggleReleasNode()
{
	if(IsDragItemEnvPoint())
	{
		PrepareUndo("Toggle Envelope Release Node");
		if(EnvToggleReleaseNode(m_nDragItem - 1))
		{
			SetModified(InstrumentHint().Envelope(), true);
		}
	}
}


void CViewInstrument::OnEnvVolChanged()
{
	GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Volume Envelope", ENV_VOLUME);
	if(EnvSetVolEnv(!EnvGetVolEnv()))
	{
		SetModified(InstrumentHint().Envelope(), false);
		UpdateNcButtonState();
	}
}


void CViewInstrument::OnEnvPanChanged()
{
	GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Panning Envelope", ENV_PANNING);
	if(EnvSetPanEnv(!EnvGetPanEnv()))
	{
		SetModified(InstrumentHint().Envelope(), false);
		UpdateNcButtonState();
	}
}


void CViewInstrument::OnEnvPitchChanged()
{
	GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Pitch Envelope", ENV_PITCH);
	if(EnvSetPitchEnv(!EnvGetPitchEnv()))
	{
		SetModified(InstrumentHint().Envelope(), false);
		UpdateNcButtonState();
	}
}


void CViewInstrument::OnEnvFilterChanged()
{
	GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Filter Envelope", ENV_PITCH);
	if(EnvSetFilterEnv(!EnvGetFilterEnv()))
	{
		SetModified(InstrumentHint().Envelope(), false);
		UpdateNcButtonState();
	}
}


void CViewInstrument::OnEnvToggleGrid()
{
	m_bGrid = !m_bGrid;
	if(m_bGrid)
		m_bGridForceRedraw = true;
	CModDoc *pModDoc = GetDocument();
	if(pModDoc)
		pModDoc->UpdateAllViews(nullptr, InstrumentHint(m_nInstrument).Envelope());
}


void CViewInstrument::OnEnvRemovePoint()
{
	if(m_nDragItem > 0)
	{
		EnvRemovePoint(m_nDragItem - 1);
	}
}


void CViewInstrument::OnEnvInsertPoint()
{
	const int tick = ScreenToTick(m_ptMenu.x), value = ScreenToValue(m_ptMenu.y);
	if(!EnvInsertPoint(tick, value))
	{
		// Couldn't insert point, maybe because there's already a point at this tick
		// => Try next tick
		EnvInsertPoint(tick + 1, value);
	}
}


bool CViewInstrument::InsertAtPoint(CPoint pt)
{
	auto item = EnvInsertPoint(ScreenToTick(pt.x), ScreenToValue(pt.y));  // returns point ID + 1 if successful, else 0.
	if(item > 0)
	{
		// Drag point if successful
		SetCapture();
		m_dwStatus |= INSSTATUS_DRAGGING;
		m_nDragItem = item;
	}
	return item > 0;
}


void CViewInstrument::OnEditCopy()
{
	CModDoc *pModDoc = GetDocument();
	if(pModDoc)
		pModDoc->CopyEnvelope(m_nInstrument, m_nEnv);
}


void CViewInstrument::OnEditPaste()
{
	CModDoc *pModDoc = GetDocument();
	PrepareUndo("Paste Envelope");
	if(pModDoc->PasteEnvelope(m_nInstrument, m_nEnv))
	{
		SetModified(InstrumentHint().Envelope(), true);
	} else
	{
		pModDoc->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
	}
}


void CViewInstrument::PlayNote(ModCommand::NOTE note)
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	CModDoc *pModDoc = GetDocument();
	if(pModDoc == nullptr || pMainFrm == nullptr)
	{
		return;
	}
	if(note > 0 && note < 128)
	{
		if(m_nInstrument && !m_baPlayingNote[note])
		{
			CSoundFile &sndFile = pModDoc->GetSoundFile();
			ModInstrument *pIns = sndFile.Instruments[m_nInstrument];
			if((!pIns) || (!pIns->Keyboard[note - NOTE_MIN] && !pIns->nMixPlug))
				return;
			{
				if(pMainFrm->GetModPlaying() != pModDoc)
				{
					sndFile.m_SongFlags.set(SONG_PAUSED);
					sndFile.ResetChannels();
					if(!pMainFrm->PlayMod(pModDoc))
						return;
				}
				pModDoc->PlayNote(PlayNoteParam(note).Instrument(m_nInstrument).CheckNNA(m_baPlayingNote), &m_noteChannel);
			}
			CString noteName;
			if(ModCommand::IsNote(note))
			{
				noteName = mpt::ToCString(sndFile.GetNoteName(note, m_nInstrument));
			}
			pMainFrm->SetInfoText(noteName);
		}
	} else
	{
		pModDoc->PlayNote(PlayNoteParam(note).Instrument(m_nInstrument));
	}
}


// Drop files from Windows
void CViewInstrument::OnDropFiles(HDROP hDropInfo)
{
	const UINT nFiles = ::DragQueryFile(hDropInfo, (UINT)-1, NULL, 0);
	CMainFrame::GetMainFrame()->SetForegroundWindow();
	for(UINT f = 0; f < nFiles; f++)
	{
		UINT size = ::DragQueryFile(hDropInfo, f, nullptr, 0) + 1;
		std::vector<TCHAR> fileName(size, _T('\0'));
		if(::DragQueryFile(hDropInfo, f, fileName.data(), size))
		{
			const mpt::PathString file = mpt::PathString::FromNative(fileName.data());
			PrepareUndo("Replace Envelope");
			if(GetDocument()->LoadEnvelope(m_nInstrument, m_nEnv, file))
			{
				SetModified(InstrumentHint(m_nInstrument).Envelope(), true);
			} else
			{
				GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
				if(SendCtrlMessage(CTRLMSG_INS_OPENFILE, (LPARAM)&file) && f < nFiles - 1)
				{
					// Insert more instrument slots
					if(!SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT))
						break;
				}
			}
		}
	}
	::DragFinish(hDropInfo);
}


BOOL CViewInstrument::OnDragonDrop(BOOL doDrop, const DRAGONDROP *dropInfo)
{
	CModDoc *modDoc = GetDocument();
	bool canDrop = false;

	if((!dropInfo) || (!modDoc))
		return FALSE;
	CSoundFile &sndFile = modDoc->GetSoundFile();
	switch(dropInfo->dropType)
	{
	case DRAGONDROP_INSTRUMENT:
		if(dropInfo->sndFile == &sndFile)
		{
			canDrop = ((dropInfo->dropItem)
			           && (dropInfo->dropItem <= sndFile.m_nInstruments)
			           && (dropInfo->sndFile == &sndFile));
		} else
		{
			canDrop = ((dropInfo->dropItem)
			           && ((dropInfo->dropParam) || (dropInfo->sndFile)));
		}
		break;

	case DRAGONDROP_DLS:
		canDrop = ((dropInfo->dropItem < CTrackApp::gpDLSBanks.size())
		            && (CTrackApp::gpDLSBanks[dropInfo->dropItem]));
		break;

	case DRAGONDROP_SOUNDFILE:
	case DRAGONDROP_MIDIINSTR:
		canDrop = !dropInfo->GetPath().empty();
		break;
	}
	
	const bool insertNew = CMainFrame::GetInputHandler()->ShiftPressed() && sndFile.GetNumInstruments() > 0;
	if(insertNew && !sndFile.CanAddMoreInstruments())
		canDrop = false;

	if(!canDrop || !doDrop)
		return canDrop;

	if(!sndFile.GetNumInstruments() && sndFile.GetModSpecifications().instrumentsMax > 0)
		SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT);
	if(!m_nInstrument || m_nInstrument > sndFile.GetNumInstruments())
		return FALSE;

	// Do the drop
	bool modified = false;
	BeginWaitCursor();
	switch(dropInfo->dropType)
	{
	case DRAGONDROP_INSTRUMENT:
		if(dropInfo->sndFile == &sndFile)
		{
			SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, dropInfo->dropItem);
		} else
		{
			if(insertNew && !SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT))
				canDrop = false;
			else
				SendCtrlMessage(CTRLMSG_INS_SONGDROP, reinterpret_cast<LPARAM>(dropInfo));
		}
		break;

	case DRAGONDROP_MIDIINSTR:
		if(CDLSBank::IsDLSBank(dropInfo->GetPath()))
		{
			CDLSBank dlsbank;
			if(dlsbank.Open(dropInfo->GetPath()))
			{
				const DLSINSTRUMENT *pDlsIns;
				UINT nIns = 0, nRgn = 0xFF;
				// Drums
				if(dropInfo->dropItem & 0x80)
				{
					UINT key = dropInfo->dropItem & 0x7F;
					pDlsIns = dlsbank.FindInstrument(true, 0xFFFF, 0xFF, key, &nIns);
					if(pDlsIns)
						nRgn = dlsbank.GetRegionFromKey(nIns, key);
				} else
				// Melodic
				{
					pDlsIns = dlsbank.FindInstrument(false, 0xFFFF, dropInfo->dropItem, 60, &nIns);
					if(pDlsIns)
						nRgn = dlsbank.GetRegionFromKey(nIns, 60);
				}
				canDrop = false;
				if(pDlsIns)
				{
					if(!insertNew || SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT))
					{
						CriticalSection cs;
						modDoc->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Replace Instrument");
						canDrop = modified = dlsbank.ExtractInstrument(sndFile, m_nInstrument, nIns, nRgn);
					}
				}
				break;
			}
		}
		// Instrument file -> fall through
		[[fallthrough]];
	case DRAGONDROP_SOUNDFILE:
		if(!insertNew || SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT))
			SendCtrlMessage(CTRLMSG_INS_OPENFILE, dropInfo->dropParam);
		break;

	case DRAGONDROP_DLS:
		{
			UINT nIns = dropInfo->dropParam & 0xFFFF;
			uint32 drumRgn = uint32_max;
			// Drums: (0x80000000) | (Region << 16) | (Instrument)
			if(dropInfo->dropParam & 0x80000000)
				drumRgn = (dropInfo->dropParam & 0x7FFF0000) >> 16;

			if(!insertNew || SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT))
			{
				CriticalSection cs;
				modDoc->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Replace Instrument");
				canDrop = modified = CTrackApp::gpDLSBanks[dropInfo->dropItem]->ExtractInstrument(sndFile, m_nInstrument, nIns, drumRgn);
			}
		}
		break;
	}
	if(modified)
	{
		SetModified(InstrumentHint().Info().Envelope().Names(), true);
		GetDocument()->UpdateAllViews(nullptr, SampleHint().Info().Names().Data(), this);
	}
	CMDIChildWnd *pMDIFrame = (CMDIChildWnd *)GetParentFrame();
	if(pMDIFrame)
	{
		pMDIFrame->MDIActivate();
		pMDIFrame->SetActiveView(this);
		SetFocus();
	}
	EndWaitCursor();
	return canDrop;
}


LRESULT CViewInstrument::OnMidiMsg(WPARAM midiDataParam, LPARAM)
{
	const uint32 midiData = static_cast<uint32>(midiDataParam);
	CModDoc *modDoc = GetDocument();
	if(modDoc != nullptr)
	{
		modDoc->ProcessMIDI(midiData, m_nInstrument, modDoc->GetSoundFile().GetInstrumentPlugin(m_nInstrument), kCtxViewInstruments);

		MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData);
		uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData);
		if(event == MIDIEvents::evNoteOn)
		{
			CMainFrame::GetMainFrame()->SetInfoText(mpt::ToCString(modDoc->GetSoundFile().GetNoteName(midiByte1 + NOTE_MIN, m_nInstrument)));
		}

		return 1;
	}
	return 0;
}


BOOL CViewInstrument::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);
			InputTargetContext ctx = (InputTargetContext)(kCtxViewInstruments);

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

			// Handle Application (menu) key
			if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS)
			{
				CPoint pt(0, 0);
				if(m_nDragItem > 0)
				{
					uint32 point = DragItemToEnvPoint();
					pt.SetPoint(PointToScreen(point), ValueToScreen(EnvGetValue(point)));
				}
				OnRButtonDown(0, pt);
			}
		}
	}

	return CModScrollView::PreTranslateMessage(pMsg);
}


LRESULT CViewInstrument::OnCustomKeyMsg(WPARAM wParam, LPARAM)
{
	CModDoc *pModDoc = GetDocument();
	if(!pModDoc)
		return kcNull;
	CSoundFile &sndFile = pModDoc->GetSoundFile();

	switch(wParam)
	{
		case kcPrevInstrument:	OnPrevInstrument(); return wParam;
		case kcNextInstrument:	OnNextInstrument(); return wParam;
		case kcEditCopy:		OnEditCopy(); return wParam;
		case kcEditPaste:		OnEditPaste(); return wParam;
		case kcEditUndo:		OnEditUndo(); return wParam;
		case kcEditRedo:		OnEditRedo(); return wParam;
		case kcNoteOff:			PlayNote(NOTE_KEYOFF); return wParam;
		case kcNoteCut:			PlayNote(NOTE_NOTECUT); return wParam;
		case kcInstrumentLoad:	SendCtrlMessage(IDC_INSTRUMENT_OPEN); return wParam;
		case kcInstrumentSave:	SendCtrlMessage(IDC_INSTRUMENT_SAVEAS); return wParam;
		case kcInstrumentNew:	SendCtrlMessage(IDC_INSTRUMENT_NEW); return wParam;

		// envelope editor
		case kcInstrumentEnvelopeLoad:					OnEnvLoad(); return wParam;
		case kcInstrumentEnvelopeSave:					OnEnvSave(); return wParam;
		case kcInstrumentEnvelopeZoomIn:				OnEnvZoomIn(); return wParam;
		case kcInstrumentEnvelopeZoomOut:				OnEnvZoomOut(); return wParam;
		case kcInstrumentEnvelopeScale:					OnEnvelopeScalePoints(); return wParam;
		case kcInstrumentEnvelopeSwitchToVolume:		OnSelectVolumeEnv(); return wParam;
		case kcInstrumentEnvelopeSwitchToPanning:		OnSelectPanningEnv(); return wParam;
		case kcInstrumentEnvelopeSwitchToPitch:			OnSelectPitchEnv(); return wParam;
		case kcInstrumentEnvelopeToggleVolume:			OnEnvVolChanged(); return wParam;
		case kcInstrumentEnvelopeTogglePanning:			OnEnvPanChanged(); return wParam;
		case kcInstrumentEnvelopeTogglePitch:			OnEnvPitchChanged(); return wParam;
		case kcInstrumentEnvelopeToggleFilter:			OnEnvFilterChanged(); return wParam;
		case kcInstrumentEnvelopeToggleLoop:			OnEnvLoopChanged(); return wParam;
		case kcInstrumentEnvelopeSelectLoopStart:		EnvKbdSelectPoint(ENV_DRAGLOOPSTART); return wParam;
		case kcInstrumentEnvelopeSelectLoopEnd:			EnvKbdSelectPoint(ENV_DRAGLOOPEND); return wParam;
		case kcInstrumentEnvelopeToggleSustain:			OnEnvSustainChanged(); return wParam;
		case kcInstrumentEnvelopeSelectSustainStart:	EnvKbdSelectPoint(ENV_DRAGSUSTAINSTART); return wParam;
		case kcInstrumentEnvelopeSelectSustainEnd:		EnvKbdSelectPoint(ENV_DRAGSUSTAINEND); return wParam;
		case kcInstrumentEnvelopeToggleCarry:			OnEnvCarryChanged(); return wParam;
		case kcInstrumentEnvelopePointPrev:				EnvKbdSelectPoint(ENV_DRAGPREVIOUS); return wParam;
		case kcInstrumentEnvelopePointNext:				EnvKbdSelectPoint(ENV_DRAGNEXT); return wParam;
		case kcInstrumentEnvelopePointMoveLeft:			EnvKbdMovePointLeft(1); return wParam;
		case kcInstrumentEnvelopePointMoveRight:		EnvKbdMovePointRight(1); return wParam;
		case kcInstrumentEnvelopePointMoveLeftCoarse:	EnvKbdMovePointLeft(sndFile.m_PlayState.m_nCurrentRowsPerBeat * sndFile.m_PlayState.m_nMusicSpeed); return wParam;
		case kcInstrumentEnvelopePointMoveRightCoarse:	EnvKbdMovePointRight(sndFile.m_PlayState.m_nCurrentRowsPerBeat * sndFile.m_PlayState.m_nMusicSpeed); return wParam;
		case kcInstrumentEnvelopePointMoveUp:			EnvKbdMovePointVertical(1); return wParam;
		case kcInstrumentEnvelopePointMoveDown:			EnvKbdMovePointVertical(-1); return wParam;
		case kcInstrumentEnvelopePointMoveUp8:			EnvKbdMovePointVertical(8); return wParam;
		case kcInstrumentEnvelopePointMoveDown8:		EnvKbdMovePointVertical(-8); return wParam;
		case kcInstrumentEnvelopePointInsert:			EnvKbdInsertPoint(); return wParam;
		case kcInstrumentEnvelopePointRemove:			EnvKbdRemovePoint(); return wParam;
		case kcInstrumentEnvelopeSetLoopStart:			EnvKbdSetLoopStart(); return wParam;
		case kcInstrumentEnvelopeSetLoopEnd:			EnvKbdSetLoopEnd(); return wParam;
		case kcInstrumentEnvelopeSetSustainLoopStart:	EnvKbdSetSustainStart(); return wParam;
		case kcInstrumentEnvelopeSetSustainLoopEnd:		EnvKbdSetSustainEnd(); return wParam;
		case kcInstrumentEnvelopeToggleReleaseNode:		EnvKbdToggleReleaseNode(); return wParam;
	}
	if(wParam >= kcInstrumentStartNotes && wParam <= kcInstrumentEndNotes)
	{
		PlayNote(pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcInstrumentStartNotes), m_nInstrument));
		return wParam;
	}
	if(wParam >= kcInstrumentStartNoteStops && wParam <= kcInstrumentEndNoteStops)
	{
		ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcInstrumentStartNoteStops), m_nInstrument);
		if(ModCommand::IsNote(note))
		{
			m_baPlayingNote[note] = false;
			pModDoc->NoteOff(note, false, m_nInstrument, m_noteChannel[note - NOTE_MIN]);
		}
		return wParam;
	}

	return kcNull;
}


void CViewInstrument::OnEnvelopeScalePoints()
{
	CModDoc *pModDoc = GetDocument();
	if(pModDoc == nullptr)
		return;
	const CSoundFile &sndFile = pModDoc->GetSoundFile();

	if(m_nInstrument >= 1
	   && m_nInstrument <= sndFile.GetNumInstruments()
	   && sndFile.Instruments[m_nInstrument])
	{
		// "Center" y value of the envelope. For panning and pitch, this is 32, for volume and filter it is 0 (minimum).
		int nOffset = ((m_nEnv != ENV_VOLUME) && !GetEnvelopePtr()->dwFlags[ENV_FILTER]) ? 32 : 0;

		CScaleEnvPointsDlg dlg(this, *GetEnvelopePtr(), nOffset);
		if(dlg.DoModal() == IDOK)
		{
			PrepareUndo("Scale Envelope");
			dlg.Apply();
			SetModified(InstrumentHint().Envelope(), true);
		}
	}
}


void CViewInstrument::EnvSetZoom(float newZoom)
{
	m_zoom = Clamp(newZoom, ENV_MIN_ZOOM, ENV_MAX_ZOOM);
	InvalidateRect(NULL, FALSE);
	UpdateScrollSize();
	UpdateNcButtonState();
}


////////////////////////////////////////
//  Envelope Editor - Keyboard actions

void CViewInstrument::EnvKbdSelectPoint(DragPoints point)
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr)
		return;

	switch(point)
	{
	case ENV_DRAGLOOPSTART:
	case ENV_DRAGLOOPEND:
		if(!pEnv->dwFlags[ENV_LOOP])
			return;
		m_nDragItem = point;
		break;
	case ENV_DRAGSUSTAINSTART:
	case ENV_DRAGSUSTAINEND:
		if(!pEnv->dwFlags[ENV_SUSTAIN])
			return;
		m_nDragItem = point;
		break;
	case ENV_DRAGPREVIOUS:
		if(m_nDragItem <= 1 || m_nDragItem > pEnv->size())
			m_nDragItem = pEnv->size();
		else
			m_nDragItem--;
		break;
	case ENV_DRAGNEXT:
		if(m_nDragItem >= pEnv->size())
			m_nDragItem = 1;
		else
			m_nDragItem++;
		break;
	}
	UpdateIndicator();
	InvalidateRect(NULL, FALSE);
}


void CViewInstrument::EnvKbdMovePointLeft(int stepsize)
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr)
		return;
	const MODTYPE modType = GetDocument()->GetModType();

	// Move loop points?
	PrepareUndo("Move Envelope Point");
	if(m_nDragItem == ENV_DRAGSUSTAINSTART)
	{
		if(pEnv->nSustainStart <= 0)
			return;
		pEnv->nSustainStart--;
		if(modType == MOD_TYPE_XM)
			pEnv->nSustainEnd = pEnv->nSustainStart;
	} else if(m_nDragItem == ENV_DRAGSUSTAINEND)
	{
		if(pEnv->nSustainEnd <= 0)
			return;
		if(pEnv->nSustainEnd <= pEnv->nSustainStart)
			pEnv->nSustainStart--;
		pEnv->nSustainEnd--;
	} else if(m_nDragItem == ENV_DRAGLOOPSTART)
	{
		if(pEnv->nLoopStart <= 0)
			return;
		pEnv->nLoopStart--;
	} else if(m_nDragItem == ENV_DRAGLOOPEND)
	{
		if(pEnv->nLoopEnd <= 0)
			return;
		if(pEnv->nLoopEnd <= pEnv->nLoopStart)
			pEnv->nLoopStart--;
		pEnv->nLoopEnd--;
	} else
	{
		// Move envelope node
		if(!IsDragItemEnvPoint() || m_nDragItem <= 1)
		{
			GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
			return;
		}
		if(!EnvSetValue(m_nDragItem - 1, pEnv->at(m_nDragItem - 1).tick - stepsize))
		{
			GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
			return;
		}
	}
	UpdateIndicator();
	SetModified(InstrumentHint().Envelope(), true);
}


void CViewInstrument::EnvKbdMovePointRight(int stepsize)
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr)
		return;
	const MODTYPE modType = GetDocument()->GetModType();

	// Move loop points?
	PrepareUndo("Move Envelope Point");
	if(m_nDragItem == ENV_DRAGSUSTAINSTART)
	{
		if(pEnv->nSustainStart >= pEnv->size() - 1)
			return;
		if(pEnv->nSustainStart >= pEnv->nSustainEnd)
			pEnv->nSustainEnd++;
		pEnv->nSustainStart++;
	} else if(m_nDragItem == ENV_DRAGSUSTAINEND)
	{
		if(pEnv->nSustainEnd >= pEnv->size() - 1)
			return;
		pEnv->nSustainEnd++;
		if(modType == MOD_TYPE_XM)
			pEnv->nSustainStart = pEnv->nSustainEnd;
	} else if(m_nDragItem == ENV_DRAGLOOPSTART)
	{
		if(pEnv->nLoopStart >= pEnv->size() - 1)
			return;
		if(pEnv->nLoopStart >= pEnv->nLoopEnd)
			pEnv->nLoopEnd++;
		pEnv->nLoopStart++;
	} else if(m_nDragItem == ENV_DRAGLOOPEND)
	{
		if(pEnv->nLoopEnd >= pEnv->size() - 1)
			return;
		pEnv->nLoopEnd++;
	} else
	{
		// Move envelope node
		if(!IsDragItemEnvPoint() || m_nDragItem <= 1)
		{
			GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
			return;
		}
		if(!EnvSetValue(m_nDragItem - 1, pEnv->at(m_nDragItem - 1).tick + stepsize))
		{
			GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
			return;
		}
	}
	UpdateIndicator();
	SetModified(InstrumentHint().Envelope(), true);
}


void CViewInstrument::EnvKbdMovePointVertical(int stepsize)
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !IsDragItemEnvPoint())
		return;
	int val = pEnv->at(m_nDragItem - 1).value + stepsize;
	PrepareUndo("Move Envelope Point");
	if(EnvSetValue(m_nDragItem - 1, int32_min, val, false))
	{
		UpdateIndicator();
		SetModified(InstrumentHint().Envelope(), true);
	} else
	{
		GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
	}
}


void CViewInstrument::EnvKbdInsertPoint()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr)
		return;
	if(!IsDragItemEnvPoint())
		m_nDragItem = pEnv->size();
	EnvelopeNode::tick_t newTick = 10;
	EnvelopeNode::value_t newVal = m_nEnv == ENV_VOLUME ? ENVELOPE_MAX : ENVELOPE_MID;
	if(m_nDragItem < pEnv->size() && (pEnv->at(m_nDragItem).tick - pEnv->at(m_nDragItem - 1).tick > 1))
	{
		// If some other point than the last is selected: interpolate between this and next point (if there's room between them)
		newTick = (pEnv->at(m_nDragItem - 1).tick + pEnv->at(m_nDragItem).tick) / 2;
		newVal = (pEnv->at(m_nDragItem - 1).value + pEnv->at(m_nDragItem).value) / 2;
	} else if(!pEnv->empty())
	{
		// Last point is selected: add point after last point
		newTick = pEnv->back().tick + 4;
		newVal = pEnv->back().value;
	}

	auto newPoint = EnvInsertPoint(newTick, newVal);
	if(newPoint > 0)
		m_nDragItem = newPoint;
	UpdateIndicator();
}


void CViewInstrument::EnvKbdRemovePoint()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !IsDragItemEnvPoint() || pEnv->empty())
		return;
	if(m_nDragItem > pEnv->size())
		m_nDragItem = pEnv->size();
	EnvRemovePoint(m_nDragItem - 1);
	UpdateIndicator();
}


void CViewInstrument::EnvKbdSetLoopStart()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !IsDragItemEnvPoint())
		return;
	PrepareUndo("Set Envelope Loop Start");
	if(!EnvGetLoop())
		EnvSetLoopStart(0);
	EnvSetLoopStart(m_nDragItem - 1);
	SetModified(InstrumentHint(m_nInstrument).Envelope(), true);
}


void CViewInstrument::EnvKbdSetLoopEnd()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !IsDragItemEnvPoint())
		return;
	PrepareUndo("Set Envelope Loop End");
	if(!EnvGetLoop())
	{
		EnvSetLoop(true);
		EnvSetLoopStart(0);
	}
	EnvSetLoopEnd(m_nDragItem - 1);
	SetModified(InstrumentHint(m_nInstrument).Envelope(), true);
}


void CViewInstrument::EnvKbdSetSustainStart()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !IsDragItemEnvPoint())
		return;
	PrepareUndo("Set Envelope Sustain Start");
	if(!EnvGetSustain())
		EnvSetSustain(true);
	EnvSetSustainStart(m_nDragItem - 1);
	SetModified(InstrumentHint(m_nInstrument).Envelope(), true);
}


void CViewInstrument::EnvKbdSetSustainEnd()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !IsDragItemEnvPoint())
		return;
	PrepareUndo("Set Envelope Sustain End");
	if(!EnvGetSustain())
	{
		EnvSetSustain(true);
		EnvSetSustainStart(0);
	}
	EnvSetSustainEnd(m_nDragItem - 1);
	SetModified(InstrumentHint(m_nInstrument).Envelope(), true);
}


void CViewInstrument::EnvKbdToggleReleaseNode()
{
	InstrumentEnvelope *pEnv = GetEnvelopePtr();
	if(pEnv == nullptr || !IsDragItemEnvPoint())
		return;
	PrepareUndo("Toggle Release Node");
	if(EnvToggleReleaseNode(m_nDragItem - 1))
	{
		UpdateIndicator();
		SetModified(InstrumentHint().Envelope(), true);
	} else
	{
		GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
	}
}


// Get a pointer to the currently active instrument.
ModInstrument *CViewInstrument::GetInstrumentPtr() const
{
	CModDoc *pModDoc = GetDocument();
	if(pModDoc == nullptr)
		return nullptr;
	return pModDoc->GetSoundFile().Instruments[m_nInstrument];
}


// Get a pointer to the currently selected envelope.
// This function also implicitely validates the moddoc and soundfile pointers.
InstrumentEnvelope *CViewInstrument::GetEnvelopePtr() const
{
	// First do some standard checks...
	ModInstrument *pIns = GetInstrumentPtr();
	if(pIns == nullptr)
		return nullptr;

	return &pIns->GetEnvelope(m_nEnv);
}


bool CViewInstrument::CanMovePoint(uint32 envPoint, int step)
{
	const InstrumentEnvelope *env = GetEnvelopePtr();
	if(env == nullptr)
		return false;

	// Can't move first point
	if(envPoint == 0)
	{
		return false;
	}
	// Can't move left of previous point
	if((step < 0) && (env->at(envPoint).tick - env->at(envPoint - 1).tick <= -step))
	{
		return false;
	}
	// Can't move right of next point
	if((step > 0) && (envPoint < env->size() - 1) && (env->at(envPoint + 1).tick - env->at(envPoint).tick <= step))
	{
		return false;
	}
	return true;
}


BOOL CViewInstrument::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
	// Ctrl + mouse wheel: envelope zoom.
	if(nFlags == MK_CONTROL)
	{
		// Speed up zoom scrolling by some factor (might need some tuning).
		const float speedUpFactor = std::max(1.0f, m_zoom * 7.0f / ENV_MAX_ZOOM);
		EnvSetZoom(m_zoom + speedUpFactor * (zDelta / WHEEL_DELTA));
	}

	return CModScrollView::OnMouseWheel(nFlags, zDelta, pt);
}


void CViewInstrument::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point)
{
	if(nButton == XBUTTON1)
		OnPrevInstrument();
	else if(nButton == XBUTTON2)
		OnNextInstrument();
	CModScrollView::OnXButtonUp(nFlags, nButton, point);
}


void CViewInstrument::OnEnvLoad()
{
	if(GetInstrumentPtr() == nullptr)
		return;

	FileDialog dlg = OpenFileDialog()
		.DefaultExtension("envelope")
		.ExtensionFilter("Instrument Envelopes (*.envelope)|*.envelope||")
		.WorkingDirectory(TrackerSettings::Instance().PathInstruments.GetWorkingDir());
	if(!dlg.Show(this)) return;
	TrackerSettings::Instance().PathInstruments.SetWorkingDir(dlg.GetWorkingDirectory());

	PrepareUndo("Replace Envelope");
	if(GetDocument()->LoadEnvelope(m_nInstrument, m_nEnv, dlg.GetFirstFile()))
	{
		SetModified(InstrumentHint(m_nInstrument).Envelope(), true);
	} else
	{
		GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument);
	}
}


void CViewInstrument::OnEnvSave()
{
	const InstrumentEnvelope *env = GetEnvelopePtr();
	if(env == nullptr || env->empty())
	{
		MessageBeep(MB_ICONWARNING);
		return;
	}

	FileDialog dlg = SaveFileDialog()
		.DefaultExtension("envelope")
		.ExtensionFilter("Instrument Envelopes (*.envelope)|*.envelope||")
		.WorkingDirectory(TrackerSettings::Instance().PathInstruments.GetWorkingDir());
	if(!dlg.Show(this)) return;
	TrackerSettings::Instance().PathInstruments.SetWorkingDir(dlg.GetWorkingDirectory());

	if(!GetDocument()->SaveEnvelope(m_nInstrument, m_nEnv, dlg.GetFirstFile()))
	{
		Reporting::Error(MPT_CFORMAT("Unable to save file {}")(dlg.GetFirstFile()), _T("OpenMPT"), this);
	}
}


void CViewInstrument::OnUpdateUndo(CCmdUI *pCmdUI)
{
	CModDoc *pModDoc = GetDocument();
	if((pCmdUI) && (pModDoc))
	{
		pCmdUI->Enable(pModDoc->GetInstrumentUndo().CanUndo(m_nInstrument));
		pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditUndo, _T("Undo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetInstrumentUndo().GetUndoName(m_nInstrument))));
	}
}


void CViewInstrument::OnUpdateRedo(CCmdUI *pCmdUI)
{
	CModDoc *pModDoc = GetDocument();
	if((pCmdUI) && (pModDoc))
	{
		pCmdUI->Enable(pModDoc->GetInstrumentUndo().CanRedo(m_nInstrument));
		pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditRedo, _T("Redo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetInstrumentUndo().GetRedoName(m_nInstrument))));
	}
}


void CViewInstrument::OnEditUndo()
{
	CModDoc *pModDoc = GetDocument();
	if(pModDoc == nullptr)
		return;
	if(pModDoc->GetInstrumentUndo().Undo(m_nInstrument))
	{
		SetModified(InstrumentHint().Info().Envelope().Names(), true);
	}
}


void CViewInstrument::OnEditRedo()
{
	CModDoc *pModDoc = GetDocument();
	if(pModDoc == nullptr)
		return;
	if(pModDoc->GetInstrumentUndo().Redo(m_nInstrument))
	{
		SetModified(InstrumentHint().Info().Envelope().Names(), true);
	}
}


INT_PTR CViewInstrument::OnToolHitTest(CPoint point, TOOLINFO *pTI) const
{
	CRect ncRect;
	ClientToScreen(&point);
	const auto ncButton = GetNcButtonAtPoint(point, &ncRect);
	if(ncButton == uint32_max)
		return CModScrollView::OnToolHitTest(point, pTI);
	
	auto buttonID = cLeftBarButtons[ncButton];
	ScreenToClient(&ncRect);

	pTI->hwnd = m_hWnd;
	pTI->uId = buttonID;
	pTI->rect = ncRect;
	CString text = LoadResourceString(buttonID);

	CommandID cmd = kcNull;
	switch(buttonID)
	{
	case ID_ENVSEL_VOLUME: cmd = kcInstrumentEnvelopeSwitchToVolume; break;
	case ID_ENVSEL_PANNING: cmd = kcInstrumentEnvelopeSwitchToPanning; break;
	case ID_ENVSEL_PITCH: cmd = kcInstrumentEnvelopeSwitchToPitch; break;
	case ID_ENVELOPE_VOLUME: cmd = kcInstrumentEnvelopeToggleVolume; break;
	case ID_ENVELOPE_PANNING: cmd = kcInstrumentEnvelopeTogglePanning; break;
	case ID_ENVELOPE_PITCH: cmd = kcInstrumentEnvelopeTogglePitch; break;
	case ID_ENVELOPE_FILTER: cmd = kcInstrumentEnvelopeToggleFilter; break;
	case ID_ENVELOPE_SETLOOP: cmd = kcInstrumentEnvelopeToggleLoop; break;
	case ID_ENVELOPE_SUSTAIN: cmd = kcInstrumentEnvelopeToggleSustain; break;
	case ID_ENVELOPE_CARRY: cmd = kcInstrumentEnvelopeToggleCarry; break;
	case ID_INSTRUMENT_SAMPLEMAP: cmd = kcInsNoteMapEditSampleMap; break;
	case ID_ENVELOPE_ZOOM_IN: cmd = kcInstrumentEnvelopeZoomIn; break;
	case ID_ENVELOPE_ZOOM_OUT: cmd = kcInstrumentEnvelopeZoomOut; break;
	case ID_ENVELOPE_LOAD: cmd = kcInstrumentEnvelopeLoad; break;
	case ID_ENVELOPE_SAVE: cmd = kcInstrumentEnvelopeSave; break;
	}
	if(cmd != kcNull)
	{
		auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0);
		if(!keyText.IsEmpty())
			text += MPT_CFORMAT(" ({})")(keyText);
	}

	// MFC will free() the text
	auto size = text.GetLength() + 1;
	TCHAR *textP = static_cast<TCHAR *>(calloc(size, sizeof(TCHAR)));
	std::copy(text.GetString(), text.GetString() + size, textP);
	pTI->lpszText = textP;

	return buttonID;
}


// Accessible description for screen readers
HRESULT CViewInstrument::get_accName(VARIANT varChild, BSTR *pszName)
{
	const InstrumentEnvelope *env = GetEnvelopePtr();
	if(env == nullptr)
		return CModScrollView::get_accName(varChild, pszName);

	const TCHAR *typeStr = _T("");
	switch(m_nEnv)
	{
	case ENV_VOLUME: typeStr = _T("Volume"); break;
	case ENV_PANNING: typeStr = _T("Panning"); break;
	case ENV_PITCH: typeStr = env->dwFlags[ENV_FILTER] ? _T("Filter") : _T("Pitch"); break;
	}

	CString str;
	if(env->empty() || m_nDragItem == 0)
	{
		str = typeStr;
		if(env->empty())
			str += _T(" envelope has no points");
		else
			str += MPT_CFORMAT(" envelope, {} point{}")(env->size(), env->size() == 1 ? CString(_T("")) : CString(_T("s")));
	} else
	{
		bool isEnvPoint = false;
		auto point = DragItemToEnvPoint();
		auto tick = EnvGetTick(point);
		switch(m_nDragItem)
		{
		case ENV_DRAGLOOPSTART: str = _T("Loop start"); break;
		case ENV_DRAGLOOPEND: str = _T("Loop end"); break;
		case ENV_DRAGSUSTAINSTART: str = _T("Sustain loop start"); break;
		case ENV_DRAGSUSTAINEND: str = _T("Sustain loop end"); break;
		default: isEnvPoint = true;
		}
		if(!isEnvPoint)
		{
			str += MPT_CFORMAT(" at point {}, tick {}")(point + 1, tick);
		} else
		{
			str = MPT_CFORMAT("Point {}, tick {}, {} {}")(point + 1, tick, CString(typeStr), EnvValueToString(EnvGetTick(point), EnvGetValue(point)));
			if(env->dwFlags[ENV_LOOP])
			{
				if(point == env->nLoopStart)
					str += _T(", loop start");
				if(point == env->nLoopEnd)
					str += _T(", loop end");
			}
			if(env->dwFlags[ENV_SUSTAIN])
			{
				if(point == env->nLoopStart)
					str += _T(", sustain loop start");
				if(point == env->nLoopEnd)
					str += _T(", sustain loop end");
			}
			if(env->nReleaseNode == point)
				str += _T(", release node");
		}
	}

	*pszName = str.AllocSysString();
	return S_OK;
}

OPENMPT_NAMESPACE_END