[Companion] Inputs and Mixes lists formatting & DnD fixes: (#4919)

* 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.
This commit is contained in:
Max Paperno
2017-06-10 23:35:16 +02:00
committed by Bertrand Songis
parent 2a86918508
commit 3f5a3995cc
5 changed files with 262 additions and 131 deletions
+16 -16
View File
@@ -75,27 +75,24 @@ void InputsPanel::update()
// i -> mixer number
QByteArray qba;
ExposlistWidget->clear();
firstLine = true;
int curDest = -1;
for (int i=0; i<CPN_MAX_EXPOS; i++) {
ExpoData *md = &model->expoData[i];
if (md->mode==0) break;
if (!md->mode)
break;
QString str;
while (curDest<(int)md->chn-1) {
while (curDest < (int)md->chn-1) {
curDest++;
AddInputLine(-curDest-1);
}
if (AddInputLine(i)) {
curDest++;
}
}
while (curDest<inputsCount-1) {
while (curDest < inputsCount-1) {
curDest++;
AddInputLine(-curDest-1);
}
@@ -122,20 +119,17 @@ bool InputsPanel::AddInputLine(int dest)
QString str = getInputText(dest, &new_ch);
QListWidgetItem *itm = new QListWidgetItem(str);
QByteArray qba(1, (quint8)dest);
unsigned destId = abs(dest);
bool hasSibs = false;
if (dest >= 0) {
//add input data
ExpoData *md = &model->expoData[dest];
qba.append((const char*)md, sizeof(ExpoData));
destId = md->chn + 1;
hasSibs = (dest < CPN_MAX_EXPOS && model->expoData[dest+1].chn == md->chn);
}
itm->setData(Qt::UserRole, qba);
#if MIX_ROW_HEIGHT_INCREASE > 0
if (new_ch && !firstLine) {
//increase size of this row
itm->setData(GroupHeaderRole, 1);
}
#endif
ExposlistWidget->addItem(itm);
firstLine = false;
ExposlistWidget->addItem(itm, destId, new_ch, hasSibs);
//qDebug() << "InputsPanel::AddInputLine(): dest" << dest << "text" << str;
return new_ch;
}
@@ -300,13 +294,17 @@ void InputsPanel::exposCopy()
void InputsPanel::mimeExpoDropped(int index, const QMimeData *data, Qt::DropAction action)
{
int idx = ExposlistWidget->item(index > 0 ? index-1 : 0)->data(Qt::UserRole).toByteArray().at(0);
int idx = ExposlistWidget->item(index)->data(Qt::UserRole).toByteArray().at(0);
if (action==Qt::CopyAction) {
pasteExpoMimeData(data, idx);
}
else if (action==Qt::MoveAction) {
QList<int> list = createExpoListFromSelected();
exposDeleteList(list);
foreach (const int del, list) {
if (del < idx)
--idx;
}
pasteExpoMimeData(data, idx);
}
}
@@ -416,6 +414,8 @@ void InputsPanel::expolistWidget_customContextMenuRequested(QPoint pos)
contextMenu.addSeparator();
contextMenu.addAction(CompanionIcon("moveup.png"), tr("Move Up"),this,SLOT(moveExpoUp()),tr("Ctrl+Up"));
contextMenu.addAction(CompanionIcon("movedown.png"), tr("Move Down"),this,SLOT(moveExpoDown()),tr("Ctrl+Down"));
contextMenu.addSeparator();
contextMenu.addActions(ExposlistWidget->actions());
contextMenu.exec(globalPos);
}
-1
View File
@@ -54,7 +54,6 @@ class InputsPanel : public ModelPanel
private:
bool expoInserted;
MixersListWidget *ExposlistWidget;
bool firstLine;
int inputsCount;
ModelPrinter modelPrinter;
+203 -87
View File
@@ -20,140 +20,256 @@
#include "mixerslistwidget.h"
#define GRP_IS_PARENT 0x01
#define GRP_HAS_SIBLING 0x02
#define GRP_HAS_DATA 0x04
#define GroupIdRole (Qt::UserRole+1) // destination Input/Channel, for alternate row colors
#define GroupHeaderRole (Qt::UserRole+2) // bitfield of row metadata (see addItem())
static QFont defaultFont;
static bool fontInit = false;
MixersListWidget::MixersListWidget(QWidget * parent, bool expo) :
QListWidget(parent),
expo(expo)
QListWidget(parent),
expo(expo)
{
setFont(QFont("Courier New",12));
setContextMenuPolicy(Qt::CustomContextMenu);
setSelectionMode(QAbstractItemView::SingleSelection);
setDragEnabled(true);
setAcceptDrops(true);
setDropIndicatorShown(true);
setItemDelegate(new MixersDelegate(parent)); //set custom paint handler
if (!fontInit) {
fontInit = true;
defaultFont = font();
defaultFont.setFamily("Courier");
defaultFont.setStyleHint(QFont::TypeWriter);
defaultFont.setPointSize(defaultFont.pointSize() + 3);
}
setFont(defaultFont);
setContextMenuPolicy(Qt::CustomContextMenu);
setSelectionMode(QAbstractItemView::SingleSelection);
setSelectionBehavior(QAbstractItemView::SelectRows);
setDragEnabled(true);
setAcceptDrops(true);
setDragDropMode(QAbstractItemView::DragDrop);
setDropIndicatorShown(true);
setItemDelegate(new MixersDelegate(this)); // set custom paint handler
setStyle(new MixerItemViewProxyStyle(style())); // custom element style painter
itemMimeFmt = (expo ? "application/x-companion-expo-item" : "application/x-companion-mix-item");
QAction * zin = new QAction(tr("Increase font size"), this);
zin->setShortcut(QKeySequence::ZoomIn);
zin->setData(1);
addAction(zin);
QAction * zout = new QAction(tr("Decrease font size"), this);
zout->setShortcut(QKeySequence::ZoomOut);
zout->setData(-1);
addAction(zout);
QAction * zrst = new QAction(tr("Default font size"), this);
zrst->setShortcut(tr("Ctrl+0"));
zrst->setData(0);
addAction(zrst);
connect(zin, &QAction::triggered, this, &MixersListWidget::zoomView);
connect(zout, &QAction::triggered, this, &MixersListWidget::zoomView);
connect(zrst, &QAction::triggered, this, &MixersListWidget::zoomView);
}
void MixersListWidget::keyPressEvent(QKeyEvent *event)
{
emit keyWasPressed(event);
emit keyWasPressed(event);
}
void MixersListWidget::addItem(QListWidgetItem * item, const unsigned & rowId, bool topLevel, bool hasSib)
{
Qt::ItemFlags f = item->flags();
f |= Qt::ItemIsDropEnabled;
quint8 hdrRole = 0;
if (topLevel)
hdrRole |= GRP_IS_PARENT;
if (hasSib)
hdrRole |= GRP_HAS_SIBLING;
if (item->data(Qt::UserRole).toByteArray().at(0) > -1)
hdrRole |= GRP_HAS_DATA;
else
f &= ~Qt::ItemIsDragEnabled; // prevent drag of empty items
item->setData(GroupIdRole, rowId);
item->setData(GroupHeaderRole, hdrRole);
item->setFlags(f);
QListWidget::addItem(item);
//qDebug() << rowId << topLevel << hasSib << item->data(GroupHeaderRole).toUInt();
}
/**
@brief Override to give us different mime type for mixers and inputs
*/
QStringList MixersListWidget::mimeTypes () const
{
QStringList types;
if (expo) types << "application/x-companion-expo-item";
else types << "application/x-companion-mix-item";
return types;
return QStringList() << itemMimeFmt;
}
void MixersListWidget::dropEvent(QDropEvent * event)
{
QList<int> list;
foreach(QListWidgetItem *item, selectedItems()) {
int idx= item->data(Qt::UserRole).toByteArray().at(0);
if(idx >= 0) list << idx;
if (event->proposedAction() == Qt::IgnoreAction)
return;
if (event->mimeData() && event->mimeData()->hasFormat(itemMimeFmt)) {
if (event->source() != this) {
// force a copy action if dragging from another window
event->setDropAction(Qt::CopyAction);
event->accept();
}
if (list.count()<1) return;
event->acceptProposedAction();
dropMimeData(indexAt(event->pos()).row(),event->mimeData(),event->dropAction());
else {
event->acceptProposedAction();
}
dropMimeData(indexAt(event->pos()).row(), event->mimeData(), event->dropAction());
}
}
bool MixersListWidget::dropMimeData(int index, const QMimeData * data, Qt::DropAction action )
{
// qDebug() << "MixersListWidget::dropMimeData() index:" << index << "formats" << data->formats();
QByteArray dropData = data->data(expo ? "application/x-companion-expo-item" : "application/x-companion-mix-item");
if (dropData.isNull() ) return false;
QDataStream stream(&dropData, QIODevice::ReadOnly);
QByteArray qba;
// qDebug() << "index:" << index << action << "formats" << data->formats();
QByteArray dropData = data->data(itemMimeFmt);
if (dropData.isEmpty() )
return false;
while (!stream.atEnd()) {
int r,c;
QMap<int, QVariant> v;
stream >> r >> c >> v;
QList<QVariant> lsVars;
lsVars = v.values();
qba.append(lsVars.at(1).toByteArray().mid(1));
// qDebug() << "MixersListWidget::dropMimeData() added" << lsVars.count() << "items, data:" << lsVars;
}
QDataStream stream(&dropData, QIODevice::ReadOnly);
QByteArray qba;
if(qba.length()>0) {
QMimeData *mimeData = new QMimeData;
mimeData->setData(expo ? "application/x-companion-expo" : "application/x-companion-mix", qba);
emit mimeDropped(index, mimeData, action);
delete mimeData; //is this correct? is mimeData freed elswere??
}
while (!stream.atEnd()) {
int r,c;
QMap<int, QVariant> v;
stream >> r >> c >> v;
qba.append(v.values().at(1).toByteArray().mid(1));
}
return true;
if(qba.size()) {
QMimeData mimeData;
mimeData.setData(QString(itemMimeFmt).remove("-item"), qba);
emit mimeDropped(index, &mimeData, action);
}
return true;
}
void MixersListWidget::zoomView()
{
static int origSz = QFontInfo(font()).pixelSize();
int fsz = origSz;
int step = qobject_cast<QAction *>(sender())->data().toInt();
if (step)
fsz = QFontInfo(font()).pixelSize() + step;
if (fsz < 8)
return;
QFont fnt(font());
fnt.setPixelSize(fsz);
setFont(fnt);
defaultFont = fnt;
}
/*
* MixersDelegate
*/
/**
@brief Paints our HTML formated list item text
*/
void MixersDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
painter->save();
QTextDocument doc;
SetupDocument(doc, options, index);
options.text.clear();
//setup html document
QTextDocument doc;
SetupDocument(doc, options);
QAbstractTextDocumentLayout::PaintContext ctx;
if (options.widget)
ctx.palette = options.widget->palette();
options.text = "";
options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter);
QBrush brush = ((index.data(GroupIdRole).toUInt() % 2) ? ctx.palette.base() : ctx.palette.alternateBase());
options.backgroundBrush = brush;
if ( index.model()->data(index, GroupHeaderRole).toInt() > 0 ) {
//paint with added space
painter->translate(options.rect.left(), options.rect.top() + MIX_ROW_HEIGHT_INCREASE);
QRect clip(0, 0, options.rect.width(), options.rect.height() - MIX_ROW_HEIGHT_INCREASE);
doc.drawContents(painter, clip);
}
else {
painter->translate(options.rect.left(), options.rect.top());
QRect clip(0, 0, options.rect.width(), options.rect.height());
doc.drawContents(painter, clip);
}
painter->restore();
if (!(index.model()->data(index, GroupHeaderRole).toUInt() & GRP_HAS_DATA)) {
// lighter text for empty lines
ctx.palette.setBrush(QPalette::Text, ctx.palette.mid());
}
#ifndef Q_OS_WIN
// Linux and OS X show a solid dark blue color on selected items
else if ((options.state & QStyle::State_Selected)) {
ctx.palette.setBrush(QPalette::Text, ctx.palette.highlightedText());
}
#endif
QStyle * style = (options.widget ? options.widget->style() : QApplication::style());
style->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &options);
QRectF clipRect = textRect.translated(-textRect.topLeft());
ctx.clip = clipRect;
painter->save();
painter->translate(textRect.topLeft());
painter->setClipRect(clipRect);
doc.documentLayout()->draw(painter, ctx);
painter->restore();
}
/**
@brief Returns needed size for our HTML formated list item text
*/
QSize MixersDelegate::sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const
QSize MixersDelegate::sizeHint (const QStyleOptionViewItem & option, const QModelIndex & index) const
{
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
//setup html document
QTextDocument doc;
SetupDocument(doc, options);
//qDebug() << "MixersDelegate::sizeHint() UserRole-> " << index.model()->data(index, Qt::UserRole + 2);
int height = doc.size().height();
if ( index.model()->data(index, Qt::UserRole + 2).toInt() > 0 ) {
//qDebug() << "MixersDelegate::sizeHint() detected channel head";
height = doc.size().height() + MIX_ROW_HEIGHT_INCREASE;
}
//qDebug() << "MixersDelegate::sizeHint() options.rect " << options.rect;
//qDebug() << "MixersDelegate::sizeHint() result " << QSize(doc.idealWidth(), height) << options.rect.width();
return QSize(doc.idealWidth(), height);
QTextDocument doc;
SetupDocument(doc, options, index);
return QSize(doc.idealWidth(), doc.size().height());
}
void MixersDelegate::SetupDocument(QTextDocument & doc, const QStyleOptionViewItem & options) const
void MixersDelegate::SetupDocument(QTextDocument & doc, const QStyleOptionViewItem & options, const QModelIndex & index) const
{
//setup html document
doc.setHtml(options.text);
doc.setDefaultFont(options.font);
//setup html document
quint8 hdrRole = index.model()->data(index, GroupHeaderRole).toUInt();
//minimize margins (default margins look ugly)
QTextFrame *tf = doc.rootFrame();
QTextFrameFormat tff = tf->frameFormat();
tff.setMargin(0);
tf->setFrameFormat(tff);
doc.setDefaultFont(options.font);
doc.setHtml(options.text);
// adjust margins to visually group items per input/channel
QTextFrame *tf = doc.rootFrame();
QTextFrameFormat tff = tf->frameFormat();
if (!(hdrRole & GRP_HAS_DATA)) {
tff.setTopMargin(0.75f);
tff.setBottomMargin(0.75f);
}
else {
if (!(hdrRole & GRP_IS_PARENT))
tff.setTopMargin(0.5f);
if (hdrRole & GRP_HAS_SIBLING)
tff.setBottomMargin(0.5f);
}
tf->setFrameFormat(tff);
}
/*
* MixerItemViewProxyStyle
*/
void MixerItemViewProxyStyle::drawPrimitive(QStyle::PrimitiveElement element, const QStyleOption * option, QPainter * painter, const QWidget * widget) const
{
painter->save();
if (element == QStyle::PE_IndicatorItemViewItemDrop) {
painter->setRenderHint(QPainter::HighQualityAntialiasing, true);
// set a wider stroke for the drop indicator
QPen pen(painter->pen());
pen.setWidthF(2.25f);
painter->setPen(pen);
}
QProxyStyle::drawPrimitive(element, option, painter, widget);
painter->restore();
}
+19 -6
View File
@@ -22,9 +22,9 @@
#define _MIXERSLISTWIDGET_H_
#include <QtWidgets>
#define MIX_ROW_HEIGHT_INCREASE 8 //how much space is added above mixer row (for new channel), if 0 space adding is disabled
const int GroupHeaderRole = (Qt::UserRole+2); //defines new user role for list items. If value is > 0, then space is added before that item
#include <QProxyStyle>
#include <QStyleOption>
#include <QStyledItemDelegate>
class MixersListWidget : public QListWidget
{
@@ -40,13 +40,15 @@ class MixersListWidget : public QListWidget
void keyWasPressed(QKeyEvent *event);
public slots:
void addItem(QListWidgetItem *item, const unsigned & rowId, bool topLevel = false, bool hasSib = false);
bool dropMimeData(int index, const QMimeData *data, Qt::DropAction action);
void zoomView();
protected:
virtual QStringList mimeTypes() const;
private:
QPoint dragStartPosition;
QString itemMimeFmt;
bool expo;
};
@@ -60,14 +62,25 @@ class MixersDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
inline MixersDelegate(QObject *parent) : QStyledItemDelegate(parent) {};
inline MixersDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
private:
void SetupDocument(QTextDocument & doc, const QStyleOptionViewItem & options) const;
void SetupDocument(QTextDocument & doc, const QStyleOptionViewItem & options, const QModelIndex & index) const;
};
/**
@brief Provides custom painting of items on a per-element basis, eg. to change style of drop indicator
*/
class MixerItemViewProxyStyle: public QProxyStyle
{
public:
explicit MixerItemViewProxyStyle(QStyle * style = 0) : QProxyStyle(style) {}
void drawPrimitive(PrimitiveElement element, const QStyleOption * option, QPainter * painter, const QWidget * widget = 0) const;
};
#endif // _MIXERSLISTWIDGET_H_
+24 -21
View File
@@ -109,20 +109,17 @@ bool MixesPanel::AddMixerLine(int dest)
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);
#if MIX_ROW_HEIGHT_INCREASE > 0
// TODO why?
if (new_ch && mixersListWidget->count() == 0) {
// increase size of this row
itm->setData(GroupHeaderRole, 1);
}
#endif
mixersListWidget->addItem(itm);
mixersListWidget->addItem(itm, destId, new_ch, hasSibs);
return new_ch;
}
@@ -140,11 +137,11 @@ QString MixesPanel::getMixerText(int dest, bool * new_ch)
QString str;
bool newChannel = false;
if (dest < 0) {
str = modelPrinter.printChannelName(abs(dest)-1);
dest = abs(dest);
str = modelPrinter.printChannelName(dest-1);
//highlight channel if needed
if (-dest == (int)highlightedSource) {
if (dest == (int)highlightedSource)
str = "<b>" + str + "</b>";
}
newChannel = true;
}
else {
@@ -155,9 +152,8 @@ QString MixesPanel::getMixerText(int dest, bool * new_ch)
if ((dest == 0) || (model->mixData[dest-1].destCh != mix.destCh)) {
newChannel = true;
//highlight channel if needed
if (mix.destCh == highlightedSource) {
if (mix.destCh == highlightedSource)
str = "<b>" + str + "</b>";
}
}
else {
str.fill(' ');
@@ -165,7 +161,8 @@ QString MixesPanel::getMixerText(int dest, bool * new_ch)
str += modelPrinter.printMixerLine(mix, !newChannel, highlightedSource);
}
if (new_ch) *new_ch = newChannel;
if (new_ch)
*new_ch = newChannel;
return str.replace(" ", "&nbsp;");
}
@@ -446,21 +443,27 @@ void MixesPanel::mixerlistWidget_customContextMenuRequested(QPoint pos)
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 > 0 ? index-1 : 0)->data(Qt::UserRole).toByteArray().at(0);
int idx = mixersListWidget->item(index)->data(Qt::UserRole).toByteArray().at(0);
//qDebug() << "MixesPanel::mimeMixerDropped()" << index << data;
if (action==Qt::CopyAction) {
pasteMixerMimeData(data, idx);
if (action == Qt::CopyAction) {
pasteMixerMimeData(data, idx);
}
else if (action==Qt::MoveAction) {
QList<int> list = createMixListFromSelected();
mixersDeleteList(list);
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);
}
}