Files
ckb-next/src/gui/animscript.cpp
T
2020-09-03 02:54:27 +03:00

502 lines
18 KiB
C++

#include <cmath>
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QUrl>
#include <ckbnextconfig.h>
#include "animscript.h"
#include <QStandardPaths>
QHash<QUuid, AnimScript*> AnimScript::scripts;
AnimScript::AnimScript(QObject* parent, const QString& path) :
QObject(parent), _path(path), initialized(false), process(0)
{
}
AnimScript::AnimScript(QObject* parent, const AnimScript& base) :
QObject(parent), _info(base._info), _path(base._path), initialized(false), process(0)
{
}
AnimScript::~AnimScript(){
if(process){
process->kill();
process->waitForFinished(1000);
delete process;
}
}
QStringList AnimScript::paths(){
QStringList list(CKB_NEXT_ANIMATIONS_PATH);
// Path for when running the GUI from the build dir
#ifdef Q_OS_LINUX
QString appDirPath = QCoreApplication::applicationDirPath();
if(!appDirPath.startsWith("/usr"))
list << appDirPath;
#endif
list << QDir::toNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation).append("/ckb-next/animations"));
return list;
}
void AnimScript::scan(){
// Clear old animations
foreach(AnimScript* script, scripts)
delete script;
scripts.clear();
// Search for animations in all directories, in reverse
// This is done so that the user animation dir takes priority over the system one
for(int i = paths().length() - 1; i >= 0; i--){
QDir dir(paths().at(i));
if(!dir.exists())
continue;
foreach(QString file, dir.entryList(QDir::Files | QDir::Executable)){
#ifdef Q_OS_LINUX
// Ignore the GUI and daemon binaries if loading animations from the build dir
if(file.endsWith("ckb-next") || file.endsWith("ckb-next-daemon"))
continue;
#endif
AnimScript* script = new AnimScript(qApp, dir.absoluteFilePath(file));
if(script->load() && !scripts.contains(script->_info.guid) && script->presets().count()){
scripts[script->_info.guid] = script;
continue;
}
if(!script->presets().count())
qWarning() << script->name() << "has no default preset and will not be loaded.";
delete script;
}
}
}
QList<const AnimScript*> AnimScript::list(){
// Gather the animations into an alphabetically-sorted list
QMap<QString, const AnimScript*> result;
foreach(AnimScript* script, scripts.values()){
QString name = script->name();
if(result.contains(name)){
// If duplicate names exist, make them unique by including their GUIDs
AnimScript* last = (AnimScript*)result[name];
last->_info.name += " " + last->guid().toString().toUpper();
script->_info.name += " " + script->guid().toString().toUpper();
}
result[script->name()] = script;
}
return result.values();
}
AnimScript* AnimScript::copy(QObject* parent, const QUuid& id){
return scripts.contains(id) ? new AnimScript(parent, *scripts.value(id)) : 0;
}
inline QString urlParam(const QString& param){
return QUrl::fromPercentEncoding(param.trimmed().toLatin1()).trimmed();
}
const static double ONE_DAY = 24. * 60. * 60.;
bool AnimScript::load(){
// Run the process to get script info
QProcess infoProcess;
infoProcess.start(_path, QStringList("--ckb-info"));
qDebug() << "Scanning" << _path;
infoProcess.waitForFinished(1000);
if(infoProcess.state() == QProcess::Running){
// Kill the process if it takes more than 1s
infoProcess.kill();
return false;
}
// Set defaults for performance info
_info.kpMode = KP_NONE;
_info.absoluteTime = _info.preempt = _info.liveParams = false;
_info.repeat = true;
// Read output
QString line;
while((line = infoProcess.readLine()) != ""){
line = line.trimmed();
QStringList components = line.split(" ");
int count = components.count();
if(count < 2)
continue;
QString param = components[0].trimmed();
if(param == "guid")
_info.guid = QUuid(urlParam(components[1]));
else if(param == "name")
_info.name = urlParam(components[1]);
else if(param == "version")
_info.version = urlParam(components[1]);
else if(param == "year")
_info.year = urlParam(components[1]);
else if(param == "author")
_info.author = urlParam(components[1]);
else if(param == "license")
_info.license = urlParam(components[1]);
else if(param == "description")
_info.description = urlParam(components[1]);
else if(param == "kpmode")
_info.kpMode = (components[1] == "position") ? KP_POSITION : (components[1] == "name") ? KP_NAME : KP_NONE;
else if(param == "time")
_info.absoluteTime = (components[1] == "absolute");
else if(param == "repeat")
_info.repeat = (components[1] == "on");
else if(param == "preempt")
_info.preempt = (components[1] == "on");
else if(param == "parammode")
_info.liveParams = (components[1] == "live");
else if(param == "param"){
// Read parameter
if(count < 3)
continue;
while(components.count() < 8)
components.append("");
Param::Type type = Param::INVALID;
QString sType = components[1].toLower();
if(sType == "long")
type = Param::LONG;
else if(sType == "double")
type = Param::DOUBLE;
else if(sType == "bool")
type = Param::BOOL;
else if(sType == "rgb")
type = Param::RGB;
else if(sType == "argb")
type = Param::ARGB;
else if(sType == "gradient")
type = Param::GRADIENT;
else if(sType == "agradient")
type = Param::AGRADIENT;
else if(sType == "angle")
type = Param::ANGLE;
else if(sType == "string")
type = Param::STRING;
else if(sType == "label")
type = Param::LABEL;
else
continue;
// "param <type> <name> <prefix> <postfix> <default>"
QString name = components[2].toLower();
// Make sure it's not present already
if(hasParam(name))
continue;
QString prefix = urlParam(components[3]), postfix = urlParam(components[4]);
QVariant def = urlParam(components[5]), minimum = urlParam(components[6]), maximum = urlParam(components[7]);
// Don't allow predefined params
if(name == "trigger" || name == "kptrigger" || name == "kpmode" || name == "duration" || name == "delay" || name == "kpdelay" || name == "repeat" || name == "kprepeat" || name == "stop" || name == "kpstop" || name == "kpmodestop" || name == "kprelease")
continue;
Param parsed_param = { type, name, prefix, postfix, def, minimum, maximum };
_info.params.append(parsed_param);
} else if(param == "preset"){
// Add preset
QString name = urlParam(components.at(1));
QMap<QString, QVariant> preset;
for(int i = 2; i < count; i++){
// Scan name/value setting pairs
QString setting = components.at(i);
QStringList sComponents = setting.split("=");
if(sComponents.count() != 2)
continue;
QString sParam = sComponents.first().trimmed();
QString value = urlParam(sComponents.last());
preset[sParam] = value;
}
// If the preset contains a duration, set the repeat time to the same value
if(preset.contains("duration")){
QVariant duration = preset.value("duration");
preset["repeat"] = duration;
preset["kprepeat"] = duration;
}
_presets.append(name);
_presetValues.append(preset);
}
}
// Make sure the required parameters are filled out
if(_info.guid.isNull() || _info.name == "" || _info.version == "" || _info.year == "" || _info.author == "" || _info.license == "")
return false;
// Add timing parameters
double defaultDuration = -1.;
if(!_info.absoluteTime){
defaultDuration = 1.;
Param duration = { Param::DOUBLE, "duration", "", "", defaultDuration, 0.1, ONE_DAY };
_info.params.append(duration);
}
Param trigger = { Param::BOOL, "trigger", "", "", true, 0, 0 };
_info.params.append(trigger);
Param kptrigger = { Param::BOOL, "kptrigger", "", "", false, 0, 0 };
_info.params.append(kptrigger);
if(_info.absoluteTime || !_info.repeat)
_info.preempt = false;
Param kpmode = { Param::LONG, "kpmode", "", "", 1, 0, 0 };
if(_info.kpMode)
kpmode.defaultValue = 0;
_info.params.append(kpmode);
Param delay = { Param::DOUBLE, "delay", "", "", 0., 0., ONE_DAY };
Param kpdelay = { Param::DOUBLE, "kpdelay", "", "", 0., 0., ONE_DAY };
_info.params.append(delay);
_info.params.append(kpdelay);
Param kpmodestop = { Param::BOOL, "kpmodestop", "", "", false, 0, 0 };
Param kprelease = { Param::BOOL, "kprelease", "", "", false, 0, 0 };
_info.params.append(kpmodestop);
_info.params.append(kprelease);
if(_info.repeat){
Param repeat = { Param::DOUBLE, "repeat", "", "", defaultDuration, 0.1, ONE_DAY };
Param kprepeat = { Param::DOUBLE, "kprepeat", "", "", defaultDuration, 0.1, ONE_DAY };
// When repeats are enabled, stop and kpstop are LONG values (number of repeats)
Param stop = { Param::LONG, "stop", "", "", -1, 0, 1000 };
Param kpstop = { Param::LONG, "kpstop", "", "", 0, 0, 1000 };
_info.params.append(repeat);
_info.params.append(kprepeat);
_info.params.append(stop);
_info.params.append(kpstop);
} else {
// When repeats are disabled, stop and kpstop are DOUBLE values (seconds)
Param stop = { Param::DOUBLE, "stop", "", "", -1., 0.1, ONE_DAY };
Param kpstop = { Param::DOUBLE, "kpstop", "", "", -1., 0.1, ONE_DAY };
_info.params.append(stop);
_info.params.append(kpstop);
}
return true;
}
void AnimScript::init(const KeyMap& map, const QStringList& keys, const QMap<QString, QVariant>& paramValues){
if(_path == "")
return;
end();
_map = map;
_colors.init(map);
_colorBuffer.init(map);
_keys = keys;
_paramValues = paramValues;
setDuration();
stopped = firstFrame = false;
initialized = true;
}
void AnimScript::setDuration(){
if(_info.absoluteTime){
durationMsec = 1000;
repeatMsec = 0;
} else {
durationMsec = round(_paramValues.value("duration").toDouble() * 1000.);
if(durationMsec <= 0)
durationMsec = -1;
repeatMsec = round(_paramValues.value("repeat").toDouble() * 1000.);
}
}
void AnimScript::parameters(const QMap<QString, QVariant>& paramValues){
if(!initialized || !process || !_info.liveParams)
return;
_paramValues = paramValues;
setDuration();
printParams();
}
void AnimScript::printParams(){
process->write("begin params\n");
QMapIterator<QString, QVariant> i(_paramValues);
while(i.hasNext()){
i.next();
process->write("param ");
process->write(i.key().toLatin1());
process->write(" ");
process->write(QUrl::toPercentEncoding(i.value().toString()));
process->write("\n");
}
process->write("end params\n");
}
int AnimScript::begin(quint64 timestamp){
if(!initialized)
return 1;
end();
stopped = firstFrame = readFrame = readAnyFrame = inFrame = false;
// Determine the upper left corner of the given keys
QStringList keysCopy = _keys;
minX = INT_MAX;
minY = INT_MAX;
foreach(const QString& key, keysCopy){
const Key& pos = _map.key(key);
if(!pos){
keysCopy.removeAll(key);
continue;
}
if(pos.x < minX)
minX = pos.x;
if(pos.y < minY)
minY = pos.y;
}
if(keysCopy.isEmpty()){
// If the key list is empty, don't actually start the animation but pretend it's running anyway
firstFrame = readFrame = readAnyFrame = true;
return 1;
}
process = new QProcess(this);
process->setReadChannel(QProcess::StandardOutput);
connect(process, SIGNAL(readyReadStandardOutput()), this, SLOT(readProcess()));
connect(process, SIGNAL(readyReadStandardError()), this, SLOT(readProcessErr()));
process->start(_path, QStringList("--ckb-run"));
qDebug() << "Starting" << _path;
// Write the keymap to the process
process->write("begin keymap\n");
process->write(QString("keycount %1\n").arg(keysCopy.count()).toLatin1());
foreach(const QString& key, keysCopy){
const Key& pos = _map.key(key);
process->write(QString("key %1 %2,%3\n").arg(key).arg(pos.x - minX).arg(pos.y - minY).toLatin1());
}
process->write("end keymap\n");
// Write parameters
printParams();
// Begin animating
process->write("begin run\n");
lastFrame = timestamp;
return 0;
}
void AnimScript::retrigger(quint64 timestamp, bool allowPreempt){
if(!initialized)
return;
if(allowPreempt && _info.preempt && repeatMsec > 0)
// If preemption is wanted, trigger the animation 1 duration in the past first
retrigger(timestamp - repeatMsec);
if(!process && begin(timestamp))
return;
advance(timestamp);
process->write("start\n");
}
void AnimScript::stop(quint64 timestamp){
if(!initialized)
return;
if(!process && begin(timestamp))
return;
advance(timestamp);
process->write("stop\n");
}
void AnimScript::keypress(const QString& key, bool pressed, quint64 timestamp){
if(!initialized)
return;
if(!process && begin(timestamp))
return;
int kpMode = _info.kpMode;
if(_paramValues.value("kpmode", 0).toInt() != 0)
// Disable KP mode according to user preferences
kpMode = KP_NONE;
switch(kpMode){
case KP_NONE:
// If KPs aren't allowed, call retrigger/stop instead
if(pressed)
retrigger(timestamp);
else if(_paramValues.value("kprelease", false).toBool())
stop(timestamp);
break;
case KP_NAME:
// Print keypress by name
advance(timestamp);
process->write(("key " + key + (pressed ? " down\n" : " up\n")).toLatin1());
break;
case KP_POSITION:
// Print keypress by position
const Key& kp = _map.key(key);
if(!kp)
return;
advance(timestamp);
process->write(("key " + QString("%1,%2").arg(kp.x - minX).arg(kp.y - minY) + (pressed ? " down\n" : " up\n")).toLatin1());
break;
}
}
void AnimScript::end(){
_colors.clear();
if(process){
process->kill();
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
disconnect(process, SIGNAL(readyReadStandardOutput()), this, SLOT(readProcess()));
process = 0;
}
}
void AnimScript::readProcessErr(){
QList<QByteArray> data = process->readAllStandardError().split('\n');
#if QT_VERSION >= QT_VERSION_CHECK(5, 3, 0)
QString out = _info.name + " (" + QString::number(process->processId()) + ")";
#else
QString out = _info.name;
#endif
for(int i = 0; i < data.length(); i++){
QString str(data.at(i));
if(str.isEmpty())
continue;
qDebug() << out << str;
}
}
void AnimScript::readProcess(){
while(process->canReadLine()){
QByteArray line = process->readLine().trimmed();
if(!inFrame){
// Ignore anything not between "begin frame" and "end frame", except for "end run", which indicates that the program is done.
if(line == "begin frame")
inFrame = true;
else if(line == "end run"){
stopped = true;
return;
}
continue;
}
if(line.startsWith("argb ")){
// Add a color to the buffer
char keyName[31];
QRgb keyColor = 0;
if(sscanf(line, "argb %30s %x", keyName, &keyColor) != 2)
continue;
QRgb* inMap = _colorBuffer.colorForName(keyName);
if(!inMap)
continue;
*inMap = keyColor;
}
if(line == "end frame"){
// Frame is finished. Copy color buffer back to the atomic map
memcpy(_colors.colors(), _colorBuffer.colors(), sizeof(QRgb) * _colors.count());
inFrame = false;
readFrame = readAnyFrame = true;
}
}
}
void AnimScript::frame(quint64 timestamp){
if(!initialized || stopped)
return;
// Start the animation if it's not running yet
if(!process)
begin(timestamp);
if(process){
advance(timestamp);
// Don't ask for a new frame if the animation hasn't delivered the last one yet
if(readFrame || !firstFrame)
process->write("frame\n");
}
firstFrame = true;
readFrame = false;
}
void AnimScript::advance(quint64 timestamp){
// Don't do anything if the time hasn't actually advanced.
if(timestamp <= lastFrame)
return;
double delta = (timestamp - lastFrame) / (double)durationMsec;
if(!_info.absoluteTime){
// Skip any complete durations
while(delta > 1.){
process->write("time 1\n");
delta--;
}
}
process->write(QString("time %1\n").arg(delta).toLatin1());
lastFrame = timestamp;
}