From 3f5a3995cc0fb11eaa6d480941f0d2632872115d Mon Sep 17 00:00:00 2001 From: Max Paperno Date: Sat, 10 Jun 2017 17:35:16 -0400 Subject: [PATCH] [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. --- companion/src/modeledit/inputs.cpp | 32 +- companion/src/modeledit/inputs.h | 1 - companion/src/modeledit/mixerslistwidget.cpp | 290 +++++++++++++------ companion/src/modeledit/mixerslistwidget.h | 25 +- companion/src/modeledit/mixes.cpp | 45 +-- 5 files changed, 262 insertions(+), 131 deletions(-) diff --git a/companion/src/modeledit/inputs.cpp b/companion/src/modeledit/inputs.cpp index dc497fcb2..33e405eea 100644 --- a/companion/src/modeledit/inputs.cpp +++ b/companion/src/modeledit/inputs.cpp @@ -75,27 +75,24 @@ void InputsPanel::update() // i -> mixer number QByteArray qba; ExposlistWidget->clear(); - firstLine = true; int curDest = -1; for (int i=0; iexpoData[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= 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 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); } diff --git a/companion/src/modeledit/inputs.h b/companion/src/modeledit/inputs.h index 8656c6f59..963c73544 100644 --- a/companion/src/modeledit/inputs.h +++ b/companion/src/modeledit/inputs.h @@ -54,7 +54,6 @@ class InputsPanel : public ModelPanel private: bool expoInserted; MixersListWidget *ExposlistWidget; - bool firstLine; int inputsCount; ModelPrinter modelPrinter; diff --git a/companion/src/modeledit/mixerslistwidget.cpp b/companion/src/modeledit/mixerslistwidget.cpp index a14e2583d..79b2423c1 100644 --- a/companion/src/modeledit/mixerslistwidget.cpp +++ b/companion/src/modeledit/mixerslistwidget.cpp @@ -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 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 v; - stream >> r >> c >> v; - QList 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 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(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(); } diff --git a/companion/src/modeledit/mixerslistwidget.h b/companion/src/modeledit/mixerslistwidget.h index 7ef66a187..83d948bf5 100644 --- a/companion/src/modeledit/mixerslistwidget.h +++ b/companion/src/modeledit/mixerslistwidget.h @@ -22,9 +22,9 @@ #define _MIXERSLISTWIDGET_H_ #include - -#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 +#include +#include 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_ diff --git a/companion/src/modeledit/mixes.cpp b/companion/src/modeledit/mixes.cpp index 26a8399c9..d750b641a 100644 --- a/companion/src/modeledit/mixes.cpp +++ b/companion/src/modeledit/mixes.cpp @@ -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 = "" + str + ""; - } 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 = "" + str + ""; - } } 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(" ", " "); } @@ -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 list = createMixListFromSelected(); - mixersDeleteList(list); - pasteMixerMimeData(data, idx); + else if (action == Qt::MoveAction) { + QList list = createMixListFromSelected(); + mixersDeleteList(createMixListFromSelected()); + foreach (const int del, list) { + if (del < idx) + --idx; + } + pasteMixerMimeData(data, idx); } }