diff --git a/res/skins/Deere/style.qss b/res/skins/Deere/style.qss index e04992ef587..9818f8af864 100644 --- a/res/skins/Deere/style.qss +++ b/res/skins/Deere/style.qss @@ -278,11 +278,17 @@ WLibraryTextBrowser { WLibrary QLineEdit, WLibrary QPlainTextEdit, WBeatSpinBox, +/* Track label inline editor */ +WTrackPropertyEditor, #LibraryBPMSpinBox { color: #ddd; background-color: #0f0f0f; selection-color: #000; selection-background-color: #ccc; +} +WLibrary QLineEdit, +WTrackPropertyEditor, +#LibraryBPMSpinBox { border: 1px solid #006596; } @@ -843,6 +849,7 @@ WPushButton, WKey, WTime, WTrackProperty, +WTrackPropertyEditor, WRecordingDuration, QSpinBox, WBeatSpinBox, @@ -1098,15 +1105,21 @@ WBeatSpinBox, } /* Start editable widgets in decks */ +/* emphasize editable widgets on hover */ #BpmKeyEditRowCollapsed:hover, #BpmKeyEditRowExpanded:hover, #PositionGutter:hover, #PlayPositionTextSmall:hover, -#StarratingGutter:hover { - /* emphasize editable widgets on hover */ - border: 1px solid #FF6600; +#StarratingGutter:hover, +WTrackProperty:hover { background-color: rgba(255, 102, 0, 128); -} + } + #BpmKeyEditRowCollapsed:hover, + #BpmKeyEditRowExpanded:hover, + WTrackProperty[selected="true"] { + border: 1px solid #FF6600; + } + #BpmKeyEditRowCollapsed { qproperty-layoutAlignment: 'AlignCenter'; /* emphasize active widget */ @@ -1593,6 +1606,11 @@ WStarRating { font-size: 15px; } +/* Track label inline editor in decks and samplers */ +WTrackPropertyEditor { + font-size: 15px; +} + #SampleDecksContainer { } diff --git a/res/skins/LateNight/style.qss b/res/skins/LateNight/style.qss index 599cd8daedb..f6c6a8ba098 100644 --- a/res/skins/LateNight/style.qss +++ b/res/skins/LateNight/style.qss @@ -88,6 +88,7 @@ WLibraryTextBrowser QMenu, WTrackMenu, WTrackMenu QMenu, WTrackMenu QMenu QCheckBox, +WTrackPropertyEditor, QLineEdit QMenu, WCoverArtMenu, QLabel#labelRecPrefix, @@ -244,6 +245,7 @@ WBeatSpinBox, #BpmTextSmall, #FxUnitLabel, #SamplerTitle, +WTrackPropertyEditor, WTime, WRecordingDuration { font-size: 16px; @@ -451,6 +453,13 @@ WSpinny { /* #RateSliderBoxCompactSync { margin-left: 5px; } + +WTrackProperty, +WTrackProperty[selected="false"] { + background-color: transparent; + border: 0px solid transparent; + border-radius: 1px; +} /************** Decks ********************************************************/ diff --git a/res/skins/LateNight/style_classic.qss b/res/skins/LateNight/style_classic.qss index 429af041b26..77bccd878d8 100644 --- a/res/skins/LateNight/style_classic.qss +++ b/res/skins/LateNight/style_classic.qss @@ -85,6 +85,9 @@ WLibrary, } +WTrackProperty:hover, +WTrackProperty:hover[selected="false"], +WTrackProperty:hover[selected="true"], #BpmTapContainer:hover { background-color: #151517; border-radius: 1px; @@ -2138,7 +2141,10 @@ WLibrarySidebar { /* Table cell in edit mode */ WLibrary QLineEdit, WLibrary QPlainTextEdit, -#LibraryBPMSpinBox { +#LibraryBPMSpinBox, +/* Track label inline editor in decks and samplers */ +WTrackPropertyEditor, +WTrackProperty[selected="true"] { color: #ddd; background-color: #0f0f0f; selection-color: #000; diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss index 5dd1b3233bf..715175f560a 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -337,11 +337,23 @@ WSearchLineEdit { border-color: #0c0c0c; } +WTrackProperty:hover, +WTrackProperty:hover[selected="false"], +WTrackProperty:hover[selected="true"], +WTrackProperty[selected="true"], #BpmTapContainer:hover, #PlayPositionText:hover, #PlayPositionTextSmall:hover { background-color: #151517; - border-radius: 1px; -} + } + WTrackProperty:hover[selected="true"], + WTrackProperty[selected="true"] { + background-color: #181819; + } + #BpmTap[pressed="true"], + WTrackProperty[selected="true"] { + border: 1px solid #7d350d; +/* border: 1px solid #888;*/ + } /* Disabled for now since the hover effect is stuck as soon as the track menu is opened. @@ -1676,9 +1688,6 @@ WPushButton#LoopOut[pressed="true"], #CueDeleteButton[pressed="true"]*/ { background-color: #7d350d; } - #BpmTap[pressed="true"] { - border: 1px solid #7d350d; - } /* Red */ #EQKillButtonBox WPushButton[displayValue="1"], @@ -2532,6 +2541,7 @@ WLibraryTextBrowser, WLibrarySidebar, #SkinSettings, WSearchLineEdit, +WTrackPropertyEditor, WLibrary QLineEdit, WLibrary QPlainTextEdit, #spinBoxTransition, @@ -2600,12 +2610,15 @@ WTrackTableView { /* Table cell in edit mode */ WLibrary QLineEdit, WLibrary QPlainTextEdit, -#LibraryBPMSpinBox { +#LibraryBPMSpinBox, +/* Track label inline editor in decks and samplers */ +WTrackPropertyEditor { color: #ddd; selection-color: #000; selection-background-color: #ccc; - border: 1px solid #2c454f; -} + border: 1px solid #257b82; + border-radius: 0px; + } /* Entire BPM cell */ /* Lock icon at the left */ diff --git a/res/skins/Shade/deck.xml b/res/skins/Shade/deck.xml index 3301a1d695e..d1780c3554a 100644 --- a/res/skins/Shade/deck.xml +++ b/res/skins/Shade/deck.xml @@ -58,7 +58,7 @@ style/style_bg_deck_top_mid.png - ArtistAndTimeRow + TitleEjectRow 0e,23f horizontal @@ -122,7 +122,8 @@ artist - me,max + + me,me right diff --git a/res/skins/Shade/style.qss b/res/skins/Shade/style.qss index b9eebbb2f2b..d103980b735 100644 --- a/res/skins/Shade/style.qss +++ b/res/skins/Shade/style.qss @@ -538,13 +538,26 @@ WTrackTableView { /* Table cell in edit mode */ WLibrary QLineEdit, WLibrary QPlainTextEdit, + /* Track label inline editor in decks and samplers */ + WTrackPropertyEditor, #LibraryBPMSpinBox { color: #ddd; background-color: #0f0f0f; selection-color: #000; selection-background-color: #ccc; + } + WLibrary QLineEdit, + #LibraryBPMSpinBox { border: 1px solid #656d75; } + WTrackPropertyEditor { + font-size: 13px; + border-width: 1px; + border-style: solid; + } + #TitleEjectRow WTrackPropertyEditor { + margin-top: 4px; + } /* BPM lock icon in the library "BPM" column. */ #LibraryBPMButton::indicator:checked { @@ -660,6 +673,7 @@ WLibrarySidebar { Defined by SearchBox */ margin: 0px; } + WTrackPropertyEditor, WTrackTableView:focus, WLibrarySidebar:focus, #LibraryContainer WLibraryTextBrowser:focus { diff --git a/res/skins/Shade/style_dark.qss b/res/skins/Shade/style_dark.qss index a20d798cfc5..9e3b7d34f53 100644 --- a/res/skins/Shade/style_dark.qss +++ b/res/skins/Shade/style_dark.qss @@ -210,6 +210,8 @@ WTrackTableView { /* Table cell in edit mode */ WLibrary QLineEdit, WLibrary QPlainTextEdit, + /* Track label inline editor in decks and samplers */ + WTrackPropertyEditor, #LibraryBPMSpinBox { color: #ddd; background-color: #0f0f0f; diff --git a/res/skins/Tango/style.qss b/res/skins/Tango/style.qss index a2b0649ce4e..b097b179908 100644 --- a/res/skins/Tango/style.qss +++ b/res/skins/Tango/style.qss @@ -62,6 +62,8 @@ WEffectChainPresetSelector QAbstractScrollArea, WEffectChainPresetButton QMenu, WEffectChainPresetButton QMenu QCheckBox, WSearchLineEdit QAbstractScrollArea, +/* Track label inline editor in decks and samplers */ +WTrackPropertyEditor, #fadeModeCombobox, #fadeModeCombobox QAbstractScrollArea, /* With 'Ubuntu' font the header section titles would always be top-aligned... */ @@ -790,9 +792,16 @@ WLabel#TrackComment { #PlayPositionMini { padding: 0px 1px 0px 0px; } - #PlayPosition:hover, #PlayPositionMini:hover { + #PlayPosition:hover, + #PlayPositionMini:hover, + WTrackProperty:hover, + WTrackProperty:hover[selected="false"], + WTrackProperty[selected="true"] { border: 1px solid #ccc; } + WTrackProperty:hover[selected="true"] { + border: 1px solid #fff; + } #TrackTitleMini { padding: 0px 0px 0px 1px; } @@ -2627,12 +2636,19 @@ WTrackTableView { /* Table cell in edit mode */ WLibrary QLineEdit, WLibrary QPlainTextEdit, + /* Track label inline editor in decks and samplers */ + WTrackPropertyEditor, #LibraryBPMSpinBox { color: #ddd; background-color: #0f0f0f; selection-color: #000; selection-background-color: #ccc; - border: 1px solid #555; + border-width: 1px; + border-style: solid; + } + WLibrary QLineEdit, + #LibraryBPMSpinBox { + border-color: #555; } WLibrarySidebar { @@ -2646,6 +2662,7 @@ WLibrarySidebar { Shift WSearchLineEdit instead */ margin: 0px; } + WTrackPropertyEditor, WTrackTableView:focus, WLibrarySidebar:focus, WTrackTableView:focus, diff --git a/src/skin/legacy/tooltips.cpp b/src/skin/legacy/tooltips.cpp index 3ab8fe371ed..1fec732bd35 100644 --- a/src/skin/legacy/tooltips.cpp +++ b/src/skin/legacy/tooltips.cpp @@ -35,6 +35,7 @@ void Tooltips::addStandardTooltips() { QString leftClick = tr("Left-click"); QString rightClick = tr("Right-click"); QString doubleClick = tr("Double-click"); + QString selectedClick = tr("Select and click: Show inline value editor"); QString scrollWheel = tr("Scroll-wheel"); QString shift = tr("Shift-key"); QString loopActive = "(" + tr("loop active") + ")"; @@ -814,7 +815,8 @@ void Tooltips::addStandardTooltips() { << dropTracksHere << dragItem << QString("%1: %2").arg(doubleClick, trackProperties) - << QString("%1: %2").arg(rightClick, trackMenu); + << QString("%1: %2").arg(rightClick, trackMenu) + << selectedClick; add("track_title") << tr("Track Title") @@ -823,7 +825,8 @@ void Tooltips::addStandardTooltips() { << dropTracksHere << dragItem << QString("%1: %2").arg(doubleClick, trackProperties) - << QString("%1: %2").arg(rightClick, trackMenu); + << QString("%1: %2").arg(rightClick, trackMenu) + << selectedClick; add("track_album") << tr("Track Album") @@ -832,7 +835,8 @@ void Tooltips::addStandardTooltips() { << dropTracksHere << dragItem << QString("%1: %2").arg(doubleClick, trackProperties) - << QString("%1: %2").arg(rightClick, trackMenu); + << QString("%1: %2").arg(rightClick, trackMenu) + << selectedClick; add("track_key") //: The musical key of a track @@ -856,7 +860,8 @@ void Tooltips::addStandardTooltips() { << dropTracksHere << dragItem << QString("%1: %2").arg(doubleClick, trackProperties) - << QString("%1: %2").arg(rightClick, trackMenu); + << QString("%1: %2").arg(rightClick, trackMenu) + << selectedClick; add("time") << tr("Clock") diff --git a/src/widget/wtrackproperty.cpp b/src/widget/wtrackproperty.cpp index eed11a16eac..36f03ce2a11 100644 --- a/src/widget/wtrackproperty.cpp +++ b/src/widget/wtrackproperty.cpp @@ -2,15 +2,22 @@ #include #include -#include +#include +#include -#include "control/controlpushbutton.h" +#include "control/controlobject.h" #include "moc_wtrackproperty.cpp" #include "skin/legacy/skincontext.h" #include "track/track.h" #include "util/dnd.h" #include "widget/wtrackmenu.h" +namespace { +// Duration (ms) the widget is 'selected' after left click, i.e. the duration +// a second click would open the value editor +constexpr int kSelectedClickTimeoutMs = 2000; +} // namespace + WTrackProperty::WTrackProperty( QWidget* pParent, UserSettingsPointer pConfig, @@ -21,7 +28,11 @@ WTrackProperty::WTrackProperty( m_group(group), m_pConfig(pConfig), m_pLibrary(pLibrary), - m_isMainDeck(isMainDeck) { + m_isMainDeck(isMainDeck), + m_propertyIsWritable(false), + m_pSelectedClickTimer(nullptr), + m_bSelected(false), + m_pEditor(nullptr) { setAcceptDrops(true); } @@ -38,11 +49,24 @@ void WTrackProperty::setup(const QDomNode& node, const SkinContext& context) { } // Check if property with that name exists in Track class - if (Track::staticMetaObject.indexOfProperty(property.toUtf8().constData()) == -1) { + int propertyIndex = Track::staticMetaObject.indexOfProperty(property.toUtf8().constData()); + if (propertyIndex == -1) { qWarning() << "WTrackProperty: Unknown track property:" << property; return; } - m_property = property; + m_displayProperty = property; + // Handle 'titleInfo' property: displays the title or, if both title & artist + // are empty, filename. Though, this property is not writeable, so we map + // it to 'title' for the editor. + if (property == "titleInfo") { + m_editProperty = "title"; + } else { + if (!Track::staticMetaObject.property(propertyIndex).isWritable()) { + return; + } + m_editProperty = m_displayProperty; + } + m_propertyIsWritable = true; } void WTrackProperty::slotTrackLoaded(TrackPointer pTrack) { @@ -64,6 +88,9 @@ void WTrackProperty::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldT disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr); } m_pCurrentTrack.reset(); + if (m_pEditor && m_pEditor->hasFocus()) { + m_pEditor->hide(); + } updateLabel(); } @@ -74,21 +101,81 @@ void WTrackProperty::slotTrackChanged(TrackId trackId) { void WTrackProperty::updateLabel() { if (m_pCurrentTrack) { - if (m_property.isEmpty()) { - return; - } - QVariant property = - m_pCurrentTrack->property(m_property.toUtf8().constData()); - if (property.isValid() && property.canConvert()) { - setText(property.toString()); - return; - } + setText(getPropertyStringFromTrack(m_displayProperty)); + return; } setText(""); } +const QString WTrackProperty::getPropertyStringFromTrack(QString& property) const { + if (property.isEmpty() || !m_pCurrentTrack) { + return {}; + } + QVariant propVar = m_pCurrentTrack->property(property.toUtf8().constData()); + if (propVar.isValid() && propVar.canConvert()) { + return propVar.toString(); + } + return {}; +} + void WTrackProperty::mousePressEvent(QMouseEvent* pEvent) { DragAndDropHelper::mousePressed(pEvent); + + // Check if there's another open editor. If yes, close it + WTrackPropertyEditor* otherEditor = + qobject_cast(QApplication::focusWidget()); + if (otherEditor) { + otherEditor->clearFocus(); + // and don't attempt to activate this editor right away + return; + } + + if (!pEvent->buttons().testFlag(Qt::LeftButton) || !m_pCurrentTrack) { + return; + } + + // Don't create the editor or toggle the 'selected' state for protected + // properties like duration. + if (!m_propertyIsWritable) { + return; + } + + if (!m_pSelectedClickTimer) { + // create & start the timer + m_pSelectedClickTimer = make_parented(this); + m_pSelectedClickTimer->setSingleShot(true); + m_pSelectedClickTimer->setInterval(kSelectedClickTimeoutMs); + m_pSelectedClickTimer->callOnTimeout( + this, &WTrackProperty::resetSelectedState); + } else if (m_pSelectedClickTimer->isActive()) { + resetSelectedState(); + // create the persistent editor, populate & connect + if (!m_pEditor) { + m_pEditor = make_parented(this); + connect(m_pEditor, + // use custom signal. editingFinished() doesn't suit since it's + // also emitted weh pressing Esc (which should cancel editing) + &WTrackPropertyEditor::commitEditorData, + this, + &WTrackProperty::slotCommitEditorData); + } + // Don't let the editor expand beyond its initial size + m_pEditor->setFixedSize(size()); + + QString editText = getPropertyStringFromTrack(m_editProperty); + if (m_displayProperty == "titleInfo" && editText.isEmpty()) { + editText = tr("title"); + } + m_pEditor->setText(editText); + m_pEditor->selectAll(); + m_pEditor->show(); + m_pEditor->setFocus(); + return; + } + // start timer + m_pSelectedClickTimer->start(); + m_bSelected = true; + restyleAndRepaint(); } void WTrackProperty::mouseMoveEvent(QMouseEvent* pEvent) { @@ -104,7 +191,7 @@ void WTrackProperty::mouseDoubleClickEvent(QMouseEvent* pEvent) { } ensureTrackMenuIsCreated(); m_pTrackMenu->loadTrack(m_pCurrentTrack, m_group); - m_pTrackMenu->showDlgTrackInfo(m_property); + m_pTrackMenu->showDlgTrackInfo(m_displayProperty); } void WTrackProperty::dragEnterEvent(QDragEnterEvent* pEvent) { @@ -122,6 +209,11 @@ void WTrackProperty::contextMenuEvent(QContextMenuEvent* pEvent) { m_pTrackMenu->loadTrack(m_pCurrentTrack, m_group); // Show the right-click menu m_pTrackMenu->popup(pEvent->globalPos()); + // Unset the hover state manually (stuck state is probably a Qt bug) + // TODO(ronso0) Test whether this is still required with Qt6 + QEvent lev = QEvent(QEvent::Leave); + qApp->sendEvent(this, &lev); + update(); } } @@ -198,8 +290,74 @@ void WTrackProperty::slotShowTrackMenuChangeRequest(bool show) { // Note: this widget may be hidden so the position may be unexpected, // though this is okay as long as all variants of deckN are on the same // side of the mixer. - QContextMenuEvent event(QContextMenuEvent::Mouse, + QContextMenuEvent* pEvent = new QContextMenuEvent(QContextMenuEvent::Mouse, QPoint(), mapToGlobal(rect().center())); - contextMenuEvent(&event); + contextMenuEvent(pEvent); +} + +void WTrackProperty::slotCommitEditorData(const QString& text) { + // use real track data instead of text() to be independent from display text + if (m_pCurrentTrack && text != getPropertyStringFromTrack(m_editProperty)) { + const QVariant var(QVariant::fromValue(text)); + m_pCurrentTrack->setProperty( + m_editProperty.toUtf8().constData(), + var); + // Track::changed() will update label + } +} + +void WTrackProperty::resetSelectedState() { + if (m_pSelectedClickTimer) { + m_pSelectedClickTimer->stop(); + // explicitly disconnect() queued signals? not crucial + // here since timeOut() just calls resetSelectedState() + } + m_bSelected = false; + restyleAndRepaint(); +} + +void WTrackProperty::restyleAndRepaint() { + emit selectedStateChanged(isSelected()); + + style()->unpolish(this); + style()->polish(this); + // These calls don't always trigger the repaint, so call it explicitly. + repaint(); +} + +WTrackPropertyEditor::WTrackPropertyEditor(QWidget* pParent) + : QLineEdit(pParent) { + installEventFilter(this); +} + +bool WTrackPropertyEditor::eventFilter(QObject* pObj, QEvent* pEvent) { + if (pEvent->type() == QEvent::KeyPress) { + // The widget only receives keystrokes when in edit mode. + // Esc will close & reset. + // Enter/Return confirms. + // Any other keypress is forwarded. + QKeyEvent* keyEvent = static_cast(pEvent); + const int key = keyEvent->key(); + switch (key) { + case Qt::Key_Escape: + hide(); + return true; + case Qt::Key_Return: + case Qt::Key_Enter: + hide(); + emit commitEditorData(text()); + ControlObject::set(ConfigKey("[Library]", "refocus_prev_widget"), 1); + return true; + default: + break; + } + } else if (pEvent->type() == QEvent::FocusOut) { + // Close and commit if any other widget gets focus + if (isVisible()) { + hide(); + emit commitEditorData(text()); + } + } + return QLineEdit::eventFilter(pObj, pEvent); } diff --git a/src/widget/wtrackproperty.h b/src/widget/wtrackproperty.h index 644ba0f3e44..4bc898e6e99 100644 --- a/src/widget/wtrackproperty.h +++ b/src/widget/wtrackproperty.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "preferences/usersettings.h" #include "track/track_decl.h" #include "track/trackid.h" @@ -11,6 +13,27 @@ class ControlPushButton; class Library; class WTrackMenu; +/// Custom editor that allows editing track properties via 'selected click' like +/// in the library table. Commits changed data on Enter/Return key press and on +/// FocusOut events. +class WTrackPropertyEditor : public QLineEdit { + Q_OBJECT + public: + WTrackPropertyEditor(QWidget* pParent); + + protected: + bool eventFilter(QObject* pObj, QEvent* pEvent); + + signals: + void commitEditorData(const QString& text); +}; + +// Label that displays the value of a certain track property. +// If the property is editable the value can be edited inline by first selecting +// the label with single click, then clicking again to open the editor. +// The property name is stored in m_editProperty and m_displayProperty, which are +// identical, except for 'titleInfo' (display value, not writable) which we map +// to 'title' (m_editProperty). class WTrackProperty : public WLabel, public TrackDropTarget { Q_OBJECT public: @@ -21,6 +44,15 @@ class WTrackProperty : public WLabel, public TrackDropTarget { const QString& group, bool isMainDeck); ~WTrackProperty() override; + // Custom property to allow skins to style the 'selected' state when the + // widget awaits a second click to open the editor. It's reset automatically + // if no second click is registered within the specified interval. + // Usage in css: WTrackProperty[selected="true"/"false"] { /* styles */ } + Q_PROPERTY(bool selected READ isSelected NOTIFY selectedStateChanged); + + bool isSelected() const { + return m_bSelected; + } void setup(const QDomNode& node, const SkinContext& context) override; @@ -28,6 +60,7 @@ class WTrackProperty : public WLabel, public TrackDropTarget { void trackDropped(const QString& filename, const QString& group) override; void cloneDeck(const QString& sourceGroup, const QString& targetGroup) override; void setAndConfirmTrackMenuControl(bool visible); + void selectedStateChanged(bool state); public slots: void slotTrackLoaded(TrackPointer pTrack); @@ -36,26 +69,36 @@ class WTrackProperty : public WLabel, public TrackDropTarget { protected: void contextMenuEvent(QContextMenuEvent* event) override; + + private slots: + void slotTrackChanged(TrackId); + void resetSelectedState(); + void slotCommitEditorData(const QString& text); + + private: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; - private: void updateLabel(); + const QString getPropertyStringFromTrack(QString& property) const; + void restyleAndRepaint(); void ensureTrackMenuIsCreated(); - const QString m_group; const UserSettingsPointer m_pConfig; Library* m_pLibrary; const bool m_isMainDeck; TrackPointer m_pCurrentTrack; - QString m_property; - parented_ptr m_pTrackMenu; + QString m_displayProperty; + QString m_editProperty; + bool m_propertyIsWritable; + parented_ptr m_pSelectedClickTimer; + bool m_bSelected; + parented_ptr m_pEditor; - private slots: - void slotTrackChanged(TrackId); + parented_ptr m_pTrackMenu; };