* Fix/improve margin/padding issues, consistent between inp/mix views, better x-platform and older Qt (closes #4599, closes #4341); * Allow dropping onto lines instead of only between them; * Fix dropped data always going to line above the actual drop target (closes #4622); * Always force DnD move operations between windows to copy instead (fixes deletion of any selected line in target window, and source line was never actually removed but looked like it was); * Add alternate-row coloring for each Input/Mixer group; * Fix non-contrasting selected row font color on Linux & OS X (was black on blue); * Add actions to change font size (size persists until app close); * Show inactive lines with dimmer font for input/channel name; * Make drop indicator more obvious; * Restore mouse hover highlight (Win), which also helps indicate actual drop target (also re: #4622); * cosmetics: Code indentation in MixersListWidget.
554 lines
16 KiB
C++
554 lines
16 KiB
C++
/*
|
|
* Copyright (C) OpenTX
|
|
*
|
|
* Based on code named
|
|
* th9x - http://code.google.com/p/th9x
|
|
* er9x - http://code.google.com/p/er9x
|
|
* gruvin9x - http://code.google.com/p/gruvin9x
|
|
*
|
|
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#include "mixes.h"
|
|
#include "helpers.h"
|
|
|
|
MixesPanel::MixesPanel(QWidget *parent, ModelData & model, GeneralSettings & generalSettings, Firmware * firmware):
|
|
ModelPanel(parent, model, generalSettings, firmware),
|
|
mixInserted(false),
|
|
highlightedSource(0),
|
|
modelPrinter(firmware, generalSettings, model)
|
|
{
|
|
QGridLayout * mixesLayout = new QGridLayout(this);
|
|
|
|
mixersListWidget = new MixersListWidget(this, false); // TODO enum
|
|
QPushButton * qbUp = new QPushButton(this);
|
|
QPushButton * qbDown = new QPushButton(this);
|
|
QPushButton * qbClear = new QPushButton(this);
|
|
qbUp->setText(tr("Move Up"));
|
|
qbUp->setIcon(CompanionIcon("moveup.png"));
|
|
qbUp->setShortcut(QKeySequence(tr("Ctrl+Up")));
|
|
qbDown->setText(tr("Move Down"));
|
|
qbDown->setIcon(CompanionIcon("movedown.png"));
|
|
qbDown->setShortcut(QKeySequence(tr("Ctrl+Down")));
|
|
qbClear->setText(tr("Clear Mixes"));
|
|
qbClear->setIcon(CompanionIcon("clear.png"));
|
|
|
|
mixesLayout->addWidget(mixersListWidget,1,1,1,3);
|
|
mixesLayout->addWidget(qbUp,2,1);
|
|
mixesLayout->addWidget(qbClear,2,2);
|
|
mixesLayout->addWidget(qbDown,2,3);
|
|
|
|
connect(mixersListWidget,SIGNAL(customContextMenuRequested(QPoint)),this,SLOT(mixerlistWidget_customContextMenuRequested(QPoint)));
|
|
connect(mixersListWidget,SIGNAL(doubleClicked(QModelIndex)),this,SLOT(mixerlistWidget_doubleClicked(QModelIndex)));
|
|
connect(mixersListWidget,SIGNAL(mimeDropped(int,const QMimeData*,Qt::DropAction)),this,SLOT(mimeMixerDropped(int,const QMimeData*,Qt::DropAction)));
|
|
|
|
connect(qbUp,SIGNAL(pressed()),SLOT(moveMixUp()));
|
|
connect(qbDown,SIGNAL(pressed()),SLOT(moveMixDown()));
|
|
connect(qbClear,SIGNAL(pressed()),SLOT(clearMixes()));
|
|
|
|
connect(mixersListWidget,SIGNAL(keyWasPressed(QKeyEvent*)), this, SLOT(mixerlistWidget_KeyPress(QKeyEvent*)));
|
|
}
|
|
|
|
MixesPanel::~MixesPanel()
|
|
{
|
|
}
|
|
|
|
void MixesPanel::update()
|
|
{
|
|
mixersListWidget->clear();
|
|
|
|
unsigned int curDest = 0;
|
|
unsigned int outputs = firmware->getCapability(Outputs);
|
|
|
|
for (int i=0; i<firmware->getCapability(Mixes); i++) {
|
|
MixData & mix = model->mixData[i];
|
|
if (mix.destCh == 0 || mix.destCh > outputs) {
|
|
// we reached the end of the mixes list
|
|
break;
|
|
}
|
|
QString str = "";
|
|
while (curDest < mix.destCh-1) {
|
|
curDest++;
|
|
AddMixerLine(-int(curDest));
|
|
}
|
|
if (AddMixerLine(i)) {
|
|
curDest++;
|
|
}
|
|
}
|
|
|
|
while (curDest < outputs) {
|
|
curDest++;
|
|
AddMixerLine(-int(curDest));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Creates new mixer line (list item) and adds it to the list widget
|
|
|
|
@note Mixer lines are now HTML formated in order to support bold text.
|
|
|
|
@param[in] dest defines which mixer line to create.
|
|
If dest < 0 then create empty channel slotr fo channel -dest ( dest=-2 -> CH2)
|
|
if dest >=0 then create used channel based on model mix data from slot dest (dest=4 -> model mix[4])
|
|
|
|
@retval true destination channel is different from the previous list item
|
|
false destination channle is the same as previous list item
|
|
*/
|
|
bool MixesPanel::AddMixerLine(int dest)
|
|
{
|
|
bool new_ch;
|
|
QString str = getMixerText(dest, &new_ch);
|
|
QListWidgetItem *itm = new QListWidgetItem(str);
|
|
QByteArray qba(1, (quint8)dest);
|
|
unsigned destId = abs(dest);
|
|
bool hasSibs = false;
|
|
if (dest >= 0) {
|
|
//add mix data
|
|
MixData * md = &model->mixData[dest];
|
|
qba.append((const char*)md, sizeof(MixData));
|
|
destId = md->destCh;
|
|
hasSibs = (dest < CPN_MAX_MIXERS && model->mixData[dest+1].destCh == md->destCh);
|
|
}
|
|
itm->setData(Qt::UserRole, qba);
|
|
mixersListWidget->addItem(itm, destId, new_ch, hasSibs);
|
|
return new_ch;
|
|
}
|
|
|
|
/**
|
|
@brief Returns HTML formated mixer representation
|
|
|
|
@param[in] dest defines which mixer line to create.
|
|
If dest < 0 then create empty channel slot for channel -dest ( dest=-2 -> CH2)
|
|
if dest >=0 then create used channel based on model mix data from slot dest (dest=4 -> model mix[4])
|
|
|
|
@retval string mixer line in HTML
|
|
*/
|
|
QString MixesPanel::getMixerText(int dest, bool * new_ch)
|
|
{
|
|
QString str;
|
|
bool newChannel = false;
|
|
if (dest < 0) {
|
|
dest = abs(dest);
|
|
str = modelPrinter.printChannelName(dest-1);
|
|
//highlight channel if needed
|
|
if (dest == (int)highlightedSource)
|
|
str = "<b>" + str + "</b>";
|
|
newChannel = true;
|
|
}
|
|
else {
|
|
MixData & mix = model->mixData[dest];
|
|
//mix->destCh from 1 to 32
|
|
str = modelPrinter.printChannelName(mix.destCh-1);
|
|
|
|
if ((dest == 0) || (model->mixData[dest-1].destCh != mix.destCh)) {
|
|
newChannel = true;
|
|
//highlight channel if needed
|
|
if (mix.destCh == highlightedSource)
|
|
str = "<b>" + str + "</b>";
|
|
}
|
|
else {
|
|
str.fill(' ');
|
|
}
|
|
|
|
str += modelPrinter.printMixerLine(mix, !newChannel, highlightedSource);
|
|
}
|
|
if (new_ch)
|
|
*new_ch = newChannel;
|
|
return str.replace(" ", " ");
|
|
}
|
|
|
|
bool MixesPanel::gm_insertMix(int idx)
|
|
{
|
|
if (idx < 0 || idx >= firmware->getCapability(Mixes) || model->mixData[firmware->getCapability(Mixes)-1].destCh > 0) {
|
|
QMessageBox::information(this, "companion", tr("Not enough available mixers!"));
|
|
return false;
|
|
}
|
|
|
|
int i = model->mixData[idx].destCh;
|
|
memmove(&model->mixData[idx+1], &model->mixData[idx],
|
|
(firmware->getCapability(Mixes)-(idx+1)) * sizeof(MixData) );
|
|
memset(&model->mixData[idx], 0, sizeof(MixData));
|
|
model->mixData[idx].srcRaw = RawSource(SOURCE_TYPE_NONE);
|
|
model->mixData[idx].destCh = i;
|
|
model->mixData[idx].weight = 100;
|
|
return true;
|
|
}
|
|
|
|
void MixesPanel::gm_deleteMix(int index)
|
|
{
|
|
memmove(&model->mixData[index], &model->mixData[index+1],
|
|
(firmware->getCapability(Mixes)-(index+1)) * sizeof(MixData));
|
|
memset(&model->mixData[firmware->getCapability(Mixes)-1], 0, sizeof(MixData));
|
|
}
|
|
|
|
void MixesPanel::gm_openMix(int index)
|
|
{
|
|
if(index < 0 || index>=firmware->getCapability(Mixes)) return;
|
|
|
|
MixData mixd(model->mixData[index]);
|
|
|
|
MixerDialog *g = new MixerDialog(this, *model, &mixd, generalSettings, firmware);
|
|
if(g->exec()) {
|
|
model->mixData[index] = mixd;
|
|
emit modified();
|
|
update();
|
|
}
|
|
else {
|
|
if (mixInserted) {
|
|
gm_deleteMix(index);
|
|
}
|
|
mixInserted = false;
|
|
update();
|
|
}
|
|
}
|
|
|
|
int MixesPanel::getMixerIndex(unsigned int dch)
|
|
{
|
|
for (int i=0; i < firmware->getCapability(Mixes); i++) {
|
|
if (!model->mixData[i].destCh) {
|
|
// we reached the end of used mixes
|
|
return i;
|
|
}
|
|
if (model->mixData[i].destCh > dch) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void MixesPanel::mixerlistWidget_doubleClicked(QModelIndex index)
|
|
{
|
|
int idx = mixersListWidget->item(index.row())->data(Qt::UserRole).toByteArray().at(0);
|
|
if (idx<0) {
|
|
int i = -idx;
|
|
idx = getMixerIndex(i); //get mixer index to insert
|
|
if (!gm_insertMix(idx)) return;
|
|
model->mixData[idx].destCh = i;
|
|
mixInserted = true;
|
|
}
|
|
else {
|
|
mixInserted = false;
|
|
}
|
|
gm_openMix(idx);
|
|
}
|
|
|
|
void MixesPanel::mixersDeleteList(QList<int> list)
|
|
{
|
|
qSort(list.begin(), list.end());
|
|
|
|
int iDec = 0;
|
|
foreach(int idx, list) {
|
|
gm_deleteMix(idx - iDec);
|
|
iDec++;
|
|
}
|
|
}
|
|
|
|
QList<int> MixesPanel::createMixListFromSelected()
|
|
{
|
|
QList<int> list;
|
|
foreach(QListWidgetItem *item, mixersListWidget->selectedItems()) {
|
|
int idx= item->data(Qt::UserRole).toByteArray().at(0);
|
|
if(idx >= 0 && idx<firmware->getCapability(Mixes)) list << idx;
|
|
}
|
|
return list;
|
|
}
|
|
|
|
// TODO duplicated code
|
|
void MixesPanel::setSelectedByMixList(QList<int> list)
|
|
{
|
|
for(int i=0; i<mixersListWidget->count(); i++) {
|
|
int t = mixersListWidget->item(i)->data(Qt::UserRole).toByteArray().at(0);
|
|
if(list.contains(t)) {
|
|
mixersListWidget->item(i)->setSelected(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void MixesPanel::mixersDelete(bool ask)
|
|
{
|
|
QList<int> list = createMixListFromSelected();
|
|
if(list.isEmpty()) return;
|
|
|
|
QMessageBox::StandardButton ret = QMessageBox::No;
|
|
|
|
if(ask) {
|
|
ret = QMessageBox::warning(this, "companion",
|
|
tr("Delete Selected Mixes?"),
|
|
QMessageBox::Yes | QMessageBox::No);
|
|
}
|
|
|
|
if ((ret == QMessageBox::Yes) || (!ask)) {
|
|
mixersDeleteList(list);
|
|
emit modified();
|
|
update();
|
|
}
|
|
}
|
|
|
|
void MixesPanel::mixersCut()
|
|
{
|
|
mixersCopy();
|
|
mixersDelete(false);
|
|
}
|
|
|
|
void MixesPanel::mixersCopy()
|
|
{
|
|
QList<int> list = createMixListFromSelected();
|
|
|
|
QByteArray mxData;
|
|
foreach(int idx, list) {
|
|
mxData.append((char*)&model->mixData[idx], sizeof(MixData));
|
|
}
|
|
|
|
QMimeData *mimeData = new QMimeData;
|
|
mimeData->setData("application/x-companion-mix", mxData);
|
|
QApplication::clipboard()->setMimeData(mimeData,QClipboard::Clipboard);
|
|
}
|
|
|
|
void MixesPanel::pasteMixerMimeData(const QMimeData * mimeData, int destIdx)
|
|
{
|
|
if (mimeData->hasFormat("application/x-companion-mix")) {
|
|
int idx; // mixer index
|
|
int dch;
|
|
|
|
if (destIdx < 0) {
|
|
dch = -destIdx;
|
|
idx = getMixerIndex(dch) - 1; //get mixer index to insert
|
|
}
|
|
else {
|
|
idx = destIdx;
|
|
dch = model->mixData[idx].destCh;
|
|
}
|
|
|
|
QByteArray mxData = mimeData->data("application/x-companion-mix");
|
|
|
|
int i = 0;
|
|
while (i < mxData.size()) {
|
|
idx++;
|
|
if (idx == firmware->getCapability(Mixes)) break;
|
|
if (!gm_insertMix(idx)) break;
|
|
|
|
MixData * md = &model->mixData[idx];
|
|
memcpy(md, mxData.mid(i, sizeof(MixData)).constData(), sizeof(MixData));
|
|
md->destCh = dch;
|
|
i += sizeof(MixData);
|
|
}
|
|
|
|
emit modified();
|
|
update();
|
|
}
|
|
}
|
|
|
|
|
|
void MixesPanel::mixersPaste()
|
|
{
|
|
const QClipboard * clipboard = QApplication::clipboard();
|
|
const QMimeData * mimeData = clipboard->mimeData();
|
|
QListWidgetItem * item = mixersListWidget->currentItem();
|
|
if (item) {
|
|
pasteMixerMimeData(mimeData, item->data(Qt::UserRole).toByteArray().at(0));
|
|
}
|
|
}
|
|
|
|
void MixesPanel::mixersDuplicate()
|
|
{
|
|
mixersCopy();
|
|
mixersPaste();
|
|
}
|
|
|
|
void MixesPanel::mixerOpen()
|
|
{
|
|
int idx = mixersListWidget->currentItem()->data(Qt::UserRole).toByteArray().at(0);
|
|
if(idx < 0) {
|
|
int i = -idx;
|
|
idx = getMixerIndex(i); //get mixer index to insert
|
|
if (!gm_insertMix(idx)) return;
|
|
model->mixData[idx].destCh = i;
|
|
mixInserted = true;
|
|
}
|
|
else {
|
|
mixInserted = false;
|
|
}
|
|
|
|
gm_openMix(idx);
|
|
}
|
|
|
|
void MixesPanel::mixerHighlight()
|
|
{
|
|
int idx = mixersListWidget->currentItem()->data(Qt::UserRole).toByteArray().at(0);
|
|
int dest;
|
|
if (idx<0) {
|
|
dest = -idx;
|
|
}
|
|
else {
|
|
dest = model->mixData[idx].destCh;
|
|
}
|
|
highlightedSource = ( (int)highlightedSource == dest) ? 0 : dest;
|
|
// qDebug() << "MixesPanel::mixerHighlight(): " << highlightedSource ;
|
|
for(int i=0; i<mixersListWidget->count(); i++) {
|
|
int t = mixersListWidget->item(i)->data(Qt::UserRole).toByteArray().at(0);
|
|
mixersListWidget->item(i)->setText(getMixerText(t, 0));
|
|
}
|
|
}
|
|
|
|
void MixesPanel::mixerAdd()
|
|
{
|
|
if (!mixersListWidget->currentItem()) return;
|
|
|
|
int index = mixersListWidget->currentItem()->data(Qt::UserRole).toByteArray().at(0);
|
|
|
|
if(index < 0) { // if empty then return relavent index
|
|
int i = -index;
|
|
index = getMixerIndex(i); //get mixer index to insert
|
|
if (!gm_insertMix(index)) return;
|
|
model->mixData[index].destCh = i;
|
|
mixInserted = true;
|
|
}
|
|
else {
|
|
index++;
|
|
if (!gm_insertMix(index)) return;
|
|
model->mixData[index].destCh = model->mixData[index-1].destCh;
|
|
mixInserted = true;
|
|
}
|
|
gm_openMix(index);
|
|
}
|
|
|
|
void MixesPanel::mixerlistWidget_customContextMenuRequested(QPoint pos)
|
|
{
|
|
QPoint globalPos = mixersListWidget->mapToGlobal(pos);
|
|
|
|
const QClipboard *clipboard = QApplication::clipboard();
|
|
const QMimeData *mimeData = clipboard->mimeData();
|
|
bool hasData = mimeData->hasFormat("application/x-companion-mix");
|
|
|
|
QMenu contextMenu;
|
|
contextMenu.addAction(CompanionIcon("add.png"), tr("&Add"), this, SLOT(mixerAdd()), tr("Ctrl+A"));
|
|
contextMenu.addAction(CompanionIcon("edit.png"), tr("&Edit"), this, SLOT(mixerOpen()), tr("Enter"));
|
|
contextMenu.addAction(CompanionIcon("fuses.png"), tr("&Toggle highlight"), this, SLOT(mixerHighlight()), tr("Ctrl+T"));
|
|
contextMenu.addSeparator();
|
|
contextMenu.addAction(CompanionIcon("clear.png"), tr("&Delete"), this, SLOT(mixersDelete()), tr("Delete"));
|
|
contextMenu.addAction(CompanionIcon("copy.png"), tr("&Copy"), this, SLOT(mixersCopy()), tr("Ctrl+C"));
|
|
contextMenu.addAction(CompanionIcon("cut.png"), tr("C&ut"), this, SLOT(mixersCut()), tr("Ctrl+X"));
|
|
contextMenu.addAction(CompanionIcon("paste.png"), tr("&Paste"), this, SLOT(mixersPaste()), tr("Ctrl+V"))->setEnabled(hasData);
|
|
contextMenu.addAction(CompanionIcon("duplicate.png"), tr("Du&plicate"), this, SLOT(mixersDuplicate()), tr("Ctrl+U"));
|
|
contextMenu.addSeparator();
|
|
contextMenu.addAction(CompanionIcon("moveup.png"), tr("Move Up"), this, SLOT(moveMixUp()), tr("Ctrl+Up"));
|
|
contextMenu.addAction(CompanionIcon("movedown.png"), tr("Move Down"), this, SLOT(moveMixDown()), tr("Ctrl+Down"));
|
|
contextMenu.addSeparator();
|
|
contextMenu.addActions(mixersListWidget->actions());
|
|
|
|
contextMenu.exec(globalPos);
|
|
}
|
|
|
|
void MixesPanel::mimeMixerDropped(int index, const QMimeData *data, Qt::DropAction action)
|
|
{
|
|
int idx = mixersListWidget->item(index)->data(Qt::UserRole).toByteArray().at(0);
|
|
//qDebug() << "MixesPanel::mimeMixerDropped()" << index << data;
|
|
if (action == Qt::CopyAction) {
|
|
pasteMixerMimeData(data, idx);
|
|
}
|
|
else if (action == Qt::MoveAction) {
|
|
QList<int> list = createMixListFromSelected();
|
|
mixersDeleteList(createMixListFromSelected());
|
|
foreach (const int del, list) {
|
|
if (del < idx)
|
|
--idx;
|
|
}
|
|
pasteMixerMimeData(data, idx);
|
|
}
|
|
}
|
|
|
|
void MixesPanel::mixerlistWidget_KeyPress(QKeyEvent *event)
|
|
{
|
|
if(event->matches(QKeySequence::SelectAll)) mixerAdd(); //Ctrl A
|
|
if(event->matches(QKeySequence::Delete)) mixersDelete();
|
|
if(event->matches(QKeySequence::Copy)) mixersCopy();
|
|
if(event->matches(QKeySequence::Cut)) mixersCut();
|
|
if(event->matches(QKeySequence::Paste)) mixersPaste();
|
|
if(event->matches(QKeySequence::Underline)) mixersDuplicate();
|
|
if(event->matches(QKeySequence::AddTab)) mixerHighlight();
|
|
|
|
if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) mixerOpen();
|
|
|
|
if(event->matches(QKeySequence::MoveToNextLine)) {
|
|
mixersListWidget->setCurrentRow(mixersListWidget->currentRow()+1);
|
|
}
|
|
if(event->matches(QKeySequence::MoveToPreviousLine)) {
|
|
mixersListWidget->setCurrentRow(mixersListWidget->currentRow()-1);
|
|
}
|
|
}
|
|
|
|
int MixesPanel::gm_moveMix(int idx, bool dir) //true=inc=down false=dec=up
|
|
{
|
|
if(idx > firmware->getCapability(Mixes) || (idx == firmware->getCapability(Mixes) && dir)) return idx;
|
|
|
|
MixData &src=model->mixData[idx];
|
|
|
|
if (idx == 0 && !dir) {
|
|
//special case: topmost mixer moving up
|
|
if (src.destCh > 1) src.destCh--;
|
|
return idx;
|
|
}
|
|
|
|
int tdx = dir ? idx+1 : idx-1;
|
|
MixData & tgt = model->mixData[tdx];
|
|
|
|
unsigned int outputs = firmware->getCapability(Outputs);
|
|
if(src.destCh == 0 || src.destCh > outputs || tgt.destCh > outputs) return idx;
|
|
|
|
if (tgt.destCh != src.destCh) {
|
|
if (dir && src.destCh < outputs) src.destCh++;
|
|
if (!dir && src.destCh > 0) src.destCh--;
|
|
return idx;
|
|
}
|
|
|
|
//flip between idx and tgt
|
|
MixData temp;
|
|
memcpy(&temp, &src, sizeof(MixData));
|
|
memcpy(&src, &tgt, sizeof(MixData));
|
|
memcpy(&tgt, &temp, sizeof(MixData));
|
|
return tdx;
|
|
}
|
|
|
|
void MixesPanel::moveMixUp()
|
|
{
|
|
QList<int> list = createMixListFromSelected();
|
|
QList<int> highlightList;
|
|
foreach(int idx, list) {
|
|
highlightList << gm_moveMix(idx, false);
|
|
}
|
|
emit modified();
|
|
update();
|
|
setSelectedByMixList(highlightList);
|
|
}
|
|
|
|
void MixesPanel::moveMixDown()
|
|
{
|
|
QList<int> list = createMixListFromSelected();
|
|
QList<int> highlightList;
|
|
foreach(int idx, list) {
|
|
highlightList << gm_moveMix(idx, true);
|
|
}
|
|
emit modified();
|
|
update();
|
|
setSelectedByMixList(highlightList);
|
|
}
|
|
|
|
void MixesPanel::clearMixes()
|
|
{
|
|
if (QMessageBox::question(this, tr("Clear Mixes?"), tr("Really clear all the mixes?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
|
|
model->clearMixes();
|
|
emit modified();
|
|
update();
|
|
}
|
|
}
|