code:
/*
 * SampleTrack.cpp - implementation of class SampleTrack, a track which
 *                   provides arrangement of samples
 *
 * Copyright (c) 2005-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
 *
 * This file is part of LMMS - https://lmms.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program (see COPYING); if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 */
#include "SampleTrack.h"

#include <QDomElement>
#include <QDropEvent>
#include <QFileInfo>
#include <QMenu>
#include <QLayout>
#include <QLineEdit>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QPainter>
#include <QPushButton>

#include "BBTrack.h"
#include "EffectRackView.h"
#include "embed.h"
#include "FxMixerView.h"
#include "gui_templates.h"
#include "GuiApplication.h"
#include "Knob.h"
#include "MainWindow.h"
#include "Mixer.h"
#include "PathUtil.h"
#include "SamplePlayHandle.h"
#include "SampleRecordHandle.h"
#include "Song.h"
#include "SongEditor.h"
#include "StringPairDrag.h"
#include "TabWidget.h"
#include "TimeLineWidget.h"
#include "ToolTip.h"
#include "TrackLabelButton.h"

SampleTCO::SampleTCO( Track * _track ) :
	TrackContentObject( _track ),
	m_sampleBuffer( new SampleBuffer ),
	m_isPlaying( false )
{
	saveJournallingState( false );
	setSampleFile( "" );
	restoreJournallingState();

	// we need to receive bpm-change-events, because then we have to
	// change length of this TCO
	connect( Engine::getSong(), SIGNAL( tempoChanged( bpm_t ) ),
					this, SLOT( updateLength() ), Qt::DirectConnection );
	connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int,int ) ),
					this, SLOT( updateLength() ) );

	//care about positionmarker
	TimeLineWidget * timeLine = Engine::getSong()->getPlayPos( Engine::getSong()->Mode_PlaySong ).m_timeLine;
	if( timeLine )
	{
		connect( timeLine, SIGNAL( positionMarkerMoved() ), this, SLOT( playbackPositionChanged() ) );
	}
	//playbutton clicked or space key / on Export Song set isPlaying to false
	connect( Engine::getSong(), SIGNAL( playbackStateChanged() ),
			this, SLOT( playbackPositionChanged() ), Qt::DirectConnection );
	//care about loops
	connect( Engine::getSong(), SIGNAL( updateSampleTracks() ),
			this, SLOT( playbackPositionChanged() ), Qt::DirectConnection );
	//care about mute TCOs
	connect( this, SIGNAL( dataChanged() ), this, SLOT( playbackPositionChanged() ) );
	//care about mute track
	connect( getTrack()->getMutedModel(), SIGNAL( dataChanged() ),
			this, SLOT( playbackPositionChanged() ), Qt::DirectConnection );
	//care about TCO position
	connect( this, SIGNAL( positionChanged() ), this, SLOT( updateTrackTcos() ) );

	switch( getTrack()->trackContainer()->type() )
	{
		case TrackContainer::BBContainer:
			setAutoResize( true );
			break;

		case TrackContainer::SongContainer:
			// move down
		default:
			setAutoResize( false );
			break;
	}
	updateTrackTcos();
}

SampleTCO::SampleTCO(const SampleTCO& orig) :
	SampleTCO(orig.getTrack())
{
	// TODO: This creates a new SampleBuffer for the new TCO, eating up memory
	// & eventually causing performance issues. Letting tracks share buffers
	// when they're identical would fix this, but isn't possible right now.
	*m_sampleBuffer = *orig.m_sampleBuffer;
	m_isPlaying = orig.m_isPlaying;
}

SampleTCO::~SampleTCO()
{
	SampleTrack * sampletrack = dynamic_cast<SampleTrack*>( getTrack() );
	if ( sampletrack )
	{
		sampletrack->updateTcos();
	}
	Engine::mixer()->requestChangeInModel();
	sharedObject::unref( m_sampleBuffer );
	Engine::mixer()->doneChangeInModel();
}

void SampleTCO::changeLength( const TimePos & _length )
{
	TrackContentObject::changeLength( qMax( static_cast<int>( _length ), 1 ) );
}

const QString & SampleTCO::sampleFile() const
{
	return m_sampleBuffer->audioFile();
}

void SampleTCO::setSampleBuffer( SampleBuffer* sb )
{
	Engine::mixer()->requestChangeInModel();
	sharedObject::unref( m_sampleBuffer );
	Engine::mixer()->doneChangeInModel();
	m_sampleBuffer = sb;
	updateLength();

	emit sampleChanged();
}

void SampleTCO::setSampleFile( const QString & _sf )
{
	int length;
	if ( _sf.isEmpty() )
	{	//When creating an empty sample pattern make it a bar long
		float nom = Engine::getSong()->getTimeSigModel().getNumerator();
		float den = Engine::getSong()->getTimeSigModel().getDenominator();
		length = DefaultTicksPerBar * ( nom / den );
	}
	else
	{	//Otherwise set it to the sample's length
		m_sampleBuffer->setAudioFile( _sf );
		length = sampleLength();
	}
	changeLength(length);

	setStartTimeOffset( 0 );

	emit sampleChanged();
	emit playbackPositionChanged();
}

void SampleTCO::toggleRecord()
{
	m_recordModel.setValue( !m_recordModel.value() );
	emit dataChanged();
}

void SampleTCO::playbackPositionChanged()
{
	Engine::mixer()->removePlayHandlesOfTypes( getTrack(), PlayHandle::TypeSamplePlayHandle );
	SampleTrack * st = dynamic_cast<SampleTrack*>( getTrack() );
	st->setPlayingTcos( false );
}

void SampleTCO::updateTrackTcos()
{
	SampleTrack * sampletrack = dynamic_cast<SampleTrack*>( getTrack() );
	if( sampletrack)
	{
		sampletrack->updateTcos();
	}
}

bool SampleTCO::isPlaying() const
{
	return m_isPlaying;
}

void SampleTCO::setIsPlaying(bool isPlaying)
{
	m_isPlaying = isPlaying;
}

void SampleTCO::updateLength()
{
	emit sampleChanged();
}

TimePos SampleTCO::sampleLength() const
{
	return (int)( m_sampleBuffer->frames() / Engine::framesPerTick() );
}

void SampleTCO::setSampleStartFrame(f_cnt_t startFrame)
{
	m_sampleBuffer->setStartFrame( startFrame );
}

void SampleTCO::setSamplePlayLength(f_cnt_t length)
{
	m_sampleBuffer->setEndFrame( length );
}

void SampleTCO::saveSettings( QDomDocument & _doc, QDomElement & _this )
{
	if( _this.parentNode().nodeName() == "clipboard" )
	{
		_this.setAttribute( "pos", -1 );
	}
	else
	{
		_this.setAttribute( "pos", startPosition() );
	}
	_this.setAttribute( "len", length() );
	_this.setAttribute( "muted", isMuted() );
	_this.setAttribute( "src", sampleFile() );
	_this.setAttribute( "off", startTimeOffset() );
	if( sampleFile() == "" )
	{
		QString s;
		_this.setAttribute( "data", m_sampleBuffer->toBase64( s ) );
	}

	_this.setAttribute( "sample_rate", m_sampleBuffer->sampleRate());
	if( usesCustomClipColor() )
	{
		_this.setAttribute( "color", color().name() );
	}
	if (m_sampleBuffer->reversed())
	{
		_this.setAttribute("reversed", "true");
	}
	// TODO: start- and end-frame
}

void SampleTCO::loadSettings( const QDomElement & _this )
{
	if( _this.attribute( "pos" ).toInt() >= 0 )
	{
		movePosition( _this.attribute( "pos" ).toInt() );
	}
	setSampleFile( _this.attribute( "src" ) );
	if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) )
	{
		m_sampleBuffer->loadFromBase64( _this.attribute( "data" ) );
	}
	changeLength( _this.attribute( "len" ).toInt() );
	setMuted( _this.attribute( "muted" ).toInt() );
	setStartTimeOffset( _this.attribute( "off" ).toInt() );

	if ( _this.hasAttribute( "sample_rate" ) ) {
		m_sampleBuffer->setSampleRate( _this.attribute( "sample_rate" ).toInt() );
	}

	if( _this.hasAttribute( "color" ) )
	{
		useCustomClipColor( true );
		setColor( _this.attribute( "color" ) );
	}

	if(_this.hasAttribute("reversed"))
	{
		m_sampleBuffer->setReversed(true);
		emit wasReversed(); // tell SampleTCOView to update the view
	}
}

TrackContentObjectView * SampleTCO::createView( TrackView * _tv )
{
	return new SampleTCOView( this, _tv );
}

SampleTCOView::SampleTCOView( SampleTCO * _tco, TrackView * _tv ) :
	TrackContentObjectView( _tco, _tv ),
	m_tco( _tco ),
	m_paintPixmap()
{
	// update UI and tooltip
	updateSample();

	// track future changes of SampleTCO
	connect(m_tco, SIGNAL(sampleChanged()), this, SLOT(updateSample()));

	connect(m_tco, SIGNAL(wasReversed()), this, SLOT(update()));

	setStyle( QApplication::style() );
}

void SampleTCOView::updateSample()
{
	update();
	// set tooltip to filename so that user can see what sample this
	// sample-tco contains
	ToolTip::add( this, ( m_tco->m_sampleBuffer->audioFile() != "" ) ?
					PathUtil::toAbsolute(m_tco->m_sampleBuffer->audioFile()) :
					tr( "Double-click to open sample" ) );
}

void SampleTCOView::contextMenuEvent( QContextMenuEvent * _cme )
{
	// Depending on whether we right-clicked a selection or an individual TCO we will have
	// different labels for the actions.
	bool individualTCO = getClickedTCOs().size() <= 1;

	if( _cme->modifiers() )
	{
		return;
	}

	QMenu contextMenu( this );

	if( fixedTCOs() == false )
	{
		contextMenu.addAction(
			embed::getIconPixmap( "cancel" ),
			individualTCO
				? tr("Delete (middle mousebutton)")
				: tr("Delete selection (middle mousebutton)"),
			[this](){ contextMenuAction( Remove ); } );

		contextMenu.addSeparator();

		contextMenu.addAction(
			embed::getIconPixmap( "edit_cut" ),
			individualTCO
				? tr("Cut")
				: tr("Cut selection"),
			[this](){ contextMenuAction( Cut ); } );
	}

	contextMenu.addAction(
		embed::getIconPixmap( "edit_copy" ),
		individualTCO
			? tr("Copy")
			: tr("Copy selection"),
		[this](){ contextMenuAction( Copy ); } );

	contextMenu.addAction(
		embed::getIconPixmap( "edit_paste" ),
		tr( "Paste" ),
		[this](){ contextMenuAction( Paste ); } );

	contextMenu.addSeparator();

	contextMenu.addAction(
		embed::getIconPixmap( "muted" ),
		(individualTCO
			? tr("Mute/unmute (<%1> + middle click)")
			: tr("Mute/unmute selection (<%1> + middle click)")).arg(UI_CTRL_KEY),
		[this](){ contextMenuAction( Mute ); } );

	/*contextMenu.addAction( embed::getIconPixmap( "record" ),
				tr( "Set/clear record" ),
						m_tco, SLOT( toggleRecord() ) );*/

	contextMenu.addAction(
		embed::getIconPixmap("flip_x"),
		tr("Reverse sample"),
		this,
		SLOT(reverseSample())
	);

	contextMenu.addSeparator();

	contextMenu.addAction( embed::getIconPixmap( "colorize" ),
			tr( "Set clip color" ), this, SLOT( changeClipColor() ) );
	contextMenu.addAction( embed::getIconPixmap( "colorize" ),
			tr( "Use track color" ), this, SLOT( useTrackColor() ) );

	constructContextMenu( &contextMenu );

	contextMenu.exec( QCursor::pos() );
}

void SampleTCOView::dragEnterEvent( QDragEnterEvent * _dee )
{
	if( StringPairDrag::processDragEnterEvent( _dee,
					"samplefile,sampledata" ) == false )
	{
		TrackContentObjectView::dragEnterEvent( _dee );
	}
}

void SampleTCOView::dropEvent( QDropEvent * _de )
{
	if( StringPairDrag::decodeKey( _de ) == "samplefile" )
	{
		m_tco->setSampleFile( StringPairDrag::decodeValue( _de ) );
		_de->accept();
	}
	else if( StringPairDrag::decodeKey( _de ) == "sampledata" )
	{
		m_tco->m_sampleBuffer->loadFromBase64(
					StringPairDrag::decodeValue( _de ) );
		m_tco->updateLength();
		update();
		_de->accept();
		Engine::getSong()->setModified();
	}
	else
	{
		TrackContentObjectView::dropEvent( _de );
	}
}

void SampleTCOView::mousePressEvent( QMouseEvent * _me )
{
	if( _me->button() == Qt::LeftButton &&
		_me->modifiers() & Qt::ControlModifier &&
		_me->modifiers() & Qt::ShiftModifier )
	{
		m_tco->toggleRecord();
	}
	else
	{
		if( _me->button() == Qt::MiddleButton && _me->modifiers() == Qt::ControlModifier )
		{
			SampleTCO * sTco = dynamic_cast<SampleTCO*>( getTrackContentObject() );
			if( sTco )
			{
				sTco->updateTrackTcos();
			}
		}
		TrackContentObjectView::mousePressEvent( _me );
	}
}

void SampleTCOView::mouseReleaseEvent(QMouseEvent *_me)
{
	if( _me->button() == Qt::MiddleButton && !_me->modifiers() )
	{
		SampleTCO * sTco = dynamic_cast<SampleTCO*>( getTrackContentObject() );
		if( sTco )
		{
			sTco->playbackPositionChanged();
		}
	}
	TrackContentObjectView::mouseReleaseEvent( _me );
}

void SampleTCOView::mouseDoubleClickEvent( QMouseEvent * )
{
	QString af = m_tco->m_sampleBuffer->openAudioFile();

	if ( af.isEmpty() ) {} //Don't do anything if no file is loaded
	else if ( af == m_tco->m_sampleBuffer->audioFile() )
	{	//Instead of reloading the existing file, just reset the size
		int length = (int) ( m_tco->m_sampleBuffer->frames() / Engine::framesPerTick() );
		m_tco->changeLength(length);
	}
	else
	{	//Otherwise load the new file as ususal
		m_tco->setSampleFile( af );
		Engine::getSong()->setModified();
	}
}

void SampleTCOView::paintEvent( QPaintEvent * pe )
{
	QPainter painter( this );

	if( !needsUpdate() )
	{
		painter.drawPixmap( 0, 0, m_paintPixmap );
		return;
	}

	setNeedsUpdate( false );

	if (m_paintPixmap.isNull() || m_paintPixmap.size() != size())
	{
		m_paintPixmap = QPixmap(size());
	}

	QPainter p( &m_paintPixmap );

	QLinearGradient lingrad( 0, 0, 0, height() );
	QColor c = getColorForDisplay( painter.background().color() );
	bool muted = m_tco->getTrack()->isMuted() || m_tco->isMuted();

	lingrad.setColorAt( 1, c.darker( 300 ) );
	lingrad.setColorAt( 0, c );

	// paint a black rectangle under the pattern to prevent glitches with transparent backgrounds
	p.fillRect( rect(), QColor( 0, 0, 0 ) );

	if( gradient() )
	{
		p.fillRect( rect(), lingrad );
	}
	else
	{
		p.fillRect( rect(), c );
	}

	p.setPen( !muted ? painter.pen().brush().color() : mutedColor() );

	const int spacing = TCO_BORDER_WIDTH + 1;
	const float ppb = fixedTCOs() ?
			( parentWidget()->width() - 2 * TCO_BORDER_WIDTH )
					/ (float) m_tco->length().getBar() :
								pixelsPerBar();

	float nom = Engine::getSong()->getTimeSigModel().getNumerator();
	float den = Engine::getSong()->getTimeSigModel().getDenominator();
	float ticksPerBar = DefaultTicksPerBar * nom / den;

	float offset =  m_tco->startTimeOffset() / ticksPerBar * pixelsPerBar();
	QRect r = QRect( TCO_BORDER_WIDTH + offset, spacing,
			qMax( static_cast<int>( m_tco->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing );
	m_tco->m_sampleBuffer->visualize( p, r, pe->rect() );

	QString name = PathUtil::cleanName(m_tco->m_sampleBuffer->audioFile());
	paintTextLabel(name, p);

	// disable antialiasing for borders, since its not needed
	p.setRenderHint( QPainter::Antialiasing, false );

	// inner border
	p.setPen( c.lighter( 135 ) );
	p.drawRect( 1, 1, rect().right() - TCO_BORDER_WIDTH,
		rect().bottom() - TCO_BORDER_WIDTH );

	// outer border
	p.setPen( c.darker( 200 ) );
	p.drawRect( 0, 0, rect().right(), rect().bottom() );

	// draw the 'muted' pixmap only if the pattern was manualy muted
	if( m_tco->isMuted() )
	{
		const int spacing = TCO_BORDER_WIDTH;
		const int size = 14;
		p.drawPixmap( spacing, height() - ( size + spacing ),
			embed::getIconPixmap( "muted", size, size ) );
	}

	// recording sample tracks is not possible at the moment

	/* if( m_tco->isRecord() )
	{
		p.setFont( pointSize<7>( p.font() ) );

		p.setPen( textShadowColor() );
		p.drawText( 10, p.fontMetrics().height()+1, "Rec" );
		p.setPen( textColor() );
		p.drawText( 9, p.fontMetrics().height(), "Rec" );

		p.setBrush( QBrush( textColor() ) );
		p.drawEllipse( 4, 5, 4, 4 );
	}*/

	p.end();

	painter.drawPixmap( 0, 0, m_paintPixmap );
}

void SampleTCOView::reverseSample()
{
	m_tco->sampleBuffer()->setReversed(!m_tco->sampleBuffer()->reversed());
	Engine::getSong()->setModified();
	update();
}

SampleTrack::SampleTrack(TrackContainer* tc) :
	Track(Track::SampleTrack, tc),
	m_volumeModel(DefaultVolume, MinVolume, MaxVolume, 0.1f, this, tr("Volume")),
	m_panningModel(DefaultPanning, PanningLeft, PanningRight, 0.1f, this, tr("Panning")),
	m_effectChannelModel(0, 0, 0, this, tr("FX channel")),
	m_audioPort(tr("Sample track"), true, &m_volumeModel, &m_panningModel, &m_mutedModel),
	m_isPlaying(false)
{
	setName(tr("Sample track"));
	m_panningModel.setCenterValue(DefaultPanning);
	m_effectChannelModel.setRange(0, Engine::fxMixer()->numChannels()-1, 1);

	connect(&m_effectChannelModel, SIGNAL(dataChanged()), this, SLOT(updateEffectChannel()));
}

SampleTrack::~SampleTrack()
{
	Engine::mixer()->removePlayHandlesOfTypes( this, PlayHandle::TypeSamplePlayHandle );
}

bool SampleTrack::play( const TimePos & _start, const fpp_t _frames,
					const f_cnt_t _offset, int _tco_num )
{
	m_audioPort.effects()->startRunning();
	bool played_a_note = false;	// will be return variable

	tcoVector tcos;
	::BBTrack * bb_track = NULL;
	if( _tco_num >= 0 )
	{
		if (_start > getTCO(_tco_num)->length())
		{
			setPlaying(false);
		}
		if( _start != 0 )
		{
			return false;
		}
		tcos.push_back( getTCO( _tco_num ) );
		if (trackContainer() == (TrackContainer*)Engine::getBBTrackContainer())
		{
			bb_track = BBTrack::findBBTrack( _tco_num );
			setPlaying(true);
		}
	}
	else
	{
		bool nowPlaying = false;
		for( int i = 0; i < numOfTCOs(); ++i )
		{
			TrackContentObject * tco = getTCO( i );
			SampleTCO * sTco = dynamic_cast<SampleTCO*>( tco );

			if( _start >= sTco->startPosition() && _start < sTco->endPosition() )
			{
				if( sTco->isPlaying() == false && _start >= (sTco->startPosition() + sTco->startTimeOffset()) )
				{
					auto bufferFramesPerTick = Engine::framesPerTick (sTco->sampleBuffer ()->sampleRate ());
					f_cnt_t sampleStart = bufferFramesPerTick * ( _start - sTco->startPosition() - sTco->startTimeOffset() );
					f_cnt_t tcoFrameLength = bufferFramesPerTick * ( sTco->endPosition() - sTco->startPosition() - sTco->startTimeOffset() );
					f_cnt_t sampleBufferLength = sTco->sampleBuffer()->frames();
					//if the Tco smaller than the sample length we play only until Tco end
					//else we play the sample to the end but nothing more
					f_cnt_t samplePlayLength = tcoFrameLength > sampleBufferLength ? sampleBufferLength : tcoFrameLength;
					//we only play within the sampleBuffer limits
					if( sampleStart < sampleBufferLength )
					{
						sTco->setSampleStartFrame( sampleStart );
						sTco->setSamplePlayLength( samplePlayLength );
						tcos.push_back( sTco );
						sTco->setIsPlaying( true );
						nowPlaying = true;
					}
				}
			}
			else
			{
				sTco->setIsPlaying( false );
			}
			nowPlaying = nowPlaying || sTco->isPlaying();
		}
		setPlaying(nowPlaying);
	}

	for( tcoVector::Iterator it = tcos.begin(); it != tcos.end(); ++it )
	{
		SampleTCO * st = dynamic_cast<SampleTCO *>( *it );
		if( !st->isMuted() )
		{
			PlayHandle* handle;
			if( st->isRecord() )
			{
				if( !Engine::getSong()->isRecording() )
				{
					return played_a_note;
				}
				SampleRecordHandle* smpHandle = new SampleRecordHandle( st );
				handle = smpHandle;
			}
			else
			{
				SamplePlayHandle* smpHandle = new SamplePlayHandle( st );
				smpHandle->setVolumeModel( &m_volumeModel );
				smpHandle->setBBTrack( bb_track );
				handle = smpHandle;
			}
			handle->setOffset( _offset );
			// send it to the mixer
			Engine::mixer()->addPlayHandle( handle );
			played_a_note = true;
		}
	}

	return played_a_note;
}

TrackView * SampleTrack::createView( TrackContainerView* tcv )
{
	return new SampleTrackView( this, tcv );
}

TrackContentObject * SampleTrack::createTCO(const TimePos & pos)
{
	SampleTCO * sTco = new SampleTCO(this);
	sTco->movePosition(pos);
	return sTco;
}

void SampleTrack::saveTrackSpecificSettings( QDomDocument & _doc,
							QDomElement & _this )
{
	m_audioPort.effects()->saveState( _doc, _this );
#if 0
	_this.setAttribute( "icon", tlb->pixmapFile() );
#endif
	m_volumeModel.saveSettings( _doc, _this, "vol" );
	m_panningModel.saveSettings( _doc, _this, "pan" );
	m_effectChannelModel.saveSettings( _doc, _this, "fxch" );
}

void SampleTrack::loadTrackSpecificSettings( const QDomElement & _this )
{
	QDomNode node = _this.firstChild();
	m_audioPort.effects()->clear();
	while( !node.isNull() )
	{
		if( node.isElement() )
		{
			if( m_audioPort.effects()->nodeName() == node.nodeName() )
			{
				m_audioPort.effects()->restoreState( node.toElement() );
			}
		}
		node = node.nextSibling();
	}
	m_volumeModel.loadSettings( _this, "vol" );
	m_panningModel.loadSettings( _this, "pan" );
	m_effectChannelModel.setRange( 0, Engine::fxMixer()->numChannels() - 1 );
	m_effectChannelModel.loadSettings( _this, "fxch" );
}

void SampleTrack::updateTcos()
{
	Engine::mixer()->removePlayHandlesOfTypes( this, PlayHandle::TypeSamplePlayHandle );
	setPlayingTcos( false );
}

void SampleTrack::setPlayingTcos( bool isPlaying )
{
	for( int i = 0; i < numOfTCOs(); ++i )
	{
		TrackContentObject * tco = getTCO( i );
		SampleTCO * sTco = dynamic_cast<SampleTCO*>( tco );
		sTco->setIsPlaying( isPlaying );
	}
}

void SampleTrack::updateEffectChannel()
{
	m_audioPort.setNextFxChannel( m_effectChannelModel.value() );
}

SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) :
	TrackView( _t, tcv )
{
	setFixedHeight( 32 );

	m_tlb = new TrackLabelButton(this, getTrackSettingsWidget());
	m_tlb->setCheckable(true);
	connect(m_tlb, SIGNAL(clicked( bool )),
			this, SLOT(showEffects()));
	m_tlb->setIcon(embed::getIconPixmap("sample_track"));
	m_tlb->move(3, 1);
	m_tlb->show();

	m_volumeKnob = new Knob( knobSmall_17, getTrackSettingsWidget(),
						    tr( "Track volume" ) );
	m_volumeKnob->setVolumeKnob( true );
	m_volumeKnob->setModel( &_t->m_volumeModel );
	m_volumeKnob->setHintText( tr( "Channel volume:" ), "%" );

	int settingsWidgetWidth = ConfigManager::inst()->
					value( "ui", "compacttrackbuttons" ).toInt()
				? DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT
				: DEFAULT_SETTINGS_WIDGET_WIDTH;
	m_volumeKnob->move( settingsWidgetWidth - 2 * 24, 2 );
	m_volumeKnob->setLabel( tr( "VOL" ) );
	m_volumeKnob->show();

	m_panningKnob = new Knob( knobSmall_17, getTrackSettingsWidget(),
							tr( "Panning" ) );
	m_panningKnob->setModel( &_t->m_panningModel );
	m_panningKnob->setHintText( tr( "Panning:" ), "%" );
	m_panningKnob->move( settingsWidgetWidth - 24, 2 );
	m_panningKnob->setLabel( tr( "PAN" ) );
	m_panningKnob->show();

	m_activityIndicator = new FadeButton(
		QApplication::palette().color(QPalette::Active, QPalette::Background),
		QApplication::palette().color(QPalette::Active, QPalette::BrightText),
		QApplication::palette().color(QPalette::Active, QPalette::BrightText).darker(),
		getTrackSettingsWidget()
	);
	m_activityIndicator->setGeometry(settingsWidgetWidth - 2 * 24 - 11, 2, 8, 28);
	m_activityIndicator->show();
	connect(_t, SIGNAL(playingChanged()), this, SLOT(updateIndicator()));

	setModel( _t );

	m_window = new SampleTrackWindow(this);
	m_window->toggleVisibility(false);
}

void SampleTrackView::updateIndicator()
{
	if (model()->isPlaying()) { m_activityIndicator->activateOnce(); }
	else { m_activityIndicator->noteEnd(); }
}

SampleTrackView::~SampleTrackView()
{
	if(m_window != NULL)
	{
		m_window->setSampleTrackView(NULL);
		m_window->parentWidget()->hide();
	}
	m_window = NULL;
}

//FIXME: This is identical to InstrumentTrackView::createFxMenu
QMenu * SampleTrackView::createFxMenu(QString title, QString newFxLabel)
{
	int channelIndex = model()->effectChannelModel()->value();

	FxChannel *fxChannel = Engine::fxMixer()->effectChannel(channelIndex);

	// If title allows interpolation, pass channel index and name
	if (title.contains("%2"))
	{
		title = title.arg(channelIndex).arg(fxChannel->m_name);
	}

	QMenu *fxMenu = new QMenu(title);

	fxMenu->addAction(newFxLabel, this, SLOT(createFxLine()));
	fxMenu->addSeparator();

	for (int i = 0; i < Engine::fxMixer()->numChannels(); ++i)
	{
		FxChannel * currentChannel = Engine::fxMixer()->effectChannel(i);

		if (currentChannel != fxChannel)
		{
			const auto index = currentChannel->m_channelIndex;
			QString label = tr("FX %1: %2").arg(currentChannel->m_channelIndex).arg(currentChannel->m_name);
			fxMenu->addAction(label, [this, index](){
				assignFxLine(index);
			});
		}
	}

	return fxMenu;
}

void SampleTrackView::showEffects()
{
	m_window->toggleVisibility(m_window->parentWidget()->isHidden());
}

void SampleTrackView::modelChanged()
{
	SampleTrack * st = castModel<SampleTrack>();
	m_volumeKnob->setModel(&st->m_volumeModel);

	TrackView::modelChanged();
}

void SampleTrackView::dragEnterEvent(QDragEnterEvent *dee)
{
	StringPairDrag::processDragEnterEvent(dee, QString("samplefile"));
}

void SampleTrackView::dropEvent(QDropEvent *de)
{
	QString type  = StringPairDrag::decodeKey(de);
	QString value = StringPairDrag::decodeValue(de);

	if (type == "samplefile")
	{
		int trackHeadWidth = ConfigManager::inst()->value("ui", "compacttrackbuttons").toInt()==1
				? DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT + TRACK_OP_WIDTH_COMPACT
				: DEFAULT_SETTINGS_WIDGET_WIDTH + TRACK_OP_WIDTH;

		int xPos = de->pos().x() < trackHeadWidth
				? trackHeadWidth
				: de->pos().x();

		TimePos tcoPos = trackContainerView()->fixedTCOs()
				? TimePos(0)
				: TimePos(((xPos - trackHeadWidth) / trackContainerView()->pixelsPerBar()
							* TimePos::ticksPerBar()) + trackContainerView()->currentPosition()
						).quantize(1.0);

		SampleTCO * sTco = static_cast<SampleTCO*>(getTrack()->createTCO(tcoPos));
		if (sTco) { sTco->setSampleFile(value); }
	}

}

SampleTrackWindow::SampleTrackWindow(SampleTrackView * tv) :
	QWidget(),
	ModelView(NULL, this),
	m_track(tv->model()),
	m_stv(tv)
{
	// init own layout + widgets
	setFocusPolicy(Qt::StrongFocus);
	QVBoxLayout * vlayout = new QVBoxLayout(this);
	vlayout->setMargin(0);
	vlayout->setSpacing(0);

	TabWidget* generalSettingsWidget = new TabWidget(tr("GENERAL SETTINGS"), this);

	QVBoxLayout* generalSettingsLayout = new QVBoxLayout(generalSettingsWidget);

	generalSettingsLayout->setContentsMargins(8, 18, 8, 8);
	generalSettingsLayout->setSpacing(6);

	QWidget* nameWidget = new QWidget(generalSettingsWidget);
	QHBoxLayout* nameLayout = new QHBoxLayout(nameWidget);
	nameLayout->setContentsMargins(0, 0, 0, 0);
	nameLayout->setSpacing(2);

	// setup line edit for changing sample track name
	m_nameLineEdit = new QLineEdit;
	m_nameLineEdit->setFont(pointSize<9>(m_nameLineEdit->font()));
	connect(m_nameLineEdit, SIGNAL(textChanged(const QString &)),
				this, SLOT(textChanged(const QString &)));

	m_nameLineEdit->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred));
	nameLayout->addWidget(m_nameLineEdit);

	generalSettingsLayout->addWidget(nameWidget);

	QGridLayout* basicControlsLayout = new QGridLayout;
	basicControlsLayout->setHorizontalSpacing(3);
	basicControlsLayout->setVerticalSpacing(0);
	basicControlsLayout->setContentsMargins(0, 0, 0, 0);

	QString labelStyleSheet = "font-size: 6pt;";
	Qt::Alignment labelAlignment = Qt::AlignHCenter | Qt::AlignTop;
	Qt::Alignment widgetAlignment = Qt::AlignHCenter | Qt::AlignCenter;

	// set up volume knob
	m_volumeKnob = new Knob(knobBright_26, NULL, tr("Sample volume"));
	m_volumeKnob->setVolumeKnob(true);
	m_volumeKnob->setHintText(tr("Volume:"), "%");

	basicControlsLayout->addWidget(m_volumeKnob, 0, 0);
	basicControlsLayout->setAlignment(m_volumeKnob, widgetAlignment);

	QLabel *label = new QLabel(tr("VOL"), this);
	label->setStyleSheet(labelStyleSheet);
	basicControlsLayout->addWidget(label, 1, 0);
	basicControlsLayout->setAlignment(label, labelAlignment);

	// set up panning knob
	m_panningKnob = new Knob(knobBright_26, NULL, tr("Panning"));
	m_panningKnob->setHintText(tr("Panning:"), "");

	basicControlsLayout->addWidget(m_panningKnob, 0, 1);
	basicControlsLayout->setAlignment(m_panningKnob, widgetAlignment);

	label = new QLabel(tr("PAN"),this);
	label->setStyleSheet(labelStyleSheet);
	basicControlsLayout->addWidget(label, 1, 1);
	basicControlsLayout->setAlignment(label, labelAlignment);

	basicControlsLayout->setColumnStretch(2, 1);

	// setup spinbox for selecting FX-channel
	m_effectChannelNumber = new FxLineLcdSpinBox(2, NULL, tr("FX channel"), m_stv);

	basicControlsLayout->addWidget(m_effectChannelNumber, 0, 3);
	basicControlsLayout->setAlignment(m_effectChannelNumber, widgetAlignment);

	label = new QLabel(tr("FX"), this);
	label->setStyleSheet(labelStyleSheet);
	basicControlsLayout->addWidget(label, 1, 3);
	basicControlsLayout->setAlignment(label, labelAlignment);

	generalSettingsLayout->addLayout(basicControlsLayout);

	m_effectRack = new EffectRackView(tv->model()->audioPort()->effects());
	m_effectRack->setFixedSize(EffectRackView::DEFAULT_WIDTH, 242);

	vlayout->addWidget(generalSettingsWidget);
	vlayout->addWidget(m_effectRack);

	setModel(tv->model());

	QMdiSubWindow * subWin = gui->mainWindow()->addWindowedWidget(this);
	Qt::WindowFlags flags = subWin->windowFlags();
	flags |= Qt::MSWindowsFixedSizeDialogHint;
	flags &= ~Qt::WindowMaximizeButtonHint;
	subWin->setWindowFlags(flags);

	// Hide the Size and Maximize options from the system menu
	// since the dialog size is fixed.
	QMenu * systemMenu = subWin->systemMenu();
	systemMenu->actions().at(2)->setVisible(false); // Size
	systemMenu->actions().at(4)->setVisible(false); // Maximize

	subWin->setWindowIcon(embed::getIconPixmap("sample_track"));
	subWin->setFixedSize(subWin->size());
	subWin->hide();
}

SampleTrackWindow::~SampleTrackWindow()
{
}

void SampleTrackWindow::setSampleTrackView(SampleTrackView* tv)
{
	if(m_stv && tv)
	{
		m_stv->m_tlb->setChecked(false);
	}

	m_stv = tv;
}

void SampleTrackWindow::modelChanged()
{
	m_track = castModel<SampleTrack>();

	m_nameLineEdit->setText(m_track->name());

	m_track->disconnect(SIGNAL(nameChanged()), this);

	connect(m_track, SIGNAL(nameChanged()),
			this, SLOT(updateName()));

	m_volumeKnob->setModel(&m_track->m_volumeModel);
	m_panningKnob->setModel(&m_track->m_panningModel);
	m_effectChannelNumber->setModel(&m_track->m_effectChannelModel);

	updateName();
}

/*! \brief Create and assign a new FX Channel for this track */
void SampleTrackView::createFxLine()
{
	int channelIndex = gui->fxMixerView()->addNewChannel();
	auto channel = Engine::fxMixer()->effectChannel(channelIndex);

	channel->m_name = getTrack()->name();
	if (getTrack()->useColor()) { channel->setColor (getTrack()->color()); }

	assignFxLine(channelIndex);
}

/*! \brief Assign a specific FX Channel for this track */
void SampleTrackView::assignFxLine(int channelIndex)
{
	model()->effectChannelModel()->setValue(channelIndex);

	gui->fxMixerView()->setCurrentFxLine(channelIndex);
}

void SampleTrackWindow::updateName()
{
	setWindowTitle(m_track->name().length() > 25 ? (m_track->name().left(24) + "...") : m_track->name());

	if(m_nameLineEdit->text() != m_track->name())
	{
		m_nameLineEdit->setText(m_track->name());
	}
}

void SampleTrackWindow::textChanged(const QString& new_name)
{
	m_track->setName(new_name);
	Engine::getSong()->setModified();
}

void SampleTrackWindow::toggleVisibility(bool on)
{
	if(on)
	{
		show();
		parentWidget()->show();
		parentWidget()->raise();
	}
	else
	{
		parentWidget()->hide();
	}
}

void SampleTrackWindow::closeEvent(QCloseEvent* ce)
{
	ce->ignore();

	if(gui->mainWindow()->workspace())
	{
		parentWidget()->hide();
	}
	else
	{
		hide();
	}

	m_stv->m_tlb->setFocus();
	m_stv->m_tlb->setChecked(false);
}

void SampleTrackWindow::saveSettings(QDomDocument& doc, QDomElement & element)
{
	MainWindow::saveWidgetState(this, element);
	Q_UNUSED(element)
}

void SampleTrackWindow::loadSettings(const QDomElement& element)
{
	MainWindow::restoreWidgetState(this, element);
	if(isVisible())
	{
		m_stv->m_tlb->setChecked(true);
	}
}


code:
INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}")
INCLUDE_DIRECTORIES("${CMAKE_CURRENT_BINARY_DIR}")
INCLUDE_DIRECTORIES("${CMAKE_SOURCE_DIR}/include")
INCLUDE_DIRECTORIES("${CMAKE_BINARY_DIR}")
INCLUDE_DIRECTORIES("${CMAKE_BINARY_DIR}/src")

SET(CMAKE_CXX_STANDARD 14)

SET(CMAKE_AUTOMOC ON)

# FIXME: remove this once we export include directories for LMMS
IF(LMMS_BUILD_APPLE)
INCLUDE_DIRECTORIES("/usr/local/include")
ENDIF()

ADD_EXECUTABLE(tests
	EXCLUDE_FROM_ALL
	main.cpp
	QTestSuite
	$<TARGET_OBJECTS:lmmsobjs>

	src/core/AutomatableModelTest.cpp
	src/core/ProjectVersionTest.cpp
	src/core/RelativePathsTest.cpp

	src/tracks/AutomationTrackTest.cpp
)
TARGET_COMPILE_DEFINITIONS(tests
	PRIVATE $<TARGET_PROPERTY:lmmsobjs,INTERFACE_COMPILE_DEFINITIONS>
)
TARGET_LINK_LIBRARIES(tests ${QT_LIBRARIES} ${QT_QTTEST_LIBRARY})
TARGET_LINK_LIBRARIES(tests ${LMMS_REQUIRED_LIBS})

repo_name: CYBERDEViL/LMMS

license: mit


code:
#include "QTestSuite.h"

QList<QTestSuite*> QTestSuite::m_suites;

QTestSuite::QTestSuite(QObject *parent) : QObject(parent)
{
	m_suites << this;
}

QTestSuite::~QTestSuite()
{
	m_suites.removeAll(this);
}

QList<QTestSuite*> QTestSuite::suites()
{
	return m_suites;
}


code:
#ifndef QTESTSUITE_H
#define QTESTSUITE_H

#include <QtTest/QTest>
#include <QObject>
#include <QList>

class QTestSuite : public QObject
{
	Q_OBJECT
public:
	explicit QTestSuite(QObject *parent = 0);
	~QTestSuite();

	static QList<QTestSuite*> suites();

private:
	static QList<QTestSuite*> m_suites;
};

#endif // QTESTSUITE_H


code:
test-files and their MD5 sums go here

16a41b09841f6893c6a621f3e7c63692	emptyproject.wav

path: tests/README


code:
<?xml version="1.0"?>
<!DOCTYPE multimedia-project>
<multimedia-project version="1.0" creator="Linux MultiMedia Studio (LMMS)" creatorversion="0.4.0-rc2" type="song" >

  <song>
    <trackcontainer width="600" x="5" y="5" maximized="0" height="300" visible="1" type="song" minimized="0" >
      <track muted="0" type="0" name="Standard-Preset" >
        <instrumenttrack pan="0" fxch="0" pitch="0" basenote="57" vol="100" >
          <instrument name="tripleoscillator" >
            <tripleoscillator phoffset2="0" userwavefile0="" finer0="0" userwavefile1="" finer1="0" userwavefile2="" finer2="0" coarse0="0" coarse1="0" coarse2="0" finel0="0" finel1="0" modalgo1="2" modalgo2="2" finel2="0" pan0="0" modalgo3="2" pan1="0" stphdetun0="0" pan2="0" stphdetun1="0" wavetype0="0" stphdetun2="0" wavetype1="0" wavetype2="0" vol0="33" vol1="33" phoffset0="0" phoffset1="0" vol2="33" />
          </instrument>
          <eldata fres="0.5" ftype="0" fcut="14000" fwet="0" >
            <elvol lspd_denominator="4" pdel="0" userwavefile="" dec="0.5" lamt="0" syncmode="0" latt="0" rel="0.1" sus="0.5" amt="0" x100="0" att="0" lpdel="0" hold="0.5" lshp="0" lspd="0.1" ctlenvamt="0" lspd_numerator="4" />
            <elcut lspd_denominator="4" pdel="0" userwavefile="" dec="0.5" lamt="0" syncmode="0" latt="0" rel="0.1" sus="0.5" amt="0" x100="0" att="0" lpdel="0" hold="0.5" lshp="0" lspd="0.1" ctlenvamt="0" lspd_numerator="4" />
            <elres lspd_denominator="4" pdel="0" userwavefile="" dec="0.5" lamt="0" syncmode="0" latt="0" rel="0.1" sus="0.5" amt="0" x100="0" att="0" lpdel="0" hold="0.5" lshp="0" lspd="0.1" ctlenvamt="0" lspd_numerator="4" />
          </eldata>
          <chordcreator chord="0" chordrange="1" chord-enabled="0" />
          <arpeggiator arptime="100" arprange="1" arptime_denominator="4" syncmode="0" arpmode="0" arp-enabled="0" arp="0" arptime_numerator="4" arpdir="0" arpgate="100" />
          <midiport inputcontroller="0" fixedoutputvelocity="-1" inputchannel="0" outputcontroller="0" writable="0" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" readable="0" />
          <fxchain numofeffects="0" enabled="0" />
        </instrumenttrack>
        <pattern steps="16" muted="0" type="0" name="Standard-Preset" pos="12345" len="192" frozen="0" />
      </track>
      <track muted="0" type="2" name="Sample-Spur" >
        <sampletrack vol="100" >
          <fxchain numofeffects="0" enabled="0" />
        </sampletrack>
      </track>
      <track muted="0" type="1" name="Beat/Bassline 0" >
        <bbtrack>
          <trackcontainer width="504" x="610" y="5" maximized="0" height="300" visible="1" type="bbtrackcontainer" minimized="0" >
            <track muted="0" type="0" name="Standard-Preset" >
              <instrumenttrack pan="0" fxch="0" pitch="0" basenote="57" vol="100" >
                <instrument name="tripleoscillator" >
                  <tripleoscillator phoffset2="0" userwavefile0="" finer0="0" userwavefile1="" finer1="0" userwavefile2="" finer2="0" coarse0="0" coarse1="0" coarse2="0" finel0="0" finel1="0" modalgo1="2" modalgo2="2" finel2="0" pan0="0" modalgo3="2" pan1="0" stphdetun0="0" pan2="0" stphdetun1="0" wavetype0="0" stphdetun2="0" wavetype1="0" wavetype2="0" vol0="33" vol1="33" phoffset0="0" phoffset1="0" vol2="33" />
                </instrument>
                <eldata fres="0.5" ftype="0" fcut="14000" fwet="0" >
                  <elvol lspd_denominator="4" pdel="0" userwavefile="" dec="0.5" lamt="0" syncmode="0" latt="0" rel="0.1" sus="0.5" amt="0" x100="0" att="0" lpdel="0" hold="0.5" lshp="0" lspd="0.1" ctlenvamt="0" lspd_numerator="4" />
                  <elcut lspd_denominator="4" pdel="0" userwavefile="" dec="0.5" lamt="0" syncmode="0" latt="0" rel="0.1" sus="0.5" amt="0" x100="0" att="0" lpdel="0" hold="0.5" lshp="0" lspd="0.1" ctlenvamt="0" lspd_numerator="4" />
                  <elres lspd_denominator="4" pdel="0" userwavefile="" dec="0.5" lamt="0" syncmode="0" latt="0" rel="0.1" sus="0.5" amt="0" x100="0" att="0" lpdel="0" hold="0.5" lshp="0" lspd="0.1" ctlenvamt="0" lspd_numerator="4" />
                </eldata>
                <chordcreator chord="0" chordrange="1" chord-enabled="0" />
                <arpeggiator arptime="100" arprange="1" arptime_denominator="4" syncmode="0" arpmode="0" arp-enabled="0" arp="0" arptime_numerator="4" arpdir="0" arpgate="100" />
                <midiport inputcontroller="0" fixedoutputvelocity="-1" inputchannel="0" outputcontroller="0" writable="0" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" readable="0" />
                <fxchain numofeffects="0" enabled="0" />
              </instrumenttrack>
              <pattern steps="16" muted="0" type="0" name="Standard-Preset" pos="0" len="192" frozen="0" />
            </track>
          </trackcontainer>
        </bbtrack>
      </track>
      <track muted="0" type="5" name="Automation track" >
        <automationtrack/>
      </track>
    </trackcontainer>
    <track muted="0" type="6" name="Automation track" >
      <automationtrack/>
      <automationpattern name="Tempo" pos="0" >
        <time value="140" pos="0" />
        <object id="7840741" />
      </automationpattern>
      <automationpattern name="Master-Lautstärke" pos="0" >
        <time value="100" pos="0" />
        <object id="2331772" />
      </automationpattern>
      <automationpattern name="Master-Tonhöhe" pos="0" >
        <time value="0" pos="0" />
        <object id="948063" />
      </automationpattern>
    </track>
    <fxmixer width="865" x="5" y="310" maximized="0" height="278" visible="1" minimized="0" >
      <fxchannel num="0" muted="0" volume="1" name="Master" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="1" muted="0" volume="1" name="FX 1" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="2" muted="0" volume="1" name="FX 2" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="3" muted="0" volume="1" name="FX 3" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="4" muted="0" volume="1" name="FX 4" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="5" muted="0" volume="1" name="FX 5" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="6" muted="0" volume="1" name="FX 6" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="7" muted="0" volume="1" name="FX 7" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="8" muted="0" volume="1" name="FX 8" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="9" muted="0" volume="1" name="FX 9" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="10" muted="0" volume="1" name="FX 10" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="11" muted="0" volume="1" name="FX 11" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="12" muted="0" volume="1" name="FX 12" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="13" muted="0" volume="1" name="FX 13" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="14" muted="0" volume="1" name="FX 14" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="15" muted="0" volume="1" name="FX 15" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="16" muted="0" volume="1" name="FX 16" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="17" muted="0" volume="1" name="FX 17" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="18" muted="0" volume="1" name="FX 18" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="19" muted="0" volume="1" name="FX 19" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="20" muted="0" volume="1" name="FX 20" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="21" muted="0" volume="1" name="FX 21" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="22" muted="0" volume="1" name="FX 22" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="23" muted="0" volume="1" name="FX 23" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="24" muted="0" volume="1" name="FX 24" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="25" muted="0" volume="1" name="FX 25" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="26" muted="0" volume="1" name="FX 26" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="27" muted="0" volume="1" name="FX 27" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="28" muted="0" volume="1" name="FX 28" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="29" muted="0" volume="1" name="FX 29" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="30" muted="0" volume="1" name="FX 30" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="31" muted="0" volume="1" name="FX 31" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="32" muted="0" volume="1" name="FX 32" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="33" muted="0" volume="1" name="FX 33" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="34" muted="0" volume="1" name="FX 34" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="35" muted="0" volume="1" name="FX 35" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="36" muted="0" volume="1" name="FX 36" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="37" muted="0" volume="1" name="FX 37" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="38" muted="0" volume="1" name="FX 38" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="39" muted="0" volume="1" name="FX 39" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="40" muted="0" volume="1" name="FX 40" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="41" muted="0" volume="1" name="FX 41" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="42" muted="0" volume="1" name="FX 42" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="43" muted="0" volume="1" name="FX 43" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="44" muted="0" volume="1" name="FX 44" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="45" muted="0" volume="1" name="FX 45" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="46" muted="0" volume="1" name="FX 46" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="47" muted="0" volume="1" name="FX 47" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="48" muted="0" volume="1" name="FX 48" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="49" muted="0" volume="1" name="FX 49" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="50" muted="0" volume="1" name="FX 50" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="51" muted="0" volume="1" name="FX 51" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="52" muted="0" volume="1" name="FX 52" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="53" muted="0" volume="1" name="FX 53" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="54" muted="0" volume="1" name="FX 54" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="55" muted="0" volume="1" name="FX 55" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="56" muted="0" volume="1" name="FX 56" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="57" muted="0" volume="1" name="FX 57" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="58" muted="0" volume="1" name="FX 58" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="59" muted="0" volume="1" name="FX 59" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="60" muted="0" volume="1" name="FX 60" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="61" muted="0" volume="1" name="FX 61" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="62" muted="0" volume="1" name="FX 62" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="63" muted="0" volume="1" name="FX 63" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
      <fxchannel num="64" muted="0" volume="1" name="FX 64" >
        <fxchain numofeffects="0" enabled="0" />
      </fxchannel>
    </fxmixer>
    <controllerrackview width="258" x="880" y="310" maximized="0" height="278" visible="1" minimized="0" />
    <pianoroll width="840" x="-11" y="0" maximized="0" height="480" visible="0" minimized="0" />
    <automationeditor width="740" x="0" y="0" maximized="0" height="480" visible="0" minimized="0" />
    <projectnotes width="400" x="700" y="10" maximized="0" height="300" visible="0" minimized="0" ><![CDATA[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<meta name="qrichtext" content="1" />

Schreiben Sie hier Ihre Projekt-Notizen auf.
]]></projectnotes>
    <timeline lp1pos="192" lp0pos="0" lpstate="0" />
    <controllers/>
  </song>
</multimedia-project>


code:
#include "QTestSuite.h"

#include <QtTest/QTest>

#include <QDebug>

#include "Engine.h"

int main(int argc, char* argv[])
{
	new QCoreApplication(argc, argv);
	Engine::init(true);

	int numsuites = QTestSuite::suites().size();
	qDebug() << ">> Will run" << numsuites << "test suites";
	int failed = 0;
	for (QTestSuite*& suite : QTestSuite::suites())
	{
		failed += QTest::qExec(suite, argc, argv);
	}
	qDebug() << "<<" << failed << "out of"<<numsuites<<"test suites failed.";
	return failed;
}


code:
/*
 * AutomatableModelTest.cpp
 *
 * Copyright (c) 2019-2020 Johannes Lorenz <j.git$$$lorenz-ho.me, $$$=@>
 *
 * This file is part of LMMS - https://lmms.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program (see COPYING); if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 */

#include "QTestSuite.h"

#include "AutomatableModel.h"
#include "ComboBoxModel.h"

class AutomatableModelTest : QTestSuite
{
	Q_OBJECT

	bool m1Changed, m2Changed;
	void resetChanged() { m1Changed = m2Changed = false; }

private slots: // helper slots
	void onM1Changed() { m1Changed = true; }
	void onM2Changed() { m2Changed = true; }

private slots: // tests
	//! Test that upcast and exact casts work,
	//! but no downcast or any other casts
	void CastTests()
	{
		ComboBoxModel comboModel;
		AutomatableModel* amPtr = &comboModel;
		QVERIFY(nullptr == amPtr->dynamicCast<FloatModel>()); // not a parent class
		QCOMPARE(&comboModel, amPtr->dynamicCast<AutomatableModel>()); // parent class
		QCOMPARE(&comboModel, amPtr->dynamicCast<IntModel>()); // parent class
		QCOMPARE(&comboModel, amPtr->dynamicCast<ComboBoxModel>()); // same class

		IntModel intModel;
		IntModel* imPtr = &intModel;
		QVERIFY(nullptr == imPtr->dynamicCast<FloatModel>()); // not a parent class
		QCOMPARE(&intModel, imPtr->dynamicCast<AutomatableModel>()); // parent class
		QCOMPARE(&intModel, imPtr->dynamicCast<IntModel>()); // same class
		QVERIFY(nullptr == imPtr->dynamicCast<ComboBoxModel>()); // child class
	}

	void LinkTests()
	{
		BoolModel m1(false), m2(false);

		QObject::connect(&m1, SIGNAL(dataChanged()),
			this, SLOT(onM1Changed()));
		QObject::connect(&m2, SIGNAL(dataChanged()),
			this, SLOT(onM2Changed()));

		resetChanged();
		AutomatableModel::linkModels(&m1, &m1);
		QVERIFY(!m1Changed); // cannot link to itself
		QVERIFY(!m2Changed);

		resetChanged();
		AutomatableModel::linkModels(&m1, &m2);
		QVERIFY(m1Changed); // since m1 takes the value of m2
		QVERIFY(!m2Changed); // the second model is the source

		resetChanged();
		AutomatableModel::linkModels(&m1, &m2);
		QVERIFY(!m1Changed); // it's already linked
		QVERIFY(!m2Changed);

		resetChanged();
		BoolModel m3(false);
		m1.setValue(1.f);
		m2.setValue(1.f);
		AutomatableModel::linkModels(&m1, &m2);
		QVERIFY(m1.value());
		QVERIFY(m2.value());
		QVERIFY(!m3.value());
		AutomatableModel::linkModels(&m2, &m3); // drag m3, drop on m2
		// m2 should take m3's (0) value
		// due to a bug(?), this does not happen
		QVERIFY(m2.value());
		QVERIFY(!m3.value());
	}
} AutomatableModelTests;

#include "AutomatableModelTest.moc"


code:
/*
 * ProjectVersionTest.cpp
 *
 * Copyright (c) 2015 Lukas W <lukaswhl/at/gmail.com>
 *
 * This file is part of LMMS - https://lmms.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program (see COPYING); if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 */

#include "QTestSuite.h"

#include "ProjectVersion.h"

class ProjectVersionTest : QTestSuite
{
	Q_OBJECT
private slots:
	void ProjectVersionComparisonTests()
	{
		QVERIFY(ProjectVersion("1.1.0", ProjectVersion::Minor) > "1.0.3");
		QVERIFY(ProjectVersion("1.1.0", ProjectVersion::Major) < "2.1.0");
		QVERIFY(ProjectVersion("1.1.0", ProjectVersion::Release) > "0.2.1");
		QVERIFY(ProjectVersion("1.1.4", ProjectVersion::Release) < "1.1.10");
		QVERIFY(ProjectVersion("1.1.0", ProjectVersion::Minor) == "1.1.5");
		QVERIFY( ! ( ProjectVersion("3.1.0", ProjectVersion::Minor) < "2.2.5" ) );
		QVERIFY( ! ( ProjectVersion("2.5.0", ProjectVersion::Release) < "2.2.5" ) );
		//A pre-release version has lower precedence than a normal version
		QVERIFY(ProjectVersion("1.1.0") > "1.1.0-alpha");
		//But higher precedence than the previous version
		QVERIFY(ProjectVersion("1.1.0-alpha") > "1.0.0");
		//Identifiers with letters or hyphens are compare lexically in ASCII sort order
		QVERIFY(ProjectVersion("1.1.0-alpha") < "1.1.0-beta");
		QVERIFY(ProjectVersion("1.2.0-rc1") < "1.2.0-rc2");
		//Build metadata MUST be ignored when determining version precedence
		QVERIFY(ProjectVersion("1.2.2") == "1.2.2+metadata");
		QVERIFY(ProjectVersion("1.0.0-alpha") < "1.0.0-alpha.1");
		QVERIFY(ProjectVersion("1.0.0-alpha.1") < "1.0.0-alpha.beta");
		QVERIFY(ProjectVersion("1.0.0-alpha.beta") < "1.0.0-beta");
		QVERIFY(ProjectVersion("1.0.0-beta.2") < "1.0.0-beta.11");
		//Test workaround for old, nonstandard version numbers
		QVERIFY(ProjectVersion("1.2.2.42") == "1.2.3-42");
		QVERIFY(ProjectVersion("1.2.2.42") > "1.2.2.21");
		//Ensure that newer versions of the same format aren't upgraded
		//in order to discourage use of incorrect versioning
		QVERIFY(ProjectVersion("1.2.3.42") == "1.2.3");
		//CompareVersion "All" should compare every identifier
		QVERIFY(
			ProjectVersion("1.0.0-a.b.c.d.e.f.g.h.i.j.k.l", ProjectVersion::All)
			< "1.0.0-a.b.c.d.e.f.g.h.i.j.k.m"
		);
		//Prerelease identifiers may contain hyphens
		QVERIFY(ProjectVersion("1.0.0-Alpha-1.2") > "1.0.0-Alpha-1.1");
		//We shouldn't crash on invalid versions
		QVERIFY(ProjectVersion("1-invalid") == "1.0.0-invalid");
		QVERIFY(ProjectVersion("") == "0.0.0");
		//Numeric identifiers are smaller than non-numeric identiiers
		QVERIFY(ProjectVersion("1.0.0-alpha") > "1.0.0-1");
		//An identifier of the form "-x" is non-numeric, not negative
		QVERIFY(ProjectVersion("1.0.0-alpha.-1") > "1.0.0-alpha.1");
	}
} ProjectVersionTests;

#include "ProjectVersionTest.moc"


code:
/*
 * RelativePathsTest.cpp
 *
 * Copyright (c) 2017 Tres Finocchiaro <tres/dot/finocchiaro/at/gmail.com>
 *
 * This file is part of LMMS - https://lmms.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program (see COPYING); if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 */

#include "QTestSuite.h"

#include "ConfigManager.h"
#include "SampleBuffer.h"
#include "PathUtil.h"

#include <QDir>

class RelativePathsTest : QTestSuite
{
	Q_OBJECT
private slots:
	void PathUtilComparisonTests()
	{
		QFileInfo fi(ConfigManager::inst()->factorySamplesDir() + "/drums/kick01.ogg");
		QVERIFY(fi.exists());

		QString absPath = fi.absoluteFilePath();
		QString oldRelPath = "drums/kick01.ogg";
		QString relPath = PathUtil::basePrefix(PathUtil::Base::FactorySample) + "drums/kick01.ogg";
		QString fuzPath = absPath;
		fuzPath.replace(relPath, "drums/.///kick01.ogg");

		//Test nicely formatted paths
		QCOMPARE(PathUtil::toShortestRelative(absPath), relPath);
		QCOMPARE(PathUtil::toAbsolute(relPath), absPath);

		//Test upgrading old paths
		QCOMPARE(PathUtil::toShortestRelative(oldRelPath), relPath);
		QCOMPARE(PathUtil::toAbsolute(oldRelPath), absPath);

		//Test weird but valid paths
		QCOMPARE(PathUtil::toShortestRelative(fuzPath), relPath);
		QCOMPARE(PathUtil::toAbsolute(fuzPath), absPath);

		//Empty paths should stay empty
		QString empty = QString("");
		QCOMPARE(PathUtil::stripPrefix(""), empty);
		QCOMPARE(PathUtil::cleanName(""), empty);
		QCOMPARE(PathUtil::toAbsolute(""), empty);
		QCOMPARE(PathUtil::toShortestRelative(""), empty);
	}
} RelativePathTests;

#include "RelativePathsTest.moc"


code:
/*
 * AutomationTrackTest.cpp
 *
 * Copyright (c) 2017 Lukas W <lukaswhl/at/gmail.com>
 *
 * This file is part of LMMS - https://lmms.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program (see COPYING); if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 */

#include "QTestSuite.h"

#include "QCoreApplication"

#include "AutomationPattern.h"
#include "AutomationTrack.h"
#include "BBTrack.h"
#include "BBTrackContainer.h"
#include "DetuningHelper.h"
#include "InstrumentTrack.h"
#include "Pattern.h"
#include "TrackContainer.h"

#include "Engine.h"
#include "Song.h"

class AutomationTrackTest : QTestSuite
{
	Q_OBJECT
private slots:
	void initTestCase()
	{
	}

	void testPatternLinear()
	{
		AutomationPattern p(nullptr);
		p.setProgressionType(AutomationPattern::LinearProgression);
		p.putValue(0, 0.0, false);
		p.putValue(100, 1.0, false);

		QCOMPARE(p.valueAt(0), 0.0f);
		QCOMPARE(p.valueAt(25), 0.25f);
		QCOMPARE(p.valueAt(50), 0.5f);
		QCOMPARE(p.valueAt(75), 0.75f);
		QCOMPARE(p.valueAt(100), 1.0f);
		QCOMPARE(p.valueAt(150), 1.0f);
	}

	void testPatternDiscrete()
	{
		AutomationPattern p(nullptr);
		p.setProgressionType(AutomationPattern::DiscreteProgression);
		p.putValue(0, 0.0, false);
		p.putValue(100, 1.0, false);

		QCOMPARE(p.valueAt(0), 0.0f);
		QCOMPARE(p.valueAt(50), 0.0f);
		QCOMPARE(p.valueAt(100), 1.0f);
		QCOMPARE(p.valueAt(150), 1.0f);
	}

	void testPatterns()
	{
		FloatModel model;

		auto song = Engine::getSong();
		AutomationTrack track(song);

		AutomationPattern p1(&track);
		p1.setProgressionType(AutomationPattern::LinearProgression);
		p1.putValue(0, 0.0, false);
		p1.putValue(10, 1.0, false);
		p1.movePosition(0);
		p1.addObject(&model);

		AutomationPattern p2(&track);
		p2.setProgressionType(AutomationPattern::LinearProgression);
		p2.putValue(0, 0.0, false);
		p2.putValue(100, 1.0, false);
		p2.movePosition(100);
		p2.addObject(&model);

		AutomationPattern p3(&track);
		p3.addObject(&model);
		//XXX: Why is this even necessary?
		p3.clear();

		QCOMPARE(song->automatedValuesAt(  0)[&model], 0.0f);
		QCOMPARE(song->automatedValuesAt(  5)[&model], 0.5f);
		QCOMPARE(song->automatedValuesAt( 10)[&model], 1.0f);
		QCOMPARE(song->automatedValuesAt( 50)[&model], 1.0f);
		QCOMPARE(song->automatedValuesAt(100)[&model], 0.0f);
		QCOMPARE(song->automatedValuesAt(150)[&model], 0.5f);
	}

	void testLengthRespected()
	{
		FloatModel model;

		auto song = Engine::getSong();
		AutomationTrack track(song);

		AutomationPattern p(&track);
		p.setProgressionType(AutomationPattern::LinearProgression);
		p.addObject(&model);

		p.putValue(0, 0.0, false);
		p.putValue(100, 1.0, false);

		p.changeLength(100);
		QCOMPARE(song->automatedValuesAt(  0)[&model], 0.0f);
		QCOMPARE(song->automatedValuesAt( 50)[&model], 0.5f);
		QCOMPARE(song->automatedValuesAt(100)[&model], 1.0f);

		p.changeLength(50);
		QCOMPARE(song->automatedValuesAt(  0)[&model], 0.0f);
		QCOMPARE(song->automatedValuesAt( 50)[&model], 0.5f);
		QCOMPARE(song->automatedValuesAt(100)[&model], 0.5f);
	}

	void testInlineAutomation()
	{
		auto song = Engine::getSong();

		InstrumentTrack* instrumentTrack =
				dynamic_cast<InstrumentTrack*>(Track::create(Track::InstrumentTrack, song));

		Pattern* notePattern = dynamic_cast<Pattern*>(instrumentTrack->createTCO(0));
		notePattern->changeLength(TimePos(4, 0));
		Note* note = notePattern->addNote(Note(TimePos(4, 0)), false);
		note->createDetuning();

		DetuningHelper* dh = note->detuning();
		auto pattern = dh->automationPattern();
		pattern->setProgressionType( AutomationPattern::LinearProgression );
		pattern->putValue(TimePos(0, 0), 0.0);
		pattern->putValue(TimePos(4, 0), 1.0);

		QCOMPARE(pattern->valueAt(TimePos(0, 0)), 0.0f);
		QCOMPARE(pattern->valueAt(TimePos(1, 0)), 0.25f);
		QCOMPARE(pattern->valueAt(TimePos(2, 0)), 0.5f);
		QCOMPARE(pattern->valueAt(TimePos(4, 0)), 1.0f);
	}

	void testBBTrack()
	{
		auto song = Engine::getSong();
		auto bbContainer = Engine::getBBTrackContainer();
		BBTrack bbTrack(song);
		Track* automationTrack = Track::create(Track::AutomationTrack, bbContainer);

		QVERIFY(automationTrack->numOfTCOs());
		AutomationPattern* p1 = dynamic_cast<AutomationPattern*>(automationTrack->getTCO(0));
		QVERIFY(p1);

		FloatModel model;

		p1->setProgressionType(AutomationPattern::LinearProgression);
		p1->putValue(0, 0.0, false);
		p1->putValue(10, 1.0, false);
		p1->addObject(&model);

		QCOMPARE(bbContainer->automatedValuesAt( 0, bbTrack.index())[&model], 0.0f);
		QCOMPARE(bbContainer->automatedValuesAt( 5, bbTrack.index())[&model], 0.5f);
		QCOMPARE(bbContainer->automatedValuesAt(10, bbTrack.index())[&model], 1.0f);
		QCOMPARE(bbContainer->automatedValuesAt(50, bbTrack.index())[&model], 1.0f);

		BBTrack bbTrack2(song);

		QCOMPARE(bbContainer->automatedValuesAt(5, bbTrack.index())[&model], 0.5f);
		QVERIFY(! bbContainer->automatedValuesAt(5, bbTrack2.index()).size());

		BBTCO tco(&bbTrack);
		tco.changeLength(TimePos::ticksPerBar() * 2);
		tco.movePosition(0);

		QCOMPARE(song->automatedValuesAt(0)[&model], 0.0f);
		QCOMPARE(song->automatedValuesAt(5)[&model], 0.5f);
		QCOMPARE(song->automatedValuesAt(TimePos::ticksPerBar() + 5)[&model], 0.5f);
	}

	void testGlobalAutomation()
	{
		// Global automation should not have priority, see https://github.com/LMMS/lmms/issues/4268
		// Tests regression caused by 75077f6200a5aee3a5821aae48a3b8466ed8714a
		auto song = Engine::getSong();

		auto globalTrack = song->globalAutomationTrack();
		AutomationPattern globalPattern(globalTrack);

		AutomationTrack localTrack(song);
		AutomationPattern localPattern(&localTrack);

		FloatModel model;
		globalPattern.setProgressionType(AutomationPattern::DiscreteProgression);
		localPattern.setProgressionType(AutomationPattern::DiscreteProgression);
		globalPattern.addObject(&model);
		localPattern.addObject(&model);
		globalPattern.putValue(0, 100.0f, false);
		localPattern.putValue(0, 50.0f, false);

		QCOMPARE(song->automatedValuesAt(0)[&model], 50.0f);
	}

} AutomationTrackTest;

#include "AutomationTrackTest.moc"


code:
The DOKK is a free database. The data is modeled as a graph, with topics represented
as nodes connected by edges.

Naming conventions
------------------

Because the graph contains a large number of nodes, it's divided into more manageable
subgraphs (one per folder).
There are no restrictions on the ID values that can be used, but in order to keep
things well organized the following conventions are adopted:

    dokk:subgraph:id
    |    |        |_____ local ID, defined within the subgraph
    |    |______________ name of the subgraph where the node is defined
    |___________________ fixed prefix

Using the graph
---------------

Files can be parsed with any RDF library. For more advanced needs the files can
be loaded in a database server (a triplestore). For this we recommend the Fuseki
server.
Following is a demonstration with Python and RDFLib:

    g = rdflib.Graph()
    g.parse("file-1")
    g.parse("file-2")
    g.parse("...")

    # Execute queries on the graph.
    # (example: find the names of the people that know Bob
    query = """
        SELECT ?name
        WHERE { ?person :knows :bob ;
                        :name  ?name . }
    """

    results = g.query(query)
    for r in results:
        print(r.name)

    # Or convert nodes to JSON
    graph_json = json.loads(g.serialize(format="json-ld"))

Contacts
--------

If you have problems with using the database, please consider asking your questions
on WOTAS (https://wotas.net).
If you just want to say hi!, meet us on IRC in #peers at irc.libera.chat or
https://peers.community.

repo_name: vaeringjar/graph

path: README

license: unknown


code:
PREFIX : <dokk::>
PREFIX common: <dokk:schema:common:>
PREFIX ttrpg: <dokk:schema:ttrpg:>

:fenrispunk
    common:title "Fenris Punk" ;
    common:description "A cyberpunk ttrpg full of satire, groaner jokes, and propaganda." ;
    common:license "CC-BY-SA 4.0" ;
    common:website "https://fenrispunk.com" ;
    ttrpg:character :brik, :brock, :hexapus, :hood .

:brik
    common:title "Brik" .

:brock
    common:title "Brock" .

:hexapus
    common:title "Hexapus" ;
    common:description "A quantum hacker turned death mage." .

:hood
    common:title "Hood" ;
    common:description "Born Henry MacDuff on 2040-02-08 in Seattle." ;
    ttrpg:size "Medium" ;
    ttrpg:being "Human" ;
    ttrpg:align "Chaotic Neutral" ;
    ttrpg:weapon "Explosives" ;
    ttrpg:handedness "Right" .


code:
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''

---

repo_name: xianyu/navicat


code:
---
name: Failure report issue template
about: A template for failure report.
title: "[FAILURE][write OS type here] write your own description here"
labels: failure report
assignees: ''

---

BEFORE `navicat-patcher.exe` (Windows, Wine in Linux) or `navicat-patcher` (macOS) is run, you should make sure that your Navicat is official version, which means it has not been modified by anyone, and you should also make sure there's no `*.backup` files in Navicat installation folder.

If you still meet a failure, you can submit a failure report and please fill in the following:

1. What is your Navicat product? Check one of the following:
   - [ ] Navicat Premium
   - [ ] Navicat MySQL
   - [ ] Navicat Data Modeler
   - [ ] Navicat Report Viewer
   - [ ] Other, please write here: ______

2. What's your Navicat version number?
   Please write here: ______

3. What is your Navicat language version? Check one of the following:
   - [ ] English
   - [ ] Simplified Chinese
   - [ ] Traditional Chinese
   - [ ] Other, please write here: ______

4. What is your OS? Check one of the following:
   - [ ] Windows
   - [ ] macOS
   - [ ] Linux

5. Can you post console output here? If don't have, leave it empty.
```
Please post here
```

6. If __Request Code__ or __Activation Code__ is shown, please post the content of `RegPrivateKey.pem` here:
```
Please post here
```

7. If you have other information to provide, please write here.
   (If you don't have, leave it empty.)


code:
---
name: 失败报告模板
about: 失败报告模板
title: "[FAILURE][填写你的操作系统类型] 你的描述"
labels: failure report
assignees: ''

---

在运行 `navicat-patcher.exe` (Windows, Wine in Linux) 或者 `navicat-patcher` (macOS)之前，你应该确保你的Navicat是官方版本，即未被任何第三方修改，同时也应确保你的Navicat安装目录下没有 `*.backup` 文件。

如果激活依然失败了，你可以提交一份失败报告。请填写下面的内容：

1. 你的Navicat产品类型是什么? 选择下面一个：
   - [ ] Navicat Premium
   - [ ] Navicat MySQL
   - [ ] Navicat Data Modeler
   - [ ] Navicat Report Viewer
   - [ ] 其他，请写在这里：______

2. 你的Navicat版本号是多少？
   请写在这里：______

3. 你的Navicat产品语言版本是什么？选择下面一个：
   - [ ] English
   - [ ] Simplified Chinese
   - [ ] Traditional Chinese
   - [ ] 其他，请写在这里：______

4. 你的操作系统是什么？选择下面一个：
   - [ ] Windows
   - [ ] macOS
   - [ ] Linux

5. 你可以把命令行输出贴出来吗？如果你没有，请留空：
```
贴在这里
```

6. 如果 __请求码__ 或者 __激活码__ 出现了，请把 `RegPrivateKey.pem` 的内容贴在这里：
```
贴在这里
```

7. 如果你有其他信息要提供的，请写在这里：
   （如果你没有，请留空。）


code:
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json

*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Typescript v1 declaration files
typings/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush
.cr/

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
MailDataReciever/Renci.SshNet.dll

path: .gitignore


code:
# Navicat keygen - How does it work?

[中文版 How does it work?](HOW_DOES_IT_WORK.zh-CN.md)

## 1. Keyword Explanation.

* __Navicat Activation Public Key__

  It is a __RSA-2048__ public key that Navicat used to encrypt or decrypt offline activation information.

  It is stored in __navicat.exe__ as a kind of resource called __RCData__. The resource name is `"ACTIVATIONPUBKEY"`. You can see it by a kind of software [___Resource Hacker___](http://www.angusj.com/resourcehacker/). The public key is

  ```
  -----BEGIN PUBLIC KEY-----
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw1dqF3SkCaAAmMzs889I
  qdW9M2dIdh3jG9yPcmLnmJiGpBF4E9VHSMGe8oPAy2kJDmdNt4BcEygvssEfginv
  a5t5jm352UAoDosUJkTXGQhpAWMF4fBmBpO3EedG62rOsqMBgmSdAyxCSPBRJIOF
  R0QgZFbRnU0frj34fiVmgYiLuZSAmIbs8ZxiHPdp1oD4tUpvsFci4QJtYNjNnGU2
  WPH6rvChGl1IRKrxMtqLielsvajUjyrgOC6NmymYMvZNER3htFEtL1eQbCyTfDmt
  YyQ1Wt4Ot12lxf0wVIR5mcGN7XCXJRHOFHSf1gzXWabRSvmt1nrl7sW6cjxljuuQ
  awIDAQAB
  -----END PUBLIC KEY-----
  ```

  If you have the corresponding private key, you can tell me. I would be very appreciated for your generous.

  __NOTICE:__

  Start from __Navicat Premium 12.0.25__, Navicat do not load this public key from resource in `navicat.exe`. Instead, the public key is stored in `libcc.dll` and has been encrypted. To avoid being replaced easily, the public key is split into 5 parts:

  The following content is discovered from `libcc.dll` of __Navicat Premium x64 12.0.25 Simplified Chinese version__. The SHA256 value of `libcc.dll` is `607e0a84c75966b00f3d12fa833e91d159e4f51ac51b6ba66f98d0c3cbefdce0`.

  I __DO NOT__ guarantee that offset values are absolutely correct in other versions. But __char strings__ and __immediate values__ are highly possible to be found.

  1. At file offset `+0x01A12090` in `libcc.dll`, stored as __char string__:

     ```
     "D75125B70767B94145B47C1CB3C0755E
      7CCB8825C5DCE0C58ACF944E08280140
      9A02472FAFFD1CD77864BB821AE36766
      FEEDE6A24F12662954168BFA314BD950
      32B9D82445355ED7BC0B880887D650F5"
     ```

  2. At file offset `+0x0059D799` in `libcc.dll`, stored as __immediate value__ in a instruction:

     ```
     0xFE 0xEA 0xBC 0x01
     ```

     In decimal: `29158142`

  3. At file offset `+0x01A11DA0` in `libcc.dll`, stored as __char string__:

     ```
     "E1CED09B9C2186BF71A70C0FE2F1E0AE
      F3BD6B75277AAB20DFAF3D110F75912B
      FB63AC50EC4C48689D1502715243A79F
      39FF2DE2BF15CE438FF885745ED54573
      850E8A9F40EE2FF505EB7476F95ADB78
      3B28CA374FAC4632892AB82FB3BF4715
      FCFE6E82D03731FC3762B6AAC3DF1C3B
      C646FE9CD3C62663A97EE72DB932A301
      312B4A7633100C8CC357262C39A2B3A6
      4B224F5276D5EDBDF0804DC3AC4B8351
      62BB1969EAEBADC43D2511D6E0239287
      81B167A48273B953378D3D2080CC0677
      7E8A2364F0234B81064C5C739A8DA28D
      C5889072BF37685CBC94C2D31D0179AD
      86D8E3AA8090D4F0B281BE37E0143746
      E6049CCC06899401264FA471C016A96C
      79815B55BBC26B43052609D9D175FBCD
      E455392F10E51EC162F51CF732E6BB39
      1F56BBFD8D957DF3D4C55B71CEFD54B1
      9C16D458757373E698D7E693A8FC3981
      5A8BF03BA05EA8C8778D38F9873D62B4
      460F41ACF997C30E7C3AF025FA171B5F
      5AD4D6B15E95C27F6B35AD61875E5505
      449B4E"
     ```

  4. At file offset `+0x0059D77F` in `libcc.dll`, stored as __immediate value__ in a instruction:

     ```
     0x59 0x08 0x01 0x00
     ```

     In decimal: `67673`

  5. At file offset `+ 0x1A11D8C` in `libcc.dll`, stored as __char string__:

     ```
     "92933"
     ```

  Then output encrypted public key with format `"%s%d%s%d%s"`. The order is the same as it lists:

  ```

  D75125B70767B94145B47C1CB3C0755E7CCB8825C5DCE0C58ACF944E082801409A02472FAFFD1CD77864BB821AE36766FEEDE6A24F12662954168BFA314BD95032B9D82445355ED7BC0B880887D650F529158142E1CED09B9C2186BF71A70C0FE2F1E0AEF3BD6B75277AAB20DFAF3D110F75912BFB63AC50EC4C48689D1502715243A79F39FF2DE2BF15CE438FF885745ED54573850E8A9F40EE2FF505EB7476F95ADB783B28CA374FAC4632892AB82FB3BF4715FCFE6E82D03731FC3762B6AAC3DF1C3BC646FE9CD3C62663A97EE72DB932A301312B4A7633100C8CC357262C39A2B3A64B224F5276D5EDBDF0804DC3AC4B835162BB1969EAEBADC43D2511D6E023928781B167A48273B953378D3D2080CC06777E8A2364F0234B81064C5C739A8DA28DC5889072BF37685CBC94C2D31D0179AD86D8E3AA8090D4F0B281BE37E0143746E6049CCC06899401264FA471C016A96C79815B55BBC26B43052609D9D175FBCDE455392F10E51EC162F51CF732E6BB391F56BBFD8D957DF3D4C55B71CEFD54B19C16D458757373E698D7E693A8FC39815A8BF03BA05EA8C8778D38F9873D62B4460F41ACF997C30E7C3AF025FA171B5F5AD4D6B15E95C27F6B35AD61875E5505449B4E6767392933

  ```

  This encrypted public key can be decrypted by my another repo: [how-does-navicat-encrypt-password](https://github.com/DoubleLabyrinth/how-does-navicat-encrypt-password), while the key used is `b'23970790'`.

  Example:

  ```cmd
  E:\GitHub>git clone https://github.com/DoubleLabyrinth/how-does-navicat-encrypt-password.git
  ...
  E:\GitHub>cd how-does-navicat-encrypt-password\python3
  E:\GitHub\how-does-navicat-encrypt-password\python3>python
  Python 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
  Type "help", "copyright", "credits" or "license" for more information.
  >>> from NavicatCrypto import *
  >>> cipher = Navicat11Crypto(b'23970790')
  >>> print(cipher.DecryptString('D75125B70767B94145B47C1CB3C0755E7CCB8825C5DCE0C58ACF944E082801409A02472FAFFD1CD77864BB821AE36766FEEDE6A24F12662954168BFA314BD95032B9D82445355ED7BC0B880887D650F529158142E1CED09B9C2186BF71A70C0FE2F1E0AEF3BD6B75277AAB20DFAF3D110F75912BFB63AC50EC4C48689D1502715243A79F39FF2DE2BF15CE438FF885745ED54573850E8A9F40EE2FF505EB7476F95ADB783B28CA374FAC4632892AB82FB3BF4715FCFE6E82D03731FC3762B6AAC3DF1C3BC646FE9CD3C62663A97EE72DB932A301312B4A7633100C8CC357262C39A2B3A64B224F5276D5EDBDF0804DC3AC4B835162BB1969EAEBADC43D2511D6E023928781B167A48273B953378D3D2080CC06777E8A2364F0234B81064C5C739A8DA28DC5889072BF37685CBC94C2D31D0179AD86D8E3AA8090D4F0B281BE37E0143746E6049CCC06899401264FA471C016A96C79815B55BBC26B43052609D9D175FBCDE455392F10E51EC162F51CF732E6BB391F56BBFD8D957DF3D4C55B71CEFD54B19C16D458757373E698D7E693A8FC39815A8BF03BA05EA8C8778D38F9873D62B4460F41ACF997C30E7C3AF025FA171B5F5AD4D6B15E95C27F6B35AD61875E5505449B4E6767392933'))
  -----BEGIN PUBLIC KEY-----
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw1dqF3SkCaAAmMzs889I
  qdW9M2dIdh3jG9yPcmLnmJiGpBF4E9VHSMGe8oPAy2kJDmdNt4BcEygvssEfginv
  a5t5jm352UAoDosUJkTXGQhpAWMF4fBmBpO3EedG62rOsqMBgmSdAyxCSPBRJIOF
  R0QgZFbRnU0frj34fiVmgYiLuZSAmIbs8ZxiHPdp1oD4tUpvsFci4QJtYNjNnGU2
  WPH6rvChGl1IRKrxMtqLielsvajUjyrgOC6NmymYMvZNER3htFEtL1eQbCyTfDmt
  YyQ1Wt4Ot12lxf0wVIR5mcGN7XCXJRHOFHSf1gzXWabRSvmt1nrl7sW6cjxljuuQ
  awIDAQAB
  -----END PUBLIC KEY-----
  ```

  __NOTICE:__

  Start from __Navicat Premium 12.1.11__, Navicat do not load the public key through the method I talked before. Of course, the public key is still stored in `libcc.dll`. When Navicat starts, it encrypts the public key by an 8-bytes-long XOR key and stores the ciphertext in static area. When verifing __Activation Code__, Navicat will regenerate the 8-bytes-long XOR key and decrypts the ciphertext in static area to get the public key.

  In `libcc.dll`, x64 version, you can find some instructions that looks like:

  ```asm
  xor eax, 'M'
  mov byte_xxxxxx, al
  ...
  xor eax, 'I'
  mov byte_xxxxxx, al
  ...
  xor eax, 'I'
  mov byte_xxxxxx, al
  ...
  xor eax, 'B'
  mov byte_xxxxxx, al
  ...
  xor eax, 'I'
  mov byte_xxxxxx, al
  ...
  xor eax, 'j'
  mov byte_xxxxxx, al
  ...
  ...
  ```

* __Request Code__

  It is a Base64 string that represents 256-bytes-long data, while the 256-bytes-long data is the cipher text of __Offline Activation Request Information__ encrypted by __Navicat Activation Public Key__.

* __Offline Activation Request Information__

  It is just a JSON-style ASCII string which contains 3 items. They are `"K"`, `"DI"` and `"P"` respectively, which represent __snKey__, __DeviceIdentifier__ (related with your machine), __Platform__ (OS Type).

  Like

  ```
  {"K": "xxxxxxxxxxxxxxxx", "DI": "yyyyyyyyyyyyy", "P": "WIN8"}
  ```

* __Activation Code__

  It is a Base64 string that represents 256-bytes-long data, while the 256-bytes-long data is the cipher text of the __Offline Activation Response Information__ encrypted by __Navicat Activation Private Key__. So far, we don't know the official activation private key and we have to replace it in `navicat.exe` and `libcc.dll`.

* __Offline Activation Response Information__

  Just like __Offline Activation Request Information__, it is also a JSON-style ASCII string. But it contains 5 items. They are `"K"`, `"N"`, `"O"`, `"T"` and `"DI"` respectively.

  `"K"` and `"DI"` has the same meaning that is mentioned in __Offline Activation Request Information__ and must be the same with the corresponding items in __Offline Activation Request Information__.

  `"N"`, `"O"`, `"T"` represent __Name__, __Organization__, __Timestamp__ respectively. __Name__ and __Organization__ are UTF-8 strings and the type of __Timestamp__ can be string or integer. (Thanks for discoveries from @Wizr, issue #10)

  `"T"` can be omitted.

* __snKey__

  It is a 4-block-long string, while every block is 4-chars-long.

  __snKey__ is generated by 10-bytes-long data. In order to explain it easily, I use __uint8_t data[10]__ to represent the 10-bytes-long data.

  1. __data[0]__ and __data[1]__ must be `0x68` and `0x2A` respectively.

     These two bytes are Naivcat signature number.

  2. __data[2]__, __data[3]__ and __data[4]__ can be any byte. Just set them whatever you want.

  3. __data[5]__ and __data[6]__ are product language signatures.

     |  Language  |  data[5]  |  data[6]  |  Discoverer     |
     |------------|:---------:|:---------:|-----------------|
     |  English   |  0xAC     |  0x88     |                 |
     |  简体中文   |  0xCE     |  0x32     |                 |
     |  繁體中文   |  0xAA     |  0x99     |                 |
     |  日本語     |  0xAD     |  0x82     |  @dragonflylee  |
     |  Polski    |  0xBB     |  0x55     |  @dragonflylee  |
     |  Español   |  0xAE     |  0x10     |  @dragonflylee  |
     |  Français  |  0xFA     |  0x20     |  @Deltafox79    |
     |  Deutsch   |  0xB1     |  0x60     |  @dragonflylee  |
     |  한국어     |  0xB5     |  0x60     |  @dragonflylee  |
     |  Русский   |  0xEE     |  0x16     |  @dragonflylee  |
     |  Português |  0xCD     |  0x49     |  @dragonflylee  |

  4. __data[7]__ is Navicat product ID. (Thanks @dragonflylee and @Deltafox79)

     |Product Name         |Enterprise|Standard|Educational|Essentials|
     |---------------------|:--------:|:------:|:---------:|:--------:|
     |Navicat Report Viewer|0x0B      |        |           |          |
     |Navicat Data Modeler |          |0x47    |0x4A       |          |
     |Navicat Premium      |0x65      |        |0x66       |0x67      |
     |Navicat MySQL        |0x68      |0x69    |0x6A       |0x6B      |
     |Navicat PostgreSQL   |0x6C      |0x6D    |0x6E       |0x6F      |
     |Navicat Oracle       |0x70      |0x71    |0x72       |0x73      |
     |Navicat SQL Server   |0x74      |0x75    |0x76       |0x77      |
     |Navicat SQLite       |0x78      |0x79    |0x7A       |0x7B      |
     |Navicat MariaDB      |0x7C      |0x7D    |0x7E       |0x7F      |
     |Navicat MongoDB      |0x80      |0x81    |0x82       |          |

  5. High 4 bits of __data[8]__ represents __major version number__.

     Low 4 bits is unknown, but we can use it to delay activation deadline. Possible values are `0000` or `0001`.

     __Example:__

     For __Navicat 12 x64__: High 4 bits must be `1100`, which is the binary of number `12`.
     For __Navicat 11 x64__: High 4 bits must be `1011`, which is the binary of number `11`.

  6. __data[9]__ is unknown, but you can set it by `0xFD`, `0xFC` or `0xFB` if you want to use __not-for-resale license__.

     According to symbol information in __Navicat 12 for Mac x64__ version:

     * `0xFB` is __Not-For-Resale-30-days__ license.
     * `0xFC` is __Not-For-Resale-90-days__ license.
     * `0xFD` is __Not-For-Resale-365-days__ license.
     * `0xFE` is __Not-For-Resale__ license.
     * `0xFF` is __Site__ license.

  After `uint8_t data[10]` is ready, Navicat uses __DES__ with __ECB mode__ to encrypt the last 8 bytes of `uint8_t data[10]` which are from __data[2]__ to __data[9]__.

  The DES key is:

  ```cpp
  unsigned char DESKey = { 0x64, 0xAD, 0xF3, 0x2F, 0xAE, 0xF2, 0x1A, 0x27 };
  ```

  Then use Base32 to encode `uint8_t data[10]` whose encode table is

  ```cpp
  char EncodeTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  ```

  After encoding, you will get a 16-char-long string starting with `"NAV"`.

  Finally, divide the 16-char-long string to four 4-chars-long blocks and join them with `"-"` then you will get __snKey__.

## 2. Activation Process

1. Check whether __snKey__ that user inputs is valid.

2. After user clicks `Activate`, Navicat will start online activation first. If fails, user can choose offline activation.

3. Navicat will use the __snKey__ that user inputs and some information collected from user's machine to generate __Offline Activation Request Information__. Then Navicat will encrypt it by __Navicat Activation Public Key__ and return a Base64-encoded string as __Request Code__.

4. In legal way, the __Request Code__ should be sent to Navicat official activation server by a Internet-accessible computer. And Navicat official activation server will return a legal __Activation Code__.

   But now, we use keygen to play the official activation server's role.

   1. According to the __Request Code__, get `"DI"` value and `"K"` value.

   2. Fill __Offline Activation Response Information__ with `"K"` value, name, organization name, `"DI"` value and, if need, `"T"` value.

   3. Encrypt __Offline Activation Response Information__ by __Navicat Activation Private Key__ and you will get 256-byte-long data.

   4. Encode the 256-byte-long data by Base64. The result is __Activation Code__.

   5. After user input __Activation Code__, offline activation is done successfully.


code:
# Navicat keygen - 注册机是怎么工作的？

## 1. 关键词解释.

* __Navicat激活公钥__

  这是一个2048位的RSA公钥，Navicat使用这个公钥来完成相关激活信息的加密和解密。

  这个公钥被作为 __RCData__ 类型的资源储存在 __navicat.exe__ 当中。资源名为`"ACTIVATIONPUBKEY"`。你可以使用一个叫[Resource Hacker](http://www.angusj.com/resourcehacker/)的软件来查看它。这个公钥的具体内容为：

  ```
  -----BEGIN PUBLIC KEY-----
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw1dqF3SkCaAAmMzs889I
  qdW9M2dIdh3jG9yPcmLnmJiGpBF4E9VHSMGe8oPAy2kJDmdNt4BcEygvssEfginv
  a5t5jm352UAoDosUJkTXGQhpAWMF4fBmBpO3EedG62rOsqMBgmSdAyxCSPBRJIOF
  R0QgZFbRnU0frj34fiVmgYiLuZSAmIbs8ZxiHPdp1oD4tUpvsFci4QJtYNjNnGU2
  WPH6rvChGl1IRKrxMtqLielsvajUjyrgOC6NmymYMvZNER3htFEtL1eQbCyTfDmt
  YyQ1Wt4Ot12lxf0wVIR5mcGN7XCXJRHOFHSf1gzXWabRSvmt1nrl7sW6cjxljuuQ
  awIDAQAB
  -----END PUBLIC KEY-----
  ```

  如果您有相应的私钥并乐意公开的话欢迎联系我，我将非常感谢您的慷慨。

  __注意：__

  从 __Navicat Premium 12.0.25__ 开始，Navicat不再从`navicat.exe`的资源中加载私钥。事实上，公钥转为从`libcc.dll`中加载，并且已经被加密。与此同时，为了防止被轻松地替换，加密的公钥被分到5个地方储存：

  以下内容是从 __Navicat Premium x64 12.0.25 简体中文版__ 的`libcc.dll`中发现的，`libcc.dll`的SHA256值为`607e0a84c75966b00f3d12fa833e91d159e4f51ac51b6ba66f98d0c3cbefdce0`。我不保证在Navicat的其他版本中相关偏移量和下述的相同，但相关的 __字符串__ 以及 __立即数__ 是很可能找得到的。

  1. 在`libcc.dll`中，文件偏移量`+0x01A12090`的地方，储存了加密公钥的第一部分，以 __字符串__ 的形式储存：

     ```
     "D75125B70767B94145B47C1CB3C0755E
      7CCB8825C5DCE0C58ACF944E08280140
      9A02472FAFFD1CD77864BB821AE36766
      FEEDE6A24F12662954168BFA314BD950
      32B9D82445355ED7BC0B880887D650F5"
     ```

  2. 在`libcc.dll`中，文件偏移量`+0x0059D799`的地方，储存了加密公钥的第二部分，以 __立即数__ 的形式储存在一条指令中：

     ```
     0xFE 0xEA 0xBC 0x01
     ```

     相应的十进制值为: `29158142`

  3. 在`libcc.dll`中，文件偏移量`+0x01A11DA0`的地方，储存了加密公钥的第三部分，以 __字符串__ 的形式储存：

     ```
     "E1CED09B9C2186BF71A70C0FE2F1E0AE
      F3BD6B75277AAB20DFAF3D110F75912B
      FB63AC50EC4C48689D1502715243A79F
      39FF2DE2BF15CE438FF885745ED54573
      850E8A9F40EE2FF505EB7476F95ADB78
      3B28CA374FAC4632892AB82FB3BF4715
      FCFE6E82D03731FC3762B6AAC3DF1C3B
      C646FE9CD3C62663A97EE72DB932A301
      312B4A7633100C8CC357262C39A2B3A6
      4B224F5276D5EDBDF0804DC3AC4B8351
      62BB1969EAEBADC43D2511D6E0239287
      81B167A48273B953378D3D2080CC0677
      7E8A2364F0234B81064C5C739A8DA28D
      C5889072BF37685CBC94C2D31D0179AD
      86D8E3AA8090D4F0B281BE37E0143746
      E6049CCC06899401264FA471C016A96C
      79815B55BBC26B43052609D9D175FBCD
      E455392F10E51EC162F51CF732E6BB39
      1F56BBFD8D957DF3D4C55B71CEFD54B1
      9C16D458757373E698D7E693A8FC3981
      5A8BF03BA05EA8C8778D38F9873D62B4
      460F41ACF997C30E7C3AF025FA171B5F
      5AD4D6B15E95C27F6B35AD61875E5505
      449B4E"
     ```

  4. 在`libcc.dll`中，文件偏移量`+0x0059D77F`的地方，储存了加密公钥的第四部分，以 __立即数__ 的形式储存在一条指令中：

      ```
      0x59 0x08 0x01 0x00
      ```

     相应的十进制值为: `67673`

  5. 在`libcc.dll`中，文件偏移量`+0x01A11D8C`的地方，储存了加密公钥的第五部分，以 __字符串__ 的形式储存：

      ```
      "92933"
      ```

  这五部分按照`"%s%d%s%d%s"`的形式输出则为加密的公钥，顺序和上述的顺序相同，具体的输出为：

  ```

  D75125B70767B94145B47C1CB3C0755E7CCB8825C5DCE0C58ACF944E082801409A02472FAFFD1CD77864BB821AE36766FEEDE6A24F12662954168BFA314BD95032B9D82445355ED7BC0B880887D650F529158142E1CED09B9C2186BF71A70C0FE2F1E0AEF3BD6B75277AAB20DFAF3D110F75912BFB63AC50EC4C48689D1502715243A79F39FF2DE2BF15CE438FF885745ED54573850E8A9F40EE2FF505EB7476F95ADB783B28CA374FAC4632892AB82FB3BF4715FCFE6E82D03731FC3762B6AAC3DF1C3BC646FE9CD3C62663A97EE72DB932A301312B4A7633100C8CC357262C39A2B3A64B224F5276D5EDBDF0804DC3AC4B835162BB1969EAEBADC43D2511D6E023928781B167A48273B953378D3D2080CC06777E8A2364F0234B81064C5C739A8DA28DC5889072BF37685CBC94C2D31D0179AD86D8E3AA8090D4F0B281BE37E0143746E6049CCC06899401264FA471C016A96C79815B55BBC26B43052609D9D175FBCDE455392F10E51EC162F51CF732E6BB391F56BBFD8D957DF3D4C55B71CEFD54B19C16D458757373E698D7E693A8FC39815A8BF03BA05EA8C8778D38F9873D62B4460F41ACF997C30E7C3AF025FA171B5F5AD4D6B15E95C27F6B35AD61875E5505449B4E6767392933

  ```

  这个加密的公钥可以用我的另外一个repo（[how-does-navicat-encrypt-password](https://github.com/DoubleLabyrinth/how-does-navicat-encrypt-password)）解密，其中密钥为`b'23970790'`。

  例如：

  ```cmd
  E:\GitHub>git clone https://github.com/DoubleLabyrinth/how-does-navicat-encrypt-password.git
  ...
  E:\GitHub>cd how-does-navicat-encrypt-password\python3
  E:\GitHub\how-does-navicat-encrypt-password\python3>python
  Python 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
  Type "help", "copyright", "credits" or "license" for more information.
  >>> from NavicatCrypto import *
  >>> cipher = Navicat11Crypto(b'23970790')
  >>> print(cipher.DecryptString('D75125B70767B94145B47C1CB3C0755E7CCB8825C5DCE0C58ACF944E082801409A02472FAFFD1CD77864BB821AE36766FEEDE6A24F12662954168BFA314BD95032B9D82445355ED7BC0B880887D650F529158142E1CED09B9C2186BF71A70C0FE2F1E0AEF3BD6B75277AAB20DFAF3D110F75912BFB63AC50EC4C48689D1502715243A79F39FF2DE2BF15CE438FF885745ED54573850E8A9F40EE2FF505EB7476F95ADB783B28CA374FAC4632892AB82FB3BF4715FCFE6E82D03731FC3762B6AAC3DF1C3BC646FE9CD3C62663A97EE72DB932A301312B4A7633100C8CC357262C39A2B3A64B224F5276D5EDBDF0804DC3AC4B835162BB1969EAEBADC43D2511D6E023928781B167A48273B953378D3D2080CC06777E8A2364F0234B81064C5C739A8DA28DC5889072BF37685CBC94C2D31D0179AD86D8E3AA8090D4F0B281BE37E0143746E6049CCC06899401264FA471C016A96C79815B55BBC26B43052609D9D175FBCDE455392F10E51EC162F51CF732E6BB391F56BBFD8D957DF3D4C55B71CEFD54B19C16D458757373E698D7E693A8FC39815A8BF03BA05EA8C8778D38F9873D62B4460F41ACF997C30E7C3AF025FA171B5F5AD4D6B15E95C27F6B35AD61875E5505449B4E6767392933'))
  -----BEGIN PUBLIC KEY-----
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw1dqF3SkCaAAmMzs889I
  qdW9M2dIdh3jG9yPcmLnmJiGpBF4E9VHSMGe8oPAy2kJDmdNt4BcEygvssEfginv
  a5t5jm352UAoDosUJkTXGQhpAWMF4fBmBpO3EedG62rOsqMBgmSdAyxCSPBRJIOF
  R0QgZFbRnU0frj34fiVmgYiLuZSAmIbs8ZxiHPdp1oD4tUpvsFci4QJtYNjNnGU2
  WPH6rvChGl1IRKrxMtqLielsvajUjyrgOC6NmymYMvZNER3htFEtL1eQbCyTfDmt
  YyQ1Wt4Ot12lxf0wVIR5mcGN7XCXJRHOFHSf1gzXWabRSvmt1nrl7sW6cjxljuuQ
  awIDAQAB
  -----END PUBLIC KEY-----
  ```

  __注意：__

  从 __Navicat Premium 12.1.11__ 开始，Navicat不在用上面说的方法加载密钥。当然密钥还是储存在`libcc.dll`文件中。当Navicat启动时，它会用8字节长的XOR密钥来加密公钥，并储存到一个静态数据区中。当验证 __激活码__ 时，Navicat会重新生成一样的8字节XOR密钥，并解密在静态储存区中的密文，从而获取公钥。

  在`libcc.dll`，x64版本中，你会看到如下的几条指令：

  ```asm
  xor eax, 'M'
  mov byte_xxxxxx, al
  ...
  xor eax, 'I'
  mov byte_xxxxxx, al
  ...
  xor eax, 'I'
  mov byte_xxxxxx, al
  ...
  xor eax, 'B'
  mov byte_xxxxxx, al
  ...
  xor eax, 'I'
  mov byte_xxxxxx, al
  ...
  xor eax, 'j'
  mov byte_xxxxxx, al
  ...
  ...
  ```

* __请求码__

  这是一个Base64编码的字符串，代表的是长度为256字节的数据。这256字节的数据是 __离线激活信息__ 用 __Navicat激活公钥__ 加密的密文。

* __离线激活请求信息__

  这是一个JSON风格的字符串。它包含了3个Key：`"K"`、`"DI"`和`"P"`，分别代表 __序列号__、__设备识别码__（与你的电脑硬件信息相关）和 __平台__ (其实就是操作系统类型)。

  例如：

  ```
  {"K": "xxxxxxxxxxxxxxxx", "DI": "yyyyyyyyyyyyy", "P": "WIN8"}
  ```

* __激活码__

  这是一个Base64编码的字符串，代表的是长度为256字节的数据。这256字节的数据是 __离线激活回复信息__ 用 __Navicat激活私钥__ 加密的密文。目前我们不知道官方的 __Navicat激活私钥__，所以我们得替换掉软件里的公钥。

* __离线激活回复信息__

  和 __离线激活请求信息__ 一样，它也是一个JSON风格的字符串。但是它包含5个Key，分别为`"K"`、`"N"`、`"O"`、`"T"` 和 `"DI"`.

  `"K"` 和 `"DI"` 的意义与 __离线激活请求信息__ 中的相同，且Value必须与 __离线激活请求信息__ 中的相同。

  `"N"`、`"O"`、`"T"` 分别代表 __注册名__、__组织__、__授权时间__。

  __注册名__ 和 __组织__ 的值类型为UTF-8编码的字符串。__授权时间__ 的值类型可以为字符串或整数（感谢@Wizr在issue #10中的报告）。

    `"T"` 可以被省略。

* __序列号__

  这是一个被分为了4个部分的字符串，其中每个部分都是4个字符长。

  __序列号__ 是通过10个字节的数据来生成的。为了表达方便，我用 __uint8_t data[10]__ 来表示这10个字节。

  1. __data[0]__ 和 __data[1]__ 必须分别为 `0x68` 和 `0x2A`。

     这两个字节为Navicat的标志数。

  2. __data[2]__、__data[3]__ 和 __data[4]__ 可以是任意字节，你想设成什么都行。

  3. __data[5]__ 和 __data[6]__ 是Navicat的语言标志，值如下：

     |  语言类型   |  data[5]  |  data[6]  |  发现者         |
     |------------|:---------:|:---------:|-----------------|
     |  English   |  0xAC     |  0x88     |                 |
     |  简体中文   |  0xCE     |  0x32     |                 |
     |  繁體中文   |  0xAA     |  0x99     |                 |
     |  日本語     |  0xAD     |  0x82     |  @dragonflylee  |
     |  Polski    |  0xBB     |  0x55     |  @dragonflylee  |
     |  Español   |  0xAE     |  0x10     |  @dragonflylee  |
     |  Français  |  0xFA     |  0x20     |  @Deltafox79    |
     |  Deutsch   |  0xB1     |  0x60     |  @dragonflylee  |
     |  한국어     |  0xB5     |  0x60     |  @dragonflylee  |
     |  Русский   |  0xEE     |  0x16     |  @dragonflylee  |
     |  Português |  0xCD     |  0x49     |  @dragonflylee  |

  4. __data[7]__ 是Navicat产品ID。（感谢 @dragonflylee 和 @Deltafox79提供的数据）

     |产品名                |Enterprise|Standard|Educational|Essentials|
     |---------------------|:--------:|:------:|:---------:|:--------:|
     |Navicat Report Viewer|0x0B      |        |           |          |
     |Navicat Data Modeler |          |0x47    |0x4A       |          |
     |Navicat Premium      |0x65      |        |0x66       |0x67      |
     |Navicat MySQL        |0x68      |0x69    |0x6A       |0x6B      |
     |Navicat PostgreSQL   |0x6C      |0x6D    |0x6E       |0x6F      |
     |Navicat Oracle       |0x70      |0x71    |0x72       |0x73      |
     |Navicat SQL Server   |0x74      |0x75    |0x76       |0x77      |
     |Navicat SQLite       |0x78      |0x79    |0x7A       |0x7B      |
     |Navicat MariaDB      |0x7C      |0x7D    |0x7E       |0x7F      |
     |Navicat MongoDB      |0x80      |0x81    |0x82       |          |

  5. __data[8]__ 的高4位代表 __版本号__。低4位未知，但可以用来延长激活期限，可取的值有`0000`和`0001`。

     例如：

     对于 __Navicat 12__: 高4位必须是`1100`，为`12`的二进制形式。
     对于 __Navicat 11__: 高4位必须是`1011`，为`11`的二进制形式。

  6. __data[9]__ 目前暂未知，但如果你想要 __not-for-resale license__ 的话可以设成`0xFD`、`0xFC`或`0xFB`。

     根据 __Navicat 12 for Mac x64__ 版本残留的符号信息可知：

     * `0xFB`是 __Not-For-Resale-30-days__ license.
     * `0xFC`是 __Not-For-Resale-90-days__ license.
     * `0xFD`是 __Not-For-Resale-365-days__ license.
     * `0xFE`是 __Not-For-Resale__ license.
     * `0xFF`是 __Site__ license.

  之后Navicat使用 __ECB__ 模式的 __DES__ 算法来加密 __data[10]__ 的后8字节，也就是 __data[2]__ 到 __data[9]__ 的部分。

  相应的DES密钥为：

  ```cpp
  unsigned char DESKey = { 0x64, 0xAD, 0xF3, 0x2F, 0xAE, 0xF2, 0x1A, 0x27 };
  ```

  之后使用Base32编码 __data[10]__，其中编码表改为：

  ```cpp
  char EncodeTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  ```

  编码之后你应该会得到一个16字节长的字符串，并且以"NAV"打头。

  将16字节的字符串分成4个4字节的小块，然后用`"-"`连接就可以得到 __序列号__。

## 2. 激活过程

  1. 检查用户输入的 __序列号__ 是否合法。

  2. 在用户点击了`激活`按钮之后，Navicat会先尝试在线激活。如果失败，用户可以选择离线激活。

  3. Navicat会使用用户输入的 __序列号__ 以及从用户电脑收集来的信息生成 __离线激活请求信息__，然后用 __Navicat激活公钥__ 加密，并将密文用Base64编码，最后得到 __请求码__。

  4. 正常流程下，__请求码__ 应该通过可联网的电脑发送给Navicat的官方激活服务器。之后Navicat的官方激活服务器会返回一个合法的 __激活码__。

     但现在我们使用注册机来扮演官方激活服务器的角色，只是Navicat软件里的激活公钥得换成自己的公钥：

     1. 根据 __请求码__, 获得`"DI"`值和`"K"`值。

     2. 用`"K"`值、用户名、组织名和`"DI"`值填写 __离线激活回复信息__。

     3. 用自己的2048位RSA私钥加密 __离线激活回复信息__，你将会得到256字节的密文。

     4. 用Base64编码这256字节的密文，就可以得到 __激活码__。

     5. 在Navicat软件中填入 __激活码__ 即可完成离线激活。


code:
# Navicat Keygen

[中文版README](README.zh-CN.md)

This repository will tell you how Navicat offline activation works.

[How does it work?](HOW_DOES_IT_WORK.md)

## How to use?

For Windows users, see [here](README_FOR_WINDOWS.md).

For Linux users, see [here](README_FOR_LINUX.md).

## Contributor

* Deltafox79

* dragonflylee

* zenuo


code:
# Navicat Keygen

这份repo将会告诉你Navicat是怎么完成离线激活的。

[注册机是怎么工作的?](HOW_DOES_IT_WORK.zh-CN.md)

## 如何使用这个注册机

针对 Windows 用户，请见[这里](README_FOR_WINDOWS.zh-CN.md)。

针对 Linux 用户，请见[这里](README_FOR_LINUX.zh-CN.md)。

## 贡献者

* Deltafox79

* dragonflylee

* zenuo