diff --git a/build/depends.py b/build/depends.py index a0539700921..d5e05d47dd0 100644 --- a/build/depends.py +++ b/build/depends.py @@ -630,6 +630,8 @@ def sources(self, build): "controllers/dlgprefcontrollers.cpp", "dialog/dlgabout.cpp", "dialog/dlgdevelopertools.cpp", + "dialog/savedqueries/dlgsavedquerieseditor.cpp", + "dialog/savedqueries/savedqueriestablemodel.cpp", "preferences/configobject.cpp", "preferences/dialog/dlgprefautodj.cpp", @@ -828,6 +830,13 @@ def sources(self, build): "widget/wcoverartmenu.cpp", "widget/wsingletoncontainer.cpp", "widget/wmainmenubar.cpp", + "widget/wbuttonbar.cpp", + "widget/wfeatureclickbutton.cpp", + "widget/wlibrarystack.cpp", + "widget/wlibrarybreadcrumb.cpp", + "widget/wminiviewscrollbar.cpp", + "widget/wverticalscrollarea.cpp", + "widget/wtristatebutton.cpp", "musicbrainz/network.cpp", "musicbrainz/tagfetcher.cpp", @@ -840,7 +849,8 @@ def sources(self, build): "widget/wtracktableview.cpp", "widget/wtracktableviewheader.cpp", "widget/wlibrarysidebar.cpp", - "widget/wlibrary.cpp", + "widget/wlibrarypane.cpp", + "widget/wbaselibrary.cpp", "widget/wlibrarytableview.cpp", "widget/wanalysislibrarytableview.cpp", "widget/wlibrarytextbrowser.cpp", @@ -851,62 +861,80 @@ def sources(self, build): "library/librarytablemodel.cpp", "library/searchquery.cpp", "library/searchqueryparser.cpp", - "library/analysislibrarytablemodel.cpp", - "library/missingtablemodel.cpp", - "library/hiddentablemodel.cpp", "library/proxytrackmodel.cpp", "library/coverart.cpp", "library/coverartcache.cpp", "library/coverartutils.cpp", - "library/playlisttablemodel.cpp", "library/libraryfeature.cpp", - "library/analysisfeature.cpp", - "library/autodj/autodjfeature.cpp", - "library/autodj/autodjprocessor.cpp", "library/dao/directorydao.cpp", - "library/mixxxlibraryfeature.cpp", - "library/baseplaylistfeature.cpp", - "library/playlistfeature.cpp", - "library/setlogfeature.cpp", - "library/autodj/dlgautodj.cpp", - "library/dlganalysis.cpp", "library/dlgcoverartfullsize.cpp", - "library/dlghidden.cpp", - "library/dlgmissing.cpp", "library/dlgtagfetcher.cpp", "library/dlgtrackinfo.cpp", - - "library/browse/browsetablemodel.cpp", - "library/browse/browsethread.cpp", - "library/browse/browsefeature.cpp", - "library/browse/foldertreemodel.cpp", + "library/dao/savedqueriesdao.cpp", "library/export/trackexportdlg.cpp", "library/export/trackexportwizard.cpp", "library/export/trackexportworker.cpp", - "library/recording/recordingfeature.cpp", - "library/recording/dlgrecording.cpp", "recording/recordingmanager.cpp", "engine/sidechain/enginerecord.cpp", + # Library Features + "library/features/analysis/analysisfeature.cpp", + "library/features/analysis/analysislibrarytablemodel.cpp", + "library/features/analysis/dlganalysis.cpp", + + "library/features/autodj/autodjfeature.cpp", + "library/features/autodj/autodjprocessor.cpp", + "library/features/autodj/dlgautodj.cpp", + + "library/features/banshee/bansheedbconnection.cpp", + "library/features/banshee/bansheefeature.cpp", + "library/features/banshee/bansheeplaylistmodel.cpp", + + "library/features/baseplaylist/baseplaylistfeature.cpp", + + "library/features/browse/browsefeature.cpp", + "library/features/browse/browsetablemodel.cpp", + "library/features/browse/browsethread.cpp", + "library/features/browse/foldertreemodel.cpp", + + "library/features/crates/cratefeature.cpp", + "library/features/crates/cratetablemodel.cpp", + + "library/features/history/historyfeature.cpp", + "library/features/history/historytreemodel.cpp", + + "library/features/libraryfolder/libraryfoldermodel.cpp", + "library/features/libraryfolder/libraryfoldersfeature.cpp", + + "library/features/maintenance/dlghidden.cpp", + "library/features/maintenance/dlgmissing.cpp", + "library/features/maintenance/hiddentablemodel.cpp", + "library/features/maintenance/maintenancefeature.cpp", + "library/features/maintenance/missingtablemodel.cpp", + + "library/features/mixxxlibrary/mixxxlibraryfeature.cpp", + "library/features/mixxxlibrary/mixxxlibrarytreemodel.cpp", + + "library/features/playlist/playlistfeature.cpp", + "library/features/playlist/playlisttablemodel.cpp", + # External Library Features - "library/baseexternallibraryfeature.cpp", - "library/baseexternaltrackmodel.cpp", - "library/baseexternalplaylistmodel.cpp", - "library/rhythmbox/rhythmboxfeature.cpp", - - "library/banshee/bansheefeature.cpp", - "library/banshee/bansheeplaylistmodel.cpp", - "library/banshee/bansheedbconnection.cpp", - - "library/itunes/itunesfeature.cpp", - "library/traktor/traktorfeature.cpp", - - "library/cratefeature.cpp", - "library/sidebarmodel.cpp", + "library/features/baseexternalfeature/baseexternallibraryfeature.cpp", + "library/features/baseexternalfeature/baseexternalplaylistmodel.cpp", + "library/features/baseexternalfeature/baseexternaltrackmodel.cpp", + + "library/features/itunes/itunesfeature.cpp", + "library/features/recording/dlgrecording.cpp", + "library/features/recording/recordingfeature.cpp", + "library/features/rhythmbox/rhythmboxfeature.cpp", + "library/features/traktor/traktorfeature.cpp", + "library/library.cpp", + "library/librarypanemanager.cpp", + "library/librarysidebarexpandedmanager.cpp", "library/scanner/libraryscanner.cpp", "library/scanner/libraryscannerdlg.cpp", @@ -915,7 +943,6 @@ def sources(self, build): "library/scanner/recursivescandirectorytask.cpp", "library/dao/cratedao.cpp", - "library/cratetablemodel.cpp", "library/dao/cuedao.cpp", "library/dao/cue.cpp", "library/dao/trackdao.cpp", @@ -1075,6 +1102,7 @@ def sources(self, build): "util/logging.cpp", "util/cmdlineargs.cpp", "util/audiosignal.cpp", + "util/stringhelper.cpp", '#res/mixxx.qrc' ] @@ -1099,15 +1127,16 @@ def sources(self, build): 'controllers/dlgprefcontrollersdlg.ui', 'dialog/dlgaboutdlg.ui', 'dialog/dlgdevelopertoolsdlg.ui', - 'library/autodj/dlgautodj.ui', - 'library/dlganalysis.ui', + 'dialog/savedqueries/dlgsavedquerieseditor.ui', 'library/dlgcoverartfullsize.ui', - 'library/dlghidden.ui', - 'library/dlgmissing.ui', 'library/dlgtagfetcher.ui', 'library/dlgtrackinfo.ui', 'library/export/dlgtrackexport.ui', - 'library/recording/dlgrecording.ui', + 'library/features/analysis/dlganalysis.ui', + 'library/features/autodj/dlgautodj.ui', + 'library/features/maintenance/dlghidden.ui', + 'library/features/maintenance/dlgmissing.ui', + 'library/features/recording/dlgrecording.ui', 'preferences/dialog/dlgprefautodjdlg.ui', 'preferences/dialog/dlgprefbeatsdlg.ui', 'preferences/dialog/dlgprefcontrolsdlg.ui', diff --git a/res/images/library/ic_library_folder.png b/res/images/library/ic_library_folder.png new file mode 100644 index 00000000000..1c91df56502 Binary files /dev/null and b/res/images/library/ic_library_folder.png differ diff --git a/res/images/library/ic_library_lockpreselect.png b/res/images/library/ic_library_lockpreselect.png new file mode 100644 index 00000000000..629097f13cb Binary files /dev/null and b/res/images/library/ic_library_lockpreselect.png differ diff --git a/res/images/library/ic_library_maintenance.png b/res/images/library/ic_library_maintenance.png new file mode 100644 index 00000000000..58b68c9e9b1 Binary files /dev/null and b/res/images/library/ic_library_maintenance.png differ diff --git a/res/images/library/ic_library_notpreselect.png b/res/images/library/ic_library_notpreselect.png new file mode 100644 index 00000000000..705eb81942c Binary files /dev/null and b/res/images/library/ic_library_notpreselect.png differ diff --git a/res/images/library/ic_library_pinned.png b/res/images/library/ic_library_pinned.png new file mode 100644 index 00000000000..e296c37ba31 Binary files /dev/null and b/res/images/library/ic_library_pinned.png differ diff --git a/res/images/library/ic_library_preselect.png b/res/images/library/ic_library_preselect.png new file mode 100644 index 00000000000..edd51930993 Binary files /dev/null and b/res/images/library/ic_library_preselect.png differ diff --git a/res/mixxx.qrc b/res/mixxx.qrc index ec6c2b8c785..d2f8c4412e4 100644 --- a/res/mixxx.qrc +++ b/res/mixxx.qrc @@ -108,5 +108,15 @@ translations/mixxx_uz.qm translations/mixxx_zh_CN.qm translations/mixxx_zh_TW.qm + images/library/ic_library_maintenance.png + images/library/ic_library_folder.png + skins/save.png + skins/downArrow.png + skins/cross_2.png + skins/save_disabled.png + images/library/ic_library_preselect.png + images/library/ic_library_notpreselect.png + images/library/ic_library_lockpreselect.png + images/library/ic_library_pinned.png diff --git a/res/schema.xml b/res/schema.xml index e58c8033f68..36da5556791 100644 --- a/res/schema.xml +++ b/res/schema.xml @@ -426,4 +426,24 @@ METADATA ALTER TABLE cues ADD COLUMN color INTEGER DEFAULT 4294901760 NOT NULL; + + + Add new savedQueries table to store the saved queries for each + library feature + + + CREATE TABLE IF NOT EXISTS savedQueries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + libraryFeature TEXT, + query TEXT, + title TEXT, + selectedItems TEXT, + sortOrder TEXT, + vScrollbarPos INTEGER, + sortColumn INTEGER, + sortAscendingOrder NUMERIC, + pinned NUMERIC + ); + + diff --git a/res/skins/Deere/image/style_sort_down.svg b/res/skins/Deere/image/style_sort_down.svg index dcfc197e736..96d0b84931d 100644 --- a/res/skins/Deere/image/style_sort_down.svg +++ b/res/skins/Deere/image/style_sort_down.svg @@ -1,5 +1,60 @@ - - - - + + + + + + image/svg+xml + + + + + + + + + diff --git a/res/skins/Deere/image/style_sort_up.svg b/res/skins/Deere/image/style_sort_up.svg index 08ca2a566af..8a69a90ca93 100644 --- a/res/skins/Deere/image/style_sort_up.svg +++ b/res/skins/Deere/image/style_sort_up.svg @@ -1,5 +1,60 @@ - - - - + + + + + + image/svg+xml + + + + + + + + + diff --git a/res/skins/Deere/library.xml b/res/skins/Deere/library.xml index 0745dd779be..eabdac48553 100644 --- a/res/skins/Deere/library.xml +++ b/res/skins/Deere/library.xml @@ -14,35 +14,37 @@ LibraryExpanded - vertical - - me,f - 0,0 + horizontal + + vertical + min,me + + + + 6,6 + + + min,me + LibrarySidebarButtons + + + LibrarySplitter horizontal me,me [Deere],LibrarySidebarSplitSize - 2,8 + 2,4,4 vertical - - horizontal - - - - - - PreviewDeckContainer vertical @@ -153,8 +155,9 @@ 2,8 0,0 - - + + LibrarySidebarExpanded + LibraryCoverArt @@ -170,8 +173,40 @@ - - + + + + vertical + + + LibraryBreadCrumb + 1 + + + 1 + + + 1 + + + + + + + vertical + + + LibraryBreadCrumb + 2 + + + 2 + + + 2 + + + diff --git a/res/skins/Deere/skin.xml b/res/skins/Deere/skin.xml index 6e271eedbca..b50d2987542 100644 --- a/res/skins/Deere/skin.xml +++ b/res/skins/Deere/skin.xml @@ -22,9 +22,12 @@ 0 1 0 + 1 + #006596 + Mixxx diff --git a/res/skins/Deere/style.qss b/res/skins/Deere/style.qss index cd82be4f400..ab706eb0a5c 100644 --- a/res/skins/Deere/style.qss +++ b/res/skins/Deere/style.qss @@ -1,3 +1,5 @@ + + /* Deere , Skin for Mixxx 1.12.x www.mixxx.org @@ -119,14 +121,30 @@ *******************************************************************************/ /* library table */ -QTableView { +QTableView, QTreeView { color: #d2d2d2; background-color: #1F1F1F; alternate-background-color: #1A1A1A; selection-background-color: #006596; - selection-color: #D6D6D6; border: 1px solid #1A1A1A; - gridline-color: red; +} + +QTreeView::branch { + background-color: #1F1F1F; +} + +QTableView::item:selected:active, +QTreeView::item:selected:active { + color: #ffffff; + + background-color: #006596; +} + + +QTableView::item:selected:!active, +QTreeView::item:selected:!active { + color: #d2d2d2; + background-color: #0d4866; } /* checkbox in library "Played" column */ @@ -152,6 +170,109 @@ QTableView::indicator:unchecked { image: url(:/images/library/ic_library_unchecked.png); } +#LibrarySidebarButtons, WButtonBar { + background-color: #4B4B4B; +} + +QToolButton:focus, +QToolButton:focus::hover { + background-color: #006596; +} + +WLibraryBreadCrumb QLabel { + margin: 4px 4px; + font-size: 12px; + font-weight: bold; + color: #B3B3B3; +} + +WLibraryBreadCrumb QToolButton { + margin-right: 3px; + background: none; + border: none; +} + +WLibraryBreadCrumb QToolButton:hover { + background: none; +} + +WBaseLibrary QLabel { + margin: 4px 4px 0px 4px; + font-size: 12px; + font-weight: bold; + color: #B3B3B3; +} + +#LibraryCoverArtSplitter QTabWidget { + border: none; +} + +#LibraryCoverArtSplitter QTabWidget::pane { + border: 1px solid #1A1A1A; +} + +#LibraryCoverArtSplitter QTabWidget QTreeView, +#DlgAutoDJ { + margin: 0; + border: none; +} + +#LibraryCoverArtSplitter::handle { + image: url(skin:/image/style_handle_vertical_unchecked.svg); + background: none; +} + +#LibraryCoverArtSplitter::handle:pressed { + image: url(skin:/image/style_handle_vertical_checked.svg); + background: none; +} + +#LibraryCoverArtSplitter::handle:horizontal { + width: 6px; +} + +#LibraryCoverArtSplitter::handle:vertical { + height: 6px; +} + +#DlgAutoDJ #scrollAreaWidgetContents { + background-color: #222222; + border: none; +} + +#LibraryCoverArtSplitter QTabBar::tab { + padding: 4px; + border: none; + color: #006596; + background-color: #191919; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + + color: #D2D2D2; + background-color: #4B4B4B; + border: 0px solid #4B4B4B; + outline: none; +} + +#LibraryCoverArtSplitter QTabBar::tab:hover { + border-top: 0px solid #5F5F5F; + border-right: 0px solid #5F5F5F; + border-left: 0px solid #5F5F5F; + color: #D2D2D2; + background-color: #5F5F5F; +} + +#LibraryCoverArtSplitter QTabBar::tab:!enabled { + color: #969696; + background-color: #525252; +} + +#LibraryCoverArtSplitter QTabBar::tab:selected { + color: #FDFDFD; + background-color: #006596; + border: 0px solid #006596; +} + /* button in library "Preview" column */ QPushButton#LibraryPreviewButton { width: 23px; @@ -168,6 +289,17 @@ QPushButton#LibraryPreviewButton:checked { image: url(skin:/image/style_library_preview_pause.png); } + +#LibrarySidebarButtons:focus, +#LibrarySidebarButtons QToolButton:focus, +#LibrarySidebarExpanded > QWidget:focus, +#LibrarySidebarExpanded QAbstractScrollArea > QWidget:focus, +WLibrarySidebar:focus, +WLibrary:focus, +QTableView:focus { + border: 1px solid #0080BE; +} + /* library header row */ QHeaderView { color: #d2d2d2; @@ -198,17 +330,25 @@ QHeaderView::section:selected { border-right: none; } +QHeaderView::up-arrow, +QHeaderView::down-arrow { + background: #1A1A1A; + width: 12px; + padding-left: 3px; + padding-right: 3px; +} + QHeaderView::up-arrow { - image: url(skin:/image/style_sort_up.svg) + image: url(skin:/image/style_sort_up.svg); } QHeaderView::down-arrow { - image: url(skin:/image/style_sort_down.svg) + image: url(skin:/image/style_sort_down.svg); } /* library search bar */ WSearchLineEdit { - margin: 0px 0px 0px 1px; + margin: 0px 0px 5px 0px; padding: 2px; border: 1px solid #1A1A1A; background-color: #4B4B4B; @@ -216,9 +356,8 @@ WSearchLineEdit { } WSearchLineEdit:focus { - margin: 2px 0px 0px 1px; padding: 2px; - border: 1px solid #FF6600; + border: 2px solid #006596; background-color: #4B4B4B; color: #D6D6D6; selection-color: #222222; @@ -296,25 +435,6 @@ QTreeView::branch:open:has-children:has-siblings { height: 6px; } -/* QSplitter between LibrarySidebar and CoverArt */ -#LibraryCoverArtSplitter::handle { - image: url(skin:/image/style_handle_vertical_unchecked.svg); - background: none; -} - -#LibraryCoverArtSplitter::handle:pressed { - image: url(skin:/image/style_handle_vertical_checked.svg); - background: none; -} - -#LibraryCoverArtSplitter::handle:horizontal { - width: 6px; -} - -#LibraryCoverArtSplitter::handle:vertical { - height: 6px; -} - /* library cover art widget */ #LibraryCoverArt { padding: 8px 4px 4px 4px; @@ -322,203 +442,138 @@ QTreeView::branch:open:has-children:has-siblings { border: 1px solid #1A1A1A; } /* transition time in Auto DJ tab */ -WLibrary QSpinBox { +WBaseLibrary QSpinBox { min-height: 20px; max-height: 20px; min-width: 40px; max-width: 40px; } -WLibrary QSpinBox:editable { +WBaseLibrary QSpinBox:editable { background: transparent; color:#d2d2d2; } -/* Extra declaration for QRadionButton otherwise it shows up with wrong colors in Linux with Gnome */ -WLibrary QLabel, WLibrary QRadioButton { - /* same as QTreeview */ +WBaseLibrary QLabel, WBaseLibrary QRadioButton { color: #d2d2d2; - margin: 9px 10px 6px 0px; } -WLibrary QRadioButton::indicator { +WBaseLibrary QRadioButton::indicator { margin: 0px 5px 0px 2px; width: 18px; height: 18px; } -/* Additional space for the first QRadionButton in the row */ -WLibrary QRadioButton#radioButtonRecentlyAdded { - margin: 9px 10px 6px 14px; -} - -WLibrary QRadioButton::indicator:checked { +WBaseLibrary QRadioButton::indicator:checked { background: url(skin:/icon/ic_radio_button_on_18px.svg); } -WLibrary QRadioButton::indicator:unchecked { +WBaseLibrary QRadioButton::indicator:unchecked { background: url(skin:/icon/ic_radio_button_off_18px.svg); } + +WButtonBar QAbstractButton { + border-radius: 0px; +} + /* buttons in library (in hierarchical order of appearance) Style them just as the other regular buttons */ -#DlgMissing > QPushButton, -#DlgHidden > QPushButton, -#DlgAutoDJ > QPushButton, -#DlgRecording > QPushButton, -#DlgAnalysis > QPushButton { - margin: 9px 3px 6px 3px; - padding: 4px; +WBaseLibrary QPushButton { + margin: 2px 3px 2px 3px; + padding: 2px; color: #D2D2D2; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #4B4B4B, - stop: 1 #4B4B4B); + + background-color: #4B4B4B; border: 1px solid #4B4B4B; border-radius: 2px; outline: none; } -#DlgMissing > QPushButton:!enabled, -#DlgHidden > QPushButton:!enabled, -#DlgAutoDJ > QPushButton:!enabled, -#DlgAnalysis > QPushButton:!enabled { +WBaseLibrary QPushButton:focus { + background-color: #006596; +} + +WBaseLibrary QPushButton:!enabled { /* buttons in "disabled" (not click-able) state. They are nearly invisible by default QT palette, so style accordingly */ - margin: 9px 3px 6px 3px; - padding: 4px; color: #808080; /* Default #A3A3A3 -90L HSL*/ - background-color: qlineargradient(spread:pad, - x1:0, y1:0, x2:1, y2:0, - stop:0 rgba(95, 95, 95, 127), - stop:1 rgba(95, 95, 95, 127)); - /* 50% #5F5F5F = RGBA#5F5F5F7F */ + background-color: rgba(95, 95, 95, 127); border: 0px solid #5F5F5F; - border-radius: 2px; - outline: none; } -#DlgMissing > QPushButton:hover, -#DlgHidden > QPushButton:hover, -#DlgAutoDJ > QPushButton:hover, -#DlgRecording > QPushButton:hover, -#DlgAnalysis > QPushButton:hover { - margin: 9px 3px 6px 3px; - padding: 4px; +WBaseLibrary QPushButton:hover { color: #D2D2D2; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #5F5F5F, - stop: 1 #5F5F5F); + background-color: #5F5F5F; border: 0px solid #5F5F5F; - border-radius: 2px; - outline: none; } -#DlgAutoDJ > QPushButton:checked, -#DlgRecording > QPushButton:checked, -#DlgAnalysis > QPushButton:checked { +WBaseLibrary QPushButton:checked { /* checkbuttons in active state */ - margin: 9px 3px 6px 3px; - padding: 4px; color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #006596, - stop: 1 #006596); + background-color: #006596; border: 0px solid #006596; - border-radius: 2px; - outline: none; } -#DlgAutoDJ > QPushButton:checked:hover, -#DlgRecording > QPushButton:checked:hover, -#DlgAnalysis > QPushButton:checked:hover { +WBaseLibrary QPushButton:checked:hover { /* checkbuttons hovered over in "active" state */ - margin: 9px 3px 6px 3px; - padding: 4px; color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #0080BE, - stop: 1 #0080BE); + background-color: #0080BE; border: 0px solid #0080BE; - border-radius: 2px; - outline: none; } -#DlgMissing > QPushButton:pressed, -#DlgHidden > QPushButton:pressed, -#DlgAutoDJ > QPushButton:pressed, -#DlgAnalysis > QPushButton:pressed { +WBaseLibrary QPushButton:pressed { /* pushbuttons in "down" state */ - margin: 9px 3px 6px 3px; - padding: 4px; color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #006596, - stop: 1 #006596); + background-color: #006596; border: 0px solid #006596; - border-radius: 2px; - outline: none; -} - -/* Additional space for the first and the last QPushButton in the row */ -#DlgMissing > QPushButton#btnPurge, -#DlgHidden > QPushButton#btnUnhide, -#DlgAutoDJ > QPushButton#pushButtonAutoDJ, -#DlgRecording > QPushButton#pushButtonRecording, -#DlgAnalysis > QPushButton#pushButtonAnalyze { - margin: 9px 12px 6px 3px; } -#DlgAutoDJ > QPushButton#pushButtonShuffle { - margin: 9px 3px 6px 12px; +/* Scroll bars */ +QScrollBar { + color: #d2d2d2; + font-size: 8pt; + background: #222222; + padding: 3px; } - -/* Scroll bars */ QScrollBar:horizontal { border-top: 1px solid #141414; min-width: 12px; height: 15px; - background: #222222; - padding: 3px; } QScrollBar:vertical { border-left: 1px solid #141414; min-height: 12px; width: 15px; - background: #222222; - padding: 3px; } /* "add-page" and "sub-page" are the gutter of the scrollbar */ QScrollBar::add-page, QScrollBar::sub-page { background: none; } -QScrollBar::handle:horizontal { - min-width: 25px; - background: #5F5F5F; - border-radius: 3px; - border: none; -} -QScrollBar::handle:vertical { + +QScrollBar::handle { min-height: 25px; - background: #5F5F5F; + background: rgba(95, 95, 95, 150); border-radius: 3px; border: none; } -QScrollBar::handle:horizontal:hover, QScrollBar::handle:vertical:hover { - background: #B3B3B3; +QScrollBar::handle:hover { + background: rgba(179, 179, 179, 150); } /* Turn off buttons */ -QScrollBar::add-line:horizontal, QScrollBar::add-line:vertical { +QScrollBar::add-line, QScrollBar::sub-line { width: 0px; height: 0px; border: none; } -QScrollBar::sub-line:horizontal, QScrollBar::sub-line:vertical { - width: 0px; - height: 0px; - border: none; + +QScrollArea { + border: 1px solid #1A1A1A; + background-color: #222222; } + /******************************************************************************* ** END LIBRARY ***************************************************************** *******************************************************************************/ @@ -582,10 +637,13 @@ QScrollBar::sub-line:horizontal, QScrollBar::sub-line:vertical { WWidget, QLabel { font-family: "Open Sans"; - font-size: 8px; text-transform: uppercase; } +WWidget { + font-size: 8px; +} + /* Start spacing for Deck overview row (small waveform, option grid) */ #OptionGrid { background-color: #333333; @@ -712,13 +770,13 @@ WWidget, QLabel { /* Start editable widgets in decks */ #BpmEditRow:hover, #KeyEditRow:hover, #PositionGutter:hover, #StarratingGutter:hover { /* emphasize editable widgets on hover */ - border: 1px solid #FF6600; + border: 1px solid #0080BE; background-color: rgba(255, 102, 0, 128); } #BpmEditRowExpanded, #KeyEditRowExpanded { /* emphasize active widget */ - border: 1px solid #FF6600; + border: 1px solid #006596; background: rgba(255, 102, 0, 64); color: #D6D6D6; } @@ -1077,39 +1135,31 @@ WWidgetGroup { ** Buttons ******************************************************************* *******************************************************************************/ -WPushButton { +WPushButton, QToolButton { color: #D2D2D2; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #4B4B4B, - stop: 1 #4B4B4B); - border: 1px solid #4B4B4B; + background-color: #4B4B4B; + border: 0px solid #4B4B4B; border-radius: 2px; outline: none; } -WPushButton:hover { +WPushButton:hover, QToolButton:hover { color: #D2D2D2; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #5F5F5F, - stop: 1 #5F5F5F); + background-color: #5F5F5F; border: 0px solid #5F5F5F; } /*"Pressed" state*/ -WPushButton[value="1"] { +WPushButton[value="1"], QToolButton:pressed { /*color: #FDFDFD;*/ color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #006596, - stop: 1 #006596); + background-color: #006596; border: 0px solid #006596; } WPushButton[value="1"]:hover { color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #0080bE, - stop: 1 #0080BE); + background-color: #0080BE; border: 0px solid #0080BE; } @@ -1120,17 +1170,13 @@ WPushButton[value="1"]:hover { */ WPushButton[value="2"] { color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #006596, - stop: 1 #006596); + background-color: #006596; border: 0px solid #006596; } WPushButton[value="2"]:hover { color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #0080BE, - stop: 1 #0080BE); + background-color: #0080BE; border: 0px solid #0080BE; } @@ -1141,9 +1187,7 @@ WPushButton[value="2"]:hover { */ #OrientationButton[value="1"] { color: #D2D2D2; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #4B4B4B, - stop: 1 #4B4B4B); + background-color: #4B4B4B; border: 1px solid #4B4B4B; border-radius: 2px; outline: none; @@ -1151,41 +1195,31 @@ WPushButton[value="2"]:hover { #OrientationButton:hover { color: #D2D2D2; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #5F5F5F, - stop: 1 #5F5F5F); + background-color: #5F5F5F; border: 0px solid #5F5F5F; } #OrientationButton[value="2"] { color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #006596, - stop: 1 #006596); + background-color: #006596; border: 0px solid #006596; } #OrientationButton[value="2"]:hover { color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #0080BE, - stop: 1 #0080BE); + background-color: #0080BE; border: 0px solid #0080BE; } #OrientationButton[value="0"] { color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #006596, - stop: 1 #006596); + background-color: #006596; border: 0px solid #006596; } #OrientationButton[value="0"]:hover { color: #FDFDFD; - background-color: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0 #0080BE, - stop: 1 #0080BE); + background-color: #0080BE; border: 0px solid #0080BE; } diff --git a/res/skins/LateNight/image/style_sort_down.svg b/res/skins/LateNight/image/style_sort_down.svg new file mode 100644 index 00000000000..f60ad295e75 --- /dev/null +++ b/res/skins/LateNight/image/style_sort_down.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/res/skins/LateNight/image/style_sort_up.svg b/res/skins/LateNight/image/style_sort_up.svg new file mode 100644 index 00000000000..e92f24780d7 --- /dev/null +++ b/res/skins/LateNight/image/style_sort_up.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/res/skins/LateNight/library.xml b/res/skins/LateNight/library.xml index 4c1c1d4ad66..8fbf1419962 100644 --- a/res/skins/LateNight/library.xml +++ b/res/skins/LateNight/library.xml @@ -13,32 +13,36 @@ horizontal + + LibrarySidebarButtons + LibrarySplitter me,me - 1,12 + 1,6,6 [LateNight],LibrarySidebarSplitSize vertical - - CoverArtSplitter me,me - 1,1 + 30,30 + 2,8 [LateNight],CoverArtSplitSize vertical 0,0 - + + LibrarySidebarExpanded + me,me - 40,40 + 30,30, [Library],show_coverart visible @@ -49,14 +53,31 @@ - - vertical - - #585858 - #eece33 - + + 1 + + + 1 + + + 1 + + + + + vertical + + + 2 + + + 2 + + + 2 + diff --git a/res/skins/LateNight/skin.xml b/res/skins/LateNight/skin.xml index 4a9db896c78..c415d807d26 100644 --- a/res/skins/LateNight/skin.xml +++ b/res/skins/LateNight/skin.xml @@ -55,6 +55,7 @@ 2 1 1 + 1 0 1 1 diff --git a/res/skins/LateNight/style.qss b/res/skins/LateNight/style.qss index 048b542d72e..64dbec14bba 100644 --- a/res/skins/LateNight/style.qss +++ b/res/skins/LateNight/style.qss @@ -1,3 +1,4 @@ + #debugbg { background-color: #fff; } @@ -49,19 +50,6 @@ background-color: #0f0f0f; } -#LibrarySingleton { - padding-top: 5px; - padding-bottom: 2px; -} - -#Library { - border-left: 0px solid #585858; - background-color: #0e0e0e; - padding-top: 5px; - padding-bottom: 2px; - /*border-top: 1px solid #585858;*/ -} - #DeckRowOne { qproperty-layoutAlignment: 'AlignLeft | AlignTop'; border-bottom: 1px solid #585858; @@ -1156,9 +1144,132 @@ /* Library styling is hard */ -QTableView, QTextBrowser, QTreeView { +#LibrarySingleton { + padding-top: 5px; + padding-bottom: 2px; +} + +#Library { + border-left: 0px solid #585858; + background-color: #0e0e0e; + padding-top: 5px; + padding-bottom: 2px; + /*border-top: 1px solid #585858;*/ +} + +#LibrarySidebarExpanded QWidget { + background-color: #0f0f0f; +} + +#LibrarySidebarExpanded QScrollArea { + border:none; +} + +#LibrarySidebarExpanded QTabWidget QTreeView, #DlgAutoDJ QScrollArea { + margin: 0; + border: none; +} + +#LibrarySidebarExpanded QTabWidget { + border: none +} + +#LibrarySidebarExpanded QTabWidget::pane, +#LibrarySidebarExpanded #DlgRecording, +#LibrarySidebarExpanded #DlgAnalysis { + border: 1px solid #585858; +} + +#LibrarySidebarExpanded QTabBar::tab { + padding: 5px; + border: none; + color: #cfb32c; + background-color: #191919; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +#LibrarySidebarExpanded QTabBar::tab:hover { + border-top: 1px solid #585858; + border-right: 1px solid #585858; + border-left: 1px solid #585858; + background-color: #232323; +} + +#LibrarySidebarExpanded QTabBar::tab:!enabled { + color: #7A6919; + background-color: #191919; +} + + +#LibrarySidebarExpanded QTabBar::tab:focus { + border-top: 1px solid #8E5C00; + border-right: 1px solid #8E5C00; + border-left: 1px solid #8E5C00; + background-color: #232323; +} + +#LibrarySidebarExpanded QTabBar::tab:selected { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #585858, stop:1 #0f0f0f); +} + +#LibrarySidebarExpanded QWidget:!enabled { + color: #666666; +} + +#LibrarySidebarButtons QToolButton { + margin: 0px 0px 0px 0px; + padding: 0px 0px 0px 0px; + border: 1px solid #191919; + color: #cfb32c; + background-color: #191919; +} + +#LibrarySidebarExpanded { + margin: -5px 0px 0px 5px; +} + +#LibrarySidebarExpanded QPushButton { + margin: 2px 3px 2px 3px; + padding: 4px 6px; + min-width: 65px; + border: none; + color: #cfb32c; + background-color: #191919; +} + +#LibrarySidebarButtons QToolButton:hover, +#LibrarySidebarExpanded QPushButton:hover { + border: 1px solid #585858; + background-color: #232323; + color: #cfb32c; +} + +#LibrarySidebarButtons QToolButton:hover { + border-radius: 10px; +} + +#LibrarySidebarButtons QToolButton:focus { + border-radius: 10px; +} + +#LibrarySidebarButtons:focus, +#LibrarySidebarButtons QToolButton:focus, +#LibrarySidebarExpanded > QWidget:focus, +#LibrarySidebarExpanded QAbstractScrollArea > QWidget:focus, +WLibrarySidebar:focus, +WLibrary:focus, +QTableView:focus { + border: 1px solid #8E5C00; +} + +#LibrarySidebarButtons WButtonBar { + background-color: #191919; + margin: 0px 1px 0px 0px; +} + +#LibrarySidebarButtons, QTableView, QTextBrowser, QTreeView { border: 1px solid #585858; - /*font: 15px/18px;*/ color: #cfb32c; background-color: #0f0f0f; alternate-background-color: #1a1a1a; @@ -1167,14 +1278,43 @@ QTableView, QTextBrowser, QTreeView { selection-background-color: #725309; } +WLibraryBreadCrumb QLabel { + margin: 4px 0px 6px 3px; +} + +WLibraryBreadCrumb QToolButton { + margin-right: 3px; + background: none; + border: none; +} + +WBaseLibrary QLabel { + padding: 0px 0px 0px 0px; + margin: 100px 100px 100px 100px; + background-color: blue; + border: 0px solid blue; +} + +QTreeView::item:selected:active, +QTableView::item:selected:active { + color: #cfb32c; + background-color: #725309; +} + +QTreeView::item:selected:!active, +QTableView::item:selected:!active { + color: #cfb32c; + background-color: #4c3b11; +} + +QTableView:!focus { + selection-background-color: #4c3b11; +} + /* checkbox in library "Played" column */ QTableView::indicator { width: 12px; height: 12px;} QTableView::indicator:checked { background: url(skin:/style/style_checkbox_checked.png);} QTableView::indicator:unchecked { background: url(skin:/style/style_checkbox_unchecked.png);} -QTableView::item:selected { - color: #cfb32c; - background-color: #725309; -} /* BPM lock icon in the library "BPM" column. */ #LibraryBPMButton::indicator:checked { image: url(:/images/library/ic_library_checked.png); } @@ -1185,18 +1325,18 @@ QTableView::item:selected { } /* Button in library "Preview" column */ -QPushButton#LibraryPreviewButton { + +#LibraryPreviewButton { width: 23px; height: 12px; - background: transparent; + image: url(skin:/style/style_library_preview_play.png); + background-color: transparent; border: 0px; } -/*QPushButton#LibraryPreviewButton:focus { - background-color: #725309; -}*/ -QPushButton#LibraryPreviewButton:!checked{ image: url(skin:/style/style_library_preview_play.png); } -QPushButton#LibraryPreviewButton:checked{ image: url(skin:/style/style_library_preview_pause.png); } +#LibraryPreviewButton:checked { + image: url(skin:/style/style_library_preview_pause.png); +} QHeaderView { font: 13px/15px; @@ -1207,26 +1347,45 @@ QHeaderView::section { height: 18px; border: 1px solid #585858; border-left: 0px; - background-color:qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #585858, stop:1 #0f0f0f); + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #585858, stop:1 #0f0f0f); +} + +QHeaderView::up-arrow, +QHeaderView::down-arrow { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #585858, stop:1 #0f0f0f); + width: 12px; + padding-left: 3px; + padding-right: 3px; + border-bottom: 1px solid #585858; +} + +QHeaderView::up-arrow { + image: url(skin:/image/style_sort_up.svg); } +QHeaderView::down-arrow { + image: url(skin:/image/style_sort_down.svg); +} + + /* QScrollbar styling is even harder */ -QScrollBar:horizontal { +QScrollBar { border: 1px solid #585858; - min-width: 12px; - height: 15px; background: #1a1a1a; border-radius: 3px; padding: 1px; + color: #999999; + font-size: 8pt; +} +QScrollBar:horizontal { + min-width: 12px; + height: 15px; } QScrollBar:vertical { - border: 1px solid #585858; min-height: 12px; width: 15px; - background: #1a1a1a; - border-radius: 3px; - padding: 1px; } + /* "add-page" and "sub-page" are the gutter of the scrollbar */ QScrollBar::add-page, QScrollBar::sub-page { min-width: 15px; @@ -1235,25 +1394,14 @@ QScrollBar::add-page, QScrollBar::sub-page { background-color: #1a1a1a; border-radius: 3px; } -QScrollBar::handle:horizontal { - min-width: 25px; - background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #725309, stop:1 #412f05); - border-radius: 3px; - border: 1px solid #725309; -} -QScrollBar::handle:vertical { +QScrollBar::handle { min-height: 25px; - background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #725309, stop:1 #412f05); + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(114, 83, 9, 190), stop:1 rgba(65, 47, 5, 190)); border-radius: 3px; border: 1px solid #725309; } /* Turn off buttons */ -QScrollBar::add-line:horizontal, QScrollBar::add-line:vertical { - width: 0px; - height: 0px; - border: 0px solid white; -} -QScrollBar::sub-line:horizontal, QScrollBar::sub-line:vertical { +QScrollBar::add-line, QScrollBar::sub-line { width: 0px; height: 0px; border: 0px solid white; @@ -1264,8 +1412,21 @@ QSpinBox:editable { background: transparent; color: #cfb32c; } QSpinBox { min-height: 20px; max-height: 20px;min-width: 40px; max-width: 40px;} /* library search bar */ -WSearchLineEdit { padding: 2px; border: 1px solid #656565; background: #181818; color: #cfb32c; } -WSearchLineEdit:focus { padding: 2px; border: 2px solid #FF6600; background: #0f0f0f; color: #eece33;} +WSearchLineEdit { + margin-bottom: 2px; + padding: 2px; + border: 1px solid #656565; + background: #181818; + color: #cfb32c; +} + + +WSearchLineEdit:focus { + border: 1px solid #8E5C00; + background: #0f0f0f; + color: #eece33; +} + /* cover art */ WCoverArt { background: transparent; color: #ACACAC; } @@ -1286,50 +1447,45 @@ WCoverArt { background: transparent; color: #ACACAC; } QLabel, QRadioButton { background: transparent; color: #cfb32c; } /* Additional space for QRadionButtons and QLabels */ -WLibrary QRadioButton, WLibrary QLabel { margin: 9px 3px 6px 3px; } - -/* Additional space for the first QRadionButton in the row */ -WLibrary QRadioButton#radioButtonRecentlyAdded { margin: 9px 3px 6px 12px; } - -/* Additional space for the QPushButtons */ -#DlgMissing > QPushButton, -#DlgHidden > QPushButton, -#DlgAutoDJ > QPushButton, -#DlgRecording > QPushButton, -#DlgAnalysis > QPushButton { margin: 9px 3px 6px 3px; padding: 3px 8px; min-width: 65px; } - -/* Additional space for the first QPushButton in the row */ -#DlgMissing > QPushButton#btnPurge, -#DlgHidden > QPushButton#btnUnhide, -#DlgAutoDJ > QPushButton#pushButtonAutoDJ, -#DlgRecording > QPushButton#pushButtonRecording, -#DlgAnalysis > QPushButton#pushButtonAnalyze { margin: 9px 12px 6px 3px; } +WBaseLibrary QRadioButton, WBaseLibrary QLabel { margin: 9px 3px 6px 3px; } -/* Additional space for the last QPushButton in the row */ -#DlgAutoDJ > QPushButton#pushButtonShuffle { margin: 9px 3px 6px 12px; } +/* Additional space for the first QRadionButton in the row +WBaseLibrary QRadioButton#radioButtonRecentlyAdded { margin: 9px 3px 6px 12px; } */ /* Spacing between treeview and searchbar */ -QTreeView { margin: 10px 0px 0px 0px; } +QTreeView { margin: 0px 0px 0px 0px; } /* triangle for closed/opened branches in treeview */ -QTreeView { show-decoration-selected: 0; background-color: #151515; } /* Suppresses that selected sidebar items branch indicator shows wrong color when out of focus ; lp:880588 */ +QTreeView::branch, +QTreeView { + background-color: #0f0f0f; } /* Suppresses that selected sidebar items branch indicator shows wrong color when out of focus ; lp:880588 */ QTreeView::branch:has-children:!has-siblings:closed, -QTreeView::branch:closed:has-children:has-siblings { border-image: none; image: url(skin:/style/style_branch_closed.png); +QTreeView::branch:closed:has-children:has-siblings { + border-image: none; + image: url(skin:/style/style_branch_closed.png); background-color:#0f0f0f; } QTreeView::branch:open:has-children:!has-siblings, -QTreeView::branch:open:has-children:has-siblings { border-image: none; image: url(skin:/style/style_branch_open.png); +QTreeView::branch:open:has-children:has-siblings { + border-image: none; + image: url(skin:/style/style_branch_open.png); background-color:#0f0f0f; } QTreeView::branch:has-children:!has-siblings:closed:selected, -QTreeView::branch:closed:has-children:has-siblings:selected { border-image: none; image: url(skin:/style/style_branch_closed.png); - background-color:qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #585858, stop:1 #0f0f0f); +QTreeView::branch:closed:has-children:has-siblings:selected { + border-image: none; + image: url(skin:/style/style_branch_closed.png); } QTreeView::branch:open:has-children:!has-siblings:selected, -QTreeView::branch:open:has-children:has-siblings:selected { border-image: none; image: url(skin:/style/style_branch_open.png); - background-color:qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #585858, stop:1 #0f0f0f); +QTreeView::branch:open:has-children:has-siblings:selected { + border-image: none; + image: url(skin:/style/style_branch_open.png); } -QTreeView::item:selected { - background-color:qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #585858, stop:1 #0f0f0f); - color: #cfb32c; + +WLibrary[showFocus="0"] { + border-top: 2px solid #0f0f0f; +} + +WLibrary[showFocus="1"] { + border-top: 2px solid #FF7100; } diff --git a/res/skins/Shade/skin.xml b/res/skins/Shade/skin.xml index e61033ff9b0..f4ecc35abf8 100644 --- a/res/skins/Shade/skin.xml +++ b/res/skins/Shade/skin.xml @@ -1,149 +1,155 @@ - + You are free: + to Share - to copy, distribute and transmit the work + to Remix - to adapt the work + + Under the following conditions: + Attribution - You must attribute the work in the manner specified by the author or licensor + (but not in any way that suggests that they endorse you or your use of the work). + A attribution should include the following: The name of the author and/or licensor, + the title of the work, the URL that is associated with the work. + + Share Alike - If you alter, transform, or build upon this work, you may distribute + the resulting work only under the same or similar license to this one. + + See CHANGELOG.txt for the list of changes. + --> - - - - Shade - jus - 1.12.0.01 - A 2-deck split-waveform skin with 4 samplers. - en - Creative Commons Attribution, Share-Alike 3.0 Unported - - 2 - 4 - 1 - - 1 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - - - - - - - - Classic - - - - Dark - - - 90 - -30 - -30 - - - -40 - - - - - Summer Sunset - - - 100 - 0.7 - 0.3 - - - 50 - 0 - 50 - 120 - -10 - - - - - - - - - Mixxx - - 1024,500 - me,me - vertical + + + + Shade + jus + 1.12.0.01 + A 2-deck split-waveform skin with 4 samplers. + en + Creative Commons Attribution, Share-Alike 3.0 Unported + + 2 + 4 + 1 + + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + + + + + #e74421 + + + + + Classic + + + + Dark + + + 90 + -30 + -30 + + + -40 + + + + + Summer Sunset + + + 100 + 0.7 + 0.3 + + + 50 + 0 + 50 + 120 + -10 + + + + + + + + + Mixxx + + 1024,500 + me,me + vertical - LaunchImage { background-color: #202020; } + LaunchImage { background-color: #202020; } QLabel { - image: url(skin:/style/mixxx-icon-logo-symbolic.png); - padding:0; + image: url(skin:/style/mixxx-icon-logo-symbolic.png); + padding:0; margin:0; border:none; min-width: 208px; min-height: 48px; - max-width: 208px; + max-width: 208px; max-height: 48px; } QProgressBar { - background-color: #202020; + background-color: #202020; border:none; min-width: 208px; min-height: 3px; @@ -153,117 +159,117 @@ QProgressBar::chunk { background-color: #ec4522; } - - - DecksAndMixer - 1024,265 - e,f - horizontal - - - 378,265 - f,f - - - 1,6 + + + DecksAndMixer + 1024,265 + e,f + horizontal + + + 378,265 + f,f + + + 1,6 375,255 - - style/style_bg_deck.png - - - - - 5,200 - 372,50 + + style/style_bg_deck.png + + + + + 5,200 + 372,50 f,f - - - - 100,0 - 93,50 - - - - + + + + 100,0 + 93,50 + + + + [Microphone2],enabled visible - - - - [Microphone],show_microphone - visible - - - - - - - - 260,265 - - - - - - 378,265 - f,f - - - 1,6 - 375,255 - - style/style_bg_deck.png - - - - - 5,200 - 372,50 + + + + [Microphone],show_microphone + visible + + + + + + + + 260,265 + + + + + + 378,265 + f,f + + + 1,6 + 375,255 + + style/style_bg_deck.png + + + + + 5,200 + 372,50 f,f - - - - 109,0 - 108,50 - - - - + + + + 109,0 + 108,50 + + + + [Auxiliary2],enabled visible - - - [Microphone],show_microphone - visible - - - - - - - + + + [Microphone],show_microphone + visible + + + + + + + [Master],maximize_library visible @@ -271,24 +277,24 @@ - - - - - 1024,105 - me,me - vertical - - - + + + + + 1024,105 + me,me + vertical + + + me,f @@ -364,405 +370,538 @@ - - - Library - 0,0 - e,e - me,me - horizontal - - - + + QPushButton#LibraryPreviewButton { width: 23px; height: 12px; background: transparent; border: 0; } + QPushButton#LibraryPreviewButton:!checked{ image: url(skin:/style/style_library_preview_play.png); } + QPushButton#LibraryPreviewButton:checked{ image: url(skin:/style/style_library_preview_pause.png); } + + QPushButton#LibraryBPMButton {background: transparent; border: 0;} + QPushButton#LibraryBPMButton::checked {image: url(:/images/library/ic_library_checked.png);} + QPushButton#LibraryBPMButton::!checked {image: url(:/images/library/ic_library_unchecked.png);} + + QHeaderView { font: 11px/13px sans-serif; + font-family: "Open Sans";} + + + + QSpinBox:editable { + font: 12px/14px sans-serif; + font-family: "Open Sans"; + background: transparent; + color: #ACACAC; } + QSpinBox { min-height: 20px; max-height: 20px;min-width: 40px; max-width: 40px;} + + + WSearchLineEdit { + margin: 3px 0px 3px 0px; + padding: 1px; + border: 1px solid #656565; + font: 12px/14px sans-serif; + font-family: "Open Sans"; + background: transparent; + color: #ACACAC; + } + WSearchLineEdit:focus { + padding: 2px; + border: 2px solid #e74421; + font: 12px/14px sans-serif; + font-family: "Open Sans"; + background: rgba(255, 102, 0,50); + color: #D6D6D6;} + + + WCoverArt { + font: 13px/15px sans-serif; + font-family: "Open Sans"; + background: transparent; + color: #ACACAC; + } + + + QSplitter::handle { image: url(skin:/style/style_handle_unchecked.png); background: none; } + QSplitter::handle:pressed { image: url(skin:/style/style_handle_checked.png); background: none; } + QSplitter::handle:horizontal { width: 6px; } + QSplitter::handle:vertical { height: 6px;} + + QAbstractButton { font: 12px/14px sans-serif; + font-family: "Open Sans";} + + QLabel, QRadioButton { + font: 12px/14px sans-serif; + font-family: "Open Sans"; + background: transparent; + color: #C1C1C1;} + + + WBaseLibrary QPushButton { + margin: 2px 3px; + padding: 3px 3px; + } + + /* Scroll bars */ + QScrollBar { + color: #474747; + font-size: 8pt; + padding: 3px; + background-color: #aab2b7; + } + + QScrollBar:horizontal { + border-top: 1px solid #141414; + min-width: 12px; + height: 15px; + } + QScrollBar:vertical { + border-left: 1px solid #141414; + min-height: 12px; + width: 15px; + } + /* "add-page" and "sub-page" are the gutter of the scrollbar */ + QScrollBar::add-page, QScrollBar::sub-page { + background: none; + } + + QScrollBar::handle { + min-height: 25px; + background: rgba(95, 95, 95, 150); + border: none; + } + QScrollBar::handle:hover { + background: rgba(120, 120, 120, 150); + } + + /* Turn off buttons */ + QScrollBar::add-line, QScrollBar::sub-line { + width: 0px; + height: 0px; + border: none; + } + + WBaseLibrary QScrollArea, + QScrollArea QWidget { + border: none; + background-color: transparent; + } + + WBaseLibrary QTabWidget QTreeView, + #DlgAutoDJ QScrollArea { + background-color: #191919; + border: none; + } + + #DlgAutoDJ QScrollArea QScrollBar { + background-color: #aab2b7; + } + + WBaseLibrary QTabWidget::pane { + background-color: #191919; + border: 1px solid #646464; + } + + WBaseLibrary QTreeView { + margin: 0px; + } + + WBaseLibrary QPushButton { + border: 1px solid black; + background-color: #aab2b7; + } + + #LibrarySidebarButtons { + border: none; + background-color: #aab2b7; + } + + #LibrarySidebarButtons WButtonBar { + background-color: #aab2b7; + } + + WButtonBar { + border: none; + } + + WButtonBar QToolButton { + padding: 2px 2px 2px 2px; + color: black; + background-color: #aab2b7; + border: 2px solid #aab2b7; + } + + + + WButtonBar QToolButton:hover { + border: 2px solid #626f87; + } + + WButtonBar QToolButton:focus { + border: 2px solid #e74421; + } + + #LibrarySidebarButtons:focus, + #LibrarySidebarButtons QToolButton:focus, + #LibrarySidebarExpanded > QWidget:focus, + #LibrarySidebarExpanded QAbstractScrollArea > QWidget:focus, + WLibrarySidebar:focus, + WLibrary:focus, + QTableView:focus { + border: 1px solid #e74421; + } + + WLibraryBreadCrumb QLabel { + margin: 2px 0px 2px 3px; + } + + WLibraryBreadCrumb QToolButton { + margin-right: 3px; + background: none; + border: none; + } + + WBaseLibrary QLabel { + margin-left: 5px; + margin-bottom: 5px; + } + + + QTreeView { margin: 0px 0px 0px 5px; } + + + QTreeView { show-decoration-selected: 0;} + QTreeView::branch:has-children:!has-siblings:closed, + QTreeView::branch:closed:has-children:has-siblings { border-image: none; image: url(skin:/style/style_branch_closed.png);} + QTreeView::branch:open:has-children:!has-siblings, + QTreeView::branch:open:has-children:has-siblings { border-image: none; image: url(skin:/style/style_branch_open.png);} + + + + + me,me + [Shade],LibrarySidebarSplitSize + 1,e + + + vertical + + + + + horizontal + + + + vertical + + + + horizontal + + + + text + + [PreviewDeck1] + + 50me,15f + right + + + + eject + + 1 + + 0 + btn_eject1_over.png + btn_eject1.png + + + + [PreviewDeck1],eject + true + LeftButton + false + + + + + + + + horizontal + + + play_start + + 2 + true + + 0 + btn_play_sampler_down.png + btn_play_sampler.png + + + 1 + btn_play_sampler_overdown.png + btn_play_sampler_over.png + + 2,2 + + [PreviewDeck1],play + true + LeftButton + + + [PreviewDeck1],start + true + RightButton + false + + + + waveform_overview + + [PreviewDeck1] + me,30f + #8D98A3 + #FFE300 + #0099FF + #FF0035 + #FF8000 + #00FF00 + + bottom + #FFFFFF + #00FF00 + %1 + + + cue_point + C + top + #FF001C + #00FF00 + + + [PreviewDeck1],playposition + false + + + + + + + + + + 20,62 + + + + + preview_PeakIndicator + + btn_clipping_previewdeck_over.png + btn_clipping_previewdeck.png + 0,1 + + [PreviewDeck1],PeakIndicator + + + + + + channel_VuMeter + + btn_volume_display_previewdeck_over.png + btn_volume_display_previewdeck.png + 0,12 + false + 3 + 250 + 50 + 3 + + [PreviewDeck1],VuMeter + + + + + + [Deere],LibrarySidebarSplitSizepregain + + knob_volume_previewdeck.png + slider_volume_previewdeck.png + 10,2 + false + + [PreviewDeck1],pregain + false + + + + + + + [PreviewDeck],show_previewdeck + visible + + + + + horizontal + + + LibrarySidebarButtons + + + 3,0 + + + vertical + 8,2 + me,me + 30,30 + + + LibrarySidebarExpanded + 100,100 + + + + 30,30 + + [Library],show_coverart + visible + + + + + + + + + + vertical + + + 1 + + + 1 + + + 1 + + + + + vertical + + + 2 + + + 2 + + + 2 + + + + + + + + + + diff --git a/res/skins/cross.png b/res/skins/cross.png index 7cdc6e33788..ea1424ff16f 100644 Binary files a/res/skins/cross.png and b/res/skins/cross.png differ diff --git a/res/skins/cross_2.png b/res/skins/cross_2.png new file mode 100644 index 00000000000..ef516f72adf Binary files /dev/null and b/res/skins/cross_2.png differ diff --git a/res/skins/downArrow.png b/res/skins/downArrow.png new file mode 100644 index 00000000000..ac35ac64988 Binary files /dev/null and b/res/skins/downArrow.png differ diff --git a/res/skins/save.png b/res/skins/save.png new file mode 100644 index 00000000000..88275724d40 Binary files /dev/null and b/res/skins/save.png differ diff --git a/res/skins/save_disabled.png b/res/skins/save_disabled.png new file mode 100644 index 00000000000..0e0de61d790 Binary files /dev/null and b/res/skins/save_disabled.png differ diff --git a/src/controllers/controlpickermenu.cpp b/src/controllers/controlpickermenu.cpp index ac5f5e523e0..552c4b07919 100644 --- a/src/controllers/controlpickermenu.cpp +++ b/src/controllers/controlpickermenu.cpp @@ -732,6 +732,9 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent) addControl("[Library]", "show_coverart", tr("Cover Art Show/Hide"), tr("Show/hide cover art"), guiMenu); + addControl("[Library]", "show_icon_text", + tr("Icons' text Show/Hide"), + tr("Show/Hide icons' text"), guiMenu); QString spinnyTitle = tr("Vinyl Spinner Show/Hide"); QString spinnyDescription = tr("Show/hide spinning vinyl widget"); diff --git a/src/dialog/savedqueries/dlgsavedquerieseditor.cpp b/src/dialog/savedqueries/dlgsavedquerieseditor.cpp new file mode 100644 index 00000000000..a71bc4adf10 --- /dev/null +++ b/src/dialog/savedqueries/dlgsavedquerieseditor.cpp @@ -0,0 +1,50 @@ +#include + +#include "dialog/savedqueries/dlgsavedquerieseditor.h" +#include "dialog/savedqueries/savedqueriestablemodel.h" +#include "library/libraryfeature.h" +#include "library/trackcollection.h" + +DlgSavedQueriesEditor::DlgSavedQueriesEditor(LibraryFeature* pFeature, + TrackCollection* pTrackCollection, + QWidget* parent) + : QDialog(parent), + m_pTrackCollection(pTrackCollection), + m_pFeature(pFeature) { + setupUi(this); + SavedQueriesTableModel *pSaveModel = + new SavedQueriesTableModel(m_pFeature, + m_pTrackCollection->getSavedQueriesDAO(), + parent); + tableView->setModel(pSaveModel); + for (int i = 0; i < SavedQueryColumns::NUM_COLUMNS; ++i) { + tableView->setColumnHidden(i, pSaveModel->isColumnInternal(i)); + } + + tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + tableView->setSelectionMode(QAbstractItemView::SingleSelection); + tableView->verticalHeader()->hide(); + + connect(pushRemove, SIGNAL(pressed()), this, SLOT(removeQuery())); +} + +void DlgSavedQueriesEditor::accept() { + tableView->model()->submit(); + QDialog::accept(); +} + +void DlgSavedQueriesEditor::removeQuery() { + QItemSelectionModel* model = tableView->selectionModel(); + if (model == nullptr) return; + + QModelIndexList selected = model->selectedRows(); + + QSet removedRows; + for (const QModelIndex& index : selected) { + removedRows << index.row(); + } + + for (int row : removedRows) { + tableView->model()->removeRow(row); + } +} diff --git a/src/dialog/savedqueries/dlgsavedquerieseditor.h b/src/dialog/savedqueries/dlgsavedquerieseditor.h new file mode 100644 index 00000000000..4b27f14dc4d --- /dev/null +++ b/src/dialog/savedqueries/dlgsavedquerieseditor.h @@ -0,0 +1,32 @@ +#ifndef DLGSAVEDQUERIESEDITOR_H +#define DLGSAVEDQUERIESEDITOR_H + +#include "dialog/savedqueries/ui_dlgsavedquerieseditor.h" +#include "library/dao/savedqueriesdao.h" + +class TrackCollection; + +class DlgSavedQueriesEditor : public QDialog, private Ui::DlgSavedQueriesEditor +{ + Q_OBJECT + + public: + explicit DlgSavedQueriesEditor(LibraryFeature* pFeature, + TrackCollection* pTrackCollection, + QWidget* parent = nullptr); + + public slots: + + void accept() override; + + private slots: + + void removeQuery(); + + private: + + TrackCollection* m_pTrackCollection; + LibraryFeature* m_pFeature; +}; + +#endif // DLGSAVEDQUERIESEDITOR_H diff --git a/src/dialog/savedqueries/dlgsavedquerieseditor.ui b/src/dialog/savedqueries/dlgsavedquerieseditor.ui new file mode 100644 index 00000000000..bfb4b8f8abc --- /dev/null +++ b/src/dialog/savedqueries/dlgsavedquerieseditor.ui @@ -0,0 +1,91 @@ + + + DlgSavedQueriesEditor + + + + 0 + 0 + 499 + 376 + + + + Dialog + + + + + + + + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + acceptButtons + accepted() + DlgSavedQueriesEditor + accept() + + + 248 + 254 + + + 157 + 274 + + + + + acceptButtons + rejected() + DlgSavedQueriesEditor + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/dialog/savedqueries/savedqueriestablemodel.cpp b/src/dialog/savedqueries/savedqueriestablemodel.cpp new file mode 100644 index 00000000000..4d78b5e7b0a --- /dev/null +++ b/src/dialog/savedqueries/savedqueriestablemodel.cpp @@ -0,0 +1,135 @@ +#include +#include +#include + +#include "library/dao/savedqueriesdao.h" + +#include "dialog/savedqueries/savedqueriestablemodel.h" + +SavedQueriesTableModel::SavedQueriesTableModel(LibraryFeature* pFeature, + SavedQueriesDAO& savedDao, + QObject* parent) + : QAbstractTableModel(parent), + m_pFeature(pFeature), + m_savedDao(savedDao) { + m_cachedData = m_savedDao.getSavedQueries(m_pFeature); +} + +bool SavedQueriesTableModel::isColumnInternal(int column) const { + return column != SavedQueryColumns::QUERY && + column != SavedQueryColumns::TITLE && + column != SavedQueryColumns::PINNED; +} + +QVariant SavedQueriesTableModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) return QVariant(); + + const SavedSearchQuery& sQuery = m_cachedData.at(index.row()); + switch (index.column()) { + case SavedQueryColumns::QUERY: + if (role == Qt::DisplayRole || role == Qt::EditRole) return sQuery.query; + break; + + case SavedQueryColumns::TITLE: + if (role == Qt::DisplayRole || role == Qt::EditRole) return sQuery.title; + break; + + case SavedQueryColumns::PINNED: + if (role == Qt::CheckStateRole) { + return sQuery.pinned ? Qt::Checked : Qt::Unchecked; + } + break; + } + return QVariant(); +} + +bool SavedQueriesTableModel::setData(const QModelIndex& index, const QVariant& value, int role) { + if (!index.isValid()) return false; + + SavedSearchQuery& sQuery = m_cachedData[index.row()]; + switch (index.column()) { + case SavedQueryColumns::QUERY: + if (role == Qt::EditRole) { + sQuery.query = value.toString(); + return true; + } + break; + + case SavedQueryColumns::TITLE: + if (role == Qt::EditRole) { + sQuery.title = value.toString(); + return true; + } + break; + + case SavedQueryColumns::PINNED: + if (role == Qt::CheckStateRole) { + sQuery.pinned = !sQuery.pinned; + return true; + } + } + return false; +} + +QVariant SavedQueriesTableModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) { + return QAbstractTableModel::headerData(section, orientation, role); + } + + switch (section) { + case SavedQueryColumns::QUERY: + return tr("Query"); + case SavedQueryColumns::TITLE: + return tr("Title"); + case SavedQueryColumns::PINNED: + return tr("Pinned"); + } + return ""; +} + +Qt::ItemFlags SavedQueriesTableModel::flags(const QModelIndex& index) const { + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + flags |= Qt::ItemIsSelectable; + flags |= Qt::ItemIsEnabled; + flags |= Qt::ItemIsEditable; + + if (index.column() == SavedQueryColumns::PINNED) { + flags |= Qt::ItemIsUserCheckable; + } + return flags; +} + +int SavedQueriesTableModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) return 0; + return m_cachedData.length(); +} + +int SavedQueriesTableModel::columnCount(const QModelIndex& parent) const { + if (parent.isValid()) return 0; + return SavedQueryColumns::NUM_COLUMNS; +} + +bool SavedQueriesTableModel::removeRows(int row, int count, const QModelIndex& parent) { + if (parent.isValid()) return false; + + beginRemoveRows(QModelIndex(), row, row + count - 1); + while (count > 0 && row < m_cachedData.size()) { + m_removedQueries << m_cachedData.takeAt(row).id; + --count; + } + endRemoveRows(); + + return true; +} + +bool SavedQueriesTableModel::submit() { + for (const SavedSearchQuery& sQuery : m_cachedData) { + m_savedDao.updateSavedQuery(sQuery); + } + + for (int id : m_removedQueries) { + m_savedDao.deleteSavedQuery(id); + } + + return QAbstractTableModel::submit(); +} diff --git a/src/dialog/savedqueries/savedqueriestablemodel.h b/src/dialog/savedqueries/savedqueriestablemodel.h new file mode 100644 index 00000000000..62bb72e488a --- /dev/null +++ b/src/dialog/savedqueries/savedqueriestablemodel.h @@ -0,0 +1,39 @@ +#ifndef SAVEDQUERIESTABLEMODEL_H +#define SAVEDQUERIESTABLEMODEL_H + +#include + +#include "library/libraryfeature.h" + +class SavedQueriesTableModel : public QAbstractTableModel +{ + public: + SavedQueriesTableModel(LibraryFeature* pFeature, + SavedQueriesDAO& savedDao, + QObject* parent = nullptr); + + bool isColumnInternal(int column) const; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()); + +public slots: + + bool submit() override; + void removeQuery(const QModelIndex& index); + + private: + + QList m_cachedData; + QList m_removedQueries; + + LibraryFeature* m_pFeature; + SavedQueriesDAO& m_savedDao; +}; + +#endif // SAVEDQUERIESTABLEMODEL_H diff --git a/src/library/abstractmodelroles.h b/src/library/abstractmodelroles.h new file mode 100644 index 00000000000..00ff7e7d9a1 --- /dev/null +++ b/src/library/abstractmodelroles.h @@ -0,0 +1,16 @@ +#ifndef ABSTRACTMODELROLES_H +#define ABSTRACTMODELROLES_H + +#include + +enum AbstractRole { + RoleDataPath = Qt::UserRole, + RoleBold, + RoleDivider, + RoleQuery, + RoleBreadCrumb, + RoleSettings, + RoleGroupingLetter +}; + +#endif // ABSTRACTMODELROLES_H diff --git a/src/library/autodj/dlgautodj.h b/src/library/autodj/dlgautodj.h deleted file mode 100644 index fda36bcdcbb..00000000000 --- a/src/library/autodj/dlgautodj.h +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef DLGAUTODJ_H -#define DLGAUTODJ_H - -#include -#include - -#include "library/autodj/ui_dlgautodj.h" -#include "preferences/usersettings.h" -#include "track/track.h" -#include "library/libraryview.h" -#include "library/library.h" -#include "library/trackcollection.h" -#include "library/autodj/autodjprocessor.h" -#include "controllers/keyboard/keyboardeventfilter.h" - -class PlaylistTableModel; -class WTrackTableView; - -class DlgAutoDJ : public QWidget, public Ui::DlgAutoDJ, public LibraryView { - Q_OBJECT - public: - DlgAutoDJ(QWidget* parent, UserSettingsPointer pConfig, - Library* pLibrary, - AutoDJProcessor* pProcessor, TrackCollection* pTrackCollection, - KeyboardEventFilter* pKeyboard); - virtual ~DlgAutoDJ(); - - void onShow(); - void onSearch(const QString& text); - void loadSelectedTrack(); - void loadSelectedTrackToGroup(QString group, bool play); - void moveSelection(int delta); - - public slots: - void shufflePlaylistButton(bool buttonChecked); - void skipNextButton(bool buttonChecked); - void fadeNowButton(bool buttonChecked); - void toggleAutoDJButton(bool enable); - void transitionTimeChanged(int time); - void transitionSliderChanged(int value); - void autoDJStateChanged(AutoDJProcessor::AutoDJState state); - void setTrackTableFont(const QFont& font); - void setTrackTableRowHeight(int rowHeight); - void updateSelectionInfo(); - - signals: - void addRandomButton(bool buttonChecked); - void loadTrack(TrackPointer tio); - void loadTrackToPlayer(TrackPointer tio, QString group, bool); - void trackSelected(TrackPointer pTrack); - - private: - AutoDJProcessor* m_pAutoDJProcessor; - WTrackTableView* m_pTrackTableView; - PlaylistTableModel* m_pAutoDJTableModel; -}; - -#endif //DLGAUTODJ_H diff --git a/src/library/autodj/dlgautodj.ui b/src/library/autodj/dlgautodj.ui deleted file mode 100644 index f71a2a1cbd6..00000000000 --- a/src/library/autodj/dlgautodj.ui +++ /dev/null @@ -1,175 +0,0 @@ - - - DlgAutoDJ - - - - 0 - 0 - 560 - 399 - - - - Auto DJ - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Shuffle the content of the Auto DJ playlist. - - - Shuffle - - - false - - - - - - - Add a random track from track sources (crates) or Library to the Auto DJ playlist. - - - Add Random - - - - - - - Skip the next track in the Auto DJ playlist. - - - Skip Track - - - false - - - - - - - Trigger the transition to the next track. - - - Fade Now - - - - - - - - 0 - 0 - - - - Determines the duration of the transition. - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - -9 - - - - - - - Seconds - - - sec. - - - - - - - Qt::Horizontal - - - - 1 - 20 - - - - - - - - - - - - - - - Turn Auto DJ on or off. - - - Enable Auto DJ - - - true - - - - - - - - - true - - - - - - - - - - diff --git a/src/library/basesqltablemodel.cpp b/src/library/basesqltablemodel.cpp index ddeb55fa66b..d1326b45ce5 100644 --- a/src/library/basesqltablemodel.cpp +++ b/src/library/basesqltablemodel.cpp @@ -20,6 +20,7 @@ #include "util/dnd.h" #include "util/assert.h" #include "util/performancetimer.h" +#include "util/stringhelper.h" static const bool sDebug = false; @@ -110,6 +111,14 @@ void BaseSqlTableModel::initHeaderData() { tr("ReplayGain"), 50); } +QSet BaseSqlTableModel::getTrackIdsFromIndices(const QModelIndexList& list) const { + QSet ret; + for (const QModelIndex& index : list) { + ret.insert(getTrackId(index)); + } + return ret; +} + QSqlDatabase BaseSqlTableModel::database() const { return m_database; } @@ -377,6 +386,16 @@ void BaseSqlTableModel::setSearch(const QString& searchText, const QString& extr m_currentSearchFilter = extraFilter; } +void BaseSqlTableModel::onSearchStarting() { + // Save current sorting + m_savedSortColumns = m_sortColumns; +} + +void BaseSqlTableModel::onSearchCleared() { + // Restore sorting + m_sortColumns = m_savedSortColumns; +} + void BaseSqlTableModel::search(const QString& searchText, const QString& extraFilter) { if (sDebug) { qDebug() << this << "search" << searchText; @@ -402,22 +421,7 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { // There's no item to sort already, load from Settings last sort if (m_sortColumns.isEmpty()) { QString val = getModelSetting(COLUMNS_SORTING); - QTextStream in(&val); - - while (!in.atEnd()) { - int ordI = -1; - QString name; - - in >> name >> ordI; - - int col = fieldIndex(name); - if (col < 0) continue; - - Qt::SortOrder ord; - ord = ordI > 0 ? Qt::AscendingOrder : Qt::DescendingOrder; - - m_sortColumns << SortColumn(col, ord); - } + deserialzeSortColumns(val); } if (m_sortColumns.size() > 0 && m_sortColumns.at(0).m_column == column) { // Only the order has changed @@ -441,23 +445,7 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { } // Write new sortColumns order to user settings - QString val; - QTextStream out(&val); - for (SortColumn& sc : m_sortColumns) { - - QString name; - if (sc.m_column > 0 && sc.m_column < m_tableColumns.size()) { - name = m_tableColumns[sc.m_column]; - } else { - // ccColumn between 1..x to skip the id column - int ccColumn = sc.m_column - m_tableColumns.size() + 1; - name = m_trackSource->columnNameForFieldIndex(ccColumn); - } - - out << name << " "; - out << (sc.m_order == Qt::AscendingOrder ? 1 : -1) << " "; - } - out.flush(); + QString val = serializedSortColumns(); setModelSetting(COLUMNS_SORTING, val); if (sDebug) { @@ -596,12 +584,16 @@ QVariant BaseSqlTableModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::CheckStateRole && - role != Qt::ToolTipRole)) { + role != Qt::ToolTipRole && + role != AbstractRole::RoleGroupingLetter)) { return QVariant(); } int row = index.row(); int column = index.column(); + if (role == AbstractRole::RoleGroupingLetter && !m_sortColumns.isEmpty()) { + column = m_sortColumns.first().m_column; + } // This value is the value in its most raw form. It was looked up either // from the SQL table or from the cached track layer. @@ -695,6 +687,19 @@ QVariant BaseSqlTableModel::data(const QModelIndex& index, int role) const { value = locked ? Qt::Checked : Qt::Unchecked; } break; + case AbstractRole::RoleGroupingLetter: + if (isValidColumn(column)) { + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR) && + value.toString().size() == 4) { + // Show the decade + value = value.toString().at(2); + } else { + value = StringHelper::getFirstCharForGrouping(value.toString()); + } + } else { + value = QVariant(); + } + break; default: break; } @@ -935,12 +940,16 @@ QVariant BaseSqlTableModel::getBaseValue( const QModelIndex& index, int role) const { if (role != Qt::DisplayRole && role != Qt::ToolTipRole && - role != Qt::EditRole) { + role != Qt::EditRole && + role != AbstractRole::RoleGroupingLetter) { return QVariant(); } int row = index.row(); int column = index.column(); + if (role == AbstractRole::RoleGroupingLetter && !m_sortColumns.isEmpty()) { + column = m_sortColumns.first().m_column; + } if (row < 0 || row >= m_rowInfo.size()) { return QVariant(); @@ -1016,6 +1025,45 @@ QMimeData* BaseSqlTableModel::mimeData(const QModelIndexList &indexes) const { return mimeData; } +void BaseSqlTableModel::saveSelection(const QModelIndexList& selection) { + m_savedSelectionIndices = getTrackIdsFromIndices(selection); +} + +QModelIndexList BaseSqlTableModel::getSavedSelectionIndices() { + QModelIndexList ret; + for (const TrackId& id : m_savedSelectionIndices) { + QLinkedList rows = getTrackRows(id); + for (const int row : rows) { + ret << index(row, 0); + } + } + return ret; +} + +void BaseSqlTableModel::restoreQuery(const SavedSearchQuery& query) { + // Restore selection + m_savedSelectionIndices.clear(); + for (const DbId& id : query.selectedItems) { + m_savedSelectionIndices.insert(TrackId(id.toVariant())); + } + + deserialzeSortColumns(query.sortOrder); + search(query.query); +} + +SavedSearchQuery BaseSqlTableModel::saveQuery(const QModelIndexList& indices, + SavedSearchQuery query) const { + query.selectedItems.clear(); + auto ids = getTrackIdsFromIndices(indices); + for (const TrackId& id : ids) { + query.selectedItems.insert(id); + } + + query.sortOrder = serializedSortColumns(); + + return query; +} + QAbstractItemDelegate* BaseSqlTableModel::delegateForColumn(const int i, QObject* pParent) { if (i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { return new StarDelegate(pParent); @@ -1050,3 +1098,57 @@ void BaseSqlTableModel::hideTracks(const QModelIndexList& indices) { // there. select(); //Repopulate the data model. } + +QString BaseSqlTableModel::serializedSortColumns() const { + QString val; + QTextStream out(&val); + for (const SortColumn& sc : m_sortColumns) { + + QString name; + if (sc.m_column > 0 && sc.m_column < m_tableColumns.size()) { + name = m_tableColumns[sc.m_column]; + } else { + // ccColumn between 1..x to skip the id column + int ccColumn = sc.m_column - m_tableColumns.size() + 1; + name = m_trackSource->columnNameForFieldIndex(ccColumn); + } + + out << name << " "; + out << (sc.m_order == Qt::AscendingOrder ? 1 : -1) << " "; + } + out.flush(); + return val; +} + +void BaseSqlTableModel::deserialzeSortColumns(QString serialized) { + QTextStream in(&serialized); + m_sortColumns.clear(); + + while (!in.atEnd()) { + int ordI = -1; + QString name; + + in >> name >> ordI; + + int col = fieldIndex(name); + if (col < 0) continue; + + Qt::SortOrder ord; + ord = ordI > 0 ? Qt::AscendingOrder : Qt::DescendingOrder; + + m_sortColumns << SortColumn(col, ord); + } +} + +bool BaseSqlTableModel::isValidColumn(int column) const { + return + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR) || + column == fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_ARTIST) || + column == fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_TITLE); +} diff --git a/src/library/basesqltablemodel.h b/src/library/basesqltablemodel.h index ea956980ddc..f64b8001a6b 100644 --- a/src/library/basesqltablemodel.h +++ b/src/library/basesqltablemodel.h @@ -5,6 +5,7 @@ #include #include +#include "library/abstractmodelroles.h" #include "library/basetrackcache.h" #include "library/dao/trackdao.h" #include "library/trackcollection.h" @@ -30,27 +31,34 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { } void setSearch(const QString& searchText, const QString& extraFilter = QString()); + void onSearchStarting() override; + void onSearchCleared() override; void setSort(int column, Qt::SortOrder order); - int fieldIndex(ColumnCache::Column column) const; /////////////////////////////////////////////////////////////////////////// // Inherited from TrackModel /////////////////////////////////////////////////////////////////////////// + int fieldIndex(ColumnCache::Column column) const; int fieldIndex(const QString& fieldName) const final; - /////////////////////////////////////////////////////////////////////////// - // Inherited from QAbstractItemModel - /////////////////////////////////////////////////////////////////////////// - void sort(int column, Qt::SortOrder order) final; - int rowCount(const QModelIndex& parent=QModelIndex()) const final; + // Methods reimplemented from QAbstractItemModel + void sort(int column, Qt::SortOrder order) override final; + int rowCount(const QModelIndex& parent=QModelIndex()) const override final; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const final; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); int columnCount(const QModelIndex& parent = QModelIndex()) const final; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::DisplayRole) final; QVariant headerData(int section, Qt::Orientation orientation, int role=Qt::DisplayRole) const final; - QMimeData* mimeData(const QModelIndexList &indexes) const final; + virtual QMimeData* mimeData(const QModelIndexList &indexes) const final; + + void saveSelection(const QModelIndexList& selection) override; + QModelIndexList getSavedSelectionIndices() override; + + void restoreQuery(const SavedSearchQuery& query) override; + SavedSearchQuery saveQuery(const QModelIndexList &indices, SavedSearchQuery query) const override; /////////////////////////////////////////////////////////////////////////// // Functions that might be reimplemented/overridden in derived classes @@ -73,12 +81,7 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { void search(const QString& searchText, const QString& extraFilter = QString()) override; const QString currentSearch() const override; QAbstractItemDelegate* delegateForColumn(const int i, QObject* pParent) override; - - /////////////////////////////////////////////////////////////////////////// - // Inherited from QAbstractItemModel - /////////////////////////////////////////////////////////////////////////// - bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - + public slots: void select(); @@ -90,6 +93,8 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { const QStringList& tableColumns, QSharedPointer trackSource); void initHeaderData(); + + QSet getTrackIdsFromIndices(const QModelIndexList& list) const; // Use this if you want a model that is read-only. virtual Qt::ItemFlags readOnlyFlags(const QModelIndex &index) const; @@ -121,6 +126,9 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { // called. QString orderByClause() const; QSqlDatabase database() const; + QString serializedSortColumns() const; + void deserialzeSortColumns(QString serialized); + bool isValidColumn(int column) const; struct RowInfo { TrackId trackId; @@ -153,8 +161,10 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { QHash > m_trackIdToRows; QString m_currentSearch; QString m_currentSearchFilter; + QList m_savedSortColumns; QVector > m_headerInfo; QString m_trackSourceOrderBy; + QSet m_savedSelectionIndices; DISALLOW_COPY_AND_ASSIGN(BaseSqlTableModel); }; diff --git a/src/library/columncache.cpp b/src/library/columncache.cpp index 1d8e405dde3..b8a34493088 100644 --- a/src/library/columncache.cpp +++ b/src/library/columncache.cpp @@ -71,6 +71,7 @@ void ColumnCache::setColumns(const QStringList& columns) { m_columnIndexByEnum[COLUMN_LIBRARYTABLE_COVERART_HASH] = fieldIndex(LIBRARYTABLE_COVERART_HASH); m_columnIndexByEnum[COLUMN_TRACKLOCATIONSTABLE_FSDELETED] = fieldIndex(TRACKLOCATIONSTABLE_FSDELETED); + m_columnIndexByEnum[COLUMN_TRACKLOCATIONSTABLE_DIRECTORY] = fieldIndex(TRACKLOCATIONSTABLE_DIRECTORY); m_columnIndexByEnum[COLUMN_PLAYLISTTRACKSTABLE_TRACKID] = fieldIndex(PLAYLISTTRACKSTABLE_TRACKID); m_columnIndexByEnum[COLUMN_PLAYLISTTRACKSTABLE_POSITION] = fieldIndex(PLAYLISTTRACKSTABLE_POSITION); diff --git a/src/library/columncache.h b/src/library/columncache.h index c9a745b6d7a..5ba9514fdac 100644 --- a/src/library/columncache.h +++ b/src/library/columncache.h @@ -57,6 +57,7 @@ class ColumnCache : public QObject { COLUMN_LIBRARYTABLE_COVERART_HASH, COLUMN_TRACKLOCATIONSTABLE_FSDELETED, + COLUMN_TRACKLOCATIONSTABLE_DIRECTORY, COLUMN_PLAYLISTTRACKSTABLE_TRACKID, COLUMN_PLAYLISTTRACKSTABLE_POSITION, diff --git a/src/library/coverartdelegate.cpp b/src/library/coverartdelegate.cpp index d93639d62d6..7669a561335 100644 --- a/src/library/coverartdelegate.cpp +++ b/src/library/coverartdelegate.cpp @@ -1,3 +1,4 @@ +#include #include #include "library/coverartdelegate.h" @@ -81,12 +82,18 @@ void CoverArtDelegate::slotCoverFound(const QObject* pRequestor, } } -void CoverArtDelegate::paint(QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const { - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.highlight()); - } +void CoverArtDelegate::paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const { + + QStyleOptionViewItemV4 opt = option; + initStyleOption(&opt, index); + + // Draw original item without text as background + opt.text = QString(); + const QWidget *widget = opt.widget; + QStyle *style = widget ? widget->style() : QApplication::style(); + style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget); CoverArtCache* pCache = CoverArtCache::instance(); if (pCache == NULL || m_iIdColumn == -1 || m_iCoverSourceColumn == -1 || diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index 50aa06c9c8f..368febf6a4e 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -1,11 +1,11 @@ #include #include -#include "track/track.h" #include "library/dao/playlistdao.h" +#include "library/features/autodj/autodjprocessor.h" #include "library/queryutil.h" #include "library/trackcollection.h" -#include "library/autodj/autodjprocessor.h" +#include "track/track.h" #include "util/math.h" PlaylistDAO::PlaylistDAO(QSqlDatabase& database) diff --git a/src/library/dao/savedqueriesdao.cpp b/src/library/dao/savedqueriesdao.cpp new file mode 100644 index 00000000000..f3f58dc0691 --- /dev/null +++ b/src/library/dao/savedqueriesdao.cpp @@ -0,0 +1,203 @@ +#include + +#include "library/dao/savedqueriesdao.h" +#include "library/libraryfeature.h" +#include "library/queryutil.h" + +const QString SavedQueriesDAO::kSelectStart = + "SELECT query, title, selectedItems, sortOrder, " + "vScrollbarPos, sortColumn, sortAscendingOrder, pinned, id "; + +SavedQueriesDAO::SavedQueriesDAO(QSqlDatabase& database) + : m_database(database) { + +} + +void SavedQueriesDAO::initialize() { +} + +SavedSearchQuery SavedQueriesDAO::saveQuery(LibraryFeature* pFeature, + SavedSearchQuery sQuery) { + if (pFeature == nullptr) { + return SavedSearchQuery(); + } + + QSqlQuery query(m_database); + query.prepare("INSERT INTO " SAVEDQUERYTABLE + "(libraryFeature, query, title, selectedItems," + "sortOrder, vScrollbarPos, sortColumn, sortAscendingOrder, pinned) " + "VALUES (:libraryFeature, :query, :title, :selectedItems, " + ":sortOrder, :vScrollbarPos, :sortColumn, :sortAscendingOrder, :pinned)"); + + query.bindValue(":libraryFeature", pFeature->getSettingsName()); + query.bindValue(":query", sQuery.query); + query.bindValue(":title", sQuery.title); + query.bindValue(":selectedItems", serializeItems(sQuery.selectedItems)); + query.bindValue(":sortOrder", sQuery.sortOrder); + query.bindValue(":vScrollbarPos", sQuery.vScrollBarPos); + query.bindValue(":sortColumn", sQuery.sortColumn); + query.bindValue(":sortAscendingOrder", (int) sQuery.sortAscendingOrder); + query.bindValue(":pinned", (int) sQuery.pinned); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } + + if (!query.exec("SELECT id FROM " SAVEDQUERYTABLE + " WHERE ROWID=last_inserted_rowid()")) { + LOG_FAILED_QUERY(query); + } + + query.next(); + sQuery.id = query.value(0).toInt(); + return sQuery; +} + +QList SavedQueriesDAO::getSavedQueries(const QString& settingsName) const { + QSqlQuery query(m_database); + QString queryStr = kSelectStart + + "FROM " SAVEDQUERYTABLE + " WHERE libraryFeature = :featureName " + "ORDER BY pinned DESC, id DESC"; + query.prepare(queryStr); + query.bindValue(":featureName", settingsName); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } + + QList res; + while (query.next()) { + res << valueToQuery(query); + } + return res; +} + +QList SavedQueriesDAO::getSavedQueries(const LibraryFeature* pFeature) const { + if (pFeature == nullptr) { + return QList(); + } + + return getSavedQueries(pFeature->getSettingsName()); +} + +SavedSearchQuery SavedQueriesDAO::getSavedQuery(int id) const { + QSqlQuery query(m_database); + QString queryStr = kSelectStart + + "FROM " SAVEDQUERYTABLE + " WHERE id = :id " + "ORDER BY id DESC"; + query.prepare(queryStr); + query.bindValue(":id", id); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } + + query.next(); + return valueToQuery(query); +} + +SavedSearchQuery SavedQueriesDAO::moveToFirst(LibraryFeature* pFeature, + const SavedSearchQuery& sQuery) { + // To move to the first item we delete the item and insert againt it + // to assign it a new ID + deleteSavedQuery(sQuery.id); + return saveQuery(pFeature, sQuery); +} + +SavedSearchQuery SavedQueriesDAO::moveToFirst(LibraryFeature* pFeature, int id) { + return moveToFirst(pFeature, getSavedQuery(id)); +} + +bool SavedQueriesDAO::updateSavedQuery(const SavedSearchQuery& sQuery) { + QSqlQuery query(m_database); + query.prepare("UPDATE " SAVEDQUERYTABLE " SET " + "query = :query, title = :title, selectedItems = :selectedItems, " + "sortOrder = :sortOrder, vScrollbarPos = :vScrollbarPos, " + "sortColumn = :sortColumn, " + "sortAscendingOrder = :sortAscendingOrder, pinned = :pinned " + "WHERE id = :id"); + + query.bindValue(":query", sQuery.query); + query.bindValue(":title", sQuery.title); + query.bindValue(":selectedItems", serializeItems(sQuery.selectedItems)); + query.bindValue(":sortOrder", sQuery.sortOrder); + query.bindValue(":vScrollbarPos", sQuery.vScrollBarPos); + query.bindValue(":sortColumn", sQuery.sortColumn); + query.bindValue(":sortAscendingOrder", (int) sQuery.sortAscendingOrder); + query.bindValue(":pinned", (int) sQuery.pinned); + query.bindValue(":id", sQuery.id); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + return true; +} + +bool SavedQueriesDAO::deleteSavedQuery(int id) { + QSqlQuery query(m_database); + query.prepare("DELETE FROM " SAVEDQUERYTABLE " WHERE id=:id"); + query.bindValue(":id", id); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + return true; +} + +bool SavedQueriesDAO::exists(const SavedSearchQuery& sQuery) { + return getQueryId(sQuery) >= 0; +} + +int SavedQueriesDAO::getQueryId(const SavedSearchQuery& sQuery) { + QSqlQuery query(m_database); + query.prepare("SELECT id FROM " SAVEDQUERYTABLE " " + "WHERE query=:query AND title=:title"); + query.bindValue(":query", sQuery.query); + query.bindValue(":title", sQuery.title); + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } + if (query.next()) { + return query.value(0).toInt(); + } + return -1; +} + +QString SavedQueriesDAO::serializeItems(const QSet& items) { + QStringList ret; + + for (const DbId& id : items) { + ret << id.toString(); + } + return ret.join(" "); +} + +QSet SavedQueriesDAO::deserializeItems(QString text) { + QSet ret; + QTextStream ss(&text); + while (!ss.atEnd()) { + int value; + ss >> value; + ret.insert(DbId(QVariant(value))); + } + return ret; +} + +SavedSearchQuery SavedQueriesDAO::valueToQuery(const QSqlQuery& query) { + SavedSearchQuery q; + q.query = query.value(0).toString(); + q.title = query.value(1).toString(); + q.selectedItems = deserializeItems(query.value(2).toString()); + q.sortOrder = query.value(3).toString(); + q.vScrollBarPos = query.value(4).toInt(); + q.sortColumn = query.value(5).toInt(); + q.sortAscendingOrder = query.value(6).toBool(); + q.pinned = query.value(7).toBool(); + q.id = query.value(8).toInt(); + + return q; +} diff --git a/src/library/dao/savedqueriesdao.h b/src/library/dao/savedqueriesdao.h new file mode 100644 index 00000000000..a172977c061 --- /dev/null +++ b/src/library/dao/savedqueriesdao.h @@ -0,0 +1,97 @@ +#ifndef SAVEDQUERIESDAO_H +#define SAVEDQUERIESDAO_H + +#include +#include +#include + +#include "library/dao/dao.h" +#include "util/dbid.h" + +#define SAVEDQUERYTABLE "savedQueries" + +enum SavedQueryColumns { + ID, + LIBRARYFEATURE, + QUERY, + TITLE, + SELECTEDITEMS, + SORTORDER, + VSCROLLBARPOS, + SORTCOLUMN, + SORTASCENDINGORDER, + PINNED, + + // NUM_COLUMNS should be always the last item + NUM_COLUMNS +}; + +const QString SAVEDQUERYTABLE_ID = "id"; +const QString SAVEDQUERYTABLE_LIBRARYFEATURE = "libraryFeature"; +const QString SAVEDQUERYTABLE_QUERY = "query"; +const QString SAVEDQUERYTABLE_TITLE = "title"; +const QString SAVEDQUERYTABLE_SELECTEDITEMS = "selectedItems"; +const QString SAVEDQUERYTABLE_SORTORDER = "sortOrder"; +const QString SAVEDQUERYTABLE_VSCROLLBARPOS = "vScrollbarPos"; +const QString SAVEDQUERYTABLE_SORTCOLUMN = "sortColumn"; +const QString SAVEDQUERYTABLE_SORTASCENDINGORDER = "sortAscendingOrder"; +const QString SAVEDQUERYTABLE_PINNED = "pinned"; + +// This struct allows to save some data to allow interaction between +// the search bar and the library features +struct SavedSearchQuery { + + SavedSearchQuery() : + vScrollBarPos(-1), + sortColumn(-1), + sortAscendingOrder(false), + pinned(false), + id(-1) {} + + SavedSearchQuery(const SavedSearchQuery& other) = default; + SavedSearchQuery& operator=(const SavedSearchQuery& other) = default; + bool operator==(const SavedSearchQuery& other) const { + return other.title == this->title && other.query == this->query; + } + + QString query; + QString title; + QSet selectedItems; + QString sortOrder; + + int vScrollBarPos; + int sortColumn; + bool sortAscendingOrder; + bool pinned; + int id; +}; + +class LibraryFeature; + +class SavedQueriesDAO : public DAO { + public: + SavedQueriesDAO(QSqlDatabase& database); + + void initialize(); + SavedSearchQuery saveQuery(LibraryFeature* pFeature, SavedSearchQuery sQuery); + QList getSavedQueries(const QString& settingsName) const; + QList getSavedQueries(const LibraryFeature *pFeature) const; + SavedSearchQuery getSavedQuery(int id) const; + SavedSearchQuery moveToFirst(LibraryFeature* pFeature, const SavedSearchQuery& sQuery); + SavedSearchQuery moveToFirst(LibraryFeature* pFeature, int id); + bool updateSavedQuery(const SavedSearchQuery& sQuery); + bool deleteSavedQuery(int id); + bool exists(const SavedSearchQuery& sQuery); + int getQueryId(const SavedSearchQuery& sQuery); + + private: + static QString serializeItems(const QSet& items); + static QSet deserializeItems(QString text); + static SavedSearchQuery valueToQuery(const QSqlQuery& query); + + static const QString kSelectStart; + + QSqlDatabase& m_database; +}; + +#endif // SAVEDQUERIESDAO_H diff --git a/src/library/dlganalysis.cpp b/src/library/dlganalysis.cpp deleted file mode 100644 index 6d8ec9f3801..00000000000 --- a/src/library/dlganalysis.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include - -#include "widget/wwidget.h" -#include "widget/wskincolor.h" -#include "widget/wanalysislibrarytableview.h" -#include "library/trackcollection.h" -#include "library/dlganalysis.h" -#include "util/assert.h" - -DlgAnalysis::DlgAnalysis(QWidget* parent, - UserSettingsPointer pConfig, - TrackCollection* pTrackCollection) - : QWidget(parent), - m_pConfig(pConfig), - m_pTrackCollection(pTrackCollection), - m_bAnalysisActive(false), - m_tracksInQueue(0), - m_currentTrack(0) { - setupUi(this); - m_songsButtonGroup.addButton(radioButtonRecentlyAdded); - m_songsButtonGroup.addButton(radioButtonAllSongs); - - m_pAnalysisLibraryTableView = new WAnalysisLibraryTableView(this, pConfig, pTrackCollection); - connect(m_pAnalysisLibraryTableView, SIGNAL(loadTrack(TrackPointer)), - this, SIGNAL(loadTrack(TrackPointer))); - connect(m_pAnalysisLibraryTableView, SIGNAL(loadTrackToPlayer(TrackPointer, QString)), - this, SIGNAL(loadTrackToPlayer(TrackPointer, QString))); - - connect(m_pAnalysisLibraryTableView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); - - QBoxLayout* box = dynamic_cast(layout()); - DEBUG_ASSERT_AND_HANDLE(box) { // Assumes the form layout is a QVBox/QHBoxLayout! - } else { - box->removeWidget(m_pTrackTablePlaceholder); - m_pTrackTablePlaceholder->hide(); - box->insertWidget(1, m_pAnalysisLibraryTableView); - } - - m_pAnalysisLibraryTableModel = new AnalysisLibraryTableModel(this, pTrackCollection); - m_pAnalysisLibraryTableView->loadTrackModel(m_pAnalysisLibraryTableModel); - - connect(radioButtonRecentlyAdded, SIGNAL(clicked()), - this, SLOT(showRecentSongs())); - connect(radioButtonAllSongs, SIGNAL(clicked()), - this, SLOT(showAllSongs())); - - // TODO(rryan): This triggers a library search before the UI has even - // started up. Accounts for 0.2% of skin creation time. Get rid of this! - radioButtonRecentlyAdded->click(); - - labelProgress->setText(""); - pushButtonAnalyze->setEnabled(false); - connect(pushButtonAnalyze, SIGNAL(clicked()), - this, SLOT(analyze())); - - connect(pushButtonSelectAll, SIGNAL(clicked()), - this, SLOT(selectAll())); - - connect(m_pAnalysisLibraryTableView->selectionModel(), - SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection&)), - this, - SLOT(tableSelectionChanged(const QItemSelection &, const QItemSelection&))); -} - -DlgAnalysis::~DlgAnalysis() { -} - -void DlgAnalysis::onShow() { - // Refresh table - // There might be new tracks dropped to other views - m_pAnalysisLibraryTableModel->select(); -} - -void DlgAnalysis::onSearch(const QString& text) { - m_pAnalysisLibraryTableModel->search(text); -} - -void DlgAnalysis::loadSelectedTrack() { - m_pAnalysisLibraryTableView->loadSelectedTrack(); -} - -void DlgAnalysis::loadSelectedTrackToGroup(QString group, bool play) { - m_pAnalysisLibraryTableView->loadSelectedTrackToGroup(group, play); -} - -void DlgAnalysis::slotSendToAutoDJ() { - // append to auto DJ - m_pAnalysisLibraryTableView->slotSendToAutoDJ(); -} - -void DlgAnalysis::slotSendToAutoDJTop() { - m_pAnalysisLibraryTableView->slotSendToAutoDJTop(); -} - -void DlgAnalysis::moveSelection(int delta) { - m_pAnalysisLibraryTableView->moveSelection(delta); -} - -void DlgAnalysis::tableSelectionChanged(const QItemSelection& selected, - const QItemSelection& deselected) { - Q_UNUSED(selected); - Q_UNUSED(deselected); - bool tracksSelected = m_pAnalysisLibraryTableView->selectionModel()->hasSelection(); - pushButtonAnalyze->setEnabled(tracksSelected || m_bAnalysisActive); -} - -void DlgAnalysis::selectAll() { - m_pAnalysisLibraryTableView->selectAll(); -} - -void DlgAnalysis::analyze() { - //qDebug() << this << "analyze()"; - if (m_bAnalysisActive) { - emit(stopAnalysis()); - } else { - QList trackIds; - - QModelIndexList selectedIndexes = m_pAnalysisLibraryTableView->selectionModel()->selectedRows(); - foreach(QModelIndex selectedIndex, selectedIndexes) { - TrackId trackId(selectedIndex.sibling( - selectedIndex.row(), - m_pAnalysisLibraryTableModel->fieldIndex(LIBRARYTABLE_ID)).data()); - if (trackId.isValid()) { - trackIds.append(trackId); - } - } - m_currentTrack = 1; - emit(analyzeTracks(trackIds)); - } -} - -void DlgAnalysis::analysisActive(bool bActive) { - qDebug() << this << "analysisActive" << bActive; - m_bAnalysisActive = bActive; - if (bActive) { - pushButtonAnalyze->setEnabled(true); - pushButtonAnalyze->setText(tr("Stop Analysis")); - labelProgress->setEnabled(true); - } else { - pushButtonAnalyze->setText(tr("Analyze")); - labelProgress->setText(""); - labelProgress->setEnabled(false); - } -} - -// slot -void DlgAnalysis::trackAnalysisFinished(int size) { - qDebug() << "Analysis finished" << size << "tracks left"; - if (size > 0) { - m_currentTrack = m_tracksInQueue - size + 1; - } -} - -// slot -void DlgAnalysis::trackAnalysisProgress(int progress) { - if (m_bAnalysisActive) { - QString text = tr("Analyzing %1/%2 %3%").arg( - QString::number(m_currentTrack), - QString::number(m_tracksInQueue), - QString::number(progress)); - labelProgress->setText(text); - } -} - -int DlgAnalysis::getNumTracks() { - return m_tracksInQueue; -} - -void DlgAnalysis::trackAnalysisStarted(int size) { - m_tracksInQueue = size; -} - -void DlgAnalysis::showRecentSongs() { - m_pAnalysisLibraryTableModel->showRecentSongs(); -} - -void DlgAnalysis::showAllSongs() { - m_pAnalysisLibraryTableModel->showAllSongs(); -} - -void DlgAnalysis::installEventFilter(QObject* pFilter) { - QWidget::installEventFilter(pFilter); - m_pAnalysisLibraryTableView->installEventFilter(pFilter); -} diff --git a/src/library/dlganalysis.h b/src/library/dlganalysis.h deleted file mode 100644 index 84520b38cf6..00000000000 --- a/src/library/dlganalysis.h +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef DLGANALYSIS_H -#define DLGANALYSIS_H - -#include - -#include "preferences/usersettings.h" -#include "library/analysislibrarytablemodel.h" -#include "library/libraryview.h" -#include "library/trackcollection.h" -#include "library/ui_dlganalysis.h" - -class AnalysisLibraryTableModel; -class WAnalysisLibraryTableView; - -class DlgAnalysis : public QWidget, public Ui::DlgAnalysis, public virtual LibraryView { - Q_OBJECT - public: - DlgAnalysis(QWidget *parent, - UserSettingsPointer pConfig, - TrackCollection* pTrackCollection); - virtual ~DlgAnalysis(); - - virtual void onSearch(const QString& text); - virtual void onShow(); - virtual void loadSelectedTrack(); - virtual void loadSelectedTrackToGroup(QString group, bool play); - virtual void slotSendToAutoDJ(); - virtual void slotSendToAutoDJTop(); - virtual void moveSelection(int delta); - inline const QString currentSearch() { - return m_pAnalysisLibraryTableModel->currentSearch(); - } - int getNumTracks(); - - public slots: - void tableSelectionChanged(const QItemSelection& selected, - const QItemSelection& deselected); - void selectAll(); - void analyze(); - void trackAnalysisFinished(int size); - void trackAnalysisProgress(int progress); - void trackAnalysisStarted(int size); - void showRecentSongs(); - void showAllSongs(); - void installEventFilter(QObject* pFilter); - void analysisActive(bool bActive); - - signals: - void loadTrack(TrackPointer pTrack); - void loadTrackToPlayer(TrackPointer pTrack, QString player); - void analyzeTracks(QList trackIds); - void stopAnalysis(); - void trackSelected(TrackPointer pTrack); - - private: - //Note m_pTrackTablePlaceholder is defined in the .ui file - UserSettingsPointer m_pConfig; - TrackCollection* m_pTrackCollection; - bool m_bAnalysisActive; - QButtonGroup m_songsButtonGroup; - WAnalysisLibraryTableView* m_pAnalysisLibraryTableView; - AnalysisLibraryTableModel* m_pAnalysisLibraryTableModel; - int m_tracksInQueue; - int m_currentTrack; -}; - -#endif //DLGTRIAGE_H diff --git a/src/library/dlganalysis.ui b/src/library/dlganalysis.ui deleted file mode 100644 index de46af0a67d..00000000000 --- a/src/library/dlganalysis.ui +++ /dev/null @@ -1,122 +0,0 @@ - - - DlgAnalysis - - - - 0 - 0 - 500 - 399 - - - - Analyze - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Shows tracks added to the library within the last 7 days. - - - New - - - - - - - Shows all tracks in the library. - - - All - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Progress - - - - - - - Selects all tracks in the table below. - - - Select All - - - - - - - Runs beatgrid, key, and ReplayGain detection on the selected tracks. Does not generate waveforms for the selected tracks to save disk space. - - - Analyze - - - - - - - - - true - - - - - - - - diff --git a/src/library/dlghidden.cpp b/src/library/dlghidden.cpp deleted file mode 100644 index 9cddafb9912..00000000000 --- a/src/library/dlghidden.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "QItemSelection" - -#include "library/dlghidden.h" -#include "library/hiddentablemodel.h" -#include "widget/wtracktableview.h" -#include "util/assert.h" - -DlgHidden::DlgHidden(QWidget* parent, UserSettingsPointer pConfig, - Library* pLibrary, TrackCollection* pTrackCollection, - KeyboardEventFilter* pKeyboard) - : QWidget(parent), - Ui::DlgHidden(), - m_pTrackTableView( - new WTrackTableView(this, pConfig, pTrackCollection, false)) { - setupUi(this); - m_pTrackTableView->installEventFilter(pKeyboard); - - // Install our own trackTable - QBoxLayout* box = dynamic_cast(layout()); - DEBUG_ASSERT_AND_HANDLE(box) { //Assumes the form layout is a QVBox/QHBoxLayout! - } else { - box->removeWidget(m_pTrackTablePlaceholder); - m_pTrackTablePlaceholder->hide(); - box->insertWidget(1, m_pTrackTableView); - } - - m_pHiddenTableModel = new HiddenTableModel(this, pTrackCollection); - m_pTrackTableView->loadTrackModel(m_pHiddenTableModel); - - connect(btnUnhide, SIGNAL(clicked()), - m_pTrackTableView, SLOT(slotUnhide())); - connect(btnUnhide, SIGNAL(clicked()), - this, SLOT(clicked())); - connect(btnPurge, SIGNAL(clicked()), - m_pTrackTableView, SLOT(slotPurge())); - connect(btnPurge, SIGNAL(clicked()), - this, SLOT(clicked())); - connect(btnSelect, SIGNAL(clicked()), - this, SLOT(selectAll())); - connect(m_pTrackTableView->selectionModel(), - SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), - this, - SLOT(selectionChanged(const QItemSelection&, const QItemSelection&))); - - connect(m_pTrackTableView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); - connect(pLibrary, SIGNAL(setTrackTableFont(QFont)), - m_pTrackTableView, SLOT(setTrackTableFont(QFont))); - connect(pLibrary, SIGNAL(setTrackTableRowHeight(int)), - m_pTrackTableView, SLOT(setTrackTableRowHeight(int))); -} - -DlgHidden::~DlgHidden() { - // Delete m_pTrackTableView before the table model. This is because the - // table view saves the header state using the model. - delete m_pTrackTableView; - delete m_pHiddenTableModel; -} - -void DlgHidden::onShow() { - m_pHiddenTableModel->select(); - // no buttons can be selected - activateButtons(false); -} - -void DlgHidden::onSearch(const QString& text) { - m_pHiddenTableModel->search(text); -} - -void DlgHidden::clicked() { - // all marked tracks are gone now anyway - onShow(); -} - -void DlgHidden::selectAll() { - m_pTrackTableView->selectAll(); -} - -void DlgHidden::activateButtons(bool enable) { - btnPurge->setEnabled(enable); - btnUnhide->setEnabled(enable); -} - -void DlgHidden::selectionChanged(const QItemSelection &selected, - const QItemSelection &deselected) { - Q_UNUSED(deselected); - activateButtons(!selected.indexes().isEmpty()); -} - -void DlgHidden::setTrackTableFont(const QFont& font) { - m_pTrackTableView->setTrackTableFont(font); -} - -void DlgHidden::setTrackTableRowHeight(int rowHeight) { - m_pTrackTableView->setTrackTableRowHeight(rowHeight); -} diff --git a/src/library/dlghidden.h b/src/library/dlghidden.h deleted file mode 100644 index 6b78d85f1fc..00000000000 --- a/src/library/dlghidden.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef DLGHIDDEN_H -#define DLGHIDDEN_H - -#include "library/ui_dlghidden.h" -#include "preferences/usersettings.h" -#include "library/library.h" -#include "library/libraryview.h" -#include "library/trackcollection.h" -#include "controllers/keyboard/keyboardeventfilter.h" - -class WTrackTableView; -class HiddenTableModel; -class QItemSelection; - -class DlgHidden : public QWidget, public Ui::DlgHidden, public LibraryView { - Q_OBJECT - public: - DlgHidden(QWidget* parent, UserSettingsPointer pConfig, - Library* pLibrary, TrackCollection* pTrackCollection, - KeyboardEventFilter* pKeyboard); - virtual ~DlgHidden(); - - void onShow(); - void onSearch(const QString& text); - - public slots: - void clicked(); - void selectAll(); - void selectionChanged(const QItemSelection&, const QItemSelection&); - void setTrackTableFont(const QFont& font); - void setTrackTableRowHeight(int rowHeight); - - signals: - void trackSelected(TrackPointer pTrack); - - private: - void activateButtons(bool enable); - WTrackTableView* m_pTrackTableView; - HiddenTableModel* m_pHiddenTableModel; -}; - -#endif //DLGHIDDEN_H diff --git a/src/library/dlghidden.ui b/src/library/dlghidden.ui deleted file mode 100644 index d80de64e421..00000000000 --- a/src/library/dlghidden.ui +++ /dev/null @@ -1,116 +0,0 @@ - - - DlgHidden - - - - 0 - 0 - 560 - 399 - - - - Hidden Tracks - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Selects all tracks in the table below. - - - Select All - - - - - - - Purge selected tracks from the library. - - - Purge - - - false - - - - - - - Unhide selected tracks from the library. - - - Unhide - - - Ctrl+S - - - false - - - - - - - - - true - - - - - - - - - - diff --git a/src/library/dlgmissing.cpp b/src/library/dlgmissing.cpp deleted file mode 100644 index 3f5918bfb3e..00000000000 --- a/src/library/dlgmissing.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "library/dlgmissing.h" - -#include "library/missingtablemodel.h" -#include "widget/wtracktableview.h" -#include "util/assert.h" - -DlgMissing::DlgMissing(QWidget* parent, UserSettingsPointer pConfig, - Library* pLibrary, - TrackCollection* pTrackCollection, KeyboardEventFilter* pKeyboard) - : QWidget(parent), - Ui::DlgMissing(), - m_pTrackTableView( - new WTrackTableView(this, pConfig, pTrackCollection, false)) { - setupUi(this); - m_pTrackTableView->installEventFilter(pKeyboard); - - // Install our own trackTable - QBoxLayout* box = dynamic_cast(layout()); - DEBUG_ASSERT_AND_HANDLE(box) { //Assumes the form layout is a QVBox/QHBoxLayout! - } else { - box->removeWidget(m_pTrackTablePlaceholder); - m_pTrackTablePlaceholder->hide(); - box->insertWidget(1, m_pTrackTableView); - } - - m_pMissingTableModel = new MissingTableModel(this, pTrackCollection); - m_pTrackTableView->loadTrackModel(m_pMissingTableModel); - - connect(btnPurge, SIGNAL(clicked()), - m_pTrackTableView, SLOT(slotPurge())); - connect(btnPurge, SIGNAL(clicked()), - this, SLOT(clicked())); - connect(btnSelect, SIGNAL(clicked()), - this, SLOT(selectAll())); - connect(m_pTrackTableView->selectionModel(), - SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), - this, - SLOT(selectionChanged(const QItemSelection&, const QItemSelection&))); - connect(pLibrary, SIGNAL(setTrackTableFont(QFont)), - m_pTrackTableView, SLOT(setTrackTableFont(QFont))); - connect(pLibrary, SIGNAL(setTrackTableRowHeight(int)), - m_pTrackTableView, SLOT(setTrackTableRowHeight(int))); - - connect(m_pTrackTableView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); -} - -DlgMissing::~DlgMissing() { - // Delete m_pTrackTableView before the table model. This is because the - // table view saves the header state using the model. - delete m_pTrackTableView; - delete m_pMissingTableModel; -} - -void DlgMissing::onShow() { - m_pMissingTableModel->select(); - activateButtons(false); -} - -void DlgMissing::clicked() { - // all marked tracks are gone now anyway - onShow(); -} - -void DlgMissing::onSearch(const QString& text) { - m_pMissingTableModel->search(text); -} - -void DlgMissing::selectAll() { - m_pTrackTableView->selectAll(); -} - -void DlgMissing::activateButtons(bool enable) { - btnPurge->setEnabled(enable); -} - -void DlgMissing::selectionChanged(const QItemSelection &selected, - const QItemSelection &deselected) { - Q_UNUSED(deselected); - activateButtons(!selected.indexes().isEmpty()); -} - -void DlgMissing::setTrackTableFont(const QFont& font) { - m_pTrackTableView->setTrackTableFont(font); -} - -void DlgMissing::setTrackTableRowHeight(int rowHeight) { - m_pTrackTableView->setTrackTableRowHeight(rowHeight); -} diff --git a/src/library/dlgmissing.h b/src/library/dlgmissing.h deleted file mode 100644 index fe287e8e8ef..00000000000 --- a/src/library/dlgmissing.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef DLGMISSING_H -#define DLGMISSING_H - -#include "library/ui_dlgmissing.h" -#include "preferences/usersettings.h" -#include "library/library.h" -#include "library/libraryview.h" -#include "library/trackcollection.h" -#include "controllers/keyboard/keyboardeventfilter.h" - -class WTrackTableView; -class MissingTableModel; - -class DlgMissing : public QWidget, public Ui::DlgMissing, public LibraryView { - Q_OBJECT - public: - DlgMissing(QWidget* parent, UserSettingsPointer pConfig, - Library* pLibrary, TrackCollection* pTrackCollection, - KeyboardEventFilter* pKeyboard); - virtual ~DlgMissing(); - - void onShow(); - void onSearch(const QString& text); - - public slots: - void clicked(); - void selectAll(); - void selectionChanged(const QItemSelection&, const QItemSelection&); - void setTrackTableFont(const QFont& font); - void setTrackTableRowHeight(int rowHeight); - - signals: - void trackSelected(TrackPointer pTrack); - - private: - void activateButtons(bool enable); - WTrackTableView* m_pTrackTableView; - MissingTableModel* m_pMissingTableModel; -}; - -#endif //DLGMISSING_H diff --git a/src/library/dlgmissing.ui b/src/library/dlgmissing.ui deleted file mode 100644 index 8db680a895c..00000000000 --- a/src/library/dlgmissing.ui +++ /dev/null @@ -1,100 +0,0 @@ - - - DlgMissing - - - - 0 - 0 - 560 - 399 - - - - Missing Tracks - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Selects all tracks in the table below. - - - Select All - - - - - - - Purge selected tracks from the library. - - - Purge - - - false - - - - - - - - - true - - - - - - - - - - diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index f4ec52fc255..013e8615d0b 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -70,21 +70,29 @@ QMap createCopylist(const QList& tracks) { void TrackExportWorker::run() { int i = 0; - QMap copy_list = createCopylist(m_tracks); - for (auto it = copy_list.constBegin(); it != copy_list.constEnd(); ++it) { + QMap copyList = createCopylist(m_tracks); + for (auto it = copyList.constBegin(); it != copyList.constEnd(); ++it) { // We emit progress twice per loop, which may seem excessive, but it // guarantees that we emit a sane progress before we start and after // we end. In between, each filename will get its own visible tick // on the bar, which looks really nice. - emit(progress(it->fileName(), i, copy_list.size())); + emit(progress(it->fileName(), i, copyList.size())); copyFile(*it, it.key()); if (load_atomic(m_bStop)) { emit(canceled()); return; } ++i; - emit(progress(it->fileName(), i, copy_list.size())); + emit(progress(it->fileName(), i, copyList.size())); } + + // If no file is being copied sleep a bit of time to allow the dialog to be + // shown, otherwise no dialog is shown and it seems that anything is done + if (copyList.size() <= 0) { + QThread::msleep(500); + } + + emit progress("", copyList.size(), copyList.size()); } void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, diff --git a/src/library/analysisfeature.cpp b/src/library/features/analysis/analysisfeature.cpp similarity index 56% rename from src/library/analysisfeature.cpp rename to src/library/features/analysis/analysisfeature.cpp index ab925a9a2b7..31c4bebeedf 100644 --- a/src/library/analysisfeature.cpp +++ b/src/library/features/analysis/analysisfeature.cpp @@ -2,37 +2,35 @@ // Created 8/23/2009 by RJ Ryan (rryan@mit.edu) // Forked 11/11/2009 by Albert Santoni (alberts@mixxx.org) +#include #include -#include "library/analysisfeature.h" +#include "analyzer/analyzerqueue.h" +#include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/analysis/analysisfeature.h" +#include "library/features/analysis/dlganalysis.h" #include "library/librarytablemodel.h" #include "library/trackcollection.h" -#include "library/dlganalysis.h" -#include "widget/wlibrary.h" -#include "controllers/keyboard/keyboardeventfilter.h" -#include "analyzer/analyzerqueue.h" #include "sources/soundsourceproxy.h" -#include "util/dnd.h" #include "util/debug.h" - -const QString AnalysisFeature::m_sAnalysisViewName = QString("Analysis"); - -AnalysisFeature::AnalysisFeature(QObject* parent, - UserSettingsPointer pConfig, - TrackCollection* pTrackCollection) : - LibraryFeature(parent), - m_pConfig(pConfig), - m_pTrackCollection(pTrackCollection), - m_pAnalyzerQueue(NULL), +#include "util/dnd.h" +#include "widget/wanalysislibrarytableview.h" +#include "widget/wtracktableview.h" + +AnalysisFeature::AnalysisFeature(UserSettingsPointer pConfig, + Library* pLibrary, TrackCollection* pTrackCollection, + QObject* parent) : + LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), + m_pAnalyzerQueue(nullptr), m_iOldBpmEnabled(0), m_analysisTitleName(tr("Analyze")), - m_pAnalysisView(NULL) { + m_pAnalysisView(nullptr){ + + m_childModel.setRootItem(new TreeItem("$root", "$root", this, nullptr)); setTitleDefault(); } AnalysisFeature::~AnalysisFeature() { - // TODO(XXX) delete these - //delete m_pLibraryTableModel; cleanupAnalyzer(); } @@ -54,39 +52,43 @@ QVariant AnalysisFeature::title() { return m_Title; } -QIcon AnalysisFeature::getIcon() { - return QIcon(":/images/library/ic_library_prepare.png"); +QString AnalysisFeature::getIconPath() { + return ":/images/library/ic_library_prepare.png"; } -void AnalysisFeature::bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard) { - m_pAnalysisView = new DlgAnalysis(libraryWidget, - m_pConfig, - m_pTrackCollection); - connect(m_pAnalysisView, SIGNAL(loadTrack(TrackPointer)), - this, SIGNAL(loadTrack(TrackPointer))); - connect(m_pAnalysisView, SIGNAL(loadTrackToPlayer(TrackPointer, QString)), - this, SIGNAL(loadTrackToPlayer(TrackPointer, QString))); - connect(m_pAnalysisView, SIGNAL(analyzeTracks(QList)), - this, SLOT(analyzeTracks(QList))); - connect(m_pAnalysisView, SIGNAL(stopAnalysis()), - this, SLOT(stopAnalysis())); +QString AnalysisFeature::getSettingsName() const { + return "AnalysisFeature"; +} - connect(m_pAnalysisView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); +QWidget* AnalysisFeature::createPaneWidget(KeyboardEventFilter*, int paneId) { + WTrackTableView* pTable = createTableWidget(paneId); + pTable->loadTrackModel(getAnalysisTableModel()); + connect(pTable->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + this, + SLOT(tableSelectionChanged(const QItemSelection&, const QItemSelection&))); + + return pTable; +} +QWidget* AnalysisFeature::createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) { + m_pAnalysisView = new DlgAnalysis(nullptr, this, m_pTrackCollection); + + m_pAnalysisView->setTableModel(getAnalysisTableModel()); + connect(this, SIGNAL(analysisActive(bool)), m_pAnalysisView, SLOT(analysisActive(bool))); connect(this, SIGNAL(trackAnalysisStarted(int)), m_pAnalysisView, SLOT(trackAnalysisStarted(int))); - m_pAnalysisView->installEventFilter(keyboard); - + m_pAnalysisView->installEventFilter(pKeyboard); + // Let the DlgAnalysis know whether or not analysis is active. - bool bAnalysisActive = m_pAnalyzerQueue != NULL; + bool bAnalysisActive = m_pAnalyzerQueue != nullptr; emit(analysisActive(bAnalysisActive)); - - libraryWidget->registerView(m_sAnalysisViewName, m_pAnalysisView); + m_pAnalysisView->onShow(); + + return m_pAnalysisView; } TreeItemModel* AnalysisFeature::getChildModel() { @@ -94,22 +96,30 @@ TreeItemModel* AnalysisFeature::getChildModel() { } void AnalysisFeature::refreshLibraryModels() { - if (m_pAnalysisView) { + if (!m_pAnalysisView.isNull()) { m_pAnalysisView->onShow(); } } +void AnalysisFeature::selectAll() { + QPointer pTable = LibraryFeature::getFocusedTable(); + if (!pTable.isNull()) { + pTable->selectAll(); + } +} + void AnalysisFeature::activate() { - //qDebug() << "AnalysisFeature::activate()"; - emit(switchToView(m_sAnalysisViewName)); - if (m_pAnalysisView) { - emit(restoreSearch(m_pAnalysisView->currentSearch())); + //qDebug() << "AnalysisFeature::activate()"; + switchToFeature(); + showBreadCrumb(); + + if (!m_pAnalysisView.isNull()) { + restoreSearch(m_pAnalysisView->currentSearch()); } - emit(enableCoverArtDisplay(true)); } void AnalysisFeature::analyzeTracks(QList trackIds) { - if (m_pAnalyzerQueue == NULL) { + if (m_pAnalyzerQueue == nullptr) { // Save the old BPM detection prefs setting (on or off) m_iOldBpmEnabled = m_pConfig->getValueString(ConfigKey("[BPM]","BPMDetectionEnabled")).toInt(); // Force BPM detection to be on. @@ -153,7 +163,7 @@ void AnalysisFeature::slotProgressUpdate(int num_left) { void AnalysisFeature::stopAnalysis() { //qDebug() << this << "stopAnalysis()"; - if (m_pAnalyzerQueue != NULL) { + if (m_pAnalyzerQueue != nullptr) { m_pAnalyzerQueue->stop(); } } @@ -161,20 +171,34 @@ void AnalysisFeature::stopAnalysis() { void AnalysisFeature::cleanupAnalyzer() { setTitleDefault(); emit(analysisActive(false)); - if (m_pAnalyzerQueue != NULL) { + if (m_pAnalyzerQueue != nullptr) { m_pAnalyzerQueue->stop(); m_pAnalyzerQueue->deleteLater(); - m_pAnalyzerQueue = NULL; + m_pAnalyzerQueue = nullptr; // Restore old BPM detection setting for preferences... m_pConfig->set(ConfigKey("[BPM]","BPMDetectionEnabled"), ConfigValue(m_iOldBpmEnabled)); } } +void AnalysisFeature::tableSelectionChanged(const QItemSelection&, + const QItemSelection&) { + //qDebug() << "AnalysisFeature::tableSelectionChanged" << sender(); + QPointer pTable = LibraryFeature::getFocusedTable(); + if (pTable.isNull()) { + return; + } + + QModelIndexList indexes = pTable->selectionModel()->selectedIndexes(); + m_pAnalysisView->setSelectedIndexes(indexes); +} + bool AnalysisFeature::dropAccept(QList urls, QObject* pSource) { Q_UNUSED(pSource); - QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); + QList files = + DragAndDropHelper::supportedTracksFromUrls(urls, false, true); // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files, true); + QList trackIds = + m_pTrackCollection->getTrackDAO().addMultipleTracks(files, true); analyzeTracks(trackIds); return trackIds.size() > 0; } @@ -182,3 +206,12 @@ bool AnalysisFeature::dropAccept(QList urls, QObject* pSource) { bool AnalysisFeature::dragMoveAccept(QUrl url) { return SoundSourceProxy::isUrlSupported(url); } + +AnalysisLibraryTableModel* AnalysisFeature::getAnalysisTableModel() { + if (m_pAnalysisLibraryTableModel.isNull()) { + m_pAnalysisLibraryTableModel = + new AnalysisLibraryTableModel(this, m_pTrackCollection); + } + + return m_pAnalysisLibraryTableModel; +} diff --git a/src/library/analysisfeature.h b/src/library/features/analysis/analysisfeature.h similarity index 66% rename from src/library/analysisfeature.h rename to src/library/features/analysis/analysisfeature.h index bbdd853fc45..8e9e5e15dbf 100644 --- a/src/library/analysisfeature.h +++ b/src/library/features/analysis/analysisfeature.h @@ -13,9 +13,9 @@ #include #include "library/libraryfeature.h" +#include "library/features/analysis/dlganalysis.h" +#include "library/treeitemmodel.h" #include "preferences/usersettings.h" -#include "treeitemmodel.h" -#include "library/dlganalysis.h" class AnalyzerQueue; class TrackCollection; @@ -23,34 +23,40 @@ class TrackCollection; class AnalysisFeature : public LibraryFeature { Q_OBJECT public: - AnalysisFeature(QObject* parent, - UserSettingsPointer pConfig, - TrackCollection* pTrackCollection); + AnalysisFeature(UserSettingsPointer pConfig, + Library* pLibrary, + TrackCollection* pTrackCollection, + QObject* parent); virtual ~AnalysisFeature(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; bool dropAccept(QList urls, QObject* pSource); bool dragMoveAccept(QUrl url); - void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard); - + + QWidget* createPaneWidget(KeyboardEventFilter*, int paneId) override; + QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) override; + TreeItemModel* getChildModel(); void refreshLibraryModels(); + void stopAnalysis(); signals: void analysisActive(bool bActive); void trackAnalysisStarted(int size); public slots: + void selectAll(); void activate(); void analyzeTracks(QList trackIds); private slots: void slotProgressUpdate(int num_left); - void stopAnalysis(); void cleanupAnalyzer(); + void tableSelectionChanged(const QItemSelection&, + const QItemSelection&); private: // Sets the title of this feature to the default name, given by @@ -61,18 +67,18 @@ class AnalysisFeature : public LibraryFeature { // where x is the current track being analyzed and y is the total number of // tracks in the job void setTitleProgress(int trackNum, int totalNum); + + AnalysisLibraryTableModel* getAnalysisTableModel(); - UserSettingsPointer m_pConfig; - TrackCollection* m_pTrackCollection; AnalyzerQueue* m_pAnalyzerQueue; // Used to temporarily enable BPM detection in the prefs before we analyse int m_iOldBpmEnabled; // The title returned by title() QVariant m_Title; TreeItemModel m_childModel; - const static QString m_sAnalysisViewName; QString m_analysisTitleName; - DlgAnalysis* m_pAnalysisView; + QPointer m_pAnalysisView; + QPointer m_pAnalysisLibraryTableModel; }; diff --git a/src/library/analysislibrarytablemodel.cpp b/src/library/features/analysis/analysislibrarytablemodel.cpp similarity index 92% rename from src/library/analysislibrarytablemodel.cpp rename to src/library/features/analysis/analysislibrarytablemodel.cpp index 1c0bb7bcd46..fb5fb598712 100644 --- a/src/library/analysislibrarytablemodel.cpp +++ b/src/library/features/analysis/analysislibrarytablemodel.cpp @@ -1,6 +1,6 @@ #include -#include "analysislibrarytablemodel.h" +#include "library/features/analysis/analysislibrarytablemodel.h" #include "library/trackcollection.h" const QString RECENT_FILTER = "datetime_added > datetime('now', '-7 days')"; diff --git a/src/library/analysislibrarytablemodel.h b/src/library/features/analysis/analysislibrarytablemodel.h similarity index 91% rename from src/library/analysislibrarytablemodel.h rename to src/library/features/analysis/analysislibrarytablemodel.h index a3bfe38a6ea..fe5a72bb777 100644 --- a/src/library/analysislibrarytablemodel.h +++ b/src/library/features/analysis/analysislibrarytablemodel.h @@ -2,7 +2,7 @@ #define ANALYSISLIBRARYTABLEMODEL_H_ #include -#include "librarytablemodel.h" +#include "library/librarytablemodel.h" class AnalysisLibraryTableModel : public LibraryTableModel { diff --git a/src/library/features/analysis/dlganalysis.cpp b/src/library/features/analysis/dlganalysis.cpp new file mode 100644 index 00000000000..de0caca9c78 --- /dev/null +++ b/src/library/features/analysis/dlganalysis.cpp @@ -0,0 +1,119 @@ +#include + +#include "library/features/analysis/analysisfeature.h" +#include "library/features/analysis/dlganalysis.h" +#include "library/trackcollection.h" +#include "util/assert.h" +#include "widget/wanalysislibrarytableview.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +DlgAnalysis::DlgAnalysis(QWidget* parent, AnalysisFeature *pAnalysis, + TrackCollection* pTrackCollection) + : QFrame(parent), + m_pTrackCollection(pTrackCollection), + m_bAnalysisActive(false), + m_pAnalysis(pAnalysis), + m_tracksInQueue(0), + m_currentTrack(0) { + setupUi(this); + m_songsButtonGroup.addButton(radioButtonRecentlyAdded); + m_songsButtonGroup.addButton(radioButtonAllSongs); + + labelProgress->setText(""); + pushButtonAnalyze->setEnabled(false); + connect(pushButtonAnalyze, SIGNAL(clicked()), + this, SLOT(analyze())); + + connect(pushButtonSelectAll, SIGNAL(clicked()), + m_pAnalysis, SLOT(selectAll())); +} + +DlgAnalysis::~DlgAnalysis() { +} + +void DlgAnalysis::onShow() { + // Refresh table + // There might be new tracks dropped to other views + m_pAnalysisLibraryTableModel->select(); + + // TODO(rryan): This triggers a library search before the UI has even + // started up. Accounts for 0.2% of skin creation time. Get rid of this! + radioButtonRecentlyAdded->click(); +} + +void DlgAnalysis::analyze() { + //qDebug() << this << "analyze()"; + if (m_bAnalysisActive) { + m_pAnalysis->stopAnalysis(); + } else { + QList trackIds; + for (QModelIndex selectedIndex : m_selectedIndexes) { + TrackId trackId(selectedIndex.sibling( + selectedIndex.row(), + m_pAnalysisLibraryTableModel->fieldIndex(LIBRARYTABLE_ID)).data()); + if (trackId.isValid()) { + trackIds.append(trackId); + } + } + m_currentTrack = 1; + m_pAnalysis->analyzeTracks(trackIds); + } +} + +void DlgAnalysis::analysisActive(bool bActive) { + //qDebug() << this << "analysisActive" << bActive; + m_bAnalysisActive = bActive; + if (bActive) { + pushButtonAnalyze->setEnabled(true); + pushButtonAnalyze->setText(tr("Stop Analysis")); + labelProgress->setEnabled(true); + } else { + pushButtonAnalyze->setText(tr("Analyze")); + labelProgress->setText(""); + labelProgress->setEnabled(false); + } +} + +// slot +void DlgAnalysis::trackAnalysisFinished(int size) { + //qDebug() << "Analysis finished" << size << "tracks left"; + if (size > 0) { + m_currentTrack = m_tracksInQueue - size + 1; + } +} + +// slot +void DlgAnalysis::trackAnalysisProgress(int progress) { + if (m_bAnalysisActive) { + QString text = tr("Analyzing %1/%2 %3%").arg( + QString::number(m_currentTrack), + QString::number(m_tracksInQueue), + QString::number(progress)); + labelProgress->setText(text); + } +} + +int DlgAnalysis::getNumTracks() { + return m_tracksInQueue; +} + +void DlgAnalysis::setSelectedIndexes(const QModelIndexList& selectedIndexes) { + //qDebug() << "DlgAnalysis::setSelectedIndexes" << selectedIndexes; + m_selectedIndexes = selectedIndexes; + pushButtonAnalyze->setEnabled(m_selectedIndexes.size() > 0 || + !m_bAnalysisActive); +} + +void DlgAnalysis::setTableModel(AnalysisLibraryTableModel* pTableModel) { + m_pAnalysisLibraryTableModel = pTableModel; + + connect(radioButtonRecentlyAdded, SIGNAL(clicked()), + m_pAnalysisLibraryTableModel, SLOT(showRecentSongs())); + connect(radioButtonAllSongs, SIGNAL(clicked()), + m_pAnalysisLibraryTableModel, SLOT(showAllSongs())); +} + +void DlgAnalysis::trackAnalysisStarted(int size) { + m_tracksInQueue = size; +} diff --git a/src/library/features/analysis/dlganalysis.h b/src/library/features/analysis/dlganalysis.h new file mode 100644 index 00000000000..4a338b3ddb3 --- /dev/null +++ b/src/library/features/analysis/dlganalysis.h @@ -0,0 +1,59 @@ +#ifndef DLGANALYSIS_H +#define DLGANALYSIS_H + +#include +#include + +#include "library/features/analysis/analysislibrarytablemodel.h" +#include "library/features/analysis/ui_dlganalysis.h" +#include "library/libraryview.h" +#include "library/trackcollection.h" +#include "preferences/usersettings.h" + +class AnalysisLibraryTableModel; +class WAnalysisLibraryTableView; +class AnalysisFeature; + +class DlgAnalysis : public QFrame, public Ui::DlgAnalysis { + + Q_OBJECT + + public: + + DlgAnalysis(QWidget *parent, + AnalysisFeature* pAnalysis, + TrackCollection* pTrackCollection); + virtual ~DlgAnalysis(); + + virtual void onShow(); + inline const QString currentSearch() { + return m_pAnalysisLibraryTableModel->currentSearch(); + } + int getNumTracks(); + + // The selected indexes are always from the focused pane + void setSelectedIndexes(const QModelIndexList& selectedIndexes); + void setTableModel(AnalysisLibraryTableModel* pTableModel); + + public slots: + + void analyze(); + void trackAnalysisFinished(int size); + void trackAnalysisProgress(int progress); + void trackAnalysisStarted(int size); + void analysisActive(bool bActive); + + private: + //Note m_pTrackTablePlaceholder is defined in the .ui file + TrackCollection* m_pTrackCollection; + bool m_bAnalysisActive; + QButtonGroup m_songsButtonGroup; + AnalysisLibraryTableModel* m_pAnalysisLibraryTableModel; + AnalysisFeature* m_pAnalysis; + int m_tracksInQueue; + int m_currentTrack; + + QModelIndexList m_selectedIndexes; +}; + +#endif //DLGTRIAGE_H diff --git a/src/library/features/analysis/dlganalysis.ui b/src/library/features/analysis/dlganalysis.ui new file mode 100644 index 00000000000..48e7249aba2 --- /dev/null +++ b/src/library/features/analysis/dlganalysis.ui @@ -0,0 +1,81 @@ + + + DlgAnalysis + + + + 0 + 0 + 500 + 399 + + + + Analyze + + + + + + Shows tracks added to the library within the last 7 days. + + + New + + + + + + + Shows all tracks in the library. + + + All + + + + + + + Progress + + + + + + + Selects all tracks in the table below. + + + Select All + + + + + + + Runs beatgrid, key, and ReplayGain detection on the selected tracks. Does not generate waveforms for the selected tracks to save disk space. + + + Analyze + + + + + + + Qt::Vertical + + + + 20 + 235 + + + + + + + + + diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/features/autodj/autodjfeature.cpp similarity index 80% rename from src/library/autodj/autodjfeature.cpp rename to src/library/features/autodj/autodjfeature.cpp index 78ce04f40a5..17d085225cd 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/features/autodj/autodjfeature.cpp @@ -5,37 +5,35 @@ #include #include #include +#include +#include -#include "library/autodj/autodjfeature.h" +#include "library/features/autodj/autodjfeature.h" -#include "library/library.h" +#include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/autodj/autodjprocessor.h" +#include "library/features/autodj/dlgautodj.h" #include "library/parser.h" -#include "mixer/playermanager.h" -#include "library/autodj/autodjprocessor.h" #include "library/trackcollection.h" -#include "library/autodj/dlgautodj.h" -#include "library/treeitem.h" -#include "widget/wlibrary.h" -#include "controllers/keyboard/keyboardeventfilter.h" +#include "mixer/playermanager.h" #include "sources/soundsourceproxy.h" #include "util/dnd.h" +#include "widget/wlibrarysidebar.h" -const QString AutoDJFeature::m_sAutoDJViewName = QString("Auto DJ"); static const int kMaxRetrieveAttempts = 3; -AutoDJFeature::AutoDJFeature(Library* pLibrary, - UserSettingsPointer pConfig, +AutoDJFeature::AutoDJFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, PlayerManagerInterface* pPlayerManager, TrackCollection* pTrackCollection) - : LibraryFeature(pLibrary), - m_pConfig(pConfig), - m_pLibrary(pLibrary), + : LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), m_pTrackCollection(pTrackCollection), m_crateDao(pTrackCollection->getCrateDAO()), m_playlistDao(pTrackCollection->getPlaylistDAO()), m_iAutoDJPlaylistId(-1), - m_pAutoDJProcessor(NULL), - m_pAutoDJView(NULL), + m_pAutoDJProcessor(nullptr), + m_pAutoDJView(nullptr), m_autoDjCratesDao(pTrackCollection->getDatabase(), pTrackCollection->getTrackDAO(), pTrackCollection->getCrateDAO(), @@ -57,6 +55,8 @@ AutoDJFeature::AutoDJFeature(Library* pLibrary, // Create the "Crates" tree-item under the root item. TreeItem* root = m_childModel.getItem(QModelIndex()); + root->setLibraryFeature(this); + m_pCratesTreeItem = new TreeItem(tr("Crates"), "", this, root); m_pCratesTreeItem->setIcon(QIcon(":/images/library/ic_library_crates.png")); root->appendChild(m_pCratesTreeItem); @@ -92,32 +92,50 @@ QVariant AutoDJFeature::title() { return tr("Auto DJ"); } -QIcon AutoDJFeature::getIcon() { - return QIcon(":/images/library/ic_library_autodj.png"); +QString AutoDJFeature::getIconPath() { + return ":/images/library/ic_library_autodj.png"; } -void AutoDJFeature::bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard) { - m_pAutoDJView = new DlgAutoDJ(libraryWidget, - m_pConfig, - m_pLibrary, - m_pAutoDJProcessor, - m_pTrackCollection, - keyboard); - libraryWidget->registerView(m_sAutoDJViewName, m_pAutoDJView); - connect(m_pAutoDJView, SIGNAL(loadTrack(TrackPointer)), - this, SIGNAL(loadTrack(TrackPointer))); - connect(m_pAutoDJView, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool)), - this, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool))); +QString AutoDJFeature::getSettingsName() const { + return "AutoDJFeature"; +} + +QWidget* AutoDJFeature::createPaneWidget(KeyboardEventFilter*, int paneId) { + WTrackTableView* pTrackTableView = createTableWidget(paneId); + pTrackTableView->loadTrackModel(m_pAutoDJProcessor->getTableModel()); + + connect(pTrackTableView->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + this, + SLOT(selectionChanged(const QItemSelection&, const QItemSelection&))); + + return pTrackTableView; +} - connect(m_pAutoDJView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); +QWidget* AutoDJFeature::createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) { + QTabWidget* pContainer = new QTabWidget(nullptr); + + // Add controls + m_pAutoDJView = new DlgAutoDJ(pContainer, m_pAutoDJProcessor); + m_pAutoDJView->installEventFilter(pKeyboard); + QScrollArea* pScroll = new QScrollArea(pContainer); + pScroll->setWidget(m_pAutoDJView); + pScroll->setWidgetResizable(true); + pContainer->addTab(pScroll, tr("Controls")); + + // Add drop target + WLibrarySidebar* pSidebar = createLibrarySidebarWidget(pKeyboard); + pSidebar->setParent(pContainer); + + pContainer->addTab(pSidebar, tr("Track source")); // Be informed when the user wants to add another random track. connect(m_pAutoDJProcessor,SIGNAL(randomTrackRequested(int)), this,SLOT(slotRandomQueue(int))); connect(m_pAutoDJView, SIGNAL(addRandomButton(bool)), this, SLOT(slotAddRandomTrack(bool))); + + return pContainer; } TreeItemModel* AutoDJFeature::getChildModel() { @@ -126,9 +144,16 @@ TreeItemModel* AutoDJFeature::getChildModel() { void AutoDJFeature::activate() { //qDebug() << "AutoDJFeature::activate()"; - emit(switchToView(m_sAutoDJViewName)); - emit(restoreSearch(QString())); //Null String disables search box - emit(enableCoverArtDisplay(true)); + DEBUG_ASSERT_AND_HANDLE(!m_pAutoDJView.isNull()) { + return; + } + + m_pAutoDJView->onShow(); + + switchToFeature(); + showBreadCrumb(); + restoreSearch(QString()); //Null String disables search box + } bool AutoDJFeature::dropAccept(QList urls, QObject* pSource) { @@ -264,6 +289,10 @@ void AutoDJFeature::slotAddRandomTrack(bool) { TrackPointer addedTrack = (m_pTrackCollection->getTrackDAO()).getTrack(trackId); if(addedTrack->exists()) { playlistDao.appendTrackToPlaylist(trackId, m_iAutoDJPlaylistId); + DEBUG_ASSERT_AND_HANDLE(!m_pAutoDJView.isNull()) { + return; + } + m_pAutoDJView->onShow(); return; } else { @@ -284,6 +313,10 @@ void AutoDJFeature::slotAddRandomTrack(bool) { if(addedTrack->exists()) { if(!addedTrack->getPlayCounter().isPlayed()) { playlistDao.appendTrackToPlaylist(trackId, m_iAutoDJPlaylistId); + DEBUG_ASSERT_AND_HANDLE(!m_pAutoDJView.isNull()) { + return; + } + m_pAutoDJView->onShow(); return; } @@ -334,21 +367,31 @@ void AutoDJFeature::constructCrateChildModel() { void AutoDJFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index) { //Save the model index so we can get it in the action slots... - m_lastRightClickedIndex = index; - - TreeItem *item = static_cast(index.internalPointer()); - QString crateName = item->dataPath().toString(); - if (crateName.length() > 0) { + QString crateName; + if (index.isValid()) { + m_lastRightClickedIndex = index; + + TreeItem* item = static_cast(index.internalPointer()); + DEBUG_ASSERT_AND_HANDLE(item) { + return; + } + + crateName = item->dataPath().toString(); + } + + if (!crateName.isEmpty()) { // A crate was right-clicked. // Bring up the context menu. - QMenu menu(NULL); + QMenu menu(nullptr); menu.addAction(m_pRemoveCrateFromAutoDj); menu.exec(globalPos); - } else { + return; + } + else { // The "Crates" tree-item was right-clicked. // Bring up the context menu. - QMenu menu(NULL); - QMenu crateMenu(NULL); + QMenu menu(nullptr); + QMenu crateMenu(nullptr); crateMenu.setTitle(tr("Add Crate as Track Source")); QMap crateMap; m_crateDao.getAutoDjCrates(false, &crateMap); @@ -375,3 +418,12 @@ void AutoDJFeature::slotRandomQueue(int tracksToAdd) { tracksToAdd -= 1; } } + +void AutoDJFeature::selectionChanged(const QItemSelection&, const QItemSelection&) { + QPointer pTable = getFocusedTable(); + DEBUG_ASSERT_AND_HANDLE(!m_pAutoDJView.isNull() && !pTable.isNull()) { + return; + } + + m_pAutoDJView->setSelectedRows(pTable->selectionModel()->selectedRows()); +} diff --git a/src/library/autodj/autodjfeature.h b/src/library/features/autodj/autodjfeature.h similarity index 85% rename from src/library/autodj/autodjfeature.h rename to src/library/features/autodj/autodjfeature.h index 7cc7d2803c8..c3212325d9f 100644 --- a/src/library/autodj/autodjfeature.h +++ b/src/library/features/autodj/autodjfeature.h @@ -16,11 +16,11 @@ #include #include +#include "library/dao/autodjcratesdao.h" #include "library/libraryfeature.h" -#include "preferences/usersettings.h" #include "library/treeitemmodel.h" - -#include "library/dao/autodjcratesdao.h" +#include "preferences/usersettings.h" +#include "widget/wtracktableview.h" class DlgAutoDJ; class Library; @@ -31,20 +31,22 @@ class AutoDJProcessor; class AutoDJFeature : public LibraryFeature { Q_OBJECT public: - AutoDJFeature(Library* pLibrary, - UserSettingsPointer pConfig, + AutoDJFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, PlayerManagerInterface* pPlayerManager, TrackCollection* pTrackCollection); virtual ~AutoDJFeature(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; bool dropAccept(QList urls, QObject* pSource); bool dragMoveAccept(QUrl url); - void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard); + QWidget* createPaneWidget(KeyboardEventFilter*, int paneId) override; + QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) override; TreeItemModel* getChildModel(); @@ -55,17 +57,14 @@ class AutoDJFeature : public LibraryFeature { void onRightClickChild(const QPoint& globalPos, QModelIndex index); private: - UserSettingsPointer m_pConfig; - Library* m_pLibrary; TrackCollection* m_pTrackCollection; CrateDAO& m_crateDao; PlaylistDAO& m_playlistDao; // The id of the AutoDJ playlist. int m_iAutoDJPlaylistId; AutoDJProcessor* m_pAutoDJProcessor; - const static QString m_sAutoDJViewName; TreeItemModel m_childModel; - DlgAutoDJ* m_pAutoDJView; + QPointer m_pAutoDJView; // Initialize the list of crates loaded into the auto-DJ queue. void constructCrateChildModel(); @@ -118,6 +117,7 @@ class AutoDJFeature : public LibraryFeature { // of tracks in the playlist void slotRandomQueue(int); + void selectionChanged(const QItemSelection&, const QItemSelection&); }; diff --git a/src/library/autodj/autodjprocessor.cpp b/src/library/features/autodj/autodjprocessor.cpp similarity index 99% rename from src/library/autodj/autodjprocessor.cpp rename to src/library/features/autodj/autodjprocessor.cpp index ef4c06bdf74..934340580c3 100644 --- a/src/library/autodj/autodjprocessor.cpp +++ b/src/library/features/autodj/autodjprocessor.cpp @@ -1,11 +1,11 @@ -#include "library/autodj/autodjprocessor.h" - -#include "library/trackcollection.h" -#include "control/controlpushbutton.h" #include "control/controlproxy.h" -#include "util/math.h" -#include "mixer/playermanager.h" +#include "control/controlpushbutton.h" +#include "library/trackcollection.h" #include "mixer/basetrackplayer.h" +#include "mixer/playermanager.h" +#include "util/math.h" + +#include "library/features/autodj/autodjprocessor.h" #define kConfigKey "[Auto DJ]" const char* kTransitionPreferenceName = "Transition"; diff --git a/src/library/autodj/autodjprocessor.h b/src/library/features/autodj/autodjprocessor.h similarity index 99% rename from src/library/autodj/autodjprocessor.h rename to src/library/features/autodj/autodjprocessor.h index 8a560a50b9b..c50f9a94402 100644 --- a/src/library/autodj/autodjprocessor.h +++ b/src/library/features/autodj/autodjprocessor.h @@ -1,14 +1,14 @@ #ifndef AUTODJPROCESSOR_H #define AUTODJPROCESSOR_H +#include #include #include -#include -#include "preferences/usersettings.h" #include "control/controlproxy.h" #include "engine/enginechannel.h" -#include "library/playlisttablemodel.h" +#include "library/features/playlist/playlisttablemodel.h" +#include "preferences/usersettings.h" #include "track/track.h" #include "util/class.h" diff --git a/src/library/autodj/dlgautodj.cpp b/src/library/features/autodj/dlgautodj.cpp similarity index 57% rename from src/library/autodj/dlgautodj.cpp rename to src/library/features/autodj/dlgautodj.cpp index 9b2197e422e..7817f781807 100644 --- a/src/library/autodj/dlgautodj.cpp +++ b/src/library/features/autodj/dlgautodj.cpp @@ -1,74 +1,36 @@ #include -#include "library/autodj/dlgautodj.h" - -#include "library/playlisttablemodel.h" -#include "widget/wtracktableview.h" +#include "library/features/playlist/playlisttablemodel.h" #include "util/assert.h" #include "util/duration.h" +#include "widget/wtracktableview.h" + +#include "library/features/autodj/dlgautodj.h" DlgAutoDJ::DlgAutoDJ(QWidget* parent, - UserSettingsPointer pConfig, - Library* pLibrary, - AutoDJProcessor* pProcessor, - TrackCollection* pTrackCollection, - KeyboardEventFilter* pKeyboard) - : QWidget(parent), + AutoDJProcessor* pProcessor) + : QFrame(parent), Ui::DlgAutoDJ(), m_pAutoDJProcessor(pProcessor), // no sorting - m_pTrackTableView(new WTrackTableView(this, pConfig, - pTrackCollection, false)), - m_pAutoDJTableModel(NULL) { + m_pAutoDJTableModel(nullptr) { setupUi(this); - m_pTrackTableView->installEventFilter(pKeyboard); - connect(m_pTrackTableView, SIGNAL(loadTrack(TrackPointer)), - this, SIGNAL(loadTrack(TrackPointer))); - connect(m_pTrackTableView, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool)), - this, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool))); - connect(m_pTrackTableView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); - connect(pLibrary, SIGNAL(setTrackTableFont(QFont)), - m_pTrackTableView, SLOT(setTrackTableFont(QFont))); - connect(pLibrary, SIGNAL(setTrackTableRowHeight(int)), - m_pTrackTableView, SLOT(setTrackTableRowHeight(int))); - connect(m_pTrackTableView, SIGNAL(trackSelected(TrackPointer)), - this, SLOT(updateSelectionInfo())); - - - QBoxLayout* box = dynamic_cast(layout()); - DEBUG_ASSERT_AND_HANDLE(box) { //Assumes the form layout is a QVBox/QHBoxLayout! - } else { - box->removeWidget(m_pTrackTablePlaceholder); - m_pTrackTablePlaceholder->hide(); - box->insertWidget(1, m_pTrackTableView); - } - // We do _NOT_ take ownership of this from AutoDJProcessor. m_pAutoDJTableModel = m_pAutoDJProcessor->getTableModel(); - m_pTrackTableView->loadTrackModel(m_pAutoDJTableModel); // Override some playlist-view properties: - // Do not set this because it disables auto-scrolling - //m_pTrackTableView->setDragDropMode(QAbstractItemView::InternalMove); - connect(pushButtonShuffle, SIGNAL(clicked(bool)), this, SLOT(shufflePlaylistButton(bool))); - connect(pushButtonSkipNext, SIGNAL(clicked(bool)), this, SLOT(skipNextButton(bool))); - connect(pushButtonAddRandom, SIGNAL(clicked(bool)), this, SIGNAL(addRandomButton(bool))); - connect(pushButtonFadeNow, SIGNAL(clicked(bool)), this, SLOT(fadeNowButton(bool))); - connect(spinBoxTransition, SIGNAL(valueChanged(int)), this, SLOT(transitionSliderChanged(int))); - connect(pushButtonAutoDJ, SIGNAL(toggled(bool)), this, SLOT(toggleAutoDJButton(bool))); @@ -81,45 +43,24 @@ DlgAutoDJ::DlgAutoDJ(QWidget* parent, connect(m_pAutoDJProcessor, SIGNAL(autoDJStateChanged(AutoDJProcessor::AutoDJState)), this, SLOT(autoDJStateChanged(AutoDJProcessor::AutoDJState))); autoDJStateChanged(m_pAutoDJProcessor->getState()); - - updateSelectionInfo(); } DlgAutoDJ::~DlgAutoDJ() { - qDebug() << "~DlgAutoDJ()"; - - // Delete m_pTrackTableView before the table model. This is because the - // table view saves the header state using the model. - delete m_pTrackTableView; + //qDebug() << "~DlgAutoDJ()"; } void DlgAutoDJ::onShow() { m_pAutoDJTableModel->select(); } -void DlgAutoDJ::onSearch(const QString& text) { - // Do not allow filtering the Auto DJ playlist, because - // Auto DJ will work from the filtered table - Q_UNUSED(text); -} - -void DlgAutoDJ::loadSelectedTrack() { - m_pTrackTableView->loadSelectedTrack(); -} - -void DlgAutoDJ::loadSelectedTrackToGroup(QString group, bool play) { - m_pTrackTableView->loadSelectedTrackToGroup(group, play); -} - -void DlgAutoDJ::moveSelection(int delta) { - m_pTrackTableView->moveSelection(delta); +void DlgAutoDJ::setSelectedRows(const QModelIndexList& selectedRows) { + m_selectedRows = selectedRows; + updateSelectionInfo(); } -void DlgAutoDJ::shufflePlaylistButton(bool) { - QModelIndexList indexList = m_pTrackTableView->selectionModel()->selectedRows(); - +void DlgAutoDJ::shufflePlaylistButton(bool) { // Activate regardless of button being checked - m_pAutoDJProcessor->shufflePlaylist(indexList); + m_pAutoDJProcessor->shufflePlaylist(m_selectedRows); } void DlgAutoDJ::skipNextButton(bool) { @@ -191,35 +132,24 @@ void DlgAutoDJ::autoDJStateChanged(AutoDJProcessor::AutoDJState state) { } } -void DlgAutoDJ::setTrackTableFont(const QFont& font) { - m_pTrackTableView->setTrackTableFont(font); -} - -void DlgAutoDJ::setTrackTableRowHeight(int rowHeight) { - m_pTrackTableView->setTrackTableRowHeight(rowHeight); -} - void DlgAutoDJ::updateSelectionInfo() { + if (m_selectedRows.isEmpty()) { + labelSelectionInfo->setText(""); + labelSelectionInfo->setEnabled(false); + return; + } + double duration = 0.0; - - QModelIndexList indices = m_pTrackTableView->selectionModel()->selectedRows(); - - for (int i = 0; i < indices.size(); ++i) { - TrackPointer pTrack = m_pAutoDJTableModel->getTrack(indices.at(i)); + for (const QModelIndex& mIndex : m_selectedRows) { + TrackPointer pTrack = m_pAutoDJTableModel->getTrack(mIndex); if (pTrack) { duration += pTrack->getDuration(); } } QString label; - - if (!indices.isEmpty()) { - label.append(mixxx::Duration::formatSeconds(duration)); - label.append(QString(" (%1)").arg(indices.size())); - labelSelectionInfo->setText(label); - labelSelectionInfo->setEnabled(true); - } else { - labelSelectionInfo->setText(""); - labelSelectionInfo->setEnabled(false); - } + label.append(mixxx::Duration::formatSeconds(duration)); + label.append(QString(" (%1)").arg(m_selectedRows.size())); + labelSelectionInfo->setText(label); + labelSelectionInfo->setEnabled(true); } diff --git a/src/library/features/autodj/dlgautodj.h b/src/library/features/autodj/dlgautodj.h new file mode 100644 index 00000000000..f5572fe7ff1 --- /dev/null +++ b/src/library/features/autodj/dlgautodj.h @@ -0,0 +1,47 @@ +#ifndef DLGAUTODJ_H +#define DLGAUTODJ_H + +#include +#include +#include + +#include "library/features/autodj/autodjprocessor.h" +#include "library/features/autodj/ui_dlgautodj.h" +#include "library/trackcollection.h" +#include "track/track.h" + +class PlaylistTableModel; +class WTrackTableView; + +class DlgAutoDJ : public QFrame, public Ui::DlgAutoDJ { + Q_OBJECT + public: + DlgAutoDJ(QWidget* parent, AutoDJProcessor* pProcessor); + virtual ~DlgAutoDJ(); + + void onShow(); + + // These seleced rows are always from the focused pane + void setSelectedRows(const QModelIndexList& selectedRows); + + public slots: + void shufflePlaylistButton(bool buttonChecked); + void skipNextButton(bool buttonChecked); + void fadeNowButton(bool buttonChecked); + void toggleAutoDJButton(bool enable); + void transitionTimeChanged(int time); + void transitionSliderChanged(int value); + void autoDJStateChanged(AutoDJProcessor::AutoDJState state); + void updateSelectionInfo(); + + signals: + void addRandomButton(bool buttonChecked); + + private: + AutoDJProcessor* m_pAutoDJProcessor; + PlaylistTableModel* m_pAutoDJTableModel; + + QModelIndexList m_selectedRows; +}; + +#endif //DLGAUTODJ_H diff --git a/src/library/features/autodj/dlgautodj.ui b/src/library/features/autodj/dlgautodj.ui new file mode 100644 index 00000000000..b8c2bb67a1c --- /dev/null +++ b/src/library/features/autodj/dlgautodj.ui @@ -0,0 +1,149 @@ + + + DlgAutoDJ + + + + 0 + 0 + 507 + 399 + + + + Auto DJ + + + + + + + + Seconds + + + sec. + + + + + + + + 0 + 0 + + + + Determines the duration of the transition. + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + -9 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Turn Auto DJ on or off. + + + Enable Auto DJ + + + true + + + + + + + + + + + + + + Trigger the transition to the next track. + + + Fade Now + + + + + + + Skip the next track in the Auto DJ playlist. + + + Skip Track + + + false + + + + + + + Add a random track from track sources (crates) or Library to the Auto DJ playlist. + + + Add Random + + + + + + + Shuffle the content of the Auto DJ playlist. + + + Shuffle + + + false + + + + + + + Qt::Vertical + + + + 20 + 172 + + + + + + + + + diff --git a/src/library/banshee/bansheedbconnection.cpp b/src/library/features/banshee/bansheedbconnection.cpp similarity index 99% rename from src/library/banshee/bansheedbconnection.cpp rename to src/library/features/banshee/bansheedbconnection.cpp index ed8acb58636..663d2b10cd8 100644 --- a/src/library/banshee/bansheedbconnection.cpp +++ b/src/library/features/banshee/bansheedbconnection.cpp @@ -6,7 +6,7 @@ #include #include "library/queryutil.h" -#include "library/banshee/bansheedbconnection.h" +#include "library/features/banshee/bansheedbconnection.h" #include "util/performancetimer.h" BansheeDbConnection::BansheeDbConnection() { diff --git a/src/library/banshee/bansheedbconnection.h b/src/library/features/banshee/bansheedbconnection.h similarity index 100% rename from src/library/banshee/bansheedbconnection.h rename to src/library/features/banshee/bansheedbconnection.h diff --git a/src/library/banshee/bansheefeature.cpp b/src/library/features/banshee/bansheefeature.cpp similarity index 82% rename from src/library/banshee/bansheefeature.cpp rename to src/library/features/banshee/bansheefeature.cpp index 05ee9c67a3d..ad6e4ac8d7d 100644 --- a/src/library/banshee/bansheefeature.cpp +++ b/src/library/features/banshee/bansheefeature.cpp @@ -2,21 +2,22 @@ #include #include -#include "library/banshee/bansheefeature.h" +#include "library/features/banshee/bansheefeature.h" -#include "library/banshee/bansheedbconnection.h" #include "library/dao/settingsdao.h" -#include "library/baseexternalplaylistmodel.h" -#include "library/banshee/bansheeplaylistmodel.h" +#include "library/features/banshee/bansheedbconnection.h" +#include "library/features/banshee/bansheeplaylistmodel.h" +#include "library/features/baseexternalfeature/baseexternalplaylistmodel.h" const QString BansheeFeature::BANSHEE_MOUNT_KEY = "mixxx.BansheeFeature.mount"; QString BansheeFeature::m_databaseFile; -BansheeFeature::BansheeFeature(QObject* parent, - TrackCollection* pTrackCollection, - UserSettingsPointer pConfig) - : BaseExternalLibraryFeature(parent, pTrackCollection), +BansheeFeature::BansheeFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : BaseExternalLibraryFeature(pConfig, pLibrary, parent, pTrackCollection), m_pTrackCollection(pTrackCollection), m_cancelImport(false) { Q_UNUSED(pConfig); @@ -56,8 +57,12 @@ QVariant BansheeFeature::title() { return m_title; } -QIcon BansheeFeature::getIcon() { - return QIcon(":/images/library/ic_library_banshee.png"); +QString BansheeFeature::getIconPath() { + return ":/images/library/ic_library_banshee.png"; +} + +QString BansheeFeature::getSettingsName() const { + return "BansheeFeature"; } void BansheeFeature::activate() { @@ -91,7 +96,8 @@ void BansheeFeature::activate() { m_isActivated = true; - TreeItem* playlist_root = new TreeItem(); + TreeItem* playlistRoot = new TreeItem(); + playlistRoot->setLibraryFeature(this); QList list = m_connection.getPlaylists(); @@ -99,12 +105,12 @@ void BansheeFeature::activate() { foreach (playlist, list) { qDebug() << playlist.name; // append the playlist to the child model - TreeItem *item = new TreeItem(playlist.name, playlist.playlistId, this, playlist_root); - playlist_root->appendChild(item); + TreeItem *item = new TreeItem(playlist.name, playlist.playlistId, this, playlistRoot); + playlistRoot->appendChild(item); } - if (playlist_root) { - m_childModel.setRootItem(playlist_root); + if (playlistRoot) { + m_childModel.setRootItem(playlistRoot); if (m_isActivated) { activate(); } @@ -117,8 +123,9 @@ void BansheeFeature::activate() { } m_pBansheePlaylistModel->setTableModel(0); // Gets the master playlist - emit(showTrackModel(m_pBansheePlaylistModel)); - emit(enableCoverArtDisplay(false)); + + showTrackModel(m_pBansheePlaylistModel); + showBreadCrumb(); } void BansheeFeature::activateChild(const QModelIndex& index) { @@ -129,8 +136,9 @@ void BansheeFeature::activateChild(const QModelIndex& index) { if (playlistID > 0) { qDebug() << "Activating " << item->data().toString(); m_pBansheePlaylistModel->setTableModel(playlistID); - emit(showTrackModel(m_pBansheePlaylistModel)); - emit(enableCoverArtDisplay(false)); + + showTrackModel(m_pBansheePlaylistModel); + showBreadCrumb(item); } } diff --git a/src/library/banshee/bansheefeature.h b/src/library/features/banshee/bansheefeature.h similarity index 75% rename from src/library/banshee/bansheefeature.h rename to src/library/features/banshee/bansheefeature.h index 56683589629..3ab83c37908 100644 --- a/src/library/banshee/bansheefeature.h +++ b/src/library/features/banshee/bansheefeature.h @@ -8,25 +8,27 @@ #include #include -#include "library/baseexternallibraryfeature.h" +#include "library/features/banshee/bansheedbconnection.h" +#include "library/features/baseexternalfeature/baseexternallibraryfeature.h" #include "library/trackcollection.h" #include "library/treeitemmodel.h" -#include "library/treeitem.h" -#include "library/banshee/bansheedbconnection.h" - class BansheePlaylistModel; class BansheeFeature : public BaseExternalLibraryFeature { Q_OBJECT public: - BansheeFeature(QObject* parent, TrackCollection* pTrackCollection, UserSettingsPointer pConfig); + BansheeFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); virtual ~BansheeFeature(); static bool isSupported(); static void prepareDbPath(UserSettingsPointer pConfig); - virtual QVariant title(); - virtual QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; virtual TreeItemModel* getChildModel(); @@ -55,7 +57,7 @@ class BansheeFeature : public BaseExternalLibraryFeature { bool m_cancelImport; static QString m_databaseFile; - + static const QString BANSHEE_MOUNT_KEY; }; diff --git a/src/library/banshee/bansheeplaylistmodel.cpp b/src/library/features/banshee/bansheeplaylistmodel.cpp similarity index 99% rename from src/library/banshee/bansheeplaylistmodel.cpp rename to src/library/features/banshee/bansheeplaylistmodel.cpp index 5bfcfb977bc..f78fd912001 100644 --- a/src/library/banshee/bansheeplaylistmodel.cpp +++ b/src/library/features/banshee/bansheeplaylistmodel.cpp @@ -1,11 +1,12 @@ #include #include -#include "library/banshee/bansheeplaylistmodel.h" -#include "library/banshee/bansheedbconnection.h" +#include "library/features/banshee/bansheeplaylistmodel.h" + +#include "library/features/banshee/bansheedbconnection.h" +#include "library/previewbuttondelegate.h" #include "library/queryutil.h" #include "library/starrating.h" -#include "library/previewbuttondelegate.h" #include "track/beatfactory.h" #include "track/beats.h" #include "mixer/playermanager.h" diff --git a/src/library/banshee/bansheeplaylistmodel.h b/src/library/features/banshee/bansheeplaylistmodel.h similarity index 96% rename from src/library/banshee/bansheeplaylistmodel.h rename to src/library/features/banshee/bansheeplaylistmodel.h index efda77a8786..fec2c561a7a 100644 --- a/src/library/banshee/bansheeplaylistmodel.h +++ b/src/library/features/banshee/bansheeplaylistmodel.h @@ -4,12 +4,12 @@ #include #include -#include "library/trackmodel.h" -#include "library/trackcollection.h" +#include "library/basesqltablemodel.h" #include "library/dao/trackdao.h" -#include "library/banshee/bansheedbconnection.h" +#include "library/features/banshee/bansheedbconnection.h" #include "library/stardelegate.h" -#include "library/basesqltablemodel.h" +#include "library/trackmodel.h" +#include "library/trackcollection.h" class BansheePlaylistModel : public BaseSqlTableModel { Q_OBJECT diff --git a/src/library/baseexternallibraryfeature.cpp b/src/library/features/baseexternalfeature/baseexternallibraryfeature.cpp similarity index 90% rename from src/library/baseexternallibraryfeature.cpp rename to src/library/features/baseexternalfeature/baseexternallibraryfeature.cpp index 27d69ee48e3..741e29dbb79 100644 --- a/src/library/baseexternallibraryfeature.cpp +++ b/src/library/features/baseexternalfeature/baseexternallibraryfeature.cpp @@ -1,13 +1,14 @@ -#include "library/baseexternallibraryfeature.h" - #include +#include "library/features/baseexternalfeature/baseexternallibraryfeature.h" + #include "library/basesqltablemodel.h" -BaseExternalLibraryFeature::BaseExternalLibraryFeature(QObject* pParent, +BaseExternalLibraryFeature::BaseExternalLibraryFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* pParent, TrackCollection* pCollection) - : LibraryFeature(pParent), - m_pTrackCollection(pCollection) { + : LibraryFeature(pConfig, pLibrary, pCollection, pParent) { m_pAddToAutoDJAction = new QAction(tr("Add to Auto DJ Queue (bottom)"), this); connect(m_pAddToAutoDJAction, SIGNAL(triggered()), this, SLOT(slotAddToAutoDJ())); @@ -87,8 +88,8 @@ void BaseExternalLibraryFeature::slotImportAsMixxxPlaylist() { playlistDao.appendTracksToPlaylist(trackIds, playlistId); } else { // Do not change strings here without also changing strings in - // src/library/baseplaylistfeature.cpp - QMessageBox::warning(NULL, + // src/library/features/baseplaylist/baseplaylistfeature.cpp + QMessageBox::warning(nullptr, tr("Playlist Creation Failed"), tr("An unknown error occurred while creating playlist: ") + playlist); diff --git a/src/library/baseexternallibraryfeature.h b/src/library/features/baseexternalfeature/baseexternallibraryfeature.h similarity index 84% rename from src/library/baseexternallibraryfeature.h rename to src/library/features/baseexternalfeature/baseexternallibraryfeature.h index 7a13f5d47d2..43ce15bbbbb 100644 --- a/src/library/baseexternallibraryfeature.h +++ b/src/library/features/baseexternalfeature/baseexternallibraryfeature.h @@ -12,7 +12,10 @@ class TrackCollection; class BaseExternalLibraryFeature : public LibraryFeature { Q_OBJECT public: - BaseExternalLibraryFeature(QObject* pParent, TrackCollection* pCollection); + BaseExternalLibraryFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* pParent, + TrackCollection* pCollection); virtual ~BaseExternalLibraryFeature(); public slots: @@ -38,7 +41,6 @@ class BaseExternalLibraryFeature : public LibraryFeature { private: void addToAutoDJ(bool bTop); - TrackCollection* m_pTrackCollection; QAction* m_pAddToAutoDJAction; QAction* m_pAddToAutoDJTopAction; QAction* m_pImportAsMixxxPlaylistAction; diff --git a/src/library/baseexternalplaylistmodel.cpp b/src/library/features/baseexternalfeature/baseexternalplaylistmodel.cpp similarity index 98% rename from src/library/baseexternalplaylistmodel.cpp rename to src/library/features/baseexternalfeature/baseexternalplaylistmodel.cpp index c1ae1253488..66d1f441e56 100644 --- a/src/library/baseexternalplaylistmodel.cpp +++ b/src/library/features/baseexternalfeature/baseexternalplaylistmodel.cpp @@ -1,4 +1,4 @@ -#include "library/baseexternalplaylistmodel.h" +#include "library/features/baseexternalfeature/baseexternalplaylistmodel.h" #include "library/queryutil.h" #include "mixer/playermanager.h" diff --git a/src/library/baseexternalplaylistmodel.h b/src/library/features/baseexternalfeature/baseexternalplaylistmodel.h similarity index 96% rename from src/library/baseexternalplaylistmodel.h rename to src/library/features/baseexternalfeature/baseexternalplaylistmodel.h index 0b8cd3e7b40..1793103f790 100644 --- a/src/library/baseexternalplaylistmodel.h +++ b/src/library/features/baseexternalfeature/baseexternalplaylistmodel.h @@ -9,7 +9,6 @@ #include "library/trackmodel.h" #include "library/basesqltablemodel.h" -#include "library/librarytablemodel.h" #include "library/dao/playlistdao.h" #include "library/dao/trackdao.h" diff --git a/src/library/baseexternaltrackmodel.cpp b/src/library/features/baseexternalfeature/baseexternaltrackmodel.cpp similarity index 98% rename from src/library/baseexternaltrackmodel.cpp rename to src/library/features/baseexternalfeature/baseexternaltrackmodel.cpp index 22aa56528cd..9589656dd1e 100644 --- a/src/library/baseexternaltrackmodel.cpp +++ b/src/library/features/baseexternalfeature/baseexternaltrackmodel.cpp @@ -1,4 +1,4 @@ -#include "library/baseexternaltrackmodel.h" +#include "library/features/baseexternalfeature/baseexternaltrackmodel.h" #include "library/trackcollection.h" #include "library/queryutil.h" #include "mixer/playermanager.h" diff --git a/src/library/baseexternaltrackmodel.h b/src/library/features/baseexternalfeature/baseexternaltrackmodel.h similarity index 100% rename from src/library/baseexternaltrackmodel.h rename to src/library/features/baseexternalfeature/baseexternaltrackmodel.h diff --git a/src/library/baseplaylistfeature.cpp b/src/library/features/baseplaylist/baseplaylistfeature.cpp similarity index 74% rename from src/library/baseplaylistfeature.cpp rename to src/library/features/baseplaylist/baseplaylistfeature.cpp index 12d30d8f44f..197714b4540 100644 --- a/src/library/baseplaylistfeature.cpp +++ b/src/library/features/baseplaylist/baseplaylistfeature.cpp @@ -1,34 +1,34 @@ -#include "library/baseplaylistfeature.h" - #include #include #include #include +#include "library/features/baseplaylist/baseplaylistfeature.h" + +#include "controllers/keyboard/keyboardeventfilter.h" #include "library/export/trackexportwizard.h" +#include "library/features/playlist/playlisttablemodel.h" #include "library/library.h" #include "library/parser.h" +#include "library/parsercsv.h" #include "library/parserm3u.h" #include "library/parserpls.h" -#include "library/parsercsv.h" -#include "library/playlisttablemodel.h" #include "library/trackcollection.h" -#include "library/treeitem.h" -#include "controllers/keyboard/keyboardeventfilter.h" -#include "widget/wlibrary.h" -#include "widget/wlibrarytextbrowser.h" +#include "library/treeitemmodel.h" #include "util/assert.h" +#include "widget/wlibrarystack.h" +#include "widget/wlibrarytextbrowser.h" -BasePlaylistFeature::BasePlaylistFeature(QObject* parent, - UserSettingsPointer pConfig, - TrackCollection* pTrackCollection, - QString rootViewName) - : LibraryFeature(pConfig, parent), - m_pTrackCollection(pTrackCollection), +BasePlaylistFeature::BasePlaylistFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), m_playlistDao(pTrackCollection->getPlaylistDAO()), m_trackDao(pTrackCollection->getTrackDAO()), - m_pPlaylistTableModel(NULL), - m_rootViewName(rootViewName) { + m_pPlaylistTableModel(nullptr) { + m_childModel = new TreeItemModel; + m_pCreatePlaylistAction = new QAction(tr("Create New Playlist"),this); connect(m_pCreatePlaylistAction, SIGNAL(triggered()), this, SLOT(slotCreatePlaylist())); @@ -92,15 +92,13 @@ BasePlaylistFeature::BasePlaylistFeature(QObject* parent, connect(&m_playlistDao, SIGNAL(lockChanged(int)), this, SLOT(slotPlaylistTableChanged(int))); - Library* pLibrary = static_cast(parent); connect(pLibrary, SIGNAL(trackSelected(TrackPointer)), this, SLOT(slotTrackSelected(TrackPointer))); - connect(pLibrary, SIGNAL(switchToView(const QString&)), - this, SLOT(slotResetSelectedTrack())); } BasePlaylistFeature::~BasePlaylistFeature() { - delete m_pPlaylistTableModel; + qDeleteAll(m_playlistTableModel); + m_playlistTableModel.clear(); delete m_pCreatePlaylistAction; delete m_pDeletePlaylistAction; delete m_pImportPlaylistAction; @@ -115,46 +113,75 @@ BasePlaylistFeature::~BasePlaylistFeature() { delete m_pAnalyzePlaylistAction; } -int BasePlaylistFeature::playlistIdFromIndex(QModelIndex index) { - TreeItem* item = static_cast(index.internalPointer()); - if (item == NULL) { - return -1; +QPointer BasePlaylistFeature::getPlaylistTableModel(int paneId) { + if (paneId < 0) { + paneId = m_featurePane; } - - QString dataPath = item->dataPath().toString(); - bool ok = false; - int playlistId = dataPath.toInt(&ok); - if (!ok) { - return -1; + auto it = m_playlistTableModel.find(paneId); + if (it == m_playlistTableModel.end() || it->isNull()) { + it = m_playlistTableModel.insert(paneId, constructTableModel()); } - return playlistId; + return *it; } void BasePlaylistFeature::activate() { - emit(switchToView(m_rootViewName)); - emit(restoreSearch(QString())); // Null String disables search box - emit(enableCoverArtDisplay(true)); + adoptPreselectedPane(); + + auto modelIt = m_lastChildClicked.constFind(m_featurePane); + if (modelIt != m_lastChildClicked.constEnd() && (*modelIt).isValid()) { + qDebug() << "BasePlaylistFeature::activate" << "m_lastChildClicked found"; + // Open last clicked Playlist in the preselectded pane + activateChild(*modelIt); + return; + } + + showBrowse(m_featurePane); + switchToFeature(); + showBreadCrumb(); + + restoreSearch(QString()); // Null String disables search box } void BasePlaylistFeature::activateChild(const QModelIndex& index) { + adoptPreselectedPane(); + + if (index == m_lastChildClicked.value(m_featurePane)) { + restoreSearch(""); + showTable(m_featurePane); + switchToFeature(); + return; + } + + m_lastChildClicked[m_featurePane] = index; + //qDebug() << "BasePlaylistFeature::activateChild()" << index; - int playlistId = playlistIdFromIndex(index); - if (playlistId != -1 && m_pPlaylistTableModel) { - m_pPlaylistTableModel->setTableModel(playlistId); - emit(showTrackModel(m_pPlaylistTableModel)); - emit(enableCoverArtDisplay(true)); + QSet playlistIds = playlistIdsFromIndex(index); + m_pPlaylistTableModel = getPlaylistTableModel(m_featurePane); + + if (!playlistIds.isEmpty() && m_pPlaylistTableModel) { + m_pPlaylistTableModel->setTableModel(playlistIds); + showTable(m_featurePane); + + // Set the feature Focus for a moment to allow the LibraryFeature class + // to find the focused WTrackTable + showTrackModel(m_pPlaylistTableModel); + + restoreSearch(""); + showBreadCrumb(index); + } } +void BasePlaylistFeature::invalidateChild() { + m_lastChildClicked.clear(); +} + void BasePlaylistFeature::activatePlaylist(int playlistId) { //qDebug() << "BasePlaylistFeature::activatePlaylist()" << playlistId; - QModelIndex index = indexFromPlaylistId(playlistId); - if (playlistId != -1 && index.isValid() && m_pPlaylistTableModel) { + if (playlistId != -1 && m_pPlaylistTableModel) { m_pPlaylistTableModel->setTableModel(playlistId); - emit(showTrackModel(m_pPlaylistTableModel)); - emit(enableCoverArtDisplay(true)); - // Update selection - emit(featureSelect(this, m_lastRightClickedIndex)); + showTrackModel(m_pPlaylistTableModel); + //m_pPlaylistTableModel->select(); activateChild(m_lastRightClickedIndex); } } @@ -269,47 +296,30 @@ void BasePlaylistFeature::slotCreatePlaylist() { return; } - QString name; - bool validNameGiven = false; - - while (!validNameGiven) { - bool ok = false; - name = QInputDialog::getText(NULL, - tr("Create New Playlist"), - tr("Enter name for new playlist:"), - QLineEdit::Normal, - tr("New Playlist"), - &ok).trimmed(); - if (!ok) - return; - - int existingId = m_playlistDao.getPlaylistIdFromName(name); - - if (existingId != -1) { - QMessageBox::warning(NULL, - tr("Playlist Creation Failed"), - tr("A playlist by that name already exists.")); - } else if (name.isEmpty()) { - QMessageBox::warning(NULL, - tr("Playlist Creation Failed"), - tr("A playlist cannot have a blank name.")); - } else { - validNameGiven = true; - } + QString name = getValidPlaylistName(); + if (name.isNull()) { + // The user has canceled + return; } - int playlistId = m_playlistDao.createPlaylist(name); if (playlistId != -1) { + m_lastRightClickedIndex = constructChildModel(playlistId); + m_lastChildClicked[m_featurePane] = m_lastRightClickedIndex; activatePlaylist(playlistId); } else { - QMessageBox::warning(NULL, + QMessageBox::warning(nullptr, tr("Playlist Creation Failed"), tr("An unknown error occurred while creating playlist: ") + name); } } +void BasePlaylistFeature::setFeaturePaneId(int focus) { + m_pPlaylistTableModel = getPlaylistTableModel(focus); + LibraryFeature::setFeaturePaneId(focus); +} + void BasePlaylistFeature::slotDeletePlaylist() { //qDebug() << "slotDeletePlaylist() row:" << m_lastRightClickedIndex.data(); int playlistId = playlistIdFromIndex(m_lastRightClickedIndex); @@ -327,8 +337,22 @@ void BasePlaylistFeature::slotDeletePlaylist() { DEBUG_ASSERT_AND_HANDLE(playlistId >= 0) { return; } - + m_playlistDao.deletePlaylist(playlistId); + + // This avoids a bug where the m_lastChildClicked index is still a valid + // index but it's not true since we just deleted it + for (auto it = m_playlistTableModel.begin(); + it != m_playlistTableModel.end(); ++it) { + + if ((*it)->getPlaylist() == playlistId) { + // Show the browse widget, this avoids a problem when the same + // playlist is shown twice and gets deleted. One of the panes + // gets still showing the unexisting playlist. + m_lastChildClicked[it.key()] = QModelIndex(); + showBrowse(it.key()); + } + } activate(); } } @@ -437,13 +461,15 @@ void BasePlaylistFeature::slotCreateImportPlaylist() { slotImportPlaylistFile(playlistFile); } - activatePlaylist(lastPlaylistId); + m_lastChildClicked[m_featurePane] = constructChildModel(lastPlaylistId); } void BasePlaylistFeature::slotExportPlaylist() { - if (!m_pPlaylistTableModel) { + QPointer pTableModel = getPlaylistTableModel(); + if (pTableModel.isNull()) { return; } + int playlistId = playlistIdFromIndex(m_lastRightClickedIndex); if (playlistId == -1) { return; @@ -494,7 +520,7 @@ void BasePlaylistFeature::slotExportPlaylist() { new PlaylistTableModel(this, m_pTrackCollection, "mixxx.db.model.playlist_export")); - pPlaylistTableModel->setTableModel(m_pPlaylistTableModel->getPlaylist()); + pPlaylistTableModel->setTableModel(pTableModel->getPlaylist()); pPlaylistTableModel->setSort(pPlaylistTableModel->fieldIndex( ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION), Qt::AscendingOrder); pPlaylistTableModel->select(); @@ -554,8 +580,8 @@ void BasePlaylistFeature::slotExportTrackFiles() { tracks.push_back(pPlaylistTableModel->getTrack(index)); } - TrackExportWizard track_export(nullptr, m_pConfig, tracks); - track_export.exportTracks(); + TrackExportWizard trackExport(nullptr, m_pConfig, tracks); + trackExport.exportTracks(); } void BasePlaylistFeature::slotAddToAutoDJ() { @@ -579,6 +605,81 @@ void BasePlaylistFeature::addToAutoDJ(bool bTop) { } } +QString BasePlaylistFeature::getValidPlaylistName() const { + QString name; + bool validNameGiven = false; + + while (!validNameGiven) { + bool ok = false; + name = QInputDialog::getText(nullptr, + tr("Create New Playlist"), + tr("Enter name for new playlist:"), + QLineEdit::Normal, + tr("New Playlist"), + &ok).trimmed(); + if (!ok) { + // Cancel button clicked + return QString(); + } + + int existingId = m_playlistDao.getPlaylistIdFromName(name); + + if (existingId != -1) { + QMessageBox::warning(NULL, + tr("Playlist Creation Failed"), + tr("A playlist by that name already exists.")); + } else if (name.isEmpty()) { + QMessageBox::warning(NULL, + tr("Playlist Creation Failed"), + tr("A playlist cannot have a blank name.")); + } else { + validNameGiven = true; + } + } + return name; +} + +QSet BasePlaylistFeature::playlistIdsFromIndex(const QModelIndex &index) const { + bool ok = false; + int playlistId = index.data(AbstractRole::RoleDataPath).toInt(&ok); + QSet set; + if (ok) { + set.insert(playlistId); + } + return set; +} + + +int BasePlaylistFeature::playlistIdFromIndex(const QModelIndex& index) const { + QSet playlistIds = playlistIdsFromIndex(index); + if (playlistIds.empty() || playlistIds.size() > 1) { + return -1; + } + + return *playlistIds.begin(); +} + +void BasePlaylistFeature::showTable(int paneId) { + auto it = m_panes.find(paneId); + auto itId = m_tableIndexByPaneId.find(paneId); + if (it == m_panes.end() || it->isNull() || itId == m_tableIndexByPaneId.end()) { + return; + } + + (*it)->setCurrentIndex(*itId); +} + +void BasePlaylistFeature::showBrowse(int paneId) { + auto it = m_panes.find(paneId); + auto itId = m_browseIndexByPaneId.find(paneId); + if (it == m_panes.end() || it->isNull() || itId == m_browseIndexByPaneId.end()) { + return; + } + + (*it)->setCurrentIndex(*itId); +} + + void BasePlaylistFeature::slotAnalyzePlaylist() { if (m_lastRightClickedIndex.isValid()) { int playlistId = playlistIdFromIndex(m_lastRightClickedIndex); @@ -590,18 +691,27 @@ void BasePlaylistFeature::slotAnalyzePlaylist() { } TreeItemModel* BasePlaylistFeature::getChildModel() { - return &m_childModel; + return m_childModel; } -void BasePlaylistFeature::bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard) { - Q_UNUSED(keyboard); - WLibraryTextBrowser* edit = new WLibraryTextBrowser(libraryWidget); +QWidget* BasePlaylistFeature::createPaneWidget(KeyboardEventFilter* pKeyboard, + int paneId) { + WLibraryStack* pStack = new WLibraryStack(nullptr); + m_panes[paneId] = pStack; + + WLibraryTextBrowser* edit = new WLibraryTextBrowser(pStack); edit->setHtml(getRootViewHtml()); edit->setOpenLinks(false); + edit->installEventFilter(pKeyboard); connect(edit, SIGNAL(anchorClicked(const QUrl)), this, SLOT(htmlLinkClicked(const QUrl))); - libraryWidget->registerView(m_rootViewName, edit); + m_browseIndexByPaneId[paneId] = pStack->addWidget(edit); + + QWidget* pTable = LibraryFeature::createPaneWidget(pKeyboard, paneId); + pTable->setParent(pStack); + m_tableIndexByPaneId[paneId] = pStack->addWidget(pTable); + + return pStack; } void BasePlaylistFeature::htmlLinkClicked(const QUrl& link) { @@ -617,80 +727,65 @@ void BasePlaylistFeature::htmlLinkClicked(const QUrl& link) { * we require the sidebar model not to reset. * This method queries the database and does dynamic insertion */ -QModelIndex BasePlaylistFeature::constructChildModel(int selected_id) { +QModelIndex BasePlaylistFeature::constructChildModel(int selectedId) { buildPlaylistList(); - QList data_list; - int selected_row = -1; + + m_childModel->setRootItem(new TreeItem("$root", "$root", this, nullptr)); + QList dataList; + int selectedRow = -1; // Access the invisible root item - TreeItem* root = m_childModel.getItem(QModelIndex()); + TreeItem* root = m_childModel->getItem(QModelIndex()); int row = 0; - for (QList >::const_iterator it = m_playlistList.begin(); - it != m_playlistList.end(); ++it, ++row) { - int playlist_id = it->first; - QString playlist_name = it->second; - - if (selected_id == playlist_id) { + for (const PlaylistItem& p : m_playlistList) { + if (selectedId == p.id) { // save index for selection - selected_row = row; - m_childModel.index(selected_row, 0); + selectedRow = row; } // Create the TreeItem whose parent is the invisible root item - TreeItem* item = new TreeItem(playlist_name, QString::number(playlist_id), this, root); - item->setBold(m_playlistsSelectedTrackIsIn.contains(playlist_id)); + TreeItem* item = new TreeItem(p.name, QString::number(p.id), this, root); + item->setBold(m_playlistsSelectedTrackIsIn.contains(p.id)); - decorateChild(item, playlist_id); - data_list.append(item); + decorateChild(item, p.id); + dataList.append(item); + ++row; } // Append all the newly created TreeItems in a dynamic way to the childmodel - m_childModel.insertRows(data_list, 0, m_playlistList.size()); - if (selected_row == -1) { - return QModelIndex(); - } - return m_childModel.index(selected_row, 0); + m_childModel->insertRows(dataList, 0, m_playlistList.size()); + return m_childModel->index(selectedRow, 0); } -void BasePlaylistFeature::updateChildModel(int selected_id) { +void BasePlaylistFeature::updateChildModel(int selectedId) { buildPlaylistList(); - - int row = 0; - for (QList >::const_iterator it = m_playlistList.begin(); - it != m_playlistList.end(); ++it, ++row) { - int playlist_id = it->first; - QString playlist_name = it->second; - - if (selected_id == playlist_id) { - TreeItem* item = m_childModel.getItem(indexFromPlaylistId(playlist_id)); - item->setData(playlist_name, QString::number(playlist_id)); - decorateChild(item, playlist_id); - } - + + QModelIndex index = indexFromPlaylistId(selectedId); + if (!index.isValid()) { + return; } + + TreeItem* item = m_childModel->getItem(index); + DEBUG_ASSERT_AND_HANDLE(item) { + return; + } + // Update the name + int pos = m_playlistList.indexOf(PlaylistItem(selectedId)); + if (pos < 0) { + return; + } + item->setData(m_playlistList[pos].name); + + decorateChild(item, selectedId); } - - -/** - * Clears the child model dynamically, but the invisible root item remains - */ -void BasePlaylistFeature::clearChildModel() { - m_childModel.removeRows(0, m_playlistList.size()); -} - -QModelIndex BasePlaylistFeature::indexFromPlaylistId(int playlistId) { - int row = 0; - for (QList >::const_iterator it = m_playlistList.begin(); - it != m_playlistList.end(); ++it, ++row) { - int current_id = it->first; - QString playlist_name = it->second; - - if (playlistId == current_id) { - return m_childModel.index(row, 0); - } +QModelIndex BasePlaylistFeature::indexFromPlaylistId(int playlistId) const { + int row = m_playlistList.indexOf(PlaylistItem(playlistId)); + if (row < 0) { + return QModelIndex(); } - return QModelIndex(); + + return m_childModel->index(row, 0); } void BasePlaylistFeature::slotTrackSelected(TrackPointer pTrack) { @@ -701,7 +796,7 @@ void BasePlaylistFeature::slotTrackSelected(TrackPointer pTrack) { } m_playlistDao.getPlaylistsTrackIsIn(trackId, &m_playlistsSelectedTrackIsIn); - TreeItem* rootItem = m_childModel.getItem(QModelIndex()); + TreeItem* rootItem = m_childModel->getItem(QModelIndex()); if (rootItem == nullptr) { return; } @@ -709,22 +804,16 @@ void BasePlaylistFeature::slotTrackSelected(TrackPointer pTrack) { // Set all playlists the track is in bold (or if there is no track selected, // clear all the bolding). int row = 0; - for (QList >::const_iterator it = m_playlistList.begin(); - it != m_playlistList.end(); ++it, ++row) { + for (const PlaylistItem& p : m_playlistList) { TreeItem* playlist = rootItem->child(row); if (playlist == nullptr) { continue; } - - int playlistId = it->first; - bool shouldBold = m_playlistsSelectedTrackIsIn.contains(playlistId); + + bool shouldBold = m_playlistsSelectedTrackIsIn.contains(p.id); playlist->setBold(shouldBold); + ++row; } - m_childModel.triggerRepaint(); -} - - -void BasePlaylistFeature::slotResetSelectedTrack() { - slotTrackSelected(TrackPointer()); + m_childModel->triggerRepaint(); } diff --git a/src/library/baseplaylistfeature.h b/src/library/features/baseplaylist/baseplaylistfeature.h similarity index 60% rename from src/library/baseplaylistfeature.h rename to src/library/features/baseplaylist/baseplaylistfeature.h index 4b8583cc22d..35c6b8cd773 100644 --- a/src/library/baseplaylistfeature.h +++ b/src/library/features/baseplaylist/baseplaylistfeature.h @@ -11,38 +11,40 @@ #include #include -#include "library/libraryfeature.h" #include "library/dao/playlistdao.h" #include "library/dao/trackdao.h" +#include "library/libraryfeature.h" #include "track/track.h" -class WLibrary; class KeyboardEventFilter; class PlaylistTableModel; class TrackCollection; class TreeItem; +class WLibraryPane; +class WLibraryStack; class BasePlaylistFeature : public LibraryFeature { Q_OBJECT public: - BasePlaylistFeature(QObject* parent, - UserSettingsPointer pConfig, - TrackCollection* pTrackCollection, - QString rootViewName); + BasePlaylistFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); virtual ~BasePlaylistFeature(); TreeItemModel* getChildModel(); - void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard); + QWidget* createPaneWidget(KeyboardEventFilter*pKeyboard, int paneId) override; signals: void showPage(const QUrl& page); void analyzeTracks(QList); public slots: - virtual void activate(); - virtual void activateChild(const QModelIndex& index); + void activate() override; + void activateChild(const QModelIndex& index) override; + void invalidateChild() override; + virtual void activatePlaylist(int playlistId); virtual void htmlLinkClicked(const QUrl& link); @@ -50,6 +52,7 @@ class BasePlaylistFeature : public LibraryFeature { virtual void slotPlaylistContentChanged(int playlistId) = 0; virtual void slotPlaylistTableRenamed(int playlistId, QString a_strName) = 0; void slotCreatePlaylist(); + void setFeaturePaneId(int focus); protected slots: void slotDeletePlaylist(); @@ -67,22 +70,43 @@ class BasePlaylistFeature : public LibraryFeature { void slotAnalyzePlaylist(); protected: + + struct PlaylistItem { + PlaylistItem() : id(-1) {} + PlaylistItem(int id) : id(id) {} + PlaylistItem(int id, QString name) : id(id), name(name) {} + + int id; + QString name; + + bool operator==(const PlaylistItem& other) { + return this->id == other.id; + } + }; + virtual QModelIndex constructChildModel(int selected_id); - virtual void updateChildModel(int selected_id); - virtual void clearChildModel(); + virtual void updateChildModel(int selectedId); virtual void buildPlaylistList() = 0; virtual void decorateChild(TreeItem *pChild, int playlist_id) = 0; virtual void addToAutoDJ(bool bTop); - - int playlistIdFromIndex(QModelIndex index); + QString getValidPlaylistName() const; + + QPointer getPlaylistTableModel(int paneId = -1); + virtual PlaylistTableModel* constructTableModel() = 0; + + virtual QSet playlistIdsFromIndex(const QModelIndex& index) const; + int playlistIdFromIndex(const QModelIndex& index) const; + void showTable(int paneId); + void showBrowse(int paneId); + // Get the QModelIndex of a playlist based on its id. Returns QModelIndex() // on failure. - QModelIndex indexFromPlaylistId(int playlistId); + virtual QModelIndex indexFromPlaylistId(int playlistId) const; - TrackCollection* m_pTrackCollection; PlaylistDAO &m_playlistDao; TrackDAO &m_trackDao; - PlaylistTableModel* m_pPlaylistTableModel; + QPointer m_pPlaylistTableModel; + QHash > m_playlistTableModel; QAction *m_pCreatePlaylistAction; QAction *m_pDeletePlaylistAction; QAction *m_pAddToAutoDJAction; @@ -95,20 +119,24 @@ class BasePlaylistFeature : public LibraryFeature { QAction *m_pExportTrackFilesAction; QAction *m_pDuplicatePlaylistAction; QAction *m_pAnalyzePlaylistAction; - QList > m_playlistList; + QList m_playlistList; QModelIndex m_lastRightClickedIndex; - TreeItemModel m_childModel; + TreeItemModel* m_childModel; TrackPointer m_pSelectedTrack; + + QHash m_lastChildClicked; private slots: void slotTrackSelected(TrackPointer pTrack); - void slotResetSelectedTrack(); private: virtual QString getRootViewHtml() const = 0; QSet m_playlistsSelectedTrackIsIn; - QString m_rootViewName; + + QHash > m_panes; + QHash m_browseIndexByPaneId; + QHash m_tableIndexByPaneId; }; #endif /* BASEPLAYLISTFEATURE_H */ diff --git a/src/library/browse/browsefeature.cpp b/src/library/features/browse/browsefeature.cpp similarity index 85% rename from src/library/browse/browsefeature.cpp rename to src/library/features/browse/browsefeature.cpp index 9f6231b78c0..80ca4c117b3 100644 --- a/src/library/browse/browsefeature.cpp +++ b/src/library/features/browse/browsefeature.cpp @@ -1,33 +1,31 @@ // browsefeature.cpp // Created 9/8/2009 by RJ Ryan (rryan@mit.edu) -#include -#include +#include +#include +#include #include -#include #include -#include -#include #include #include +#include -#include "track/track.h" -#include "library/treeitem.h" -#include "library/browse/browsefeature.h" -#include "library/trackcollection.h" -#include "widget/wlibrarytextbrowser.h" -#include "widget/wlibrary.h" #include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/browse/browsefeature.h" +#include "library/trackcollection.h" #include "util/sandbox.h" +#include "widget/wlibrarystack.h" +#include "widget/wlibrarytextbrowser.h" + const QString kQuickLinksSeparator = "-+-"; -BrowseFeature::BrowseFeature(QObject* parent, - UserSettingsPointer pConfig, +BrowseFeature::BrowseFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, TrackCollection* pTrackCollection, RecordingManager* pRecordingManager) - : LibraryFeature(parent), - m_pConfig(pConfig), + : LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), m_browseModel(this, pTrackCollection, pRecordingManager), m_proxyModel(&m_browseModel), m_pTrackCollection(pTrackCollection), @@ -55,6 +53,7 @@ BrowseFeature::BrowseFeature(QObject* parent, // The invisible root item of the child model TreeItem* rootItem = new TreeItem(); + rootItem->setLibraryFeature(this); m_pQuickLinkItem = new TreeItem(tr("Quick Links"), QUICK_LINK_NODE, this, rootItem); rootItem->appendChild(m_pQuickLinkItem); @@ -137,6 +136,14 @@ QVariant BrowseFeature::title() { return QVariant(tr("Browse")); } +QString BrowseFeature::getIconPath() { + return ":/images/library/ic_library_browse.png"; +} + +QString BrowseFeature::getSettingsName() const { + return "BrowseFeature"; +} + void BrowseFeature::slotAddQuickLink() { if (!m_pLastRightClickedItem) { return; @@ -199,46 +206,58 @@ void BrowseFeature::slotRemoveQuickLink() { saveQuickLinks(); } -QIcon BrowseFeature::getIcon() { - return QIcon(":/images/library/ic_library_browse.png"); -} - TreeItemModel* BrowseFeature::getChildModel() { return &m_childModel; } -void BrowseFeature::bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard) { - Q_UNUSED(keyboard); - WLibraryTextBrowser* edit = new WLibraryTextBrowser(libraryWidget); - edit->setHtml(getRootViewHtml()); - libraryWidget->registerView("BROWSEHOME", edit); +QWidget* BrowseFeature::createPaneWidget(KeyboardEventFilter* pKeyboard, + int paneId) { + WLibraryStack* pStack = new WLibraryStack(nullptr); + m_panes[paneId] = pStack; + + WLibraryTextBrowser* pEdit = new WLibraryTextBrowser(nullptr); + pEdit->setHtml(getRootViewHtml()); + pEdit->installEventFilter(pKeyboard); + m_idBrowse[paneId] = pStack->addWidget(pEdit); + + QWidget* pTable = LibraryFeature::createPaneWidget(pKeyboard, paneId); + pTable->setParent(pStack); + m_idTable[paneId] = pStack->addWidget(pTable); + + return pStack; } void BrowseFeature::activate() { - emit(switchToView("BROWSEHOME")); - emit(restoreSearch(QString())); - emit(enableCoverArtDisplay(false)); + if (m_lastClickedChild.isValid()) { + activateChild(m_lastClickedChild); + return; + } + + showBrowse(m_featurePane); + switchToFeature(); + showBreadCrumb(); + restoreSearch(QString()); + } // Note: This is executed whenever you single click on an child item // Single clicks will not populate sub folders void BrowseFeature::activateChild(const QModelIndex& index) { - TreeItem *item = static_cast(index.internalPointer()); - qDebug() << "BrowseFeature::activateChild " << item->data() << " " - << item->dataPath(); - - QString path = item->dataPath().toString(); - if (path == QUICK_LINK_NODE || path == DEVICE_NODE) { + m_lastClickedChild = index; + QString data = index.data().toString(); + QString dataPath = index.data(AbstractRole::RoleDataPath).toString(); + qDebug() << "BrowseFeature::activateChild " << data << dataPath; + + if (dataPath == QUICK_LINK_NODE || dataPath == DEVICE_NODE) { m_browseModel.setPath(MDir()); } else { // Open a security token for this path and if we do not have access, ask // for it. - MDir dir(path); + MDir dir(dataPath); if (!dir.canAccess()) { - if (Sandbox::askForAccess(path)) { + if (Sandbox::askForAccess(dataPath)) { // Re-create to get a new token. - dir = MDir(path); + dir = MDir(dataPath); } else { // TODO(rryan): Activate an info page about sandboxing? return; @@ -246,8 +265,12 @@ void BrowseFeature::activateChild(const QModelIndex& index) { } m_browseModel.setPath(dir); } - emit(showTrackModel(&m_proxyModel)); - emit(enableCoverArtDisplay(false)); + + showTable(m_featurePane); + showTrackModel(&m_proxyModel); + QString bread = index.data(AbstractRole::RoleBreadCrumb).toString(); + showBreadCrumb(bread, getIcon()); + } void BrowseFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index) { @@ -465,3 +488,19 @@ QStringList BrowseFeature::getDefaultQuickLinks() const { qDebug() << "Default quick links:" << result; return result; } + +void BrowseFeature::showBrowse(int paneId) { + auto it = m_panes.find(paneId); + auto itId = m_idBrowse.find(paneId); + if (it != m_panes.end() && !it->isNull() && itId != m_idBrowse.end()) { + (*it)->setCurrentIndex(*itId); + } +} + +void BrowseFeature::showTable(int paneId) { + auto itId = m_idTable.find(paneId); + auto it = m_panes.find(paneId); + if (it != m_panes.end() && !it->isNull() && itId != m_idTable.end()) { + (*it)->setCurrentIndex(*itId); + } +} diff --git a/src/library/browse/browsefeature.h b/src/library/features/browse/browsefeature.h similarity index 73% rename from src/library/browse/browsefeature.h rename to src/library/features/browse/browsefeature.h index 94188ee6b0d..d839b43174b 100644 --- a/src/library/browse/browsefeature.h +++ b/src/library/features/browse/browsefeature.h @@ -8,14 +8,15 @@ #include #include #include +#include #include #include #include #include #include "preferences/usersettings.h" -#include "library/browse/browsetablemodel.h" -#include "library/browse/foldertreemodel.h" +#include "library/features/browse/browsetablemodel.h" +#include "library/features/browse/foldertreemodel.h" #include "library/libraryfeature.h" #include "library/proxytrackmodel.h" @@ -23,21 +24,23 @@ #define DEVICE_NODE "::mixxx_device_node::" class TrackCollection; +class WLibraryStack; class BrowseFeature : public LibraryFeature { Q_OBJECT public: - BrowseFeature(QObject* parent, - UserSettingsPointer pConfig, + BrowseFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, TrackCollection* pTrackCollection, RecordingManager* pRec); virtual ~BrowseFeature(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; - void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard); + QWidget* createPaneWidget(KeyboardEventFilter*pKeyboard, int paneId) override; TreeItemModel* getChildModel(); @@ -63,8 +66,9 @@ class BrowseFeature : public LibraryFeature { QStringList getDefaultQuickLinks() const; void saveQuickLinks(); void loadQuickLinks(); + void showBrowse(int paneId); + void showTable(int paneId); - UserSettingsPointer m_pConfig; BrowseTableModel m_browseModel; ProxyTrackModel m_proxyModel; TrackCollection* m_pTrackCollection; @@ -75,6 +79,11 @@ class BrowseFeature : public LibraryFeature { TreeItem* m_pLastRightClickedItem; TreeItem* m_pQuickLinkItem; QStringList m_quickLinkList; + QModelIndex m_lastClickedChild; + + QHash > m_panes; + QHash m_idBrowse; + QHash m_idTable; }; #endif // BROWSEFEATURE_H diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/features/browse/browsetablemodel.cpp similarity index 99% rename from src/library/browse/browsetablemodel.cpp rename to src/library/features/browse/browsetablemodel.cpp index 93f3ae3ff6f..11b250dcee7 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/features/browse/browsetablemodel.cpp @@ -5,8 +5,8 @@ #include #include -#include "library/browse/browsetablemodel.h" -#include "library/browse/browsethread.h" +#include "library/features/browse/browsetablemodel.h" +#include "library/features/browse/browsethread.h" #include "library/previewbuttondelegate.h" #include "mixer/playerinfo.h" #include "control/controlobject.h" diff --git a/src/library/browse/browsetablemodel.h b/src/library/features/browse/browsetablemodel.h similarity index 98% rename from src/library/browse/browsetablemodel.h rename to src/library/features/browse/browsetablemodel.h index e31135c0ded..fd1ce20d52b 100644 --- a/src/library/browse/browsetablemodel.h +++ b/src/library/features/browse/browsetablemodel.h @@ -8,7 +8,7 @@ #include "library/trackcollection.h" #include "recording/recordingmanager.h" #include "util/file.h" -#include "library/browse/browsethread.h" +#include "library/features/browse/browsethread.h" //constants const int COLUMN_PREVIEW = 0; diff --git a/src/library/browse/browsethread.cpp b/src/library/features/browse/browsethread.cpp similarity index 99% rename from src/library/browse/browsethread.cpp rename to src/library/features/browse/browsethread.cpp index 56afcc24a0f..605742bf516 100644 --- a/src/library/browse/browsethread.cpp +++ b/src/library/features/browse/browsethread.cpp @@ -7,7 +7,7 @@ #include #include -#include "library/browse/browsetablemodel.h" +#include "library/features/browse/browsetablemodel.h" #include "sources/soundsourceproxy.h" #include "track/trackmetadata.h" #include "util/trace.h" diff --git a/src/library/browse/browsethread.h b/src/library/features/browse/browsethread.h similarity index 100% rename from src/library/browse/browsethread.h rename to src/library/features/browse/browsethread.h diff --git a/src/library/browse/foldertreemodel.cpp b/src/library/features/browse/foldertreemodel.cpp similarity index 94% rename from src/library/browse/foldertreemodel.cpp rename to src/library/features/browse/foldertreemodel.cpp index 62b28afc412..49b6a9c23e7 100644 --- a/src/library/browse/foldertreemodel.cpp +++ b/src/library/features/browse/foldertreemodel.cpp @@ -12,9 +12,8 @@ #include -#include "library/treeitem.h" -#include "library/browse/foldertreemodel.h" -#include "library/browse/browsefeature.h" +#include "library/features/browse/foldertreemodel.h" +#include "library/features/browse/browsefeature.h" #include "util/file.h" FolderTreeModel::FolderTreeModel(QObject *parent) @@ -33,11 +32,19 @@ FolderTreeModel::~FolderTreeModel() { * is only called if necessary. */ bool FolderTreeModel::hasChildren(const QModelIndex& parent) const { + if (!parent.isValid()) { + return true; + } + TreeItem *item = static_cast(parent.internalPointer()); /* Usually the child count is 0 becuase we do lazy initalization * However, for, buid-in items such as 'Quick Links' there exist * child items at init time */ + if (item == nullptr) { + return false; + } + if(item->dataPath().toString() == QUICK_LINK_NODE) return true; //Can only happen on Windows diff --git a/src/library/browse/foldertreemodel.h b/src/library/features/browse/foldertreemodel.h similarity index 94% rename from src/library/browse/foldertreemodel.h rename to src/library/features/browse/foldertreemodel.h index 240e26b4472..6bac312ce0e 100644 --- a/src/library/browse/foldertreemodel.h +++ b/src/library/features/browse/foldertreemodel.h @@ -8,9 +8,7 @@ #include #include "library/treeitemmodel.h" -#include "library/treeitem.h" -class TreeItem; // This class represents a folder item within the Browse Feature // The class is derived from TreeItemModel to support lazy model // initialization. diff --git a/src/library/cratefeature.cpp b/src/library/features/crates/cratefeature.cpp similarity index 80% rename from src/library/cratefeature.cpp rename to src/library/features/crates/cratefeature.cpp index dd884c6ea52..578a4029c9c 100644 --- a/src/library/cratefeature.cpp +++ b/src/library/features/crates/cratefeature.cpp @@ -7,31 +7,33 @@ #include #include -#include "library/cratefeature.h" +#include "library/features/crates/cratefeature.h" + +#include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/crates/cratetablemodel.h" #include "library/export/trackexportwizard.h" +#include "library/library.h" +#include "library/parsercsv.h" #include "library/parser.h" #include "library/parserm3u.h" #include "library/parserpls.h" -#include "library/parsercsv.h" - -#include "library/cratetablemodel.h" -#include "library/trackcollection.h" #include "library/queryutil.h" -#include "widget/wlibrarytextbrowser.h" -#include "widget/wlibrary.h" -#include "controllers/keyboard/keyboardeventfilter.h" -#include "treeitem.h" +#include "library/trackcollection.h" #include "sources/soundsourceproxy.h" #include "util/dnd.h" #include "util/duration.h" +#include "util/time.h" +#include "widget/wlibrarystack.h" +#include "widget/wlibrarytextbrowser.h" -CrateFeature::CrateFeature(Library* pLibrary, - TrackCollection* pTrackCollection, - UserSettingsPointer pConfig) - : LibraryFeature(pConfig), +CrateFeature::CrateFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), m_pTrackCollection(pTrackCollection), m_crateDao(pTrackCollection->getCrateDAO()), - m_crateTableModel(this, pTrackCollection) { + m_pCrateTableModel(nullptr) { m_pCreateCrateAction = new QAction(tr("Create New Crate"),this); connect(m_pCreateCrateAction, SIGNAL(triggered()), @@ -94,14 +96,13 @@ CrateFeature::CrateFeature(Library* pLibrary, this, SLOT(slotCrateTableChanged(int))); // construct child model - TreeItem *rootItem = new TreeItem(); - m_childModel.setRootItem(rootItem); + TreeItem *pRootItem = new TreeItem(); + pRootItem->setLibraryFeature(this); + m_childModel.setRootItem(pRootItem); constructChildModel(-1); connect(pLibrary, SIGNAL(trackSelected(TrackPointer)), this, SLOT(slotTrackSelected(TrackPointer))); - connect(pLibrary, SIGNAL(switchToView(const QString&)), - this, SLOT(slotResetSelectedTrack())); } CrateFeature::~CrateFeature() { @@ -121,31 +122,34 @@ QVariant CrateFeature::title() { return tr("Crates"); } -QIcon CrateFeature::getIcon() { - return QIcon(":/images/library/ic_library_crates.png"); +QString CrateFeature::getIconPath() { + return ":/images/library/ic_library_crates.png"; } -int CrateFeature::crateIdFromIndex(QModelIndex index) { - TreeItem* item = static_cast(index.internalPointer()); - if (item == NULL) { - return -1; - } +QString CrateFeature::getSettingsName() const { + return "CrateFeature"; +} - QString dataPath = item->dataPath().toString(); +bool CrateFeature::isSinglePane() const { + return false; +} + +int CrateFeature::crateIdFromIndex(QModelIndex index) { bool ok = false; - int playlistId = dataPath.toInt(&ok); - if (!ok) { - return -1; - } - return playlistId; + int crateId = index.data(AbstractRole::RoleDataPath).toInt(&ok); + return ok ? crateId : -1; } +bool CrateFeature::dragMoveAccept(QUrl url) { + return SoundSourceProxy::isUrlSupported(url) || + Parser::isPlaylistFilenameSupported(url.toLocalFile()); +} + + bool CrateFeature::dropAcceptChild(const QModelIndex& index, QList urls, QObject* pSource) { int crateId = crateIdFromIndex(index); - if (crateId == -1) { - return false; - } + QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); QList trackIds; if (pSource) { @@ -155,38 +159,59 @@ bool CrateFeature::dropAcceptChild(const QModelIndex& index, QList urls, // Adds track, does not insert duplicates, handles unremoving logic. trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files, true); } - qDebug() << "CrateFeature::dropAcceptChild adding tracks" - << trackIds.size() << " to crate "<< crateId; + //qDebug() << "CrateFeature::dropAcceptChild adding tracks" + // << trackIds.size() << " to crate "<< crateId; // remove tracks that could not be added for (int trackIdIndex = 0; trackIdIndex < trackIds.size(); ++trackIdIndex) { if (!trackIds.at(trackIdIndex).isValid()) { trackIds.removeAt(trackIdIndex--); } } + + // Request a name for the crate if it's a new crate + if (crateId < 0) { + QString name = getValidCrateName(); + if (name.isNull()) { + return false; + } + + crateId = m_crateDao.createCrate(name); + // An error happened + if (crateId < 0) { + return false; + } + } + m_crateDao.addTracksToCrate(crateId, &trackIds); return true; } bool CrateFeature::dragMoveAcceptChild(const QModelIndex& index, QUrl url) { int crateId = crateIdFromIndex(index); - if (crateId == -1) { - return false; - } bool locked = m_crateDao.isCrateLocked(crateId); bool formatSupported = SoundSourceProxy::isUrlSupported(url) || Parser::isPlaylistFilenameSupported(url.toLocalFile()); return !locked && formatSupported; } -void CrateFeature::bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard) { - Q_UNUSED(keyboard); - WLibraryTextBrowser* edit = new WLibraryTextBrowser(libraryWidget); - edit->setHtml(getRootViewHtml()); - edit->setOpenLinks(false); - connect(edit, SIGNAL(anchorClicked(const QUrl)), +QWidget* CrateFeature::createPaneWidget(KeyboardEventFilter *pKeyboard, + int paneId) { + WLibraryStack* pContainer = new WLibraryStack(nullptr); + m_panes[paneId] = pContainer; + + WLibraryTextBrowser* pEdit = new WLibraryTextBrowser(pContainer); + pEdit->setHtml(getRootViewHtml()); + pEdit->setOpenLinks(false); + pEdit->installEventFilter(pKeyboard); + connect(pEdit, SIGNAL(anchorClicked(const QUrl)), this, SLOT(htmlLinkClicked(const QUrl))); - libraryWidget->registerView("CRATEHOME", edit); + + m_idBrowse[paneId] = pContainer->addWidget(pEdit); + + QWidget* pTable = LibraryFeature::createPaneWidget(pKeyboard, paneId); + m_idTable[paneId] = pContainer->addWidget(pTable); + + return pContainer; } TreeItemModel* CrateFeature::getChildModel() { @@ -194,30 +219,49 @@ TreeItemModel* CrateFeature::getChildModel() { } void CrateFeature::activate() { - emit(switchToView("CRATEHOME")); - emit(restoreSearch(QString())); //disable search on crate home - emit(enableCoverArtDisplay(true)); + adoptPreselectedPane(); + + auto modelIt = m_lastClickedIndex.constFind(m_featurePane); + if (modelIt != m_lastClickedIndex.constEnd() && (*modelIt).isValid()) { + activateChild(*modelIt); + return; + } + + showBrowse(m_featurePane); + switchToFeature(); + showBreadCrumb(); + restoreSearch(QString()); //disable search on crate home } void CrateFeature::activateChild(const QModelIndex& index) { - if (!index.isValid()) - return; + adoptPreselectedPane(); + + m_lastClickedIndex[m_featurePane] = index; int crateId = crateIdFromIndex(index); if (crateId == -1) { return; } - m_crateTableModel.setTableModel(crateId); - emit(showTrackModel(&m_crateTableModel)); - emit(enableCoverArtDisplay(true)); + + m_pCrateTableModel = getTableModel(m_featurePane); + m_pCrateTableModel->setTableModel(crateId); + showTable(m_featurePane); + restoreSearch(""); + showBreadCrumb(index); + showTrackModel(m_pCrateTableModel); +} + +void CrateFeature::invalidateChild() { + m_lastClickedIndex.clear(); } void CrateFeature::activateCrate(int crateId) { //qDebug() << "CrateFeature::activateCrate()" << crateId; + m_pCrateTableModel = getTableModel(m_featurePane); + QModelIndex index = indexFromCrateId(crateId); if (crateId != -1 && index.isValid()) { - m_crateTableModel.setTableModel(crateId); - emit(showTrackModel(&m_crateTableModel)); - emit(enableCoverArtDisplay(true)); + m_pCrateTableModel->setTableModel(crateId); + showTrackModel(m_pCrateTableModel); // Update selection emit(featureSelect(this, m_lastRightClickedIndex)); activateChild(m_lastRightClickedIndex); @@ -227,20 +271,17 @@ void CrateFeature::activateCrate(int crateId) { void CrateFeature::onRightClick(const QPoint& globalPos) { m_lastRightClickedIndex = QModelIndex(); - QMenu menu(NULL); + QMenu menu(nullptr); menu.addAction(m_pCreateCrateAction); menu.addSeparator(); menu.addAction(m_pCreateImportPlaylistAction); menu.exec(globalPos); } -void CrateFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index) { +void CrateFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex& index) { //Save the model index so we can get it in the action slots... m_lastRightClickedIndex = index; int crateId = crateIdFromIndex(index); - if (crateId == -1) { - return; - } bool locked = m_crateDao.isCrateLocked(crateId); @@ -252,52 +293,33 @@ void CrateFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index) m_pLockCrateAction->setText(locked ? tr("Unlock") : tr("Lock")); - QMenu menu(NULL); + QMenu menu(nullptr); menu.addAction(m_pCreateCrateAction); menu.addSeparator(); - menu.addAction(m_pRenameCrateAction); - menu.addAction(m_pDuplicateCrateAction); - menu.addAction(m_pDeleteCrateAction); - menu.addAction(m_pLockCrateAction); - menu.addSeparator(); - menu.addAction(m_pAutoDjTrackSource); - menu.addSeparator(); - menu.addAction(m_pAnalyzeCrateAction); - menu.addSeparator(); + if (crateId >= 0) { + menu.addAction(m_pRenameCrateAction); + menu.addAction(m_pDuplicateCrateAction); + menu.addAction(m_pDeleteCrateAction); + menu.addAction(m_pLockCrateAction); + menu.addSeparator(); + menu.addAction(m_pAutoDjTrackSource); + menu.addSeparator(); + menu.addAction(m_pAnalyzeCrateAction); + menu.addSeparator(); + } menu.addAction(m_pImportPlaylistAction); - menu.addAction(m_pExportPlaylistAction); - menu.addAction(m_pExportTrackFilesAction); + if (crateId >= 0) { + menu.addAction(m_pExportPlaylistAction); + menu.addAction(m_pExportTrackFilesAction); + } menu.exec(globalPos); } void CrateFeature::slotCreateCrate() { - QString name; - bool validNameGiven = false; - - while (!validNameGiven) { - bool ok = false; - name = QInputDialog::getText(NULL, - tr("Create New Crate"), - tr("Enter name for new crate:"), - QLineEdit::Normal, tr("New Crate"), - &ok).trimmed(); - - if (!ok) - return; - - int existingId = m_crateDao.getCrateIdByName(name); - - if (existingId != -1) { - QMessageBox::warning(NULL, - tr("Creating Crate Failed"), - tr("A crate by that name already exists.")); - } else if (name.isEmpty()) { - QMessageBox::warning(NULL, - tr("Creating Crate Failed"), - tr("A crate cannot have a blank name.")); - } else { - validNameGiven = true; - } + QString name = getValidCrateName(); + if (name.isNull()) { + // The user canceled + return; } int crateId = m_crateDao.createCrate(name); @@ -305,7 +327,7 @@ void CrateFeature::slotCreateCrate() { activateCrate(crateId); } else { qDebug() << "Error creating crate with name " << name; - QMessageBox::warning(NULL, + QMessageBox::warning(nullptr, tr("Creating Crate Failed"), tr("An unknown error occurred while creating crate: ") + name); @@ -327,6 +349,19 @@ void CrateFeature::slotDeleteCrate() { bool deleted = m_crateDao.deleteCrate(crateId); if (deleted) { + // This avoids a bug where the m_lastChildClicked index is still a valid + // index but it's not true since we just deleted it + for (auto it = m_crateTableModel.begin(); + it != m_crateTableModel.end(); ++it) { + if ((*it)->getCrate() == crateId) { + // Show the browse widget, this avoids a problem when the same + // playlist is shown twice and gets deleted. One of the panes + // gets still showing the unexisting crate. + m_lastClickedIndex[it.key()] = QModelIndex(); + showBrowse(it.key()); + } + } + activate(); } else { qDebug() << "Failed to delete crateId" << crateId; @@ -574,15 +609,17 @@ void CrateFeature::clearChildModel() { void CrateFeature::slotImportPlaylist() { //qDebug() << "slotImportPlaylist() row:" ; //<< m_lastRightClickedIndex.data(); - QString playlist_file = getPlaylistFile(); - if (playlist_file.isEmpty()) return; + QString playlistFile = getPlaylistFile(); + if (playlistFile.isEmpty()) { + return; + } // Update the import/export crate directory - QFileInfo fileName(playlist_file); + QFileInfo fileName(playlistFile); m_pConfig->set(ConfigKey("[Library]","LastImportExportCrateDirectory"), ConfigValue(fileName.dir().absolutePath())); - slotImportPlaylistFile(playlist_file); + slotImportPlaylistFile(playlistFile); activateChild(m_lastRightClickedIndex); } @@ -611,7 +648,7 @@ void CrateFeature::slotImportPlaylistFile(const QString &playlist_file) { //qDebug() << "Size of Imported Playlist: " << entries.size(); //Iterate over the List that holds URLs of playlist entires - m_crateTableModel.addTracks(QModelIndex(), entries); + m_pCrateTableModel->addTracks(QModelIndex(), entries); //delete the parser object delete playlist_parser; @@ -659,7 +696,7 @@ void CrateFeature::slotCreateImportCrate() { lastCrateId = m_crateDao.createCrate(name); if (lastCrateId != -1) { - m_crateTableModel.setTableModel(lastCrateId); + m_pCrateTableModel->setTableModel(lastCrateId); } else { QMessageBox::warning(NULL, @@ -685,7 +722,7 @@ void CrateFeature::slotAnalyzeCrate() { } void CrateFeature::slotExportPlaylist() { - int crateId = m_crateTableModel.getCrate(); + int crateId = m_pCrateTableModel->getCrate(); QString crateName = m_crateDao.crateName(crateId); qDebug() << "Export crate" << crateId << crateName; @@ -723,7 +760,7 @@ void CrateFeature::slotExportPlaylist() { // Create a new table model since the main one might have an active search. QScopedPointer pCrateTableModel( new CrateTableModel(this, m_pTrackCollection)); - pCrateTableModel->setTableModel(m_crateTableModel.getCrate()); + pCrateTableModel->setTableModel(m_pCrateTableModel->getCrate()); pCrateTableModel->select(); if (file_location.endsWith(".csv", Qt::CaseInsensitive)) { @@ -735,8 +772,8 @@ void CrateFeature::slotExportPlaylist() { QList playlist_items; int rows = pCrateTableModel->rowCount(); for (int i = 0; i < rows; ++i) { - QModelIndex index = m_crateTableModel.index(i, 0); - playlist_items << m_crateTableModel.getTrackLocation(index); + QModelIndex index = m_pCrateTableModel->index(i, 0); + playlist_items << m_pCrateTableModel->getTrackLocation(index); } if (file_location.endsWith(".pls", Qt::CaseInsensitive)) { @@ -760,18 +797,18 @@ void CrateFeature::slotExportTrackFiles() { // Create a new table model since the main one might have an active search. QScopedPointer pCrateTableModel( new CrateTableModel(this, m_pTrackCollection)); - pCrateTableModel->setTableModel(m_crateTableModel.getCrate()); + pCrateTableModel->setTableModel(m_pCrateTableModel->getCrate()); pCrateTableModel->select(); int rows = pCrateTableModel->rowCount(); QList trackpointers; for (int i = 0; i < rows; ++i) { - QModelIndex index = m_crateTableModel.index(i, 0); - trackpointers.push_back(m_crateTableModel.getTrack(index)); + QModelIndex index = m_pCrateTableModel->index(i, 0); + trackpointers.push_back(m_pCrateTableModel->getTrack(index)); } - TrackExportWizard track_export(nullptr, m_pConfig, trackpointers); - track_export.exportTracks(); + TrackExportWizard trackExport(nullptr, m_pConfig, trackpointers); + trackExport.exportTracks(); } void CrateFeature::slotCrateTableChanged(int crateId) { @@ -851,6 +888,39 @@ void CrateFeature::slotResetSelectedTrack() { slotTrackSelected(TrackPointer()); } +QString CrateFeature::getValidCrateName() { + QString name; + bool validNameGiven = false; + + while (!validNameGiven) { + bool ok = false; + name = QInputDialog::getText(nullptr, + tr("Create New Crate"), + tr("Enter name for new crate:"), + QLineEdit::Normal, tr("New Crate"), + &ok).trimmed(); + + if (!ok) { + return QString(); + } + + int existingId = m_crateDao.getCrateIdByName(name); + + if (existingId != -1) { + QMessageBox::warning(nullptr, + tr("Creating Crate Failed"), + tr("A crate by that name already exists.")); + } else if (name.isEmpty()) { + QMessageBox::warning(nullptr, + tr("Creating Crate Failed"), + tr("A crate cannot have a blank name.")); + } else { + validNameGiven = true; + } + } + return name; +} + QModelIndex CrateFeature::indexFromCrateId(int crateId) { int row = 0; for (QList >::const_iterator it = m_crateList.begin(); @@ -864,3 +934,28 @@ QModelIndex CrateFeature::indexFromCrateId(int crateId) { } return QModelIndex(); } + +QPointer CrateFeature::getTableModel(int paneId) { + auto it = m_crateTableModel.find(paneId); + if (it == m_crateTableModel.end() || it->isNull()) { + it = m_crateTableModel.insert(paneId, + new CrateTableModel(this, m_pTrackCollection)); + } + return *it; +} + +void CrateFeature::showBrowse(int paneId) { + auto it = m_panes.find(paneId); + auto itId = m_idBrowse.find(paneId); + if (it != m_panes.end() && !it->isNull() && itId != m_idBrowse.end()) { + (*it)->setCurrentIndex(*itId); + } +} + +void CrateFeature::showTable(int paneId) { + auto it = m_panes.find(paneId); + auto itId = m_idTable.find(paneId); + if (it != m_panes.end() && !it->isNull() && itId != m_idTable.end()) { + (*it)->setCurrentIndex(*itId); + } +} diff --git a/src/library/cratefeature.h b/src/library/features/crates/cratefeature.h similarity index 56% rename from src/library/cratefeature.h rename to src/library/features/crates/cratefeature.h index 495b74ad7c3..85fce0a7dab 100644 --- a/src/library/cratefeature.h +++ b/src/library/features/crates/cratefeature.h @@ -10,46 +10,53 @@ #include #include #include +#include #include "library/libraryfeature.h" -#include "library/cratetablemodel.h" -#include "library/library.h" - -#include "treeitemmodel.h" +#include "library/treeitemmodel.h" #include "preferences/usersettings.h" #include "track/track.h" class TrackCollection; +class TreeItemModel; +class CrateTableModel; +class CrateDAO; class CrateFeature : public LibraryFeature { Q_OBJECT public: - CrateFeature(Library* pLibrary, - TrackCollection* pTrackCollection, - UserSettingsPointer pConfig); + CrateFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); virtual ~CrateFeature(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; + bool isSinglePane() const override; + + void onSearch(QString&) {} + bool dragMoveAccept(QUrl url); bool dropAcceptChild(const QModelIndex& index, QList urls, QObject* pSource); bool dragMoveAcceptChild(const QModelIndex& index, QUrl url); - void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard); - + QWidget* createPaneWidget(KeyboardEventFilter* pKeyboard, int paneId) override; + TreeItemModel* getChildModel(); signals: void analyzeTracks(QList); public slots: - void activate(); - void activateChild(const QModelIndex& index); + void activate() override; + void activateChild(const QModelIndex& index) override; + void invalidateChild() override; void activateCrate(int crateId); - void onRightClick(const QPoint& globalPos); - void onRightClickChild(const QPoint& globalPos, QModelIndex index); + void onRightClick(const QPoint& globalPos) override; + void onRightClickChild(const QPoint& globalPos, const QModelIndex& index) override; void slotCreateCrate(); void slotDeleteCrate(); @@ -74,6 +81,7 @@ class CrateFeature : public LibraryFeature { void slotResetSelectedTrack(); private: + QString getValidCrateName(); QString getRootViewHtml() const; QModelIndex constructChildModel(int selected_id); void updateChildModel(int selected_id); @@ -83,26 +91,35 @@ class CrateFeature : public LibraryFeature { // Get the QModelIndex of a crate based on its id. Returns QModelIndex() // on failure. QModelIndex indexFromCrateId(int crateId); + + QPointer getTableModel(int paneId); + void showBrowse(int paneId); + void showTable(int paneId); TrackCollection* m_pTrackCollection; CrateDAO& m_crateDao; - QAction *m_pCreateCrateAction; - QAction *m_pDeleteCrateAction; - QAction *m_pRenameCrateAction; - QAction *m_pLockCrateAction; - QAction *m_pDuplicateCrateAction; - QAction *m_pAutoDjTrackSource; - QAction *m_pImportPlaylistAction; - QAction *m_pCreateImportPlaylistAction; - QAction *m_pExportPlaylistAction; - QAction *m_pExportTrackFilesAction; - QAction *m_pAnalyzeCrateAction; + QAction* m_pCreateCrateAction; + QAction* m_pDeleteCrateAction; + QAction* m_pRenameCrateAction; + QAction* m_pLockCrateAction; + QAction* m_pDuplicateCrateAction; + QAction* m_pAutoDjTrackSource; + QAction* m_pImportPlaylistAction; + QAction* m_pCreateImportPlaylistAction; + QAction* m_pExportPlaylistAction; + QAction* m_pExportTrackFilesAction; + QAction* m_pAnalyzeCrateAction; QList > m_crateList; - CrateTableModel m_crateTableModel; + QHash > m_crateTableModel; + CrateTableModel* m_pCrateTableModel; QModelIndex m_lastRightClickedIndex; TreeItemModel m_childModel; TrackPointer m_pSelectedTrack; QSet m_cratesSelectedTrackIsIn; + QHash > m_panes; + QHash m_idBrowse; + QHash m_idTable; + QHash m_lastClickedIndex; }; #endif /* CRATEFEATURE_H */ diff --git a/src/library/cratetablemodel.cpp b/src/library/features/crates/cratetablemodel.cpp similarity index 99% rename from src/library/cratetablemodel.cpp rename to src/library/features/crates/cratetablemodel.cpp index 7c2988a262a..b7525b3f8b0 100644 --- a/src/library/cratetablemodel.cpp +++ b/src/library/features/crates/cratetablemodel.cpp @@ -4,7 +4,7 @@ #include -#include "library/cratetablemodel.h" +#include "library/features/crates/cratetablemodel.h" #include "library/queryutil.h" #include "library/trackcollection.h" #include "mixer/playermanager.h" diff --git a/src/library/cratetablemodel.h b/src/library/features/crates/cratetablemodel.h similarity index 100% rename from src/library/cratetablemodel.h rename to src/library/features/crates/cratetablemodel.h diff --git a/src/library/setlogfeature.cpp b/src/library/features/history/historyfeature.cpp similarity index 56% rename from src/library/setlogfeature.cpp rename to src/library/features/history/historyfeature.cpp index 01e2ba7919d..019b9240bab 100644 --- a/src/library/setlogfeature.cpp +++ b/src/library/features/history/historyfeature.cpp @@ -2,26 +2,26 @@ #include #include -#include "library/setlogfeature.h" - #include "control/controlobject.h" -#include "library/playlisttablemodel.h" +#include "library/features/history/historytreemodel.h" +#include "library/features/playlist/playlisttablemodel.h" +#include "library/queryutil.h" #include "library/trackcollection.h" -#include "library/treeitem.h" #include "mixer/playerinfo.h" #include "mixer/playermanager.h" +#include "widget/wlibrarysidebar.h" + +#include "library/features/history/historyfeature.h" -SetlogFeature::SetlogFeature(QObject* parent, - UserSettingsPointer pConfig, +HistoryFeature::HistoryFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, TrackCollection* pTrackCollection) - : BasePlaylistFeature(parent, pConfig, pTrackCollection, "SETLOGHOME"), + : BasePlaylistFeature(pConfig, pLibrary, parent, pTrackCollection), m_playlistId(-1) { - m_pPlaylistTableModel = new PlaylistTableModel(this, pTrackCollection, - "mixxx.db.model.setlog", - true); //show all tracks - m_pJoinWithPreviousAction = new QAction(tr("Join with previous"), this); - connect(m_pJoinWithPreviousAction, SIGNAL(triggered()), - this, SLOT(slotJoinWithPrevious())); + m_pJoinWithNextAction = new QAction(tr("Join with next"), this); + connect(m_pJoinWithNextAction, SIGNAL(triggered()), + this, SLOT(slotJoinWithNext())); m_pGetNewPlaylist = new QAction(tr("Create new history playlist"), this); connect(m_pGetNewPlaylist, SIGNAL(triggered()), this, SLOT(slotGetNewPlaylist())); @@ -30,12 +30,15 @@ SetlogFeature::SetlogFeature(QObject* parent, emit(slotGetNewPlaylist()); //construct child model - TreeItem *rootItem = new TreeItem(); - m_childModel.setRootItem(rootItem); + delete m_childModel; + m_childModel = m_pHistoryTreeModel = new HistoryTreeModel(this, m_pTrackCollection); constructChildModel(-1); + + connect(&PlayerInfo::instance(), SIGNAL(currentPlayingTrackChanged(TrackPointer)), + this, SLOT(slotPlayingTrackChanged(TrackPointer))); } -SetlogFeature::~SetlogFeature() { +HistoryFeature::~HistoryFeature() { // If the history playlist we created doesn't have any tracks in it then // delete it so we don't end up with tons of empty playlists. This is mostly // for developers since they regularly open Mixxx without loading a track. @@ -45,24 +48,19 @@ SetlogFeature::~SetlogFeature() { } } -QVariant SetlogFeature::title() { +QVariant HistoryFeature::title() { return tr("History"); } -QIcon SetlogFeature::getIcon() { - return QIcon(":/images/library/ic_library_history.png"); +QString HistoryFeature::getIconPath() { + return ":/images/library/ic_library_history.png"; } -void SetlogFeature::bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard) { - BasePlaylistFeature::bindWidget(libraryWidget, - keyboard); - connect(&PlayerInfo::instance(), SIGNAL(currentPlayingTrackChanged(TrackPointer)), - this, SLOT(slotPlayingTrackChanged(TrackPointer))); +QString HistoryFeature::getSettingsName() const { + return "HistoryFeature"; } -void SetlogFeature::onRightClick(const QPoint& globalPos) { - Q_UNUSED(globalPos); +void HistoryFeature::onRightClick(const QPoint&) { m_lastRightClickedIndex = QModelIndex(); // Create the right-click menu @@ -72,37 +70,37 @@ void SetlogFeature::onRightClick(const QPoint& globalPos) { // menu.exec(globalPos); } -void SetlogFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index) { +void HistoryFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex &index) { //Save the model index so we can get it in the action slots... - m_lastRightClickedIndex = index; - QString playlistName = index.data().toString(); - int playlistId = m_playlistDao.getPlaylistIdFromName(playlistName); + m_lastChildClicked[m_featurePane] = m_lastRightClickedIndex = index; + bool ok; + int playlistId = index.data(AbstractRole::RoleDataPath).toInt(&ok); + if (!ok || playlistId < 0) { + return; + } bool locked = m_playlistDao.isPlaylistLocked(playlistId); m_pDeletePlaylistAction->setEnabled(!locked); m_pRenamePlaylistAction->setEnabled(!locked); - m_pJoinWithPreviousAction->setEnabled(!locked); + m_pJoinWithNextAction->setEnabled(!locked); m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock")); //Create the right-click menu - QMenu menu(NULL); + QMenu menu(nullptr); //menu.addAction(m_pCreatePlaylistAction); //menu.addSeparator(); menu.addAction(m_pAddToAutoDJAction); menu.addAction(m_pAddToAutoDJTopAction); menu.addAction(m_pRenamePlaylistAction); + menu.addAction(m_pJoinWithNextAction); if (playlistId != m_playlistId) { // Todays playlist should not be locked or deleted menu.addAction(m_pDeletePlaylistAction); menu.addAction(m_pLockPlaylistAction); } - if (index.row() > 0) { - // The very first setlog cannot be joint - menu.addAction(m_pJoinWithPreviousAction); - } if (playlistId == m_playlistId && m_playlistDao.tracksInPlaylist(m_playlistId) != 0) { // Todays playlists can change ! menu.addAction(m_pGetNewPlaylist); @@ -113,32 +111,25 @@ void SetlogFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index } -void SetlogFeature::buildPlaylistList() { +void HistoryFeature::buildPlaylistList() { m_playlistList.clear(); - // Setup the sidebar playlist model - QSqlTableModel playlistTableModel(this, m_pTrackCollection->getDatabase()); - playlistTableModel.setTable("Playlists"); - playlistTableModel.setFilter("hidden=2"); // PLHT_SET_LOG - playlistTableModel.setSort(playlistTableModel.fieldIndex("id"), - Qt::AscendingOrder); - playlistTableModel.select(); - while (playlistTableModel.canFetchMore()) { - playlistTableModel.fetchMore(); + + // Setup the sidebar playlist model + QSqlQuery query("SELECT id, name FROM Playlists WHERE hidden=2"); + if (!query.exec()) { + LOG_FAILED_QUERY(query); } - QSqlRecord record = playlistTableModel.record(); - int nameColumn = record.indexOf("name"); - int idColumn = record.indexOf("id"); - - for (int row = 0; row < playlistTableModel.rowCount(); ++row) { - int id = playlistTableModel.data( - playlistTableModel.index(row, idColumn)).toInt(); - QString name = playlistTableModel.data( - playlistTableModel.index(row, nameColumn)).toString(); - m_playlistList.append(qMakePair(id, name)); + + int iId = query.record().indexOf("id"); + int iName = query.record().indexOf("name"); + while (query.next()) { + int id = query.value(iId).toInt(); + QString name = query.value(iName).toString(); + m_playlistList << PlaylistItem(id, name); } } -void SetlogFeature::decorateChild(TreeItem* item, int playlist_id) { +void HistoryFeature::decorateChild(TreeItem* item, int playlist_id) { if (playlist_id == m_playlistId) { item->setIcon(QIcon(":/images/library/ic_library_history_current.png")); } else if (m_playlistDao.isPlaylistLocked(playlist_id)) { @@ -148,7 +139,38 @@ void SetlogFeature::decorateChild(TreeItem* item, int playlist_id) { } } -void SetlogFeature::slotGetNewPlaylist() { +QModelIndex HistoryFeature::constructChildModel(int selected_id) { + buildPlaylistList(); + QModelIndex index = m_pHistoryTreeModel->reloadListsTree(selected_id); + if (!m_pSidebar.isNull()) { + m_pSidebar->expandAll(); + } + return index; +} + +PlaylistTableModel* HistoryFeature::constructTableModel() { + return new PlaylistTableModel(this, m_pTrackCollection, + "mixxx.db.model.setlog", true); +} + +QSet HistoryFeature::playlistIdsFromIndex(const QModelIndex& index) const { + QList auxList = index.data(AbstractRole::RoleQuery).toList(); + QSet playlistIds; + for (QVariant& var : auxList) { + bool ok; + playlistIds.insert(var.toInt(&ok)); + if (!ok) { + return QSet(); + } + } + return playlistIds; +} + +QModelIndex HistoryFeature::indexFromPlaylistId(int playlistId) const { + return m_pHistoryTreeModel->indexFromPlaylistId(playlistId); +} + +void HistoryFeature::slotGetNewPlaylist() { //qDebug() << "slotGetNewPlaylist() succesfully triggered !"; // create a new playlist for today @@ -174,62 +196,74 @@ void SetlogFeature::slotGetNewPlaylist() { } slotPlaylistTableChanged(m_playlistId); // For moving selection - emit(showTrackModel(m_pPlaylistTableModel)); + showTrackModel(m_pPlaylistTableModel); } -void SetlogFeature::slotJoinWithPrevious() { - //qDebug() << "slotJoinWithPrevious() row:" << m_lastRightClickedIndex.data(); +QWidget* HistoryFeature::createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) { + m_pSidebar = createLibrarySidebarWidget(pKeyboard); + m_pSidebar->expandAll(); + return m_pSidebar; +} +void HistoryFeature::slotJoinWithNext() { + //qDebug() << "slotJoinWithPrevious() row:" << m_lastRightClickedIndex.data(); + m_pPlaylistTableModel = getPlaylistTableModel(m_featurePane); if (m_lastRightClickedIndex.isValid()) { - int currentPlaylistId = m_playlistDao.getPlaylistIdFromName( - m_lastRightClickedIndex.data().toString()); - - if (currentPlaylistId >= 0) { - - bool locked = m_playlistDao.isPlaylistLocked(currentPlaylistId); - - if (locked) { - qDebug() << "Skipping playlist deletion because playlist" << currentPlaylistId << "is locked."; - return; - } + bool ok; + int currentPlaylistId = + m_lastRightClickedIndex.data(AbstractRole::RoleDataPath).toInt(&ok); + + if (!ok) { + return; + } - // Add every track from right klicked playlist to that with the next smaller ID - int previousPlaylistId = m_playlistDao.getPreviousPlaylist(currentPlaylistId, PlaylistDAO::PLHT_SET_LOG); - if (previousPlaylistId >= 0) { - - m_pPlaylistTableModel->setTableModel(previousPlaylistId); - - if (currentPlaylistId == m_playlistId) { - // mark all the Tracks in the previous Playlist as played - - m_pPlaylistTableModel->select(); - int rows = m_pPlaylistTableModel->rowCount(); - for (int i = 0; i < rows; ++i) { - QModelIndex index = m_pPlaylistTableModel->index(i,0); - if (index.isValid()) { - TrackPointer track = m_pPlaylistTableModel->getTrack(index); - // Do not update the play count, just set played status. - PlayCounter playCounter(track->getPlayCounter()); - playCounter.setPlayed(); - track->setPlayCounter(playCounter); - } - } - - // Change current setlog - m_playlistId = previousPlaylistId; - } - qDebug() << "slotJoinWithPrevious() current:" << currentPlaylistId << " previous:" << previousPlaylistId; - if (m_playlistDao.copyPlaylistTracks(currentPlaylistId, previousPlaylistId)) { - m_playlistDao.deletePlaylist(currentPlaylistId); - slotPlaylistTableChanged(previousPlaylistId); // For moving selection - emit(showTrackModel(m_pPlaylistTableModel)); + bool locked = m_playlistDao.isPlaylistLocked(currentPlaylistId); + + if (locked) { + qDebug() << "Skipping playlist deletion because playlist" << currentPlaylistId << "is locked."; + return; + } + + // Add every track from right klicked playlist to that with the next smaller ID + // Although being the name "join with next" this is because it's ordered + // in descendant order so actually the next playlist in the GUI is the + // previous Id playlist. + int previousPlaylistId = m_playlistDao.getPreviousPlaylist(currentPlaylistId, PlaylistDAO::PLHT_SET_LOG); + if (previousPlaylistId < 0) { + return; + } + + m_pPlaylistTableModel->setTableModel(previousPlaylistId); + + if (currentPlaylistId == m_playlistId) { + // mark all the Tracks in the previous Playlist as played + + m_pPlaylistTableModel->select(); + int rows = m_pPlaylistTableModel->rowCount(); + for (int i = 0; i < rows; ++i) { + QModelIndex index = m_pPlaylistTableModel->index(i,0); + if (index.isValid()) { + TrackPointer track = m_pPlaylistTableModel->getTrack(index); + // Do not update the play count, just set played status. + PlayCounter playCounter(track->getPlayCounter()); + playCounter.setPlayed(); + track->setPlayCounter(playCounter); } } + + // Change current setlog + m_playlistId = previousPlaylistId; + } + qDebug() << "slotJoinWithPrevious() current:" << currentPlaylistId << " previous:" << previousPlaylistId; + if (m_playlistDao.copyPlaylistTracks(currentPlaylistId, previousPlaylistId)) { + m_playlistDao.deletePlaylist(currentPlaylistId); + slotPlaylistTableChanged(previousPlaylistId); // For moving selection + showTrackModel(m_pPlaylistTableModel); } } } -void SetlogFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { +void HistoryFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { if (!currentPlayingTrack) { return; } @@ -264,6 +298,8 @@ void SetlogFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { if (!currentPlayingTrackId.isValid()) { return; } + + m_pPlaylistTableModel = getPlaylistTableModel(-1); if (m_pPlaylistTableModel->getPlaylist() == m_playlistId) { // View needs a refresh @@ -273,9 +309,11 @@ void SetlogFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { m_playlistDao.appendTrackToPlaylist(currentPlayingTrackId, m_playlistId); } + // Refresh sidebar tree + constructChildModel(m_playlistId); } -void SetlogFeature::slotPlaylistTableChanged(int playlistId) { +void HistoryFeature::slotPlaylistTableChanged(int playlistId) { if (!m_pPlaylistTableModel) { return; } @@ -284,12 +322,12 @@ void SetlogFeature::slotPlaylistTableChanged(int playlistId) { PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); if (type == PlaylistDAO::PLHT_SET_LOG || type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); m_lastRightClickedIndex = constructChildModel(playlistId); + m_lastChildClicked[m_featurePane] = m_lastRightClickedIndex; } } -void SetlogFeature::slotPlaylistContentChanged(int playlistId) { +void HistoryFeature::slotPlaylistContentChanged(int playlistId) { if (!m_pPlaylistTableModel) { return; } @@ -302,7 +340,7 @@ void SetlogFeature::slotPlaylistContentChanged(int playlistId) { } } -void SetlogFeature::slotPlaylistTableRenamed(int playlistId, +void HistoryFeature::slotPlaylistTableRenamed(int playlistId, QString /* a_strName */) { if (!m_pPlaylistTableModel) { return; @@ -312,15 +350,15 @@ void SetlogFeature::slotPlaylistTableRenamed(int playlistId, enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); if (type == PlaylistDAO::PLHT_SET_LOG || type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); m_lastRightClickedIndex = constructChildModel(playlistId); + m_lastChildClicked[m_featurePane] = m_lastRightClickedIndex; if (type != PlaylistDAO::PLHT_UNKNOWN) { activatePlaylist(playlistId); } } } -QString SetlogFeature::getRootViewHtml() const { +QString HistoryFeature::getRootViewHtml() const { QString playlistsTitle = tr("History"); QString playlistsSummary = tr("The history section automatically keeps a list of tracks you play in your DJ sets."); QString playlistsSummary2 = tr("This is handy for remembering what worked in your DJ sets, posting set-lists, or reporting your plays to licensing organizations."); diff --git a/src/library/features/history/historyfeature.h b/src/library/features/history/historyfeature.h new file mode 100644 index 00000000000..cc2825901ef --- /dev/null +++ b/src/library/features/history/historyfeature.h @@ -0,0 +1,63 @@ +// setlogfeature.h + +#ifndef SETLOGFEATURE_H +#define SETLOGFEATURE_H + +#include +#include +#include + +#include "library/features/baseplaylist/baseplaylistfeature.h" +#include "preferences/usersettings.h" + +class TrackCollection; +class TreeItem; +class HistoryTreeModel; + +class HistoryFeature : public BasePlaylistFeature { + Q_OBJECT +public: + HistoryFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); + virtual ~HistoryFeature(); + + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; + void decorateChild(TreeItem *pChild, int playlist_id) override; + + public slots: + void onRightClick(const QPoint&) override; + void onRightClickChild(const QPoint& globalPos, const QModelIndex& index) override; + void slotJoinWithNext(); + void slotGetNewPlaylist(); + + protected: + QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) override; + + void buildPlaylistList() override; + QModelIndex constructChildModel(int selected_id); + PlaylistTableModel* constructTableModel() override; + QSet playlistIdsFromIndex(const QModelIndex &index) const override; + QModelIndex indexFromPlaylistId(int playlistId) const override; + + private slots: + void slotPlayingTrackChanged(TrackPointer currentPlayingTrack); + void slotPlaylistTableChanged(int playlistId) override; + void slotPlaylistContentChanged(int playlistId) override; + void slotPlaylistTableRenamed(int playlistId, QString a_strName) override; + + private: + QString getRootViewHtml() const override; + + QPointer m_pSidebar; + QLinkedList m_recentTracks; + QAction* m_pJoinWithNextAction; + QAction* m_pGetNewPlaylist; + HistoryTreeModel* m_pHistoryTreeModel; + int m_playlistId; +}; + +#endif // SETLOGFEATURE_H diff --git a/src/library/features/history/historytreemodel.cpp b/src/library/features/history/historytreemodel.cpp new file mode 100644 index 00000000000..334ad275563 --- /dev/null +++ b/src/library/features/history/historytreemodel.cpp @@ -0,0 +1,169 @@ +#include + +#include "library/features/history/historyfeature.h" +#include "library/queryutil.h" +#include "library/trackcollection.h" + +#include "library/features/history/historytreemodel.h" + +HistoryTreeModel::HistoryTreeModel(HistoryFeature* pFeature, + TrackCollection* pTrackCollection, + QObject* parent) + : TreeItemModel(parent), + m_pFeature(pFeature), + m_pTrackCollection(pTrackCollection){ + m_columns << PLAYLISTTABLE_ID + << PLAYLISTTABLE_DATECREATED + << PLAYLISTTABLE_NAME; + + for (QString& s : m_columns) { + s = "playlists." + s; + } +} + +QModelIndex HistoryTreeModel::reloadListsTree(int playlistId) { + TreeItem* pRootItem = new TreeItem(); + pRootItem->setLibraryFeature(m_pFeature); + setRootItem(pRootItem); + QString trackCountName = "TrackCount"; + + QString queryStr = "SELECT %1,COUNT(%2) AS %7 " + "FROM playlists LEFT JOIN PlaylistTracks " + "ON %3=%4 " + "WHERE %5=2 " + "GROUP BY %4 " + "ORDER BY %6 DESC"; + queryStr = queryStr.arg(m_columns.join(","), + "PlaylistTracks." + PLAYLISTTRACKSTABLE_TRACKID, + "PlaylistTracks." + PLAYLISTTRACKSTABLE_PLAYLISTID, + "Playlists." + PLAYLISTTABLE_ID, + PLAYLISTTABLE_HIDDEN, + PLAYLISTTABLE_DATECREATED, + trackCountName); + + QSqlQuery query(m_pTrackCollection->getDatabase()); + if (!query.exec(queryStr)) { + qDebug() << queryStr; + LOG_FAILED_QUERY(query); + return QModelIndex(); + } + + QSqlRecord record = query.record(); + HistoryQueryIndex ind; + ind.iID = record.indexOf(PLAYLISTTABLE_ID); + ind.iDate = record.indexOf(PLAYLISTTABLE_DATECREATED); + ind.iName = record.indexOf(PLAYLISTTABLE_NAME); + ind.iCount = record.indexOf(trackCountName); + + TreeItem* lastYear = nullptr; + TreeItem* lastMonth = nullptr; + TreeItem* lastPlaylist = nullptr; + bool change = true; + QDate lastDate; + int row = 0; + int selectedRow = -1; + TreeItem* selectedItem = nullptr; + + while (query.next()) { + QDateTime dTime = query.value(ind.iDate).toDateTime(); + QDate auxDate = dTime.date(); + + if (auxDate.year() != lastDate.year() || change) { + row = 0; + change = true; + QString year = QString::number(auxDate.year()); + lastYear = new TreeItem(year, -1, m_pFeature, pRootItem); + pRootItem->appendChild(lastYear); + } + + if (auxDate.month() != lastDate.month() || change) { + row = 0; + QString month = QDate::longMonthName(auxDate.month(), QDate::StandaloneFormat); + lastMonth = new TreeItem(month, -1, m_pFeature, lastYear); + lastYear->appendChild(lastMonth); + } + + QString sData = query.value(ind.iName).toString(); + sData += QString(" (%1)").arg(query.value(ind.iCount).toString()); + + int id = query.value(ind.iID).toInt(); + + lastPlaylist = new TreeItem(sData, id, m_pFeature, lastMonth); + m_pFeature->decorateChild(lastPlaylist, id); + lastMonth->appendChild(lastPlaylist); + if (id == playlistId) { + selectedRow = row; + selectedItem = lastPlaylist; + } + lastDate = auxDate; + change = false; + ++row; + } + + triggerRepaint(); + if (selectedRow < 0) return QModelIndex(); + return createIndex(selectedRow, 0, selectedItem); +} + +QModelIndex HistoryTreeModel::indexFromPlaylistId(int playlistId) { + int row = -1; + TreeItem* pItem = findItemFromPlaylistId(m_pRootItem, playlistId, row); + return createIndex(row, 0, pItem); +} + +QVariant HistoryTreeModel::data(const QModelIndex& index, int role) const { + if (role != AbstractRole::RoleQuery) { + return TreeItemModel::data(index, role); + } + + TreeItem* pTree = static_cast(index.internalPointer()); + DEBUG_ASSERT_AND_HANDLE(pTree) { + return QVariant(); + } + + return idsFromItem(pTree); +} + +QList HistoryTreeModel::idsFromItem(TreeItem* pTree) const { + if (pTree->childCount() <= 0) { + bool ok; + int value = pTree->dataPath().toInt(&ok); + QList ret; + if (!ok) { + return ret; + } + ret.append(value); + return ret; + } + + QList res; + int size = pTree->childCount(); + for (int i = 0; i < size; ++i) { + QList aux = idsFromItem(pTree->child(i)); + res.append(aux); + } + return res; +} + +TreeItem* HistoryTreeModel::findItemFromPlaylistId(TreeItem* pTree, + int playlistId, int& row) const { + int size = pTree->childCount(); + if (size <= 0) { + bool ok = false; + int value = pTree->dataPath().toInt(&ok); + if (ok && value == playlistId) { + return pTree; + } + return nullptr; + } + row = 0; + for (int i = 0; i < size; ++i) { + TreeItem* child = pTree->child(i); + TreeItem* result = findItemFromPlaylistId(child, playlistId, row); + if (result != nullptr) { + return result; + } + ++row; + } + return nullptr; +} diff --git a/src/library/features/history/historytreemodel.h b/src/library/features/history/historytreemodel.h new file mode 100644 index 00000000000..85b902db99e --- /dev/null +++ b/src/library/features/history/historytreemodel.h @@ -0,0 +1,37 @@ +#ifndef HISTORYTREEMODEL_H +#define HISTORYTREEMODEL_H + +#include + +#include "library/treeitemmodel.h" + +class HistoryFeature; +class TrackCollection; + +class HistoryTreeModel : public TreeItemModel +{ + public: + HistoryTreeModel(HistoryFeature* pFeature, TrackCollection* pTrackCollection, + QObject* parent = nullptr); + + QModelIndex reloadListsTree(int playlistId); + QModelIndex indexFromPlaylistId(int playlistId); + QVariant data(const QModelIndex& index, int role) const override; + + private: + struct HistoryQueryIndex { + int iID; + int iDate; + int iName; + int iCount; + }; + + QList idsFromItem(TreeItem* pTree) const; + TreeItem* findItemFromPlaylistId(TreeItem* pTree, int playlistId, int& row) const; + + HistoryFeature* m_pFeature; + TrackCollection* m_pTrackCollection; + QStringList m_columns; +}; + +#endif // HISTORYTREEMODEL_H diff --git a/src/library/itunes/itunesfeature.cpp b/src/library/features/itunes/itunesfeature.cpp similarity index 96% rename from src/library/itunes/itunesfeature.cpp rename to src/library/features/itunes/itunesfeature.cpp index 24a4655a31a..a8060588e6f 100644 --- a/src/library/itunes/itunesfeature.cpp +++ b/src/library/features/itunes/itunesfeature.cpp @@ -8,12 +8,13 @@ #include #include -#include "library/itunes/itunesfeature.h" +#include "library/features/itunes/itunesfeature.h" #include "library/basetrackcache.h" #include "library/dao/settingsdao.h" -#include "library/baseexternaltrackmodel.h" -#include "library/baseexternalplaylistmodel.h" +#include "library/features/baseexternalfeature/baseexternaltrackmodel.h" +#include "library/features/baseexternalfeature/baseexternalplaylistmodel.h" +#include "library/library.h" #include "library/queryutil.h" #include "util/lcs.h" #include "util/sandbox.h" @@ -28,8 +29,11 @@ QString localhost_token() { #endif } -ITunesFeature::ITunesFeature(QObject* parent, TrackCollection* pTrackCollection) - : BaseExternalLibraryFeature(parent, pTrackCollection), +ITunesFeature::ITunesFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : BaseExternalLibraryFeature(pConfig, pLibrary, parent, pTrackCollection), m_pTrackCollection(pTrackCollection), m_cancelImport(false) { QString tableName = "itunes_library"; @@ -108,13 +112,16 @@ QVariant ITunesFeature::title() { return m_title; } -QIcon ITunesFeature::getIcon() { - return QIcon(":/images/library/ic_library_itunes.png"); +QString ITunesFeature::getIconPath() { + return ":/images/library/ic_library_itunes.png"; +} + +QString ITunesFeature::getSettingsName() const { + return "ITunesFeature"; } void ITunesFeature::activate() { activate(false); - emit(enableCoverArtDisplay(false)); } void ITunesFeature::activate(bool forceReload) { @@ -143,7 +150,8 @@ void ITunesFeature::activate(bool forceReload) { NULL, tr("Select your iTunes library"), QDir::homePath(), "*.xml"); QFileInfo dbFile(m_dbfile); if (m_dbfile.isEmpty() || !dbFile.exists()) { - emit(showTrackModel(m_pITunesTrackModel)); + showBreadCrumb(); + showTrackModel(m_pITunesTrackModel); return; } @@ -173,8 +181,8 @@ void ITunesFeature::activate(bool forceReload) { emit (featureIsLoading(this, true)); } - emit(showTrackModel(m_pITunesTrackModel)); - emit(enableCoverArtDisplay(false)); + showTrackModel(m_pITunesTrackModel); + showBreadCrumb(); } void ITunesFeature::activateChild(const QModelIndex& index) { @@ -182,8 +190,9 @@ void ITunesFeature::activateChild(const QModelIndex& index) { QString playlist = index.data().toString(); qDebug() << "Activating " << playlist; m_pITunesPlaylistModel->setPlaylist(playlist); - emit(showTrackModel(m_pITunesPlaylistModel)); - emit(enableCoverArtDisplay(false)); + + showTrackModel(m_pITunesPlaylistModel); + showBreadCrumb(index); } TreeItemModel* ITunesFeature::getChildModel() { @@ -735,6 +744,7 @@ void ITunesFeature::clearTable(QString table_name) { void ITunesFeature::onTrackCollectionLoaded() { TreeItem* root = m_future.result(); + root->setLibraryFeature(this); if (root) { m_childModel.setRootItem(root); @@ -742,7 +752,7 @@ void ITunesFeature::onTrackCollectionLoaded() { m_trackSource->buildIndex(); //m_pITunesTrackModel->select(); - emit(showTrackModel(m_pITunesTrackModel)); + showTrackModel(m_pITunesTrackModel); qDebug() << "Itunes library loaded: success"; } else { QMessageBox::warning( diff --git a/src/library/itunes/itunesfeature.h b/src/library/features/itunes/itunesfeature.h similarity index 84% rename from src/library/itunes/itunesfeature.h rename to src/library/features/itunes/itunesfeature.h index 3dcf3baa5ec..83ff9ff1666 100644 --- a/src/library/itunes/itunesfeature.h +++ b/src/library/features/itunes/itunesfeature.h @@ -10,10 +10,9 @@ #include #include -#include "library/baseexternallibraryfeature.h" +#include "library/features/baseexternalfeature/baseexternallibraryfeature.h" #include "library/trackcollection.h" #include "library/treeitemmodel.h" -#include "library/treeitem.h" class BaseExternalTrackModel; class BaseExternalPlaylistModel; @@ -21,12 +20,16 @@ class BaseExternalPlaylistModel; class ITunesFeature : public BaseExternalLibraryFeature { Q_OBJECT public: - ITunesFeature(QObject* parent, TrackCollection* pTrackCollection); + ITunesFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); virtual ~ITunesFeature(); static bool isSupported(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; TreeItemModel* getChildModel(); diff --git a/src/library/features/libraryfolder/libraryfoldermodel.cpp b/src/library/features/libraryfolder/libraryfoldermodel.cpp new file mode 100644 index 00000000000..2ce0a457cd8 --- /dev/null +++ b/src/library/features/libraryfolder/libraryfoldermodel.cpp @@ -0,0 +1,197 @@ +#include + +#include "library/features/libraryfolder/libraryfoldermodel.h" + +#include "library/libraryfeature.h" +#include "library/queryutil.h" +#include "library/trackcollection.h" + +LibraryFolderModel::LibraryFolderModel(LibraryFeature* pFeature, + TrackCollection* pTrackCollection, + UserSettingsPointer pConfig, + QObject* parent) + : TreeItemModel(parent), + m_pFeature(pFeature), + m_pTrackCollection(pTrackCollection), + m_pConfig(pConfig), + m_pShowAllItem(nullptr) { + + QString recursive = m_pConfig->getValueString(ConfigKey("[Library]", + "FolderRecursive")); + m_folderRecursive = recursive.toInt() == 1; + + TrackDAO& trackDAO(pTrackCollection->getTrackDAO()); + connect(&trackDAO, SIGNAL(forceModelUpdate()), this, SLOT(reloadTree())); + connect(&trackDAO, SIGNAL(tracksAdded(QSet)), + this, SLOT(reloadTree())); + connect(&trackDAO, SIGNAL(tracksRemoved(QSet)), + this, SLOT(reloadTree())); + connect(&trackDAO, SIGNAL(trackChanged(TrackId)), + this, SLOT(reloadTree())); + + reloadTree(); +} + +bool LibraryFolderModel::setData( + const QModelIndex& index, const QVariant& value, int role) { + if (role == AbstractRole::RoleSettings) { + m_folderRecursive = value.toBool(); + m_pConfig->set(ConfigKey("[Library]", "FolderRecursive"), + ConfigValue((int)m_folderRecursive)); + return true; + } else { + return TreeItemModel::setData(index, value, role); + } +} + +QVariant LibraryFolderModel::data(const QModelIndex& index, int role) const { + if (role == AbstractRole::RoleSettings) { + return m_folderRecursive; + } + + TreeItem* pTree = static_cast(index.internalPointer()); + DEBUG_ASSERT_AND_HANDLE(pTree != nullptr) { + return TreeItemModel::data(index, role); + } + + if (role == AbstractRole::RoleBreadCrumb) { + if (pTree == m_pShowAllItem) { + return m_pFeature->title(); + } else { + return TreeItemModel::data(index, role); + } + } + + if (role == AbstractRole::RoleQuery) { + // User has clicked the show all item + if (pTree == m_pShowAllItem) { + return ""; + } + + const QString param("%1:=\"%2\""); + return param.arg("folder", pTree->dataPath().toString()); + } + + return TreeItemModel::data(index, role); +} + +void LibraryFolderModel::reloadTree() { + //qDebug() << "LibraryFolderModel::reloadTree()"; + beginResetModel(); + // Remove current root + setRootItem(new TreeItem(m_pFeature)); + + // Add "show all" item + m_pShowAllItem = new TreeItem(tr("Show all"), "", m_pFeature, m_pRootItem); + m_pRootItem->appendChild(m_pShowAllItem); + + // Get the Library directories + QStringList dirs(m_pTrackCollection->getDirectoryDAO().getDirs()); + + QString queryStr = "SELECT COUNT(%3),%1 " + "FROM track_locations INNER JOIN library ON %3=%4 " + "WHERE %2=0 AND %1 LIKE :dir " + "GROUP BY %1 " + "ORDER BY %1 COLLATE localeAwareCompare"; + queryStr = queryStr.arg(TRACKLOCATIONSTABLE_DIRECTORY, + "library." + LIBRARYTABLE_MIXXXDELETED, + "library." + LIBRARYTABLE_ID, + "track_locations." + TRACKLOCATIONSTABLE_ID); + + QSqlQuery query(m_pTrackCollection->getDatabase()); + query.prepare(queryStr); + + for (const QString& dir : dirs) { + query.bindValue(":dir", dir + "%"); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return; + } + + // For each source folder create the tree + createTreeForLibraryDir(dir, query); + } + endResetModel(); +} + +void LibraryFolderModel::createTreeForLibraryDir(const QString& dir, QSqlQuery& query) { + QStringList lastUsed; + QList parent; + parent.append(m_pRootItem); + bool first = true; + + while (query.next()) { + QString location = query.value(1).toString(); + //qDebug() << location; + + + // Remove the + QString dispValue = location.mid(dir.size()); + if (dispValue.startsWith("/")) { + dispValue = dispValue.mid(1); + } + + // Add a header + if (first) { + first = false; + QString path = dir; + if (m_folderRecursive) { + path.append("*"); + } + TreeItem* pTree = new TreeItem(dir, path, m_pFeature, m_pRootItem); + pTree->setDivider(true); + m_pRootItem->appendChild(pTree); + } + + // Do not add empty items + if (dispValue.isEmpty()) { + continue; + } + + // We always use Qt notation for folders "/" + QStringList parts = dispValue.split("/"); + int treeDepth = parts.size(); + if (treeDepth > lastUsed.size()) { + for (int i = lastUsed.size(); i < parts.size(); ++i) { + lastUsed.append(QString()); + parent.append(nullptr); + } + } + + bool change = false; + for (int i = 0; i < parts.size(); ++i) { + const QString& val = parts.at(i); + if (change || val != lastUsed.at(i)) { + change = true; + + QString fullPath = dir; + for (int j = 0; j <= i; ++j) { + fullPath += "/" + parts.at(j); + } + + if (m_folderRecursive) { + fullPath.append("*"); + } + + TreeItem* pItem = new TreeItem(val, fullPath, m_pFeature, parent[i]); + pItem->setTrackCount(0); + parent[i]->appendChild(pItem); + + parent[i + 1] = pItem; + lastUsed[i] = val; + } + } + + // Set track count + int val = query.value(0).toInt(); + for (int i = 1; i < treeDepth + 1; ++i) { + TreeItem* pItem = parent[i]; + if (pItem == nullptr) { + continue; + } + + pItem->setTrackCount(pItem->getTrackCount() + val); + } + } +} diff --git a/src/library/features/libraryfolder/libraryfoldermodel.h b/src/library/features/libraryfolder/libraryfoldermodel.h new file mode 100644 index 00000000000..982e5ee916c --- /dev/null +++ b/src/library/features/libraryfolder/libraryfoldermodel.h @@ -0,0 +1,39 @@ +#ifndef LIBRARYFOLDERMODEL_H +#define LIBRARYFOLDERMODEL_H + +#include + +#include "library/treeitemmodel.h" +#include "preferences/usersettings.h" + +class LibraryFeature; +class TrackCollection; + +class LibraryFolderModel : public TreeItemModel +{ + public: + LibraryFolderModel(LibraryFeature* pFeature, + TrackCollection* pTrackCollection, + UserSettingsPointer pConfig, + QObject* parent = nullptr); + + virtual bool setData(const QModelIndex& index, const QVariant& value, int role); + virtual QVariant data(const QModelIndex &index, int role) const; + + public slots: + void reloadTree(); + + private: + + void createTreeForLibraryDir(const QString& dir, QSqlQuery& query); + + LibraryFeature* m_pFeature; + TrackCollection* m_pTrackCollection; + UserSettingsPointer m_pConfig; + + TreeItem* m_pShowAllItem; + + bool m_folderRecursive; +}; + +#endif // LIBRARYFOLDERMODEL_H diff --git a/src/library/features/libraryfolder/libraryfoldersfeature.cpp b/src/library/features/libraryfolder/libraryfoldersfeature.cpp new file mode 100644 index 00000000000..1d53c43c60b --- /dev/null +++ b/src/library/features/libraryfolder/libraryfoldersfeature.cpp @@ -0,0 +1,58 @@ +#include +#include +#include + +#include "library/features/libraryfolder/libraryfoldersfeature.h" + +#include "library/features/libraryfolder/libraryfoldermodel.h" +#include "widget/wlibrarysidebar.h" + +LibraryFoldersFeature::LibraryFoldersFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : MixxxLibraryFeature(pConfig, pLibrary, parent, pTrackCollection) { + + setChildModel(new LibraryFolderModel(this, m_pTrackCollection, m_pConfig)); +} + +QVariant LibraryFoldersFeature::title() { + return "Folders"; +} + +QString LibraryFoldersFeature::getIconPath() { + return ":/images/library/ic_library_folder.png"; +} + +QString LibraryFoldersFeature::getSettingsName() const { + return "LibraryFoldersFeature"; +} + +QWidget* LibraryFoldersFeature::createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) { + m_pSidebar = createLibrarySidebarWidget(pKeyboard); + return m_pSidebar; +} + +void LibraryFoldersFeature::onRightClickChild(const QPoint&pos, + const QModelIndex&) { + + bool recursive = m_pChildModel->data(QModelIndex(), + AbstractRole::RoleSettings).toBool(); + + QMenu menu; + QAction* showRecursive = menu.addAction(tr("Show recursive view in folders")); + showRecursive->setCheckable(true); + showRecursive->setChecked(recursive); + + QAction* selected = menu.exec(pos); + + if (selected == showRecursive) { + m_pChildModel->setData(QModelIndex(), selected->isChecked(), + AbstractRole::RoleSettings); + } else { + // Menu rejected + return; + } + + m_pChildModel->reloadTree(); +} diff --git a/src/library/features/libraryfolder/libraryfoldersfeature.h b/src/library/features/libraryfolder/libraryfoldersfeature.h new file mode 100644 index 00000000000..398a6ee42e7 --- /dev/null +++ b/src/library/features/libraryfolder/libraryfoldersfeature.h @@ -0,0 +1,25 @@ +#ifndef LIBRARYFOLDERSFEATURE_H +#define LIBRARYFOLDERSFEATURE_H + +#include "library/features/mixxxlibrary/mixxxlibraryfeature.h" + +class LibraryFoldersModel; + +class LibraryFoldersFeature : public MixxxLibraryFeature +{ + public: + LibraryFoldersFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); + + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; + QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) override; + + public slots: + void onRightClickChild(const QPoint& pos, const QModelIndex&) override; +}; + +#endif // LIBRARYFOLDERSFEATURE_H diff --git a/src/library/features/maintenance/dlghidden.cpp b/src/library/features/maintenance/dlghidden.cpp new file mode 100644 index 00000000000..203c0088576 --- /dev/null +++ b/src/library/features/maintenance/dlghidden.cpp @@ -0,0 +1,41 @@ +#include "QItemSelection" + +#include "library/features/maintenance/dlghidden.h" +#include "library/features/maintenance/hiddentablemodel.h" +#include "widget/wtracktableview.h" +#include "util/assert.h" + +DlgHidden::DlgHidden(QWidget* parent) + : QFrame(parent), + Ui::DlgHidden() { + setupUi(this); + + connect(btnSelect, SIGNAL(clicked()), this, SIGNAL(selectAll())); + connect(btnPurge, SIGNAL(clicked()), this, SIGNAL(purge())); + connect(btnUnhide, SIGNAL(clicked()), this, SIGNAL(unhide())); +} + +DlgHidden::~DlgHidden() { + // Delete m_pTrackTableView before the table model. This is because the + // table view saves the header state using the model. + delete m_pHiddenTableModel; +} + +void DlgHidden::onShow() { + m_pHiddenTableModel->select(); + // no buttons can be selected + activateButtons(false); +} + +void DlgHidden::setSelectedIndexes(const QModelIndexList& selectedIndexes) { + activateButtons(!selectedIndexes.empty()); +} + +void DlgHidden::setTableModel(HiddenTableModel* pTableModel) { + m_pHiddenTableModel = pTableModel; +} + +void DlgHidden::activateButtons(bool enable) { + btnPurge->setEnabled(enable); + btnUnhide->setEnabled(enable); +} diff --git a/src/library/features/maintenance/dlghidden.h b/src/library/features/maintenance/dlghidden.h new file mode 100644 index 00000000000..2fd67d83ed8 --- /dev/null +++ b/src/library/features/maintenance/dlghidden.h @@ -0,0 +1,37 @@ +#ifndef DLGHIDDEN_H +#define DLGHIDDEN_H + +#include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/maintenance/ui_dlghidden.h" +#include "library/trackcollection.h" +#include "preferences/usersettings.h" + +class WTrackTableView; +class HiddenTableModel; +class QItemSelection; + +class DlgHidden : public QFrame, public Ui::DlgHidden { + Q_OBJECT + public: + DlgHidden(QWidget* parent); + virtual ~DlgHidden(); + + // The indexes are always from the focused pane + void setSelectedIndexes(const QModelIndexList& selectedIndexes); + void setTableModel(HiddenTableModel* pTableModel); + + public slots: + void onShow(); + + signals: + void selectAll(); + void unhide(); + void purge(); + void trackSelected(TrackPointer pTrack); + + private: + void activateButtons(bool enable); + HiddenTableModel* m_pHiddenTableModel; +}; + +#endif //DLGHIDDEN_H diff --git a/src/library/features/maintenance/dlghidden.ui b/src/library/features/maintenance/dlghidden.ui new file mode 100644 index 00000000000..2b96be8becc --- /dev/null +++ b/src/library/features/maintenance/dlghidden.ui @@ -0,0 +1,73 @@ + + + DlgHidden + + + + 0 + 0 + 560 + 399 + + + + Hidden Tracks + + + + + + Selects all tracks in the table below. + + + Select All + + + + + + + Purge selected tracks from the library. + + + Purge + + + false + + + + + + + Unhide selected tracks from the library. + + + Unhide + + + Ctrl+S + + + false + + + + + + + Qt::Vertical + + + + 20 + 285 + + + + + + + + + diff --git a/src/library/features/maintenance/dlgmissing.cpp b/src/library/features/maintenance/dlgmissing.cpp new file mode 100644 index 00000000000..140d8c916c6 --- /dev/null +++ b/src/library/features/maintenance/dlgmissing.cpp @@ -0,0 +1,37 @@ +#include "library/features/maintenance/dlgmissing.h" + +#include "library/features/maintenance/missingtablemodel.h" +#include "widget/wtracktableview.h" +#include "util/assert.h" + +DlgMissing::DlgMissing(QWidget* parent) + : QFrame(parent), + Ui::DlgMissing() { + setupUi(this); + + connect(btnPurge, SIGNAL(clicked()), this, SIGNAL(purge())); + connect(btnSelect, SIGNAL(clicked()), this, SIGNAL(selectAll())); +} + +DlgMissing::~DlgMissing() { + // Delete m_pTrackTableView before the table model. This is because the + // table view saves the header state using the model. + delete m_pMissingTableModel; +} + +void DlgMissing::onShow() { + m_pMissingTableModel->select(); + activateButtons(false); +} + +void DlgMissing::setSelectedIndexes(const QModelIndexList& selectedIndexes) { + activateButtons(!selectedIndexes.isEmpty()); +} + +void DlgMissing::setTableModel(MissingTableModel* pTableModel) { + m_pMissingTableModel = pTableModel; +} + +void DlgMissing::activateButtons(bool enable) { + btnPurge->setEnabled(enable); +} diff --git a/src/library/features/maintenance/dlgmissing.h b/src/library/features/maintenance/dlgmissing.h new file mode 100644 index 00000000000..22ccbe0a729 --- /dev/null +++ b/src/library/features/maintenance/dlgmissing.h @@ -0,0 +1,36 @@ +#ifndef DLGMISSING_H +#define DLGMISSING_H + +#include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/maintenance/ui_dlgmissing.h" +#include "library/trackcollection.h" +#include "preferences/usersettings.h" + +class WTrackTableView; +class MissingTableModel; + +class DlgMissing : public QFrame, public Ui::DlgMissing { + Q_OBJECT + public: + DlgMissing(QWidget* parent); + virtual ~DlgMissing(); + + // The indexes are always from the Focused pane + void setSelectedIndexes(const QModelIndexList& selectedIndexes); + void setTableModel(MissingTableModel* pTableModel); + + public slots: + void onShow(); + + signals: + void purge(); + void selectAll(); + void trackSelected(TrackPointer pTrack); + + private: + void activateButtons(bool enable); + + MissingTableModel* m_pMissingTableModel; +}; + +#endif //DLGMISSING_H diff --git a/src/library/features/maintenance/dlgmissing.ui b/src/library/features/maintenance/dlgmissing.ui new file mode 100644 index 00000000000..bc3a8805537 --- /dev/null +++ b/src/library/features/maintenance/dlgmissing.ui @@ -0,0 +1,57 @@ + + + DlgMissing + + + + 0 + 0 + 560 + 399 + + + + Missing Tracks + + + + + + Selects all tracks in the table below. + + + Select All + + + + + + + Purge selected tracks from the library. + + + Purge + + + false + + + + + + + Qt::Vertical + + + + 20 + 316 + + + + + + + + + diff --git a/src/library/hiddentablemodel.cpp b/src/library/features/maintenance/hiddentablemodel.cpp similarity index 98% rename from src/library/hiddentablemodel.cpp rename to src/library/features/maintenance/hiddentablemodel.cpp index c103ed88f22..e1a090ea5a2 100644 --- a/src/library/hiddentablemodel.cpp +++ b/src/library/features/maintenance/hiddentablemodel.cpp @@ -1,4 +1,4 @@ -#include "library/hiddentablemodel.h" +#include "library/features/maintenance/hiddentablemodel.h" HiddenTableModel::HiddenTableModel(QObject* parent, TrackCollection* pTrackCollection) diff --git a/src/library/hiddentablemodel.h b/src/library/features/maintenance/hiddentablemodel.h similarity index 100% rename from src/library/hiddentablemodel.h rename to src/library/features/maintenance/hiddentablemodel.h diff --git a/src/library/features/maintenance/maintenancefeature.cpp b/src/library/features/maintenance/maintenancefeature.cpp new file mode 100644 index 00000000000..ba88809745d --- /dev/null +++ b/src/library/features/maintenance/maintenancefeature.cpp @@ -0,0 +1,192 @@ +#include +#include + +#include "library/features/maintenance/dlghidden.h" +#include "library/features/maintenance/dlgmissing.h" +#include "library/features/maintenance/hiddentablemodel.h" +#include "library/features/maintenance/maintenancefeature.h" +#include "library/features/maintenance/missingtablemodel.h" + +#include "widget/wtracktableview.h" + +MaintenanceFeature::MaintenanceFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), + kMaintenanceTitle(tr("Maintenance")), + kHiddenTitle(tr("Hidden Tracks")), + kMissingTitle(tr("Missing Tracks")), + m_pHiddenView(nullptr), + m_pMissingView(nullptr), + m_pTab(nullptr), + m_idExpandedHidden(-1), + m_idExpandedMissing(-1) { + +} + +QVariant MaintenanceFeature::title() { + return kMaintenanceTitle; +} + +QString MaintenanceFeature::getIconPath() { + return ":/images/library/ic_library_maintenance.png"; +} + +QString MaintenanceFeature::getSettingsName() const { + return "MaintenanceFeature"; +} + +TreeItemModel* MaintenanceFeature::getChildModel() { + return nullptr; +} + +void MaintenanceFeature::activate() { + DEBUG_ASSERT_AND_HANDLE(!m_pTab.isNull()) { + return; + } + + slotTabIndexChanged(m_pTab->currentIndex()); + + switchToFeature(); +} + +void MaintenanceFeature::selectionChanged(const QItemSelection&, + const QItemSelection&) { + WTrackTableView* pTable = getFocusedTable(); + if (pTable == nullptr) { + return; + } + + auto it = m_idPaneCurrent.constFind(m_featurePane); + if (it == m_idPaneCurrent.constEnd()) { + return; + } + + const QModelIndexList& selection = pTable->selectionModel()->selectedIndexes(); + if (*it == Pane::Hidden) { + m_pHiddenView->setSelectedIndexes(selection); + } else if (*it == Pane::Missing) { + m_pMissingView->setSelectedIndexes(selection); + } +} + +void MaintenanceFeature::selectAll() { + QPointer pTable = getFocusedTable(); + if (!pTable.isNull()) { + pTable->selectAll(); + } +} + +QWidget* MaintenanceFeature::createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) { + // The inner widget is a tab with the hidden and the missing controls + m_pTab = new QTabWidget(nullptr); + m_pTab->installEventFilter(pKeyboard); + connect(m_pTab, SIGNAL(currentChanged(int)), + this, SLOT(slotTabIndexChanged(int))); + + m_pHiddenView = new DlgHidden(m_pTab); + m_pHiddenView->setTableModel(getHiddenTableModel()); + m_pHiddenView->installEventFilter(pKeyboard); + connect(m_pHiddenView, SIGNAL(unhide()), this, SLOT(slotUnhideHidden())); + connect(m_pHiddenView, SIGNAL(purge()), this, SLOT(slotPurge())); + connect(m_pHiddenView, SIGNAL(selectAll()), this, SLOT(selectAll())); + + m_pMissingView = new DlgMissing(m_pTab); + m_pMissingView->setTableModel(getMissingTableModel()); + m_pMissingView->installEventFilter(pKeyboard); + connect(m_pMissingView, SIGNAL(purge()), this, SLOT(slotPurge())); + connect(m_pMissingView, SIGNAL(selectAll()), this, SLOT(selectAll())); + + m_idExpandedHidden = m_pTab->addTab(m_pHiddenView, kHiddenTitle); + m_idExpandedMissing = m_pTab->addTab(m_pMissingView, kMissingTitle); + + return m_pTab; +} + +QWidget* MaintenanceFeature::createPaneWidget(KeyboardEventFilter* pKeyboard, + int paneId) { + Q_UNUSED(pKeyboard); + WTrackTableView* pTable = LibraryFeature::createTableWidget(paneId); + + return pTable; +} + +void MaintenanceFeature::slotTabIndexChanged(int index) { + QPointer pTable = getFocusedTable(); + if (pTable.isNull()) { + return; + } + pTable->setSortingEnabled(false); + const QString* title; + + if (index == m_idExpandedHidden) { + DEBUG_ASSERT_AND_HANDLE(!m_pHiddenView.isNull()) { + return; + } + m_idPaneCurrent[m_featurePane] = Pane::Hidden; + pTable->loadTrackModel(getHiddenTableModel()); + + title = &kHiddenTitle; + m_pHiddenView->onShow(); + } else if (index == m_idExpandedMissing) { + DEBUG_ASSERT_AND_HANDLE(!m_pMissingView.isNull()) { + return; + } + + m_idPaneCurrent[m_featurePane] = Pane::Missing; + pTable->loadTrackModel(getMissingTableModel()); + + title = &kMissingTitle; + m_pMissingView->onShow(); + } else { + return; + } + + // This is the only way to get the selection signal changing the track + // models, every time the model changes the selection model changes too + // so we need to reconnect + connect(pTable->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), + this, + SLOT(selectionChanged(const QItemSelection &, const QItemSelection &))); + + switchToFeature(); + restoreSearch(""); + showBreadCrumb(kMaintenanceTitle % " > " % (*title), getIcon()); +} + +void MaintenanceFeature::slotUnhideHidden() { + QPointer pTable = getFocusedTable(); + if (pTable.isNull()) { + return; + } + + pTable->slotUnhide(); +} + +void MaintenanceFeature::slotPurge() { + QPointer pTable = getFocusedTable(); + if (pTable.isNull()) { + return; + } + pTable->slotPurge(); + + m_pMissingView->onShow(); + m_pHiddenView->onShow(); +} + +HiddenTableModel* MaintenanceFeature::getHiddenTableModel() { + if (m_pHiddenTableModel.isNull()) { + m_pHiddenTableModel = new HiddenTableModel(this, m_pTrackCollection); + } + return m_pHiddenTableModel; +} + +MissingTableModel* MaintenanceFeature::getMissingTableModel() { + if (m_pMissingTableModel.isNull()) { + m_pMissingTableModel = new MissingTableModel(this, m_pTrackCollection); + } + return m_pMissingTableModel; +} + diff --git a/src/library/features/maintenance/maintenancefeature.h b/src/library/features/maintenance/maintenancefeature.h new file mode 100644 index 00000000000..c4a79423517 --- /dev/null +++ b/src/library/features/maintenance/maintenancefeature.h @@ -0,0 +1,72 @@ +#ifndef MAINTENANCEFEATURE_H +#define MAINTENANCEFEATURE_H + +#include +#include +#include + +#include "library/libraryfeature.h" + +class DlgHidden; +class DlgMissing; +class HiddenTableModel; +class MissingTableModel; + +class MaintenanceFeature : public LibraryFeature +{ + Q_OBJECT + public: + MaintenanceFeature(UserSettingsPointer pConfig, + Library* pLibrary, QObject* parent, + TrackCollection* pTrackCollection); + + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; + TreeItemModel* getChildModel(); + + public slots: + void activate(); + void selectionChanged(const QItemSelection&, const QItemSelection&); + void selectAll(); + + protected: + + QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard); + QWidget* createPaneWidget(KeyboardEventFilter* pKeyboard, int paneId); + + private: + + enum Pane { + Hidden = 1, + Missing = 2 + }; + + const QString kMaintenanceTitle; + const QString kHiddenTitle; + const QString kMissingTitle; + + private slots: + + void slotTabIndexChanged(int index); + void slotUnhideHidden(); + void slotPurge(); + + private: + + HiddenTableModel* getHiddenTableModel(); + MissingTableModel* getMissingTableModel(); + + QPointer m_pHiddenView; + QPointer m_pMissingView; + QPointer m_pTab; + QHash m_idPaneCurrent; + + int m_idExpandedHidden; + int m_idExpandedMissing; + + QPointer m_pHiddenTableModel; + QPointer m_pMissingTableModel; +}; + +#endif // MAINTENANCEFEATURE_H diff --git a/src/library/missingtablemodel.cpp b/src/library/features/maintenance/missingtablemodel.cpp similarity index 98% rename from src/library/missingtablemodel.cpp rename to src/library/features/maintenance/missingtablemodel.cpp index ee2cefb17b8..b652f4192d4 100644 --- a/src/library/missingtablemodel.cpp +++ b/src/library/features/maintenance/missingtablemodel.cpp @@ -1,8 +1,8 @@ #include -#include "library/trackcollection.h" -#include "library/missingtablemodel.h" +#include "library/features/maintenance/missingtablemodel.h" #include "library/librarytablemodel.h" +#include "library/trackcollection.h" namespace { diff --git a/src/library/missingtablemodel.h b/src/library/features/maintenance/missingtablemodel.h similarity index 95% rename from src/library/missingtablemodel.h rename to src/library/features/maintenance/missingtablemodel.h index 196dc40e58a..43fc9023426 100644 --- a/src/library/missingtablemodel.h +++ b/src/library/features/maintenance/missingtablemodel.h @@ -6,8 +6,8 @@ #include #include -#include "trackmodel.h" #include "library/basesqltablemodel.h" +#include "library/trackmodel.h" class TrackCollection; diff --git a/src/library/features/mixxxlibrary/mixxxlibraryfeature.cpp b/src/library/features/mixxxlibrary/mixxxlibraryfeature.cpp new file mode 100644 index 00000000000..1d144ae23cd --- /dev/null +++ b/src/library/features/mixxxlibrary/mixxxlibraryfeature.cpp @@ -0,0 +1,211 @@ +// mixxxlibraryfeature.cpp +// Created 8/23/2009 by RJ Ryan (rryan@mit.edu) + +#include +#include +#include +#include + +#include "library/features/mixxxlibrary/mixxxlibraryfeature.h" + +#include "library/basetrackcache.h" +#include "library/librarytablemodel.h" +#include "library/parser.h" +#include "library/queryutil.h" +#include "library/trackcollection.h" +#include "sources/soundsourceproxy.h" +#include "util/dnd.h" +#include "widget/wlibrarysidebar.h" +#include "widget/wlibrarystack.h" +#include "widget/wtracktableview.h" + +const QString MixxxLibraryFeature::kLibraryTitle = tr("Library"); + +const QStringList MixxxLibraryFeature::kGroupingText = + QStringList::fromStdList({ + tr("Artist > Album"), + tr("Album"), + tr("Genre > Artist > Album"), + tr("Genre > Album") +}); + +const QList MixxxLibraryFeature::kGroupingOptions = + QList::fromStdList({ + QStringList::fromStdList({ LIBRARYTABLE_ARTIST, LIBRARYTABLE_ALBUM }), + QStringList::fromStdList({ LIBRARYTABLE_ALBUM }), + QStringList::fromStdList({ LIBRARYTABLE_GENRE, LIBRARYTABLE_ARTIST, + LIBRARYTABLE_ALBUM }), + QStringList::fromStdList({ LIBRARYTABLE_GENRE, LIBRARYTABLE_ALBUM }) +}); + +MixxxLibraryFeature::MixxxLibraryFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), + m_trackDao(pTrackCollection->getTrackDAO()) { + + m_pBaseTrackCache = pTrackCollection->getTrackSource(); + connect(&m_trackDao, SIGNAL(trackDirty(TrackId)), + m_pBaseTrackCache.data(), SLOT(slotTrackDirty(TrackId))); + connect(&m_trackDao, SIGNAL(trackClean(TrackId)), + m_pBaseTrackCache.data(), SLOT(slotTrackClean(TrackId))); + connect(&m_trackDao, SIGNAL(trackChanged(TrackId)), + m_pBaseTrackCache.data(), SLOT(slotTrackChanged(TrackId))); + connect(&m_trackDao, SIGNAL(tracksAdded(QSet)), + m_pBaseTrackCache.data(), SLOT(slotTracksAdded(QSet))); + connect(&m_trackDao, SIGNAL(tracksRemoved(QSet)), + m_pBaseTrackCache.data(), SLOT(slotTracksRemoved(QSet))); + connect(&m_trackDao, SIGNAL(dbTrackAdded(TrackPointer)), + m_pBaseTrackCache.data(), SLOT(slotDbTrackAdded(TrackPointer))); + + setChildModel(new MixxxLibraryTreeModel(this, m_pTrackCollection, m_pConfig)); + m_pLibraryTableModel = new LibraryTableModel(this, pTrackCollection, "mixxx.db.model.library"); +} + +MixxxLibraryFeature::~MixxxLibraryFeature() { + delete m_pChildModel; + delete m_pLibraryTableModel; +} + +QVariant MixxxLibraryFeature::title() { + return kLibraryTitle; +} + +QString MixxxLibraryFeature::getIconPath() { + return ":/images/library/ic_library_library.png"; +} + +QString MixxxLibraryFeature::getSettingsName() const { + return "MixxxLibraryFeature"; +} + +TreeItemModel* MixxxLibraryFeature::getChildModel() { + return m_pChildModel; +} + +QWidget* MixxxLibraryFeature::createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) { + m_pSidebar = createLibrarySidebarWidget(pKeyboard); + m_pSidebar->setIconSize(m_pChildModel->getDefaultIconSize()); + m_pChildModel->reloadTree(); + return m_pSidebar; +} + +void MixxxLibraryFeature::refreshLibraryModels() { + if (m_pLibraryTableModel) { + m_pLibraryTableModel->select(); + } +} + +void MixxxLibraryFeature::onSearch(const QString&) { + showBreadCrumb(); + if (!m_pSidebar.isNull()) { + m_pSidebar->clearSelection(); + } +} + +void MixxxLibraryFeature::setChildModel(TreeItemModel* pChild) { + if (!m_pChildModel.isNull()) { + delete m_pChildModel; + } + + m_pChildModel = pChild; + connect(&m_trackDao, SIGNAL(trackChanged(TrackId)), + m_pChildModel, SLOT(reloadTree())); + connect(&m_trackDao, SIGNAL(tracksRemoved(QSet)), + m_pChildModel, SLOT(reloadTree())); + connect(&m_trackDao, SIGNAL(tracksAdded(QSet)), + m_pChildModel, SLOT(reloadTree())); +} + +void MixxxLibraryFeature::activate() { + if (m_lastClickedIndex.isValid()) { + activateChild(m_lastClickedIndex); + return; + } + + //qDebug() << "MixxxLibraryFeature::activate()"; + showTrackModel(m_pLibraryTableModel); + restoreSearch(""); + showBreadCrumb(); + +} + +void MixxxLibraryFeature::activateChild(const QModelIndex& index) { + m_lastClickedIndex = index; + if (!index.isValid()) return; + + QString query = index.data(AbstractRole::RoleQuery).toString(); + //qDebug() << "MixxxLibraryFeature::activateChild" << query; + + if (query == "$groupingSettings$") { + // Act as right click + onRightClickChild(QCursor::pos(), QModelIndex()); + return; + } + + m_pLibraryTableModel->search(query); + switchToFeature(); + showBreadCrumb(index.data(AbstractRole::RoleBreadCrumb).toString(), getIcon()); + restoreSearch(query); +} + +void MixxxLibraryFeature::invalidateChild() { + m_lastClickedIndex = QModelIndex(); +} + +void MixxxLibraryFeature::onRightClickChild(const QPoint& pos, + const QModelIndex&) { + + // Create the sort menu + QMenu menu; + QVariant varSort = m_pChildModel->data(QModelIndex(), + AbstractRole::RoleSettings); + QStringList currentSort = varSort.toStringList(); + + QActionGroup* orderGroup = new QActionGroup(&menu); + for (int i = 0; i < kGroupingOptions.size(); ++i) { + QAction* action = menu.addAction(kGroupingText.at(i)); + action->setActionGroup(orderGroup); + action->setData(kGroupingOptions.at(i)); + action->setCheckable(true); + action->setChecked(currentSort == kGroupingOptions.at(i)); + } + + QAction* selected = menu.exec(pos); + if (selected == nullptr) { + return; + } + if (!m_pGroupingCombo.isNull()) { + int index = kGroupingOptions.indexOf(selected->data().toStringList()); + m_pGroupingCombo->setCurrentIndex(index); + } + setTreeSettings(selected->data()); +} + +bool MixxxLibraryFeature::dropAccept(QList urls, QObject* pSource) { + if (pSource) { + return false; + } else { + QList files = + DragAndDropHelper::supportedTracksFromUrls(urls, false, true); + + // Adds track, does not insert duplicates, handles unremoving logic. + QList trackIds = m_trackDao.addMultipleTracks(files, true); + m_pChildModel->reloadTree(); + return trackIds.size() > 0; + } +} + +bool MixxxLibraryFeature::dragMoveAccept(QUrl url) { + return SoundSourceProxy::isUrlSupported(url) || + Parser::isPlaylistFilenameSupported(url.toLocalFile()); +} + +void MixxxLibraryFeature::setTreeSettings(const QVariant& settings) { + if (m_pChildModel.isNull()) { + return; + } + m_pChildModel->setData(QModelIndex(), settings, AbstractRole::RoleSettings); + m_pChildModel->reloadTree(); +} diff --git a/src/library/features/mixxxlibrary/mixxxlibraryfeature.h b/src/library/features/mixxxlibrary/mixxxlibraryfeature.h new file mode 100644 index 00000000000..eb3d5cd4e95 --- /dev/null +++ b/src/library/features/mixxxlibrary/mixxxlibraryfeature.h @@ -0,0 +1,90 @@ +// mixxxlibraryfeature.h +// Created 8/23/2009 by RJ Ryan (rryan@mit.edu) + +#ifndef MIXXXLIBRARYFEATURE_H +#define MIXXXLIBRARYFEATURE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "library/libraryfeature.h" +#include "library/features/mixxxlibrary/mixxxlibrarytreemodel.h" +#include "library/dao/trackdao.h" +#include "preferences/usersettings.h" + +class DlgHidden; +class DlgMissing; +class Library; +class BaseTrackCache; +class LibraryTableModel; +class TrackCollection; +class WTrackTableView; +class HiddenTableModel; +class MissingTableModel; + +class MixxxLibraryFeature : public LibraryFeature { + Q_OBJECT + + public: + MixxxLibraryFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); + virtual ~MixxxLibraryFeature(); + + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; + + bool dropAccept(QList urls, QObject* pSource); + bool dragMoveAccept(QUrl url); + TreeItemModel* getChildModel(); + QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) override; + + public slots: + void activate() override; + void activateChild(const QModelIndex& index) override; + void onRightClickChild(const QPoint& pos, const QModelIndex&) override; + void invalidateChild() override; + void refreshLibraryModels(); + + void onSearch(const QString&) override; + + signals: + void unhideHidden(); + void purgeHidden(); + void purgeMissing(); + + protected: + void setChildModel(TreeItemModel* pChild); + + QPointer m_pChildModel; + QPointer m_pSidebar; + + private: + static const QString kLibraryTitle; + static const QList kGroupingOptions; + static const QStringList kGroupingText; + + private slots: + void setTreeSettings(const QVariant &settings); + + private: + QPointer m_pGroupingCombo; + QSharedPointer m_pBaseTrackCache; + LibraryTableModel* m_pLibraryTableModel; + TrackDAO& m_trackDao; + QModelIndex m_lastClickedIndex; + +}; + +#endif /* MIXXXLIBRARYFEATURE_H */ diff --git a/src/library/features/mixxxlibrary/mixxxlibrarytreemodel.cpp b/src/library/features/mixxxlibrary/mixxxlibrarytreemodel.cpp new file mode 100644 index 00000000000..84b100b9483 --- /dev/null +++ b/src/library/features/mixxxlibrary/mixxxlibrarytreemodel.cpp @@ -0,0 +1,343 @@ +#include + +#include "library/coverartcache.h" +#include "library/libraryfeature.h" +#include "library/queryutil.h" +#include "library/trackcollection.h" +#include "util/stringhelper.h" +#include "widget/wpixmapstore.h" + +#include "library/features/mixxxlibrary/mixxxlibrarytreemodel.h" + +namespace { +// This is used since MixxxLibraryTreeModel inherits QAbstractItemModel +// and since the data() method is a const method a class atribute can't be +// changed so this is a hack to allow using coverarts in the model +// since there's another pointer to the class there's no problem in two classes +// coexisting. +QHash > m_hashToIndex; +} + +MixxxLibraryTreeModel::MixxxLibraryTreeModel(LibraryFeature* pFeature, + TrackCollection* pTrackCollection, + UserSettingsPointer pConfig, + QObject* parent) + : TreeItemModel(parent), + m_pFeature(pFeature), + m_pTrackCollection(pTrackCollection), + m_pConfig(pConfig) { + + QString sort = m_pConfig->getValueString(ConfigKey("[Library]", LIBRARYTREEMODEL_SORT)); + if (sort.isNull()) { + // By default sort by Artist -> Album + m_sortOrder << LIBRARYTABLE_ARTIST << LIBRARYTABLE_ALBUM; + } else { + m_sortOrder = sort.split(","); + } + + TrackDAO& trackDAO(pTrackCollection->getTrackDAO()); + connect(&trackDAO, SIGNAL(forceModelUpdate()), this, SLOT(reloadTree())); + connect(&trackDAO, SIGNAL(tracksAdded(QSet)), + this, SLOT(reloadTree())); + connect(&trackDAO, SIGNAL(tracksRemoved(QSet)), + this, SLOT(reloadTree())); + connect(&trackDAO, SIGNAL(trackChanged(TrackId)), + this, SLOT(reloadTree())); + + m_coverQuery << LIBRARYTABLE_COVERART_HASH + << LIBRARYTABLE_COVERART_LOCATION + << LIBRARYTABLE_COVERART_SOURCE + << LIBRARYTABLE_COVERART_TYPE; + + for (QString& s : m_coverQuery) { + s.prepend("library."); + } + m_coverQuery << "track_locations." + TRACKLOCATIONSTABLE_LOCATION; + reloadTree(); +} + + +QVariant MixxxLibraryTreeModel::data(const QModelIndex& index, int role) const { + if (role == AbstractRole::RoleSettings) { + return m_sortOrder; + } + + + TreeItem* pTree = static_cast(index.internalPointer()); + DEBUG_ASSERT_AND_HANDLE(pTree != nullptr) { + return TreeItemModel::data(index, role); + } + + if (role == AbstractRole::RoleGroupingLetter) { + if (pTree == m_pLibraryItem || pTree == m_pSettings) { + return QChar(); + } + return TreeItemModel::data(index, role); + } + + if (role == AbstractRole::RoleBreadCrumb) { + if (pTree == m_pLibraryItem) { + return m_pFeature->title(); + } else { + return TreeItemModel::data(index, role); + } + } + + if (role == AbstractRole::RoleQuery) { + return getQuery(pTree); + } + + // The decoration role contains the icon in QTreeView + if (role == Qt::DecorationRole) { + // Role is decoration role, we need to show the cover art + const CoverInfo& info = pTree->getCoverInfo(); + + // Currently we only support this two types of cover info + if (info.type != CoverInfo::METADATA && info.type != CoverInfo::FILE) { + return TreeItemModel::data(index, role); + } + + CoverArtCache* pCache = CoverArtCache::instance(); + // Set a maximum size of 32px to not use many cache + QPixmap pixmap = pCache->requestCover(info, this, 32, false, true); + + if (pixmap.isNull()) { + // The icon is not in the cache so we need to wait until the + // coverFound slot is called. Since the data function is const + // and we cannot change that we use m_hashToIndex in an anonymous + // namespace to store the future value that we will get + m_hashToIndex[this].insert(info.type, index); + + // Return a temporary icon + return QIcon(":/images/library/cover_default.png"); + } else { + // Good luck icon found + return QIcon(pixmap); + } + } + + return TreeItemModel::data(index, role); +} + +bool MixxxLibraryTreeModel::setData(const QModelIndex& index, const QVariant& value, + int role) { + if (role == AbstractRole::RoleSettings) { + m_sortOrder = value.toStringList(); + m_pConfig->set(ConfigKey("[Library]", LIBRARYTREEMODEL_SORT), + ConfigValue(m_sortOrder.join(","))); + return true; + } else { + return TreeItemModel::setData(index, value, role); + } +} + +void MixxxLibraryTreeModel::reloadTree() { + //qDebug() << "LibraryTreeModel::reloadTracksTree"; + beginResetModel(); + // Create root item + TreeItem* pRootItem = new TreeItem(); + pRootItem->setLibraryFeature(m_pFeature); + + m_pLibraryItem = new TreeItem(tr("Show all"), "", m_pFeature, pRootItem); + pRootItem->appendChild(m_pLibraryItem); + + QString groupTitle = tr("Grouping Options (%1)").arg(m_sortOrder.join(" > ")); + m_pSettings = new TreeItem(groupTitle, "", m_pFeature, pRootItem); + pRootItem->appendChild(m_pSettings); + + // Deletes the old root item if the previous root item was not null + setRootItem(pRootItem); + createTracksTree(); + endResetModel(); +} + +void MixxxLibraryTreeModel::coverFound(const QObject* requestor, int requestReference, + const CoverInfo&, QPixmap pixmap, bool fromCache) { + + if (requestor == this && !pixmap.isNull() && !fromCache) { + auto it = m_hashToIndex[this].find(requestReference); + if (it == m_hashToIndex[this].end()) { + return; + } + + const QModelIndex& index = *it; + emit(dataChanged(index, index)); + } +} + +QVariant MixxxLibraryTreeModel::getQuery(TreeItem* pTree) const { + DEBUG_ASSERT_AND_HANDLE(pTree != nullptr) { + return ""; + } + + if (pTree == m_pLibraryItem) { + return ""; + } else if (pTree == m_pSettings) { + return "$groupingSettings$"; + } + + const QString param("%1:=\"%2\""); + + int depth = 0; + TreeItem* pAux = pTree; + QStringList result; + + // We need to know the depth before doing anything + while (pAux->parent() != m_pRootItem && pAux->parent() != nullptr) { + pAux = pAux->parent(); + ++depth; + } + + // Generate the query + pAux = pTree; + while (depth >= 0) { + QString value = pAux->dataPath().toString(); + if (pAux->isDivider()) { + value.append("*"); + } + result << param.arg(m_sortOrder[depth], value); + + pAux = pAux->parent(); + --depth; + } + + return result.join(" "); +} + +void MixxxLibraryTreeModel::createTracksTree() { + + QStringList columns; + for (const QString& col : m_sortOrder) { + columns << "library." + col; + } + + QStringList sortColumns; +#ifdef __SQLITE3__ + for (const QString& col : m_sortOrder) { + sortColumns << col + " COLLATE localeAwareCompare"; + } +#else + sortColumns = m_sortOrder; +#endif + + // Sorting is required to create the tree because the tree is sorted and + // in order to create a tree with levels it must be sorted too. + QString queryStr = "SELECT COUNT(%3),%1,%2 " + "FROM library LEFT JOIN track_locations " + "ON (%3 = %4) " + "WHERE %5 != 1 AND %7 != 1 " + "GROUP BY %2 " + "ORDER BY %6 "; + queryStr = queryStr.arg(m_coverQuery.join(","), + columns.join(","), + "library." + LIBRARYTABLE_ID, + "track_locations." + TRACKLOCATIONSTABLE_ID, + "library." + LIBRARYTABLE_MIXXXDELETED, + sortColumns.join(","), + "track_locations." + TRACKLOCATIONSTABLE_FSDELETED); + + + QSqlQuery query(m_pTrackCollection->getDatabase()); + query.prepare(queryStr); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return; + } + //qDebug() << "LibraryTreeModel::createTracksTree" << query.executedQuery(); + + int treeDepth = columns.size(); + if (treeDepth <= 0) { + return; + } + QSqlRecord record = query.record(); + + int iAlbum = record.indexOf(LIBRARYTABLE_ALBUM); + CoverIndex cIndex; + cIndex.iCoverHash = record.indexOf(LIBRARYTABLE_COVERART_HASH); + cIndex.iCoverLoc = record.indexOf(LIBRARYTABLE_COVERART_LOCATION); + cIndex.iCoverSrc = record.indexOf(LIBRARYTABLE_COVERART_SOURCE); + cIndex.iCoverType = record.indexOf(LIBRARYTABLE_COVERART_TYPE); + cIndex.iTrackLoc = record.indexOf(TRACKLOCATIONSTABLE_LOCATION); + + int treeStartQueryIndex = m_coverQuery.size() + 1; + QVector lastUsed(treeDepth); + QChar lastHeader; + // We add 1 to the total parents because the first parent is the root item + // with this we can always use parent[i] to get the parent of the element at + // depth i and to set the parent we avoid checking that i + 1 < treeDepth + QVector parent(treeDepth + 1, nullptr); + parent[0] = m_pRootItem; + + while (query.next()) { + for (int i = 0; i < treeDepth; ++i) { + QString treeItemLabel = query.value(treeStartQueryIndex + i).toString(); + QString dataPath = treeItemLabel; + + bool unknown = dataPath.isNull(); + if (unknown) { + dataPath = ""; + treeItemLabel = tr("Unknown"); + } + if (!lastUsed[i].isNull() && dataPath.localeAwareCompare(lastUsed[i]) == 0) { + continue; + } + + if (i == 0 && !unknown) { + // If a new top level is added all the following levels must be + // reset + lastUsed.fill(QString()); + + // Check if a header must be added + QChar c = StringHelper::getFirstCharForGrouping(treeItemLabel); + if (lastHeader != c) { + lastHeader = c; + TreeItem* pTree = new TreeItem(lastHeader, lastHeader, + m_pFeature, parent[0]); + pTree->setDivider(true); + parent[0]->appendChild(pTree); + } + } + + lastUsed[i] = dataPath; + + // We need to create a new item + TreeItem* pTree = new TreeItem(treeItemLabel, dataPath, + m_pFeature, parent[i]); + pTree->setTrackCount(0); + parent[i]->appendChild(pTree); + parent[i + 1] = pTree; + + // Add coverart info + if (treeStartQueryIndex + i == iAlbum) { + addCoverArt(cIndex, query, pTree); + } + } + + // Set track count + int val = query.value(0).toInt(); + for (int i = 1; i < treeDepth + 1; ++i) { + TreeItem* pTree = parent[i]; + if (pTree == nullptr) { + continue; + } + + pTree->setTrackCount(pTree->getTrackCount() + val); + } + } +} + +void MixxxLibraryTreeModel::addCoverArt(const MixxxLibraryTreeModel::CoverIndex& index, + const QSqlQuery& query, TreeItem* pTree) { + CoverInfo c; + c.hash = query.value(index.iCoverHash).toInt(); + c.coverLocation = query.value(index.iCoverLoc).toString(); + c.trackLocation = query.value(index.iTrackLoc).toString(); + + quint16 source = query.value(index.iCoverSrc).toInt(); + quint16 type = query.value(index.iCoverType).toInt(); + c.source = static_cast(source); + c.type = static_cast(type); + pTree->setCoverInfo(c); + pTree->setIcon(QIcon(":/images/library/cover_default.png")); +} diff --git a/src/library/features/mixxxlibrary/mixxxlibrarytreemodel.h b/src/library/features/mixxxlibrary/mixxxlibrarytreemodel.h new file mode 100644 index 00000000000..9fce412ade0 --- /dev/null +++ b/src/library/features/mixxxlibrary/mixxxlibrarytreemodel.h @@ -0,0 +1,64 @@ +#ifndef LIBRARYTREEMODEL_H +#define LIBRARYTREEMODEL_H + +#include +#include +#include +#include + +#include "library/treeitemmodel.h" +#include "preferences/usersettings.h" + +class CoverInfo; +class LibraryFeature; +class TrackCollection; + +const QString LIBRARYTREEMODEL_SORT = "LibraryTree_Sort"; // ConfigValue key for Library Tree Model sort + +class MixxxLibraryTreeModel : public TreeItemModel { + Q_OBJECT + public: + MixxxLibraryTreeModel(LibraryFeature* pFeature, + TrackCollection* pTrackCollection, + UserSettingsPointer pConfig, + QObject* parent = nullptr); + + virtual QVariant data(const QModelIndex& index, int role) const; + virtual bool setData(const QModelIndex& index, const QVariant& value, int role); + + public slots: + void reloadTree() override; + + private: + + struct CoverIndex { + int iCoverHash; + int iCoverLoc; + int iTrackLoc; + int iCoverSrc; + int iCoverType; + }; + + private slots: + void coverFound(const QObject* requestor, int requestReference, const CoverInfo&, + QPixmap pixmap, bool fromCache); + + private: + QVariant getQuery(TreeItem* pTree) const; + void createTracksTree(); + void addCoverArt(const CoverIndex& index, const QSqlQuery& query, TreeItem* pTree); + + LibraryFeature* m_pFeature; + TrackCollection* m_pTrackCollection; + UserSettingsPointer m_pConfig; + + QStringList m_sortOrder; + QStringList m_coverQuery; + + TreeItem* m_pSettings; + TreeItem* m_pLibraryItem; + + bool m_folderRecursive; +}; + +#endif // LIBRARYTREEMODEL_H diff --git a/src/library/playlistfeature.cpp b/src/library/features/playlist/playlistfeature.cpp similarity index 69% rename from src/library/playlistfeature.cpp rename to src/library/features/playlist/playlistfeature.cpp index 9f7f81fc1a1..7396dd5ab41 100644 --- a/src/library/playlistfeature.cpp +++ b/src/library/features/playlist/playlistfeature.cpp @@ -1,34 +1,29 @@ -#include -#include +#include #include #include +#include -#include "library/playlistfeature.h" +#include "library/features/playlist/playlistfeature.h" -#include "widget/wlibrary.h" -#include "widget/wlibrarysidebar.h" -#include "widget/wlibrarytextbrowser.h" -#include "library/trackcollection.h" -#include "library/playlisttablemodel.h" -#include "library/treeitem.h" -#include "library/queryutil.h" -#include "library/parser.h" #include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/playlist/playlisttablemodel.h" +#include "library/parser.h" +#include "library/queryutil.h" +#include "library/trackcollection.h" +#include "library/treeitemmodel.h" #include "sources/soundsourceproxy.h" #include "util/dnd.h" #include "util/duration.h" +#include "widget/wlibrarytextbrowser.h" -PlaylistFeature::PlaylistFeature(QObject* parent, - TrackCollection* pTrackCollection, - UserSettingsPointer pConfig) - : BasePlaylistFeature(parent, pConfig, pTrackCollection, - "PLAYLISTHOME") { - m_pPlaylistTableModel = new PlaylistTableModel(this, pTrackCollection, - "mixxx.db.model.playlist"); - +PlaylistFeature::PlaylistFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : BasePlaylistFeature(pConfig, pLibrary, parent, pTrackCollection) { //construct child model TreeItem *rootItem = new TreeItem(); - m_childModel.setRootItem(rootItem); + m_childModel->setRootItem(rootItem); constructChildModel(-1); } @@ -39,15 +34,23 @@ QVariant PlaylistFeature::title() { return tr("Playlists"); } -QIcon PlaylistFeature::getIcon() { - return QIcon(":/images/library/ic_library_playlist.png"); +QString PlaylistFeature::getIconPath() { + return ":/images/library/ic_library_playlist.png"; +} + +QString PlaylistFeature::getSettingsName() const { + return "PlaylistFeature"; +} + +bool PlaylistFeature::isSinglePane() const { + return false; } void PlaylistFeature::onRightClick(const QPoint& globalPos) { m_lastRightClickedIndex = QModelIndex(); //Create the right-click menu - QMenu menu(NULL); + QMenu menu(nullptr); menu.addAction(m_pCreatePlaylistAction); menu.addSeparator(); menu.addAction(m_pCreateImportPlaylistAction); @@ -56,7 +59,7 @@ void PlaylistFeature::onRightClick(const QPoint& globalPos) { void PlaylistFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index) { //Save the model index so we can get it in the action slots... - m_lastRightClickedIndex = index; + m_lastChildClicked[m_featurePane] = m_lastRightClickedIndex = index; int playlistId = playlistIdFromIndex(index); bool locked = m_playlistDao.isPlaylistLocked(playlistId); @@ -66,25 +69,36 @@ void PlaylistFeature::onRightClickChild(const QPoint& globalPos, QModelIndex ind m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock")); //Create the right-click menu - QMenu menu(NULL); + QMenu menu(nullptr); menu.addAction(m_pCreatePlaylistAction); menu.addSeparator(); - menu.addAction(m_pAddToAutoDJAction); - menu.addAction(m_pAddToAutoDJTopAction); - menu.addSeparator(); - menu.addAction(m_pRenamePlaylistAction); - menu.addAction(m_pDuplicatePlaylistAction); - menu.addAction(m_pDeletePlaylistAction); - menu.addAction(m_pLockPlaylistAction); - menu.addSeparator(); - menu.addAction(m_pAnalyzePlaylistAction); - menu.addSeparator(); + if (playlistId >= 0) { + menu.addAction(m_pAddToAutoDJAction); + menu.addAction(m_pAddToAutoDJTopAction); + menu.addSeparator(); + menu.addAction(m_pRenamePlaylistAction); + menu.addAction(m_pDuplicatePlaylistAction); + menu.addAction(m_pDeletePlaylistAction); + menu.addAction(m_pLockPlaylistAction); + menu.addSeparator(); + menu.addAction(m_pAnalyzePlaylistAction); + menu.addSeparator(); + } menu.addAction(m_pImportPlaylistAction); - menu.addAction(m_pExportPlaylistAction); - menu.addAction(m_pExportTrackFilesAction); + if (playlistId >= 0) { + menu.addAction(m_pExportPlaylistAction); + menu.addAction(m_pExportTrackFilesAction); + } menu.exec(globalPos); } + +bool PlaylistFeature::dragMoveAccept(QUrl url) { + return SoundSourceProxy::isUrlSupported(url) || + Parser::isPlaylistFilenameSupported(url.toLocalFile()); +} + + bool PlaylistFeature::dropAcceptChild(const QModelIndex& index, QList urls, QObject* pSource) { int playlistId = playlistIdFromIndex(index); @@ -111,11 +125,26 @@ bool PlaylistFeature::dropAcceptChild(const QModelIndex& index, QList urls } } + // Request a name for the playlist if it's a new playlist + if (playlistId < 0) { + QString name = getValidPlaylistName(); + if (name.isNull()) { + // The user has canceled + return false; + } + + playlistId = m_playlistDao.createPlaylist(name); + // An error happened + if (playlistId < 0) { + return false; + } + } + // Return whether appendTracksToPlaylist succeeded. return m_playlistDao.appendTracksToPlaylist(trackIds, playlistId); } -bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, QUrl url) { +bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, QUrl url) { int playlistId = playlistIdFromIndex(index); bool locked = m_playlistDao.isPlaylistLocked(playlistId); @@ -132,50 +161,39 @@ void PlaylistFeature::buildPlaylistList() { "AS SELECT " " Playlists.id AS id, " " Playlists.name AS name, " - " LOWER(Playlists.name) AS sort_name, " " COUNT(case library.mixxx_deleted when 0 then 1 else null end) AS count, " " SUM(case library.mixxx_deleted when 0 then library.duration else 0 end) AS durationSeconds " "FROM Playlists " "LEFT JOIN PlaylistTracks ON PlaylistTracks.playlist_id = Playlists.id " "LEFT JOIN library ON PlaylistTracks.track_id = library.id " "WHERE Playlists.hidden = 0 " - "GROUP BY Playlists.id;"); + "GROUP BY Playlists.id " + "ORDER BY LOWER(Playlists.name);"); QSqlQuery query(m_pTrackCollection->getDatabase()); if (!query.exec(queryString)) { LOG_FAILED_QUERY(query); } - - // Setup the sidebar playlist model - QSqlTableModel playlistTableModel(this, m_pTrackCollection->getDatabase()); - playlistTableModel.setTable("PlaylistsCountsDurations"); - playlistTableModel.setSort(playlistTableModel.fieldIndex("sort_name"), - Qt::AscendingOrder); - playlistTableModel.select(); - while (playlistTableModel.canFetchMore()) { - playlistTableModel.fetchMore(); - } - QSqlRecord record = playlistTableModel.record(); - int nameColumn = record.indexOf("name"); - int idColumn = record.indexOf("id"); - int countColumn = record.indexOf("count"); - int durationColumn = record.indexOf("durationSeconds"); - - for (int row = 0; row < playlistTableModel.rowCount(); ++row) { - int id = playlistTableModel.data( - playlistTableModel.index(row, idColumn)).toInt(); - QString name = playlistTableModel.data( - playlistTableModel.index(row, nameColumn)).toString(); - int count = playlistTableModel.data( - playlistTableModel.index(row, countColumn)).toInt(); - int duration = playlistTableModel.data( - playlistTableModel.index(row, durationColumn)).toInt(); - m_playlistList.append(qMakePair(id, QString("%1 (%2) %3") - .arg(name, QString::number(count), - mixxx::Duration::formatSeconds(duration)))); + + QSqlRecord record = query.record(); + int iName = record.indexOf("name"); + int iId = record.indexOf("id"); + int iCount = record.indexOf("count"); + int iDuration = record.indexOf("durationSeconds"); + + while (query.next()) { + int id = query.value(iId).toInt(); + QString name = query.value(iName).toString(); + int count = query.value(iCount).toInt(); + int duration = query.value(iDuration).toInt(); + QString itemName = "%1 (%2) %3"; + itemName = itemName.arg(name, + QString::number(count), + mixxx::Duration::formatSeconds(duration)); + m_playlistList << PlaylistItem(id, itemName); } } -void PlaylistFeature::decorateChild(TreeItem* item, int playlist_id) { +void PlaylistFeature::decorateChild(TreeItem* item, int playlist_id) { if (m_playlistDao.isPlaylistLocked(playlist_id)) { item->setIcon(QIcon(":/images/library/ic_library_locked.png")); } else { @@ -183,6 +201,10 @@ void PlaylistFeature::decorateChild(TreeItem* item, int playlist_id) { } } +PlaylistTableModel* PlaylistFeature::constructTableModel() { + return new PlaylistTableModel(this, m_pTrackCollection, "mixxx.db.model.playlist"); +} + void PlaylistFeature::slotPlaylistTableChanged(int playlistId) { if (!m_pPlaylistTableModel) { return; @@ -192,8 +214,8 @@ void PlaylistFeature::slotPlaylistTableChanged(int playlistId) { enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); if (type == PlaylistDAO::PLHT_NOT_HIDDEN || type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); m_lastRightClickedIndex = constructChildModel(playlistId); + m_lastChildClicked[m_featurePane] = m_lastRightClickedIndex; } } @@ -222,8 +244,8 @@ void PlaylistFeature::slotPlaylistTableRenamed(int playlistId, enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); if (type == PlaylistDAO::PLHT_NOT_HIDDEN || type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); m_lastRightClickedIndex = constructChildModel(playlistId); + m_lastChildClicked[m_featurePane] = m_lastRightClickedIndex; if (type != PlaylistDAO::PLHT_UNKNOWN) { activatePlaylist(playlistId); } diff --git a/src/library/playlistfeature.h b/src/library/features/playlist/playlistfeature.h similarity index 69% rename from src/library/playlistfeature.h rename to src/library/features/playlist/playlistfeature.h index 0d129eff74b..7aa63d5c53e 100644 --- a/src/library/playlistfeature.h +++ b/src/library/features/playlist/playlistfeature.h @@ -11,7 +11,7 @@ #include #include -#include "library/baseplaylistfeature.h" +#include "library/features/baseplaylist/baseplaylistfeature.h" #include "preferences/usersettings.h" class TrackCollection; @@ -20,13 +20,18 @@ class TreeItem; class PlaylistFeature : public BasePlaylistFeature { Q_OBJECT public: - PlaylistFeature(QObject* parent, TrackCollection* pTrackCollection, - UserSettingsPointer pConfig); + PlaylistFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); virtual ~PlaylistFeature(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; + bool isSinglePane() const override; + bool dragMoveAccept(QUrl url); bool dropAcceptChild(const QModelIndex& index, QList urls, QObject* pSource); bool dragMoveAcceptChild(const QModelIndex& index, QUrl url); @@ -42,6 +47,7 @@ class PlaylistFeature : public BasePlaylistFeature { protected: void buildPlaylistList(); void decorateChild(TreeItem *pChild, int playlist_id); + PlaylistTableModel* constructTableModel(); private: QString getRootViewHtml() const; diff --git a/src/library/playlisttablemodel.cpp b/src/library/features/playlist/playlisttablemodel.cpp similarity index 78% rename from src/library/playlisttablemodel.cpp rename to src/library/features/playlist/playlisttablemodel.cpp index e34ab73f785..63cb817b990 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/features/playlist/playlisttablemodel.cpp @@ -1,4 +1,6 @@ -#include "library/playlisttablemodel.h" +#include + +#include "library/features/playlist/playlisttablemodel.h" #include "library/queryutil.h" #include "mixer/playermanager.h" @@ -15,14 +17,35 @@ PlaylistTableModel::~PlaylistTableModel() { } void PlaylistTableModel::setTableModel(int playlistId) { - //qDebug() << "PlaylistTableModel::setTableModel" << playlistId; - if (m_iPlaylistId == playlistId) { - qDebug() << "Already focused on playlist " << playlistId; + m_iPlaylistId = playlistId; + QSet temp; + temp.insert(playlistId); + setTableModel(temp); +} + +void PlaylistTableModel::setTableModel(const QSet &playlistIds) { + if (m_playlistIds == playlistIds) { + qDebug() << "Already focused on playlist " << playlistIds; return; } + + if (playlistIds.size() > 1) { + // If we are showing many playlist at once this is not a real playlist + m_iPlaylistId = -1; + } else { + m_iPlaylistId = *playlistIds.begin(); + } - m_iPlaylistId = playlistId; - QString playlistTableName = "playlist_" + QString::number(m_iPlaylistId); + m_playlistIds = playlistIds; + + QString playlistTableName = "playlist"; + QStringList sIds; + for (int n : m_playlistIds) { + QString sNum = QString::number(n); + sIds << sNum; + playlistTableName.append("_" + sNum); + } + QSqlQuery query(m_database); FieldEscaper escaper(m_database); @@ -42,10 +65,10 @@ void PlaylistTableModel::setTableModel(int playlistId) { QString queryString = QString("CREATE TEMPORARY VIEW IF NOT EXISTS %1 AS " "SELECT %2 FROM PlaylistTracks " "INNER JOIN library ON library.id = PlaylistTracks.track_id " - "WHERE PlaylistTracks.playlist_id = %3") + "WHERE PlaylistTracks.playlist_id IN (%3)") .arg(escaper.escapeString(playlistTableName), columns.join(","), - QString::number(playlistId)); + sIds.join(",")); if (!m_showAll) { queryString.append(" AND library.mixxx_deleted = 0"); } @@ -74,9 +97,8 @@ int PlaylistTableModel::addTracks(const QModelIndex& index, if (locations.isEmpty()) { return 0; } - - const int positionColumn = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - int position = index.sibling(index.row(), positionColumn).data().toInt(); + + int position = getPosition(index); // Handle weird cases like a drag and drop to an invalid index if (position <= 0) { @@ -84,7 +106,7 @@ int PlaylistTableModel::addTracks(const QModelIndex& index, } QList fileInfoList; - foreach (QString fileLocation, locations) { + for (const QString& fileLocation : locations) { QFileInfo fileInfo(fileLocation); if (fileInfo.exists()) { fileInfoList.append(fileInfo); @@ -116,9 +138,8 @@ void PlaylistTableModel::removeTrack(const QModelIndex& index) { return; } - const int positionColumnIndex = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - int position = index.sibling(index.row(), positionColumnIndex).data().toInt(); - m_pTrackCollection->getPlaylistDAO().removeTrackFromPlaylist(m_iPlaylistId, position); + m_pTrackCollection->getPlaylistDAO() + .removeTrackFromPlaylist(m_iPlaylistId, getPosition(index)); } void PlaylistTableModel::removeTracks(const QModelIndexList& indices) { @@ -126,24 +147,19 @@ void PlaylistTableModel::removeTracks(const QModelIndexList& indices) { return; } - const int positionColumnIndex = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - QList trackPositions; - foreach (QModelIndex index, indices) { - int trackPosition = index.sibling(index.row(), positionColumnIndex).data().toInt(); - trackPositions.append(trackPosition); + for (const QModelIndex& index : indices) { + trackPositions.append(getPosition(index)); } - m_pTrackCollection->getPlaylistDAO().removeTracksFromPlaylist(m_iPlaylistId,trackPositions); + m_pTrackCollection->getPlaylistDAO() + .removeTracksFromPlaylist(m_iPlaylistId, trackPositions); } void PlaylistTableModel::moveTrack(const QModelIndex& sourceIndex, const QModelIndex& destIndex) { - - int playlistPositionColumn = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - - int newPosition = destIndex.sibling(destIndex.row(), playlistPositionColumn).data().toInt(); - int oldPosition = sourceIndex.sibling(sourceIndex.row(), playlistPositionColumn).data().toInt(); + int newPosition = getPosition(destIndex); + int oldPosition = getPosition(sourceIndex); if (newPosition > oldPosition) { // new position moves up due to closing the gap of the old position @@ -170,17 +186,15 @@ bool PlaylistTableModel::isLocked() { void PlaylistTableModel::shuffleTracks(const QModelIndexList& shuffle, const QModelIndex& exclude) { QList positions; QHash allIds; - const int positionColumn = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - const int idColumn = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ID); int excludePos = -1; if (exclude.row() > -1) { // this is used to exclude the already loaded track at pos #1 if used from running Auto-DJ - excludePos = exclude.sibling(exclude.row(), positionColumn).data().toInt(); + excludePos = getPosition(exclude); } if (shuffle.count() > 1) { // if there is more then one track selected, shuffle selection only - foreach(QModelIndex shuffleIndex, shuffle) { - int oldPosition = shuffleIndex.sibling(shuffleIndex.row(), positionColumn).data().toInt(); + for (const QModelIndex& shuffleIndex : shuffle) { + int oldPosition = getPosition(shuffleIndex); if (oldPosition != excludePos) { positions.append(oldPosition); } @@ -189,7 +203,7 @@ void PlaylistTableModel::shuffleTracks(const QModelIndexList& shuffle, const QMo // if there is only one track selected, shuffle all tracks int numOfTracks = rowCount(); for (int i = 0; i < numOfTracks; i++) { - int oldPosition = index(i, positionColumn).data().toInt(); + int oldPosition = getPosition(index(i, 0)); if (oldPosition != excludePos) { positions.append(oldPosition); } @@ -198,8 +212,8 @@ void PlaylistTableModel::shuffleTracks(const QModelIndexList& shuffle, const QMo // Set up list of all IDs int numOfTracks = rowCount(); for (int i = 0; i < numOfTracks; i++) { - int position = index(i, positionColumn).data().toInt(); - TrackId trackId(index(i, idColumn).data()); + int position = getPosition(index(i, 0)); + TrackId trackId(getTrackId(index(i, 0))); allIds.insert(position, trackId); } m_pTrackCollection->getPlaylistDAO().shuffleTracks(m_iPlaylistId, positions, allIds); @@ -260,8 +274,42 @@ TrackModel::CapabilitiesFlags PlaylistTableModel::getCapabilities() const { return caps; } +void PlaylistTableModel::saveSelection(const QModelIndexList& selection) { + m_savedSelectionIndices.clear(); + + for (const QModelIndex& index : selection) { + m_savedSelectionIndices.insert(getPosition(index)); + } +} + +QModelIndexList PlaylistTableModel::getSavedSelectionIndices() { + QModelIndexList ret; + for (const int& pos : m_savedSelectionIndices) { + auto it = m_positionToRow.find(pos); + if (it != m_positionToRow.constEnd()) { + ret << index(*it, 0); + } + } + return ret; +} + +void PlaylistTableModel::select() { + BaseSqlTableModel::select(); + + m_positionToRow.clear(); + for (int i = 0; i < rowCount(); ++i) { + int pos = getPosition(index(i, 0)); + m_positionToRow[pos] = i; + } +} + void PlaylistTableModel::playlistChanged(int playlistId) { if (playlistId == m_iPlaylistId) { select(); // Repopulate the data model. } } + +int PlaylistTableModel::getPosition(const QModelIndex& index) { + const int positionColumn = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); + return index.sibling(index.row(), positionColumn).data().toInt(); +} diff --git a/src/library/playlisttablemodel.h b/src/library/features/playlist/playlisttablemodel.h similarity index 78% rename from src/library/playlisttablemodel.h rename to src/library/features/playlist/playlisttablemodel.h index 070a187f269..0e5ae2d6c9c 100644 --- a/src/library/playlisttablemodel.h +++ b/src/library/features/playlist/playlisttablemodel.h @@ -1,5 +1,6 @@ #ifndef PLAYLISTTABLEMODEL_H #define PLAYLISTTABLEMODEL_H +#include #include "library/basesqltablemodel.h" #include "library/dao/playlistdao.h" @@ -10,8 +11,9 @@ class PlaylistTableModel : public BaseSqlTableModel { PlaylistTableModel(QObject* parent, TrackCollection* pTrackCollection, const char* settingsNamespace, bool showAll = false); ~PlaylistTableModel() final; - void setTableModel(int playlistId = -1); + void setTableModel(const QSet& playlistIds); + int getPlaylist() const { return m_iPlaylistId; } @@ -21,6 +23,11 @@ class PlaylistTableModel : public BaseSqlTableModel { const QModelIndex& destIndex); void removeTrack(const QModelIndex& index); void shuffleTracks(const QModelIndexList& shuffle, const QModelIndex& exclude); + + void saveSelection(const QModelIndexList& selection); + QModelIndexList getSavedSelectionIndices(); + + void select() override; bool isColumnInternal(int column) final; bool isColumnHiddenByDefault(int column) final; @@ -36,8 +43,15 @@ class PlaylistTableModel : public BaseSqlTableModel { void playlistChanged(int playlistId); private: + + int getPosition(const QModelIndex& index); + int m_iPlaylistId; + QSet m_playlistIds; bool m_showAll; + + QHash m_positionToRow; + QSet m_savedSelectionIndices; }; #endif diff --git a/src/library/recording/dlgrecording.cpp b/src/library/features/recording/dlgrecording.cpp similarity index 50% rename from src/library/recording/dlgrecording.cpp rename to src/library/features/recording/dlgrecording.cpp index 043a9fffd15..4aa19170f24 100644 --- a/src/library/recording/dlgrecording.cpp +++ b/src/library/features/recording/dlgrecording.cpp @@ -1,36 +1,23 @@ #include #include "control/controlobject.h" -#include "library/recording/dlgrecording.h" +#include "library/features/recording/dlgrecording.h" #include "library/trackcollection.h" #include "widget/wwidget.h" #include "widget/wskincolor.h" #include "widget/wtracktableview.h" #include "util/assert.h" -DlgRecording::DlgRecording(QWidget* parent, UserSettingsPointer pConfig, - Library* pLibrary, TrackCollection* pTrackCollection, - RecordingManager* pRecordingManager, KeyboardEventFilter* pKeyboard) - : QWidget(parent), - m_pConfig(pConfig), +DlgRecording::DlgRecording(QWidget* parent, TrackCollection* pTrackCollection, + RecordingManager* pRecordingManager) + : QFrame(parent), m_pTrackCollection(pTrackCollection), - m_browseModel(this, m_pTrackCollection, pRecordingManager), - m_proxyModel(&m_browseModel), + m_pBrowseModel(nullptr), + m_pProxyModel(nullptr), m_bytesRecordedStr("--"), m_durationRecordedStr("--:--"), m_pRecordingManager(pRecordingManager) { setupUi(this); - m_pTrackTableView = new WTrackTableView(this, pConfig, m_pTrackCollection, false); // No sorting - m_pTrackTableView->installEventFilter(pKeyboard); - - connect(m_pTrackTableView, SIGNAL(loadTrack(TrackPointer)), - this, SIGNAL(loadTrack(TrackPointer))); - connect(m_pTrackTableView, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool)), - this, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool))); - connect(pLibrary, SIGNAL(setTrackTableFont(QFont)), - m_pTrackTableView, SLOT(setTrackTableFont(QFont))); - connect(pLibrary, SIGNAL(setTrackTableRowHeight(int)), - m_pTrackTableView, SLOT(setTrackTableRowHeight(int))); connect(m_pRecordingManager, SIGNAL(isRecording(bool)), this, SLOT(slotRecordingEnabled(bool))); @@ -39,22 +26,8 @@ DlgRecording::DlgRecording(QWidget* parent, UserSettingsPointer pConfig, connect(m_pRecordingManager, SIGNAL(durationRecorded(QString)), this, SLOT(slotDurationRecorded(QString))); - QBoxLayout* box = dynamic_cast(layout()); - DEBUG_ASSERT_AND_HANDLE(box) { //Assumes the form layout is a QVBox/QHBoxLayout! - } else { - box->removeWidget(m_pTrackTablePlaceholder); - m_pTrackTablePlaceholder->hide(); - box->insertWidget(1, m_pTrackTableView); - } - m_recordingDir = m_pRecordingManager->getRecordingDir(); - m_proxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive); - m_proxyModel.setSortCaseSensitivity(Qt::CaseInsensitive); - - m_browseModel.setPath(m_recordingDir); - m_pTrackTableView->loadTrackModel(&m_proxyModel); - connect(pushButtonRecording, SIGNAL(toggled(bool)), this, SLOT(toggleRecording(bool))); label->setText(""); @@ -66,39 +39,23 @@ DlgRecording::~DlgRecording() { void DlgRecording::onShow() { m_recordingDir = m_pRecordingManager->getRecordingDir(); - m_browseModel.setPath(m_recordingDir); -} - -void DlgRecording::refreshBrowseModel() { - m_browseModel.setPath(m_recordingDir); -} - -void DlgRecording::onSearch(const QString& text) { - m_proxyModel.search(text); + m_pBrowseModel->setPath(m_recordingDir); } -void DlgRecording::slotRestoreSearch() { - emit(restoreSearch(currentSearch())); -} - -void DlgRecording::loadSelectedTrack() { - m_pTrackTableView->loadSelectedTrack(); -} - -void DlgRecording::slotSendToAutoDJ() { - m_pTrackTableView->slotSendToAutoDJ(); -} +void DlgRecording::setProxyTrackModel(ProxyTrackModel* pProxyModel) { + m_pProxyModel = pProxyModel; -void DlgRecording::slotSendToAutoDJTop() { - m_pTrackTableView->slotSendToAutoDJTop(); + m_pProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_pProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); } -void DlgRecording::loadSelectedTrackToGroup(QString group, bool play) { - m_pTrackTableView->loadSelectedTrackToGroup(group, play); +void DlgRecording::setBrowseTableModel(BrowseTableModel* pBrowseModel) { + m_pBrowseModel = pBrowseModel; + m_pBrowseModel->setPath(m_recordingDir); } -void DlgRecording::moveSelection(int delta) { - m_pTrackTableView->moveSelection(delta); +void DlgRecording::refreshBrowseModel() { + m_pBrowseModel->setPath(m_recordingDir); } void DlgRecording::toggleRecording(bool toggle) { @@ -125,7 +82,7 @@ void DlgRecording::slotRecordingEnabled(bool isRecording) { label->setEnabled(false); } //This will update the recorded track table view - m_browseModel.setPath(m_recordingDir); + m_pBrowseModel->setPath(m_recordingDir); } // gets number of recorded bytes and update label @@ -149,11 +106,3 @@ void DlgRecording::refreshLabel() { .arg(m_durationRecordedStr); label->setText(text); } - -void DlgRecording::setTrackTableFont(const QFont& font) { - m_pTrackTableView->setTrackTableFont(font); -} - -void DlgRecording::setTrackTableRowHeight(int rowHeight) { - m_pTrackTableView->setTrackTableRowHeight(rowHeight); -} diff --git a/src/library/features/recording/dlgrecording.h b/src/library/features/recording/dlgrecording.h new file mode 100644 index 00000000000..e9caed4b897 --- /dev/null +++ b/src/library/features/recording/dlgrecording.h @@ -0,0 +1,51 @@ +#ifndef DLGRECORDING_H +#define DLGRECORDING_H + +#include "preferences/usersettings.h" +#include "library/features/browse/browsetablemodel.h" +#include "library/libraryview.h" +#include "library/proxytrackmodel.h" +#include "library/library.h" +#include "library/trackcollection.h" +#include "controllers/keyboard/keyboardeventfilter.h" +#include "recording/recordingmanager.h" +#include "track/track.h" +#include "library/features/recording/ui_dlgrecording.h" + +class PlaylistTableModel; +class QSqlTableModel; +class WTrackTableView; + +class DlgRecording : public QFrame, public Ui::DlgRecording { + Q_OBJECT + public: + DlgRecording(QWidget *parent, TrackCollection* pTrackCollection, + RecordingManager* pRecManager); + virtual ~DlgRecording(); + + virtual void onShow(); + void setProxyTrackModel(ProxyTrackModel* pProxyModel); + void setBrowseTableModel(BrowseTableModel* pBrowseModel); + + public slots: + void toggleRecording(bool toggle); + void slotRecordingEnabled(bool); + void slotBytesRecorded(int); + void refreshBrowseModel(); + void slotDurationRecorded(QString durationRecorded); + + private: + void refreshLabel(); + + TrackCollection* m_pTrackCollection; + BrowseTableModel* m_pBrowseModel; + ProxyTrackModel* m_pProxyModel; + QString m_recordingDir; + + QString m_bytesRecordedStr; + QString m_durationRecordedStr; + + RecordingManager* m_pRecordingManager; +}; + +#endif //DLGRECORDING_H diff --git a/src/library/features/recording/dlgrecording.ui b/src/library/features/recording/dlgrecording.ui new file mode 100644 index 00000000000..895adc9ac48 --- /dev/null +++ b/src/library/features/recording/dlgrecording.ui @@ -0,0 +1,54 @@ + + + DlgRecording + + + + 0 + 0 + 391 + 322 + + + + Recordings + + + + + + Start Recording + + + true + + + + + + + Status: + + + true + + + + + + + Qt::Vertical + + + + 20 + 247 + + + + + + + + + diff --git a/src/library/features/recording/recordingfeature.cpp b/src/library/features/recording/recordingfeature.cpp new file mode 100644 index 00000000000..9afb871d8c6 --- /dev/null +++ b/src/library/features/recording/recordingfeature.cpp @@ -0,0 +1,98 @@ +// recordingfeature.cpp +// Created 03/26/2010 by Tobias Rafreider + +#include +#include "controllers/keyboard/keyboardeventfilter.h" +#include "library/features/recording/dlgrecording.h" +#include "library/features/recording/recordingfeature.h" +#include "library/library.h" +#include "library/trackcollection.h" +#include "track/track.h" +#include "widget/wtracktableview.h" + +RecordingFeature::RecordingFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection, + RecordingManager* pRecordingManager) + : LibraryFeature(pConfig, pLibrary, pTrackCollection, parent), + m_pTrackCollection(pTrackCollection), + m_pRecordingManager(pRecordingManager), + m_pRecordingView(nullptr), + m_pBrowseModel(nullptr), + m_pProxyModel(nullptr) { + + TreeItem* pRoot = new TreeItem(); + pRoot->setLibraryFeature(this); + m_childModel.setRootItem(pRoot); +} + +RecordingFeature::~RecordingFeature() { + +} + +QVariant RecordingFeature::title() { + return QVariant(tr("Recordings")); +} + +QString RecordingFeature::getIconPath() { + return ":/images/library/ic_library_recordings.png"; +} + +QString RecordingFeature::getSettingsName() const { + return "RecordingFeature"; +} + +TreeItemModel* RecordingFeature::getChildModel() { + return &m_childModel; +} + +QWidget* RecordingFeature::createPaneWidget( + KeyboardEventFilter* pKeyboard, int paneId) { + Q_UNUSED(pKeyboard); + WTrackTableView* pTable = LibraryFeature::createTableWidget(paneId); + pTable->setSorting(false); + return pTable; +} + +QWidget *RecordingFeature::createInnerSidebarWidget( + KeyboardEventFilter* pKeyboard) { + m_pRecordingView = new DlgRecording(nullptr, + m_pTrackCollection, + m_pRecordingManager); + m_pRecordingView->installEventFilter(pKeyboard); + m_pRecordingView->setBrowseTableModel(getBrowseTableModel()); + m_pRecordingView->setProxyTrackModel(getProxyTrackModel()); + + return m_pRecordingView; +} + + +void RecordingFeature::activate() { + DEBUG_ASSERT_AND_HANDLE(!m_pRecordingView.isNull()) { + return; + } + + m_pRecordingView->refreshBrowseModel(); + showTrackModel(getProxyTrackModel()); + showBreadCrumb(); + restoreSearch(""); + +} + +BrowseTableModel* RecordingFeature::getBrowseTableModel() { + if (m_pBrowseModel.isNull()) { + m_pBrowseModel = new BrowseTableModel( + this, m_pTrackCollection, m_pRecordingManager); + } + + return m_pBrowseModel; +} + +ProxyTrackModel* RecordingFeature::getProxyTrackModel() { + if (m_pProxyModel.isNull()) { + m_pProxyModel = new ProxyTrackModel(getBrowseTableModel()); + } + + return m_pProxyModel; +} diff --git a/src/library/recording/recordingfeature.h b/src/library/features/recording/recordingfeature.h similarity index 51% rename from src/library/recording/recordingfeature.h rename to src/library/features/recording/recordingfeature.h index 7ff7e43ac46..d83b80cae3c 100644 --- a/src/library/recording/recordingfeature.h +++ b/src/library/features/recording/recordingfeature.h @@ -8,29 +8,32 @@ #include #include "preferences/usersettings.h" -#include "library/browse/browsetablemodel.h" -#include "library/browse/foldertreemodel.h" +#include "library/features/browse/browsetablemodel.h" +#include "library/features/browse/foldertreemodel.h" #include "library/libraryfeature.h" #include "library/proxytrackmodel.h" #include "recording/recordingmanager.h" -class Library; class TrackCollection; +class WTrackTableView; +class DlgRecording; class RecordingFeature : public LibraryFeature { Q_OBJECT public: - RecordingFeature(Library* parent, - UserSettingsPointer pConfig, + RecordingFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, TrackCollection* pTrackCollection, RecordingManager* pRecordingManager); virtual ~RecordingFeature(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; - void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard); + QWidget* createPaneWidget(KeyboardEventFilter *pKeyboard, int paneId) override; + QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard) override; TreeItemModel* getChildModel(); @@ -39,16 +42,19 @@ class RecordingFeature : public LibraryFeature { signals: void setRootIndex(const QModelIndex&); - void requestRestoreSearch(); - void refreshBrowseModel(); private: - UserSettingsPointer m_pConfig; - Library* m_pLibrary; + + BrowseTableModel* getBrowseTableModel(); + ProxyTrackModel* getProxyTrackModel(); + TrackCollection* m_pTrackCollection; FolderTreeModel m_childModel; - const static QString m_sRecordingViewName; RecordingManager* m_pRecordingManager; + + QPointer m_pRecordingView; + QPointer m_pBrowseModel; + QPointer m_pProxyModel; }; #endif diff --git a/src/library/rhythmbox/rhythmboxfeature.cpp b/src/library/features/rhythmbox/rhythmboxfeature.cpp similarity index 93% rename from src/library/rhythmbox/rhythmboxfeature.cpp rename to src/library/features/rhythmbox/rhythmboxfeature.cpp index 9d9f7d331d2..396607d1a32 100644 --- a/src/library/rhythmbox/rhythmboxfeature.cpp +++ b/src/library/features/rhythmbox/rhythmboxfeature.cpp @@ -3,15 +3,17 @@ #include #include -#include "library/rhythmbox/rhythmboxfeature.h" +#include "library/features/rhythmbox/rhythmboxfeature.h" -#include "library/baseexternaltrackmodel.h" -#include "library/baseexternalplaylistmodel.h" -#include "library/treeitem.h" +#include "library/features/baseexternalfeature/baseexternaltrackmodel.h" +#include "library/features/baseexternalfeature/baseexternalplaylistmodel.h" #include "library/queryutil.h" -RhythmboxFeature::RhythmboxFeature(QObject* parent, TrackCollection* pTrackCollection) - : BaseExternalLibraryFeature(parent, pTrackCollection), +RhythmboxFeature::RhythmboxFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection) + : BaseExternalLibraryFeature(pConfig, pLibrary, parent, pTrackCollection), m_pTrackCollection(pTrackCollection), m_cancelImport(false) { QString tableName = "rhythmbox_library"; @@ -99,8 +101,12 @@ QVariant RhythmboxFeature::title() { return m_title; } -QIcon RhythmboxFeature::getIcon() { - return QIcon(":/images/library/ic_library_rhythmbox.png"); +QString RhythmboxFeature::getIconPath() { + return ":/images/library/ic_library_rhythmbox.png"; +} + +QString RhythmboxFeature::getSettingsName() const { + return "RhythmboxFeature"; } TreeItemModel* RhythmboxFeature::getChildModel() { @@ -108,7 +114,7 @@ TreeItemModel* RhythmboxFeature::getChildModel() { } void RhythmboxFeature::activate() { - qDebug() << "RhythmboxFeature::activate()"; + //qDebug() << "RhythmboxFeature::activate()"; if (!m_isActivated) { m_isActivated = true; @@ -127,9 +133,9 @@ void RhythmboxFeature::activate() { //calls a slot in the sidebar model such that 'Rhythmbox (isLoading)' is displayed. emit (featureIsLoading(this, true)); } - - emit(showTrackModel(m_pRhythmboxTrackModel)); - emit(enableCoverArtDisplay(false)); + + showTrackModel(m_pRhythmboxTrackModel); + showBreadCrumb(); } void RhythmboxFeature::activateChild(const QModelIndex& index) { @@ -137,8 +143,9 @@ void RhythmboxFeature::activateChild(const QModelIndex& index) { QString playlist = index.data().toString(); qDebug() << "Activating " << playlist; m_pRhythmboxPlaylistModel->setPlaylist(playlist); - emit(showTrackModel(m_pRhythmboxPlaylistModel)); - emit(enableCoverArtDisplay(false)); + + showTrackModel(m_pRhythmboxPlaylistModel); + showBreadCrumb(index); } TreeItem* RhythmboxFeature::importMusicCollection() { @@ -149,12 +156,12 @@ TreeItem* RhythmboxFeature::importMusicCollection() { if (!db.exists()) { db.setFileName(QDir::homePath() + "/.local/share/rhythmbox/rhythmdb.xml"); if (!db.exists()) { - return NULL; + return nullptr; } } if (!db.open(QIODevice::ReadOnly | QIODevice::Text)) - return NULL; + return nullptr; //Delete all table entries of Traktor feature ScopedTransaction transaction(m_database); @@ -436,6 +443,7 @@ void RhythmboxFeature::clearTable(QString table_name) { void RhythmboxFeature::onTrackCollectionLoaded() { TreeItem* root = m_track_future.result(); + root->setLibraryFeature(this); if (root) { m_childModel.setRootItem(root); diff --git a/src/library/rhythmbox/rhythmboxfeature.h b/src/library/features/rhythmbox/rhythmboxfeature.h similarity index 84% rename from src/library/rhythmbox/rhythmboxfeature.h rename to src/library/features/rhythmbox/rhythmboxfeature.h index b2649959b52..a5ed60b4873 100644 --- a/src/library/rhythmbox/rhythmboxfeature.h +++ b/src/library/features/rhythmbox/rhythmboxfeature.h @@ -11,7 +11,7 @@ #include #include -#include "library/baseexternallibraryfeature.h" +#include "library/features/baseexternalfeature/baseexternallibraryfeature.h" #include "library/treeitemmodel.h" #include "library/trackcollection.h" @@ -21,12 +21,16 @@ class BaseExternalPlaylistModel; class RhythmboxFeature : public BaseExternalLibraryFeature { Q_OBJECT public: - RhythmboxFeature(QObject* parent, TrackCollection*); + RhythmboxFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); virtual ~RhythmboxFeature(); static bool isSupported(); QVariant title(); - QIcon getIcon(); + QString getIconPath() override; + QString getSettingsName() const override; TreeItemModel* getChildModel(); // processes the music collection diff --git a/src/library/traktor/traktorfeature.cpp b/src/library/features/traktor/traktorfeature.cpp similarity index 96% rename from src/library/traktor/traktorfeature.cpp rename to src/library/features/traktor/traktorfeature.cpp index 2cf48b224ed..79fd444feec 100644 --- a/src/library/traktor/traktorfeature.cpp +++ b/src/library/features/traktor/traktorfeature.cpp @@ -8,13 +8,11 @@ #include #include -#include "library/traktor/traktorfeature.h" +#include "library/features/traktor/traktorfeature.h" #include "library/librarytablemodel.h" -#include "library/missingtablemodel.h" #include "library/queryutil.h" #include "library/trackcollection.h" -#include "library/treeitem.h" #include "util/sandbox.h" TraktorTrackModel::TraktorTrackModel(QObject* parent, @@ -50,8 +48,10 @@ bool TraktorPlaylistModel::isColumnHiddenByDefault(int column) { return BaseSqlTableModel::isColumnHiddenByDefault(column); } -TraktorFeature::TraktorFeature(QObject* parent, TrackCollection* pTrackCollection) - : BaseExternalLibraryFeature(parent, pTrackCollection), +TraktorFeature::TraktorFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, TrackCollection* pTrackCollection) + : BaseExternalLibraryFeature(pConfig, pLibrary, parent, pTrackCollection), m_pTrackCollection(pTrackCollection), m_cancelImport(false) { QString tableName = "traktor_library"; @@ -119,8 +119,12 @@ QVariant TraktorFeature::title() { return m_title; } -QIcon TraktorFeature::getIcon() { - return QIcon(":/images/library/ic_library_traktor.png"); +QString TraktorFeature::getIconPath() { + return ":/images/library/ic_library_traktor.png"; +} + +QString TraktorFeature::getSettingsName() const { + return "TraktorFeature"; } bool TraktorFeature::isSupported() { @@ -135,7 +139,7 @@ void TraktorFeature::refreshLibraryModels() { } void TraktorFeature::activate() { - qDebug() << "TraktorFeature::activate()"; + //qDebug() << "TraktorFeature::activate()"; if (!m_isActivated) { m_isActivated = true; @@ -154,11 +158,11 @@ void TraktorFeature::activate() { m_future_watcher.setFuture(m_future); m_title = tr("(loading) Traktor"); //calls a slot in the sidebar model such that 'iTunes (isLoading)' is displayed. - emit (featureIsLoading(this, true)); + emit(featureIsLoading(this, true)); } - - emit(showTrackModel(m_pTraktorTableModel)); - emit(enableCoverArtDisplay(false)); + + showTrackModel(m_pTraktorTableModel); + showBreadCrumb(); } void TraktorFeature::activateChild(const QModelIndex& index) { @@ -171,8 +175,9 @@ void TraktorFeature::activateChild(const QModelIndex& index) { if (item->isPlaylist()) { qDebug() << "Activate Traktor Playlist: " << item->dataPath().toString(); m_pTraktorPlaylistModel->setPlaylist(item->dataPath().toString()); - emit(showTrackModel(m_pTraktorPlaylistModel)); - emit(enableCoverArtDisplay(false)); + + showTrackModel(m_pTraktorPlaylistModel); + showBreadCrumb(item); } } @@ -602,17 +607,19 @@ QString TraktorFeature::getTraktorMusicDatabase() { void TraktorFeature::onTrackCollectionLoaded() { TreeItem* root = m_future.result(); + root->setLibraryFeature(this); if (root) { m_childModel.setRootItem(root); // Tell the traktor track source that it should re-build its index. m_trackSource->buildIndex(); //m_pTraktorTableModel->select(); - emit(showTrackModel(m_pTraktorTableModel)); + showTrackModel(m_pTraktorTableModel); + showBreadCrumb(root); qDebug() << "Traktor library loaded successfully"; } else { QMessageBox::warning( - NULL, + nullptr, tr("Error Loading Traktor Library"), tr("There was an error loading your Traktor library. Some of " "your Traktor tracks or playlists may not have loaded.")); diff --git a/src/library/traktor/traktorfeature.h b/src/library/features/traktor/traktorfeature.h similarity index 82% rename from src/library/traktor/traktorfeature.h rename to src/library/features/traktor/traktorfeature.h index edf04e42664..0def70d0b23 100644 --- a/src/library/traktor/traktorfeature.h +++ b/src/library/features/traktor/traktorfeature.h @@ -11,9 +11,9 @@ #include #include -#include "library/baseexternallibraryfeature.h" -#include "library/baseexternaltrackmodel.h" -#include "library/baseexternalplaylistmodel.h" +#include "library/features/baseexternalfeature/baseexternallibraryfeature.h" +#include "library/features/baseexternalfeature/baseexternaltrackmodel.h" +#include "library/features/baseexternalfeature/baseexternalplaylistmodel.h" #include "library/treeitemmodel.h" class TrackCollection; @@ -38,11 +38,15 @@ class TraktorPlaylistModel : public BaseExternalPlaylistModel { class TraktorFeature : public BaseExternalLibraryFeature { Q_OBJECT public: - TraktorFeature(QObject* parent, TrackCollection*); + TraktorFeature(UserSettingsPointer pConfig, + Library* pLibrary, + QObject* parent, + TrackCollection* pTrackCollection); virtual ~TraktorFeature(); - QVariant title(); - QIcon getIcon(); + QVariant title() override; + QString getIconPath() override; + QString getSettingsName() const override; static bool isSupported(); TreeItemModel* getChildModel(); diff --git a/src/library/library.cpp b/src/library/library.cpp index ecd04714633..555de141967 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -1,56 +1,64 @@ // library.cpp // Created 8/23/2009 by RJ Ryan (rryan@mit.edu) +#include +#include #include #include #include -#include -#include "mixer/playermanager.h" -#include "library/library.h" +#include "controllers/keyboard/keyboardeventfilter.h" + +#include "library/features/analysis/analysisfeature.h" +#include "library/features/autodj/autodjfeature.h" +#include "library/features/banshee/bansheefeature.h" +#include "library/features/browse/browsefeature.h" +#include "library/features/crates/cratefeature.h" +#include "library/features/history/historyfeature.h" +#include "library/features/itunes/itunesfeature.h" +#include "library/features/libraryfolder/libraryfoldersfeature.h" +#include "library/features/maintenance/maintenancefeature.h" +#include "library/features/mixxxlibrary/mixxxlibraryfeature.h" +#include "library/features/playlist/playlistfeature.h" +#include "library/features/recording/recordingfeature.h" +#include "library/features/rhythmbox/rhythmboxfeature.h" +#include "library/features/traktor/traktorfeature.h" + #include "library/library_preferences.h" +#include "library/librarycontrol.h" #include "library/libraryfeature.h" +#include "library/librarypanemanager.h" +#include "library/librarysidebarexpandedmanager.h" #include "library/librarytablemodel.h" -#include "library/sidebarmodel.h" #include "library/trackcollection.h" #include "library/trackmodel.h" -#include "library/browse/browsefeature.h" -#include "library/cratefeature.h" -#include "library/rhythmbox/rhythmboxfeature.h" -#include "library/banshee/bansheefeature.h" -#include "library/recording/recordingfeature.h" -#include "library/itunes/itunesfeature.h" -#include "library/mixxxlibraryfeature.h" -#include "library/autodj/autodjfeature.h" -#include "library/playlistfeature.h" -#include "library/traktor/traktorfeature.h" -#include "library/librarycontrol.h" -#include "library/setlogfeature.h" -#include "util/sandbox.h" +#include "library/queryutil.h" +#include "mixer/playermanager.h" #include "util/assert.h" +#include "util/sandbox.h" -#include "widget/wtracktableview.h" -#include "widget/wlibrary.h" -#include "widget/wlibrarysidebar.h" - -#include "controllers/keyboard/keyboardeventfilter.h" +#include "widget/wbuttonbar.h" +#include "widget/wfeatureclickbutton.h" -// This is is the name which we use to register the WTrackTableView with the -// WLibrary -const QString Library::m_sTrackViewName = QString("WTrackTableView"); +#include "library/library.h" // The default row height of the library. const int Library::kDefaultRowHeightPx = 20; -Library::Library(QObject* parent, UserSettingsPointer pConfig, +Library::Library(UserSettingsPointer pConfig, PlayerManagerInterface* pPlayerManager, RecordingManager* pRecordingManager) : m_pConfig(pConfig), - m_pSidebarModel(new SidebarModel(parent)), m_pTrackCollection(new TrackCollection(pConfig)), m_pLibraryControl(new LibraryControl(this)), m_pRecordingManager(pRecordingManager), - m_scanner(m_pTrackCollection, pConfig) { + m_scanner(m_pTrackCollection, pConfig), + m_pSidebarExpanded(nullptr), + m_hoveredFeature(nullptr), + m_focusedFeature(nullptr), + m_focusedPaneId(-1), + m_preselectedPane(-1), + m_previewPreselectedPane(-1) { qRegisterMetaType("Library::RemovalType"); m_pKeyNotation.reset(new ControlObject(ConfigKey("[Library]", "key_notation"))); @@ -62,56 +70,9 @@ Library::Library(QObject* parent, UserSettingsPointer pConfig, // Refresh the library models when the library (re)scan is finished. connect(&m_scanner, SIGNAL(scanFinished()), this, SLOT(slotRefreshLibraryModels())); - - // TODO(rryan) -- turn this construction / adding of features into a static - // method or something -- CreateDefaultLibrary - m_pMixxxLibraryFeature = new MixxxLibraryFeature(this, m_pTrackCollection,m_pConfig); - addFeature(m_pMixxxLibraryFeature); - - addFeature(new AutoDJFeature(this, pConfig, pPlayerManager, m_pTrackCollection)); - m_pPlaylistFeature = new PlaylistFeature(this, m_pTrackCollection, m_pConfig); - addFeature(m_pPlaylistFeature); - m_pCrateFeature = new CrateFeature(this, m_pTrackCollection, m_pConfig); - addFeature(m_pCrateFeature); - BrowseFeature* browseFeature = new BrowseFeature( - this, pConfig, m_pTrackCollection, m_pRecordingManager); - connect(browseFeature, SIGNAL(scanLibrary()), - &m_scanner, SLOT(scan())); - connect(&m_scanner, SIGNAL(scanStarted()), - browseFeature, SLOT(slotLibraryScanStarted())); - connect(&m_scanner, SIGNAL(scanFinished()), - browseFeature, SLOT(slotLibraryScanFinished())); - - addFeature(browseFeature); - addFeature(new RecordingFeature(this, pConfig, m_pTrackCollection, m_pRecordingManager)); - addFeature(new SetlogFeature(this, pConfig, m_pTrackCollection)); - m_pAnalysisFeature = new AnalysisFeature(this, pConfig, m_pTrackCollection); - connect(m_pPlaylistFeature, SIGNAL(analyzeTracks(QList)), - m_pAnalysisFeature, SLOT(analyzeTracks(QList))); - connect(m_pCrateFeature, SIGNAL(analyzeTracks(QList)), - m_pAnalysisFeature, SLOT(analyzeTracks(QList))); - addFeature(m_pAnalysisFeature); - //iTunes and Rhythmbox should be last until we no longer have an obnoxious - //messagebox popup when you select them. (This forces you to reach for your - //mouse or keyboard if you're using MIDI control and you scroll through them...) - if (RhythmboxFeature::isSupported() && - pConfig->getValueString(ConfigKey("[Library]","ShowRhythmboxLibrary"),"1").toInt()) { - addFeature(new RhythmboxFeature(this, m_pTrackCollection)); - } - if (pConfig->getValueString(ConfigKey("[Library]","ShowBansheeLibrary"),"1").toInt()) { - BansheeFeature::prepareDbPath(pConfig); - if (BansheeFeature::isSupported()) { - addFeature(new BansheeFeature(this, m_pTrackCollection, pConfig)); - } - } - if (ITunesFeature::isSupported() && - pConfig->getValueString(ConfigKey("[Library]","ShowITunesLibrary"),"1").toInt()) { - addFeature(new ITunesFeature(this, m_pTrackCollection)); - } - if (TraktorFeature::isSupported() && - pConfig->getValueString(ConfigKey("[Library]","ShowTraktorLibrary"),"1").toInt()) { - addFeature(new TraktorFeature(this, m_pTrackCollection)); - } + + createTrackCache(); + createFeatures(pConfig, pPlayerManager); // On startup we need to check if all of the user's library folders are // accessible to us. If the user is using a database from <1.12.0 with @@ -137,15 +98,8 @@ Library::Library(QObject* parent, UserSettingsPointer pConfig, } Library::~Library() { - // Delete the sidebar model first since it depends on the LibraryFeatures. - delete m_pSidebarModel; - - QMutableListIterator features_it(m_features); - while(features_it.hasNext()) { - LibraryFeature* feature = features_it.next(); - features_it.remove(); - delete feature; - } + qDeleteAll(m_features); + m_features.clear(); delete m_pLibraryControl; //IMPORTANT: m_pTrackCollection gets destroyed via the QObject hierarchy somehow. @@ -156,106 +110,152 @@ Library::~Library() { delete m_pTrackCollection; } -void Library::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) { - m_pLibraryControl->bindSidebarWidget(pSidebarWidget); - - // Setup the sources view - pSidebarWidget->setModel(m_pSidebarModel); - connect(m_pSidebarModel, SIGNAL(selectIndex(const QModelIndex&)), - pSidebarWidget, SLOT(selectIndex(const QModelIndex&))); - connect(pSidebarWidget, SIGNAL(pressed(const QModelIndex&)), - m_pSidebarModel, SLOT(clicked(const QModelIndex&))); - // Lazy model: Let triangle symbol increment the model - connect(pSidebarWidget, SIGNAL(expanded(const QModelIndex&)), - m_pSidebarModel, SLOT(doubleClicked(const QModelIndex&))); - - connect(pSidebarWidget, SIGNAL(rightClicked(const QPoint&, const QModelIndex&)), - m_pSidebarModel, SLOT(rightClicked(const QPoint&, const QModelIndex&))); - - pSidebarWidget->slotSetFont(m_trackTableFont); - connect(this, SIGNAL(setTrackTableFont(QFont)), - pSidebarWidget, SLOT(slotSetFont(QFont))); +void Library::bindSearchBar(WSearchLineEdit* searchLine, int id) { + // Get the value once to avoid searching again in the hash + LibraryPaneManager* pPane = getOrCreatePane(id); + searchLine->setTrackCollection(m_pTrackCollection); + pPane->bindSearchBar(searchLine); } -void Library::bindWidget(WLibrary* pLibraryWidget, - KeyboardEventFilter* pKeyboard) { - WTrackTableView* pTrackTableView = - new WTrackTableView(pLibraryWidget, m_pConfig, m_pTrackCollection); - pTrackTableView->installEventFilter(pKeyboard); - connect(this, SIGNAL(showTrackModel(QAbstractItemModel*)), - pTrackTableView, SLOT(loadTrackModel(QAbstractItemModel*))); - connect(pTrackTableView, SIGNAL(loadTrack(TrackPointer)), - this, SLOT(slotLoadTrack(TrackPointer))); - connect(pTrackTableView, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool)), - this, SLOT(slotLoadTrackToPlayer(TrackPointer, QString, bool))); - pLibraryWidget->registerView(m_sTrackViewName, pTrackTableView); - - connect(this, SIGNAL(switchToView(const QString&)), - pLibraryWidget, SLOT(switchToView(const QString&))); - - connect(pTrackTableView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); - - connect(this, SIGNAL(setTrackTableFont(QFont)), - pTrackTableView, SLOT(setTrackTableFont(QFont))); - connect(this, SIGNAL(setTrackTableRowHeight(int)), - pTrackTableView, SLOT(setTrackTableRowHeight(int))); - - connect(this, SIGNAL(searchStarting()), - pTrackTableView, SLOT(onSearchStarting())); - connect(this, SIGNAL(searchCleared()), - pTrackTableView, SLOT(onSearchCleared())); - - m_pLibraryControl->bindWidget(pLibraryWidget, pKeyboard); - - QListIterator feature_it(m_features); - while(feature_it.hasNext()) { - LibraryFeature* feature = feature_it.next(); - feature->bindWidget(pLibraryWidget, pKeyboard); +void Library::bindSidebarButtons(WButtonBar* sidebar) { + for (LibraryFeature* f : m_features) { + WFeatureClickButton* button = sidebar->addButton(f); + + connect(button, SIGNAL(clicked(LibraryFeature*)), + this, SLOT(slotActivateFeature(LibraryFeature*))); + connect(button, SIGNAL(hoverShow(LibraryFeature*)), + this, SLOT(slotHoverFeature(LibraryFeature*))); + connect(button, SIGNAL(rightClicked(const QPoint&)), + f, SLOT(onRightClick(const QPoint&))); + connect(button, SIGNAL(hovered(LibraryFeature*)), + this, SLOT(slotSetHoveredFeature(LibraryFeature*))); + connect(button, SIGNAL(leaved(LibraryFeature*)), + this, SLOT(slotResetHoveredFeature(LibraryFeature*))); + connect(button, SIGNAL(focusIn(LibraryFeature*)), + this, SLOT(slotSetFocusedFeature(LibraryFeature*))); + connect(button, SIGNAL(focusOut(LibraryFeature*)), + this, SLOT(slotResetFocusedFeature(LibraryFeature*))); } +} +void Library::bindPaneWidget(WLibraryPane* pPaneWidget, + KeyboardEventFilter* pKeyboard, int paneId) { + + // Get the value once to avoid searching again in the hash + LibraryPaneManager* pPane = getOrCreatePane(paneId); + if (pPane == nullptr) { + return; + } + pPane->bindPaneWidget(pPaneWidget, pKeyboard); + // Set the current font and row height on all the WTrackTableViews that were // just connected to us. emit(setTrackTableFont(m_trackTableFont)); emit(setTrackTableRowHeight(m_iTrackTableRowHeight)); } +void Library::bindSidebarExpanded(WBaseLibrary* expandedPane, + KeyboardEventFilter* pKeyboard) { + //qDebug() << "Library::bindSidebarExpanded"; + m_pSidebarExpanded = new LibrarySidebarExpandedManager(this); + m_pSidebarExpanded->addFeatures(m_features); + m_pSidebarExpanded->bindPaneWidget(expandedPane, pKeyboard); +} + +void Library::bindBreadCrumb(WLibraryBreadCrumb* pBreadCrumb, int paneId) { + // Get the value once to avoid searching again in the hash + LibraryPaneManager* pPane = getOrCreatePane(paneId); + pPane->setBreadCrumb(pBreadCrumb); +} + +void Library::destroyInterface() { + m_pSidebarExpanded->deleteLater(); + m_pSidebarExpanded = nullptr; + + for (LibraryPaneManager* p : m_panes) { + p->deleteLater(); + } + + for (LibraryFeature* f : m_features) { + f->setFeaturePaneId(-1); + } + m_panes.clear(); +} + +LibraryView* Library::getActiveView() { + LibraryPaneManager* pPane = m_panes.value(m_focusedPaneId); + DEBUG_ASSERT_AND_HANDLE(pPane) { + return nullptr; + } + WBaseLibrary* pPaneWidget = pPane->getPaneWidget(); + WLibraryPane* pLibrary = qobject_cast(pPaneWidget); + DEBUG_ASSERT_AND_HANDLE(pLibrary) { + return nullptr; + } + return pLibrary->getActiveView(); +} + + void Library::addFeature(LibraryFeature* feature) { DEBUG_ASSERT_AND_HANDLE(feature) { return; } - m_features.push_back(feature); - m_pSidebarModel->addLibraryFeature(feature); - connect(feature, SIGNAL(showTrackModel(QAbstractItemModel*)), - this, SLOT(slotShowTrackModel(QAbstractItemModel*))); - connect(feature, SIGNAL(switchToView(const QString&)), - this, SLOT(slotSwitchToView(const QString&))); + m_features.append(feature); + connect(feature, SIGNAL(loadTrack(TrackPointer)), this, SLOT(slotLoadTrack(TrackPointer))); connect(feature, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool)), this, SLOT(slotLoadTrackToPlayer(TrackPointer, QString, bool))); - connect(feature, SIGNAL(restoreSearch(const QString&)), - this, SLOT(slotRestoreSearch(const QString&))); connect(feature, SIGNAL(enableCoverArtDisplay(bool)), this, SIGNAL(enableCoverArtDisplay(bool))); connect(feature, SIGNAL(trackSelected(TrackPointer)), this, SIGNAL(trackSelected(TrackPointer))); + + connect(feature, SIGNAL(hovered(LibraryFeature*)), + this, SLOT(slotSetHoveredFeature(LibraryFeature*))); + connect(feature, SIGNAL(leaved(LibraryFeature*)), + this, SLOT(slotResetHoveredFeature(LibraryFeature*))); + connect(feature, SIGNAL(focusIn(LibraryFeature*)), + this, SLOT(slotSetFocusedFeature(LibraryFeature*))); + connect(feature, SIGNAL(focusOut(LibraryFeature*)), + this, SLOT(slotResetFocusedFeature(LibraryFeature*))); } -void Library::slotShowTrackModel(QAbstractItemModel* model) { - //qDebug() << "Library::slotShowTrackModel" << model; - TrackModel* trackModel = dynamic_cast(model); - DEBUG_ASSERT_AND_HANDLE(trackModel) { +void Library::switchToFeature(LibraryFeature* pFeature) { + if (m_pSidebarExpanded) { + m_pSidebarExpanded->switchToFeature(pFeature); + } + + LibraryPaneManager* pPane = getPreselectedPane(); + if (pPane == nullptr) { + // No pane is preselected so we are handling an activateChild() method + // or similar. We only change the input focus to the feature one. + m_focusedPaneId = pFeature->getFeaturePaneId(); + handleFocus(); + pPane = getFocusedPane(); + } + + pPane->switchToFeature(pFeature); + m_preselectedPane = -1; + handlePreselection(); +} + +void Library::showBreadCrumb(int paneId, TreeItem *pTree) { + LibraryPaneManager* pPane = getOrCreatePane(paneId); + DEBUG_ASSERT_AND_HANDLE(pPane) { return; } - emit(showTrackModel(model)); - emit(switchToView(m_sTrackViewName)); - emit(restoreSearch(trackModel->currentSearch())); + + pPane->showBreadCrumb(pTree); } -void Library::slotSwitchToView(const QString& view) { - //qDebug() << "Library::slotSwitchToView" << view; - emit(switchToView(view)); +void Library::showBreadCrumb(int paneId, const QString &text, const QIcon &icon) { + LibraryPaneManager* pPane = getOrCreatePane(paneId); + DEBUG_ASSERT_AND_HANDLE(pPane) { + return; + } + + pPane->showBreadCrumb(text, icon); } void Library::slotLoadTrack(TrackPointer pTrack) { @@ -274,8 +274,57 @@ void Library::slotLoadTrackToPlayer(TrackPointer pTrack, QString group, bool pla emit(loadTrackToPlayer(pTrack, group, play)); } -void Library::slotRestoreSearch(const QString& text) { - emit(restoreSearch(text)); +void Library::restoreSearch(int paneId, const QString& text) { + LibraryPaneManager* pPane = getOrCreatePane(paneId); + DEBUG_ASSERT_AND_HANDLE(pPane) { + return; + } + pPane->restoreSearch(text); +} + + +void Library::restoreSaveButton(int paneId) { + LibraryPaneManager* pPane = getOrCreatePane(paneId); + DEBUG_ASSERT_AND_HANDLE(pPane) { + return; + } + pPane->restoreSaveButton(); +} + +void Library::paneFocused(LibraryPaneManager* pPane) { + DEBUG_ASSERT_AND_HANDLE(pPane) { + return; + } + + if (pPane != m_pSidebarExpanded) { + m_focusedPaneId = pPane->getPaneId(); + pPane->getCurrentFeature()->setFeaturePaneId(m_focusedPaneId); + DEBUG_ASSERT_AND_HANDLE(m_focusedPaneId != -1) { + return; + } + handleFocus(); + } + + //qDebug() << "Library::slotPaneFocused" << m_focusedPane; +} + +void Library::panePreselected(LibraryPaneManager* pPane, bool value) { + // Since only one pane can be preselected, set the other panes as not + // preselected + if (value) { + m_preselectedPane = pPane->getPaneId(); + } else if (m_preselectedPane == pPane->getPaneId()) { + m_preselectedPane = -1; + } + handlePreselection(); +} + +int Library::getFocusedPaneId() { + return m_focusedPaneId; +} + +int Library::getPreselectedPaneId() { + return m_preselectedPane; } void Library::slotRefreshLibraryModels() { @@ -293,7 +342,44 @@ void Library::slotCreateCrate() { void Library::onSkinLoadFinished() { // Enable the default selection when a new skin is loaded. - m_pSidebarModel->activateDefaultSelection(); + //m_pSidebarModel->activateDefaultSelection(); + if (m_panes.size() > 0) { + + auto itF = m_features.begin(); + auto itP = m_panes.begin(); + bool first = true; + + // Assign a feature to show on each pane unless there are more panes + // than features + while (itP != m_panes.end() && itF != m_features.end()) { + m_preselectedPane = itP.key(); + if (first) { + first = false; + // Set the first pane as saved pane to all features + for (LibraryFeature* pFeature : m_features) { + pFeature->setFeaturePaneId(m_preselectedPane); + } + } + + m_savedFeatures[m_preselectedPane] = *itF; + (*itP)->setCurrentFeature(*itF); + + (*itF)->setFeaturePaneId(m_preselectedPane); + (*itF)->activate(); + + ++itP; + ++itF; + } + + // The first pane always shows the Mixxx Library feature on start + m_preselectedPane = m_focusedPaneId = m_panes.begin().key(); + handleFocus(); + (*m_features.begin())->setFeaturePaneId(m_preselectedPane); + slotActivateFeature(*m_features.begin()); + } + else { + qDebug() << "Library::onSkinLoadFinished No Panes loaded!"; + } } void Library::slotRequestAddDir(QString dir) { @@ -370,6 +456,94 @@ QStringList Library::getDirs() { return m_pTrackCollection->getDirectoryDAO().getDirs(); } +void Library::paneCollapsed(int paneId) { + m_collapsedPanes.insert(paneId); + + // Automatically switch the focus to a non collapsed pane + LibraryPaneManager* pPane = m_panes.value(paneId); + if (pPane) { + pPane->setFocused(false); + } + + + bool focused = false; + for (LibraryPaneManager* pPane : m_panes) { + int auxId = pPane->getPaneId(); + if (!m_collapsedPanes.contains(auxId) && !focused) { + m_focusedPaneId = pPane->getPaneId(); + pPane->setFocused(true); + focused = true; + } + + // Save the current feature from all panes + m_savedFeatures[auxId] = pPane->getCurrentFeature(); + } +} + +void Library::paneUncollapsed(int paneId) { + m_collapsedPanes.remove(paneId); + + // If the current shown feature in some pane is the same as the uncollapsed + // pane feature, switch the feature from one pane to the other and set + // instead the saved feature + LibraryPaneManager* pPane = m_panes.value(paneId); + if (pPane == nullptr) { + return; + } + LibraryFeature* pFeature = pPane->getCurrentFeature(); + if (pFeature == nullptr) { + return; + } + pFeature->setFeaturePaneId(pPane->getPaneId()); + + for (LibraryPaneManager* pPane : m_panes) { + int auxId = pPane->getPaneId(); + if (auxId != paneId && pFeature == pPane->getCurrentFeature()) { + LibraryFeature* pSaved = m_savedFeatures[auxId]; + pPane->switchToFeature(pSaved); + pSaved->setFeaturePaneId(auxId); + pSaved->activate(); + } + } +} + +void Library::slotActivateFeature(LibraryFeature* pFeature) { + int selectedPane = m_preselectedPane; + if (selectedPane < 0) { + // No pane is preselected, use the saved pane instead + selectedPane = pFeature->getFeaturePaneId(); + } + + bool featureActivated = false; + LibraryPaneManager* pSelectedPane = m_panes.value(selectedPane); + if (pSelectedPane) { + pFeature->setFeaturePaneId(selectedPane); + + if (pSelectedPane->getCurrentFeature() != pFeature) { + pSelectedPane->setCurrentFeature(pFeature); + pFeature->activate(); + featureActivated = true; + } + } + + if (!featureActivated) { + // Feature already in a pane, we need only switch the SidebarExpanded + if (m_pSidebarExpanded) { + m_pSidebarExpanded->switchToFeature(pFeature); + } + } + m_preselectedPane = -1; + handlePreselection(); +} + +void Library::slotHoverFeature(LibraryFeature *pFeature) { + // This function only changes the sidebar expanded to allow dropping items + // directly in some features sidebar panes + if (m_pSidebarExpanded) { + m_pSidebarExpanded->switchToFeature(pFeature); + } +} + void Library::slotSetTrackTableFont(const QFont& font) { m_trackTableFont = font; emit(setTrackTableFont(font)); @@ -379,3 +553,250 @@ void Library::slotSetTrackTableRowHeight(int rowHeight) { m_iTrackTableRowHeight = rowHeight; emit(setTrackTableRowHeight(rowHeight)); } + +void Library::slotSetHoveredFeature(LibraryFeature* pFeature) { + m_hoveredFeature = pFeature; + m_previewPreselectedPane = pFeature->getFeaturePaneId(); + handlePreselection(); +} + +void Library::slotResetHoveredFeature(LibraryFeature* pFeature) { + if (pFeature == m_hoveredFeature) { + if (m_focusedFeature) { + m_previewPreselectedPane = m_focusedFeature->getFeaturePaneId(); + } else { + m_previewPreselectedPane = -1; + } + m_hoveredFeature = nullptr; + } + handlePreselection(); +} + +void Library::slotSetFocusedFeature(LibraryFeature* pFeature) { + m_focusedFeature = pFeature; + m_previewPreselectedPane = pFeature->getFeaturePaneId(); + handlePreselection(); +} + +void Library::slotResetFocusedFeature(LibraryFeature* pFeature) { + if (pFeature == m_focusedFeature) { + if (m_hoveredFeature) { + m_previewPreselectedPane = m_hoveredFeature->getFeaturePaneId(); + } else { + m_previewPreselectedPane = -1; + } + m_hoveredFeature = nullptr; + } + handlePreselection(); +} + +LibraryPaneManager* Library::getOrCreatePane(int paneId) { + //qDebug() << "Library::createPane" << id; + // Get the value once to avoid searching again in the hash + LibraryPaneManager* pPane = m_panes.value(paneId); + if (pPane) { + return pPane; + } + + // The paneId must be non negative + DEBUG_ASSERT_AND_HANDLE(paneId >= 0) { + return nullptr; + } + + // Create a new pane only if there are more features than panes + if (m_panes.size() >= m_features.size()) { + qWarning() << "Library: there are more panes declared than features"; + return nullptr; + } + + pPane = new LibraryPaneManager(paneId, this); + pPane->addFeatures(m_features); + m_panes.insert(paneId, pPane); + + m_focusedPaneId = paneId; + return pPane; +} + +LibraryPaneManager* Library::getFocusedPane() { + //qDebug() << "Focused" << m_focusedPane; + return m_panes.value(m_focusedPaneId); +} + +LibraryPaneManager* Library::getPreselectedPane() { + return m_panes.value(m_preselectedPane); +} + +void Library::createTrackCache() { + QStringList columns; + columns << "library." + LIBRARYTABLE_ID + << "library." + LIBRARYTABLE_PLAYED + << "library." + LIBRARYTABLE_TIMESPLAYED + //has to be up here otherwise Played and TimesPlayed are not show + << "library." + LIBRARYTABLE_ALBUMARTIST + << "library." + LIBRARYTABLE_ALBUM + << "library." + LIBRARYTABLE_ARTIST + << "library." + LIBRARYTABLE_TITLE + << "library." + LIBRARYTABLE_YEAR + << "library." + LIBRARYTABLE_RATING + << "library." + LIBRARYTABLE_GENRE + << "library." + LIBRARYTABLE_COMPOSER + << "library." + LIBRARYTABLE_GROUPING + << "library." + LIBRARYTABLE_TRACKNUMBER + << "library." + LIBRARYTABLE_KEY + << "library." + LIBRARYTABLE_KEY_ID + << "library." + LIBRARYTABLE_BPM + << "library." + LIBRARYTABLE_BPM_LOCK + << "library." + LIBRARYTABLE_DURATION + << "library." + LIBRARYTABLE_BITRATE + << "library." + LIBRARYTABLE_REPLAYGAIN + << "library." + LIBRARYTABLE_FILETYPE + << "library." + LIBRARYTABLE_DATETIMEADDED + << "track_locations.location" + << "track_locations.fs_deleted" + << "track_locations.directory" + << "library." + LIBRARYTABLE_COMMENT + << "library." + LIBRARYTABLE_MIXXXDELETED + << "library." + LIBRARYTABLE_COVERART_SOURCE + << "library." + LIBRARYTABLE_COVERART_TYPE + << "library." + LIBRARYTABLE_COVERART_LOCATION + << "library." + LIBRARYTABLE_COVERART_HASH; + + QSqlQuery query(m_pTrackCollection->getDatabase()); + QString tableName = "library_cache_view"; + QString queryString = QString( + "CREATE TEMPORARY VIEW IF NOT EXISTS %1 AS " + "SELECT %2 FROM library " + "INNER JOIN track_locations ON library.location = track_locations.id") + .arg(tableName, columns.join(",")); + qDebug() << queryString; + query.prepare(queryString); + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } + + // Strip out library. and track_locations. + for (QStringList::iterator it = columns.begin(); + it != columns.end(); ++it) { + if (it->startsWith("library.")) { + *it = it->replace("library.", ""); + } else if (it->startsWith("track_locations.")) { + *it = it->replace("track_locations.", ""); + } + } + + QSharedPointer pBaseTrackCache( + new BaseTrackCache( + m_pTrackCollection, tableName, LIBRARYTABLE_ID, columns, true)); + + m_pTrackCollection->setTrackSource(pBaseTrackCache); +} + + + +void Library::createFeatures(UserSettingsPointer pConfig, + PlayerManagerInterface* pPlayerManager) { + m_pMixxxLibraryFeature = new MixxxLibraryFeature( + pConfig, this, this, m_pTrackCollection); + addFeature(m_pMixxxLibraryFeature); + + addFeature(new AutoDJFeature( + pConfig, this, this, pPlayerManager, m_pTrackCollection)); + + addFeature(new LibraryFoldersFeature( + pConfig, this, this, m_pTrackCollection)); + + m_pPlaylistFeature = new PlaylistFeature( + pConfig, this, this, m_pTrackCollection); + addFeature(m_pPlaylistFeature); + + m_pCrateFeature = new CrateFeature( + pConfig, this, this, m_pTrackCollection); + addFeature(m_pCrateFeature); + + BrowseFeature* browseFeature = new BrowseFeature( + pConfig, this, this, m_pTrackCollection, m_pRecordingManager); + connect(browseFeature, SIGNAL(scanLibrary()), + &m_scanner, SLOT(scan())); + connect(&m_scanner, SIGNAL(scanStarted()), + browseFeature, SLOT(slotLibraryScanStarted())); + connect(&m_scanner, SIGNAL(scanFinished()), + browseFeature, SLOT(slotLibraryScanFinished())); + addFeature(browseFeature); + + addFeature(new RecordingFeature(pConfig, this, this, m_pTrackCollection, m_pRecordingManager)); + + addFeature(new HistoryFeature(pConfig, this, this, m_pTrackCollection)); + + m_pAnalysisFeature = new AnalysisFeature(pConfig, this, m_pTrackCollection, this); + connect(m_pPlaylistFeature, SIGNAL(analyzeTracks(QList)), + m_pAnalysisFeature, SLOT(analyzeTracks(QList))); + connect(m_pCrateFeature, SIGNAL(analyzeTracks(QList)), + m_pAnalysisFeature, SLOT(analyzeTracks(QList))); + addFeature(m_pAnalysisFeature); + + //iTunes and Rhythmbox should be last until we no longer have an obnoxious + //messagebox popup when you select them. (This forces you to reach for your + //mouse or keyboard if you're using MIDI control and you scroll through them...) + if (RhythmboxFeature::isSupported() && + pConfig->getValueString(ConfigKey("[Library]","ShowRhythmboxLibrary"),"1").toInt()) { + addFeature(new RhythmboxFeature(pConfig, this, this, m_pTrackCollection)); + } + + if (pConfig->getValueString(ConfigKey("[Library]","ShowBansheeLibrary"),"1").toInt()) { + BansheeFeature::prepareDbPath(pConfig); + if (BansheeFeature::isSupported()) { + addFeature(new BansheeFeature(pConfig, this, this, m_pTrackCollection)); + } + } + if (ITunesFeature::isSupported() && + pConfig->getValueString(ConfigKey("[Library]","ShowITunesLibrary"),"1").toInt()) { + addFeature(new ITunesFeature(pConfig, this, this, m_pTrackCollection)); + } + if (TraktorFeature::isSupported() && + pConfig->getValueString(ConfigKey("[Library]","ShowTraktorLibrary"),"1").toInt()) { + addFeature(new TraktorFeature(pConfig, this, this, m_pTrackCollection)); + } + + addFeature(new MaintenanceFeature(pConfig, this, this, m_pTrackCollection)); +} + +void Library::handleFocus() { + // Changes the visual focus effect, removes the existing one and adds the + // new focus + for (LibraryPaneManager* pPane : m_panes) { + pPane->setFocused(false); + } + LibraryPaneManager* pFocusPane = m_panes.value(m_focusedPaneId); + if (pFocusPane) { + pFocusPane->setFocused(true); + } +} + +void Library::handlePreselection() { + for (LibraryPaneManager* pPane : m_panes) { + pPane->setPreselected(false); + pPane->setPreviewed(false); + } + LibraryPaneManager* pSelectedPane = m_panes.value(m_preselectedPane); + if (pSelectedPane) { + pSelectedPane->setPreselected(true); + } else { + pSelectedPane = m_panes.value(m_previewPreselectedPane); + if (pSelectedPane) { + pSelectedPane->setPreviewed(true); + } + } +} + +void Library::focusSearch() { + LibraryPaneManager* pFocusPane = m_panes.value(m_focusedPaneId); + if (pFocusPane == nullptr) return; + bool ok = pFocusPane->focusSearch(); + if (ok) return; + for (LibraryPaneManager* pPane : m_panes) { + if (pPane == nullptr) continue; + ok = pPane->focusSearch(); + if (ok) break; + } +} + diff --git a/src/library/library.h b/src/library/library.h index 7a0b8b7a518..40d9bcad77e 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -11,45 +11,68 @@ #include #include #include +#include #include "preferences/usersettings.h" #include "track/track.h" #include "recording/recordingmanager.h" -#include "analysisfeature.h" -#include "library/coverartcache.h" -#include "library/setlogfeature.h" #include "library/scanner/libraryscanner.h" -class TrackModel; -class TrackCollection; -class SidebarModel; +class AnalysisFeature; +class CrateFeature; +class KeyboardEventFilter; +class LibraryPaneManager; +class LibraryControl; class LibraryFeature; class LibraryTableModel; -class WLibrarySidebar; -class WLibrary; -class WSearchLineEdit; +class LibrarySidebarExpandedManager; +class LibraryView; class MixxxLibraryFeature; class PlaylistFeature; -class CrateFeature; -class LibraryControl; -class KeyboardEventFilter; class PlayerManagerInterface; +class SidebarModel; +class TrackModel; +class TrackCollection; +class WBaseLibrary; +class WLibraryPane; +class WLibrarySidebar; +class WLibraryBreadCrumb; +class WButtonBar; +class WSearchLineEdit; +class TreeItem; class Library : public QObject { Q_OBJECT public: - Library(QObject* parent, - UserSettingsPointer pConfig, + enum RemovalType { + LeaveTracksUnchanged = 0, + HideTracks, + PurgeTracks + }; + + static const int kDefaultRowHeightPx; + + Library(UserSettingsPointer pConfig, PlayerManagerInterface* pPlayerManager, RecordingManager* pRecordingManager); virtual ~Library(); - - void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* pKeyboard); - void bindSidebarWidget(WLibrarySidebar* sidebarWidget); - + + void bindSearchBar(WSearchLineEdit* searchLine, int id); + void bindSidebarButtons(WButtonBar* sidebar); + void bindPaneWidget(WLibraryPane* libraryWidget, + KeyboardEventFilter* pKeyboard, int paneId); + void bindSidebarExpanded(WBaseLibrary* expandedPane, + KeyboardEventFilter* pKeyboard); + void bindBreadCrumb(WLibraryBreadCrumb *pBreadCrumb, int paneId); + + void destroyInterface(); + LibraryView* getActiveView(); + void addFeature(LibraryFeature* feature); QStringList getDirs(); + + void paneCollapsed(int paneId); + void paneUncollapsed(int paneId); // TODO(rryan) Transitionary only -- the only reason this is here is so the // waveform widgets can signal to a player to load a track. This can be @@ -66,24 +89,29 @@ class Library : public QObject { inline const QFont& getTrackTableFont() const { return m_trackTableFont; } - - //static Library* buildDefaultLibrary(); - - enum RemovalType { - LeaveTracksUnchanged = 0, - HideTracks, - PurgeTracks - }; - - static const int kDefaultRowHeightPx; + + void switchToFeature(LibraryFeature* pFeature); + void showBreadCrumb(int paneId, TreeItem* pTree); + void showBreadCrumb(int paneId, const QString& text, const QIcon& icon); + void restoreSearch(int paneId, const QString& text); + void restoreSaveButton(int paneId); + void paneFocused(LibraryPaneManager *pPane); + void panePreselected(LibraryPaneManager* pPane, bool value); + + int getFocusedPaneId(); + int getPreselectedPaneId(); + + void focusSearch(); public slots: - void slotShowTrackModel(QAbstractItemModel* model); - void slotSwitchToView(const QString& view); + + void slotActivateFeature(LibraryFeature* pFeature); + void slotHoverFeature(LibraryFeature* pFeature); + + // Updates the focus from the feature before changing the view void slotLoadTrack(TrackPointer pTrack); void slotLoadTrackToPlayer(TrackPointer pTrack, QString group, bool play); void slotLoadLocationToPlayer(QString location, QString group); - void slotRestoreSearch(const QString& text); void slotRefreshLibraryModels(); void slotCreatePlaylist(); void slotCreateCrate(); @@ -93,20 +121,20 @@ class Library : public QObject { void onSkinLoadFinished(); void slotSetTrackTableFont(const QFont& font); void slotSetTrackTableRowHeight(int rowHeight); + + void slotSetHoveredFeature(LibraryFeature* pFeature); + void slotResetHoveredFeature(LibraryFeature* pFeature); + void slotSetFocusedFeature(LibraryFeature* pFeature); + void slotResetFocusedFeature(LibraryFeature* pFeature); void scan() { m_scanner.scan(); } signals: - void showTrackModel(QAbstractItemModel* model); - void switchToView(const QString& view); void loadTrack(TrackPointer pTrack); void loadTrackToPlayer(TrackPointer pTrack, QString group, bool play = false); - void restoreSearch(const QString&); - void search(const QString& text); - void searchCleared(); - void searchStarting(); + // emit this signal to enable/disable the cover art widget void enableCoverArtDisplay(bool); void trackSelected(TrackPointer pTrack); @@ -119,12 +147,20 @@ class Library : public QObject { void scanFinished(); private: + + // If the pane exists returns it, otherwise it creates the pane + LibraryPaneManager* getOrCreatePane(int paneId); + LibraryPaneManager* getFocusedPane(); + LibraryPaneManager* getPreselectedPane(); + + void createTrackCache(); + void createFeatures(UserSettingsPointer pConfig, PlayerManagerInterface *pPlayerManager); + + void handleFocus(); + void handlePreselection(); + UserSettingsPointer m_pConfig; - SidebarModel* m_pSidebarModel; TrackCollection* m_pTrackCollection; - QList m_features; - const static QString m_sTrackViewName; - const static QString m_sAutoDJViewName; MixxxLibraryFeature* m_pMixxxLibraryFeature; PlaylistFeature* m_pPlaylistFeature; CrateFeature* m_pCrateFeature; @@ -135,6 +171,20 @@ class Library : public QObject { QFont m_trackTableFont; int m_iTrackTableRowHeight; QScopedPointer m_pKeyNotation; + + QHash m_panes; + LibraryPaneManager* m_pSidebarExpanded; + QList m_features; + QSet m_collapsedPanes; + QHash m_savedFeatures; + // Used to show the preselected pane when the mouse is over the button + LibraryFeature* m_hoveredFeature; + LibraryFeature* m_focusedFeature; + + // Can be any integer as it's used with a HashMap + int m_focusedPaneId; + int m_preselectedPane; + int m_previewPreselectedPane; }; #endif /* LIBRARY_H */ diff --git a/src/library/librarycontrol.cpp b/src/library/librarycontrol.cpp index 577b9fac172..21e5ebab471 100644 --- a/src/library/librarycontrol.cpp +++ b/src/library/librarycontrol.cpp @@ -1,3 +1,4 @@ +#include #include "library/librarycontrol.h" #include @@ -9,7 +10,6 @@ #include "control/controlobject.h" #include "control/controlpushbutton.h" #include "mixer/playermanager.h" -#include "widget/wlibrary.h" #include "widget/wlibrarysidebar.h" #include "library/library.h" #include "library/libraryview.h" @@ -50,7 +50,6 @@ void LoadToGroupController::slotLoadToGroupAndPlay(double v) { LibraryControl::LibraryControl(Library* pLibrary) : QObject(pLibrary), m_pLibrary(pLibrary), - m_pLibraryWidget(NULL), m_pSidebarWidget(NULL), m_numDecks("[Master]", "num_decks", this), m_numSamplers("[Master]", "num_samplers", this), @@ -194,30 +193,13 @@ void LibraryControl::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) { this, SLOT(sidebarWidgetDeleted())); } -void LibraryControl::bindWidget(WLibrary* pLibraryWidget, KeyboardEventFilter* pKeyboard) { - Q_UNUSED(pKeyboard); - if (m_pLibraryWidget != NULL) { - disconnect(m_pLibraryWidget, 0, this, 0); - } - m_pLibraryWidget = pLibraryWidget; - connect(m_pLibraryWidget, SIGNAL(destroyed()), - this, SLOT(libraryWidgetDeleted())); -} - -void LibraryControl::libraryWidgetDeleted() { - m_pLibraryWidget = NULL; -} - void LibraryControl::sidebarWidgetDeleted() { m_pSidebarWidget = NULL; } void LibraryControl::slotLoadSelectedTrackToGroup(QString group, bool play) { - if (m_pLibraryWidget == NULL) { - return; - } - LibraryView* activeView = m_pLibraryWidget->getActiveView(); + LibraryView* activeView = m_pLibrary->getActiveView(); if (!activeView) { return; } @@ -225,12 +207,9 @@ void LibraryControl::slotLoadSelectedTrackToGroup(QString group, bool play) { } void LibraryControl::slotLoadSelectedIntoFirstStopped(double v) { - if (m_pLibraryWidget == NULL) { - return; - } - + if (v > 0) { - LibraryView* activeView = m_pLibraryWidget->getActiveView(); + LibraryView* activeView = m_pLibrary->getActiveView(); if (!activeView) { return; } @@ -239,12 +218,8 @@ void LibraryControl::slotLoadSelectedIntoFirstStopped(double v) { } void LibraryControl::slotAutoDjAddTop(double v) { - if (m_pLibraryWidget == NULL) { - return; - } - if (v > 0) { - LibraryView* activeView = m_pLibraryWidget->getActiveView(); + LibraryView* activeView = m_pLibrary->getActiveView(); if (!activeView) { return; } @@ -253,12 +228,8 @@ void LibraryControl::slotAutoDjAddTop(double v) { } void LibraryControl::slotAutoDjAddBottom(double v) { - if (m_pLibraryWidget == NULL) { - return; - } - if (v > 0) { - LibraryView* activeView = m_pLibraryWidget->getActiveView(); + LibraryView* activeView = m_pLibrary->getActiveView(); if (!activeView) { return; } @@ -279,13 +250,9 @@ void LibraryControl::slotSelectPrevTrack(double v) { } void LibraryControl::slotSelectTrack(double v) { - if (m_pLibraryWidget == NULL) { - return; - } - int i = (int)v; - LibraryView* activeView = m_pLibraryWidget->getActiveView(); + LibraryView* activeView = m_pLibrary->getActiveView(); if (!activeView) { return; } diff --git a/src/library/librarycontrol.h b/src/library/librarycontrol.h index 7b449d9c5a0..ec9c28ee85d 100644 --- a/src/library/librarycontrol.h +++ b/src/library/librarycontrol.h @@ -8,7 +8,7 @@ class ControlObject; class ControlPushButton; class Library; -class WLibrary; +class WLibraryPane; class WLibrarySidebar; class KeyboardEventFilter; @@ -36,12 +36,10 @@ class LibraryControl : public QObject { public: LibraryControl(Library* pLibrary); virtual ~LibraryControl(); - - void bindWidget(WLibrary* pLibrary, KeyboardEventFilter* pKeyboard); + void bindSidebarWidget(WLibrarySidebar* pLibrarySidebar); private slots: - void libraryWidgetDeleted(); void sidebarWidgetDeleted(); void slotLoadSelectedTrackToGroup(QString group, bool play); void slotSelectNextTrack(double v); @@ -84,7 +82,6 @@ class LibraryControl : public QObject { ControlPushButton* m_pFontSizeIncrement; ControlPushButton* m_pFontSizeDecrement; - WLibrary* m_pLibraryWidget; WLibrarySidebar* m_pSidebarWidget; ControlProxy m_numDecks; ControlProxy m_numSamplers; diff --git a/src/library/libraryfeature.cpp b/src/library/libraryfeature.cpp index 19575cf4623..1e9eaf56387 100644 --- a/src/library/libraryfeature.cpp +++ b/src/library/libraryfeature.cpp @@ -1,23 +1,292 @@ // libraryfeature.cpp // Created 8/17/2009 by RJ Ryan (rryan@mit.edu) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "controllers/keyboard/keyboardeventfilter.h" +#include "library/library.h" #include "library/libraryfeature.h" +#include "library/trackcollection.h" +#include "library/treeitemmodel.h" +#include "widget/wbaselibrary.h" +#include "widget/wlibrarysidebar.h" +#include "widget/wtracktableview.h" +#include "widget/wminiviewscrollbar.h" +#include "widget/wpixmapstore.h" // KEEP THIS cpp file to tell scons that moc should be called on the class!!! // The reason for this is that LibraryFeature uses slots/signals and for this -// to work the code has to be precompiles by moc -LibraryFeature::LibraryFeature(QObject *parent) - : QObject(parent) { +// to work the code has to be precompiled by moc +LibraryFeature::LibraryFeature(UserSettingsPointer pConfig, + Library* pLibrary, + TrackCollection* pTrackCollection, + QObject* parent) + : QObject(parent), + m_pConfig(pConfig), + m_pLibrary(pLibrary), + m_pTrackCollection(pTrackCollection), + m_savedDAO(m_pTrackCollection->getSavedQueriesDAO()), + m_featurePane(-1) { +} +LibraryFeature::~LibraryFeature() { } -LibraryFeature::LibraryFeature(UserSettingsPointer pConfig, QObject* parent) - : QObject(parent), - m_pConfig(pConfig) { +QString LibraryFeature::getSettingsName() const { + return QString(""); } -LibraryFeature::~LibraryFeature() { +bool LibraryFeature::isSinglePane() const { + return true; +} + +QIcon LibraryFeature::getIcon() { + return WPixmapStore::getLibraryIcon(getIconPath()); +} + +bool LibraryFeature::dropAcceptChild(const QModelIndex&, QList, QObject*) { + return false; +} + +bool LibraryFeature::dragMoveAccept(QUrl) { + return false; +} + +bool LibraryFeature::dragMoveAcceptChild(const QModelIndex &, QUrl) { + return false; +} + +QWidget* LibraryFeature::createPaneWidget(KeyboardEventFilter* pKeyboard, + int paneId) { + Q_UNUSED(pKeyboard); + return createTableWidget(paneId); +} + +QWidget *LibraryFeature::createSidebarWidget(KeyboardEventFilter* pKeyboard) { + //qDebug() << "LibraryFeature::bindSidebarWidget"; + QFrame* pContainer = new QFrame(nullptr); + pContainer->setContentsMargins(0,0,0,0); + + QVBoxLayout* pLayout = new QVBoxLayout(pContainer); + pLayout->setContentsMargins(0,0,0,0); + pLayout->setSpacing(0); + pContainer->setLayout(pLayout); + + QHBoxLayout* pLayoutTitle = new QHBoxLayout(pContainer); + + QLabel* pIcon = new QLabel(pContainer); + int height = pIcon->fontMetrics().height(); + pIcon->setPixmap(getIcon().pixmap(height)); + pLayoutTitle->addWidget(pIcon); + + QLabel* pTitle = new QLabel(title().toString(), pContainer); + pLayoutTitle->addWidget(pTitle); + pLayoutTitle->addSpacerItem(new QSpacerItem(0, 0, + QSizePolicy::Expanding, + QSizePolicy::Minimum)); + pLayout->addLayout(pLayoutTitle); + + QWidget* pSidebar = createInnerSidebarWidget(pKeyboard); + pSidebar->setParent(pContainer); + pLayout->addWidget(pSidebar); + + return pContainer; +} + +void LibraryFeature::setFeaturePaneId(int paneId) { + m_featurePane = paneId; +} + +int LibraryFeature::getFeaturePaneId() { + return m_featurePane; +} + +int LibraryFeature::getFocusedPane() { + return m_pLibrary->getFocusedPaneId(); +} + +void LibraryFeature::adoptPreselectedPane() { + int preselectedPane = m_pLibrary->getPreselectedPaneId(); + if (preselectedPane >= 0 && + m_featurePane != preselectedPane) { + m_featurePane = preselectedPane; + // Refresh preselect button + emit focusIn(this); + } +} + +SavedSearchQuery LibraryFeature::saveQuery(SavedSearchQuery sQuery) { + WTrackTableView* pTable = getFocusedTable(); + if (pTable == nullptr) { + return SavedSearchQuery(); + } + + sQuery = pTable->saveQuery(sQuery); + int qId = m_savedDAO.getQueryId(sQuery); + if (qId >= 0) { + QMessageBox box; + box.setWindowTitle(tr("Query already exists")); + box.setText(tr("The query already exists in the database. Do you want " + "to overwrite it?")); + box.setDetailedText(tr("The query has title: \"%1\" and query: \"%2\"") + .arg(sQuery.title, sQuery.query)); + box.setIcon(QMessageBox::Warning); + box.addButton(QMessageBox::Yes); + box.addButton(QMessageBox::No); + box.setDefaultButton(QMessageBox::Yes); + box.setEscapeButton(QMessageBox::No); + + if (box.exec() == QMessageBox::No) { + // No pressed + m_savedDAO.moveToFirst(this, qId); + return sQuery; + } else { + // Yes pressed + m_savedDAO.deleteSavedQuery(qId); + } + } + + // A saved query goes the first in the list + return m_savedDAO.saveQuery(this, sQuery); +} + +void LibraryFeature::restoreQuery(int id) { + WTrackTableView* pTable = getFocusedTable(); + if (pTable == nullptr) { + return; + } + + // Move the query to the first position to be reused later by the user + const SavedSearchQuery& sQuery = m_savedDAO.moveToFirst(this, id); + pTable->restoreQuery(sQuery); + restoreSearch(sQuery.query.isNull() ? "" : sQuery.query); +} + +QList LibraryFeature::getSavedQueries() const { + return m_savedDAO.getSavedQueries(this); +} + +WTrackTableView* LibraryFeature::createTableWidget(int paneId) { + WTrackTableView* pTrackTableView = + new WTrackTableView(nullptr, m_pConfig, m_pTrackCollection, true); + m_trackTablesByPaneId[paneId] = pTrackTableView; + + WMiniViewScrollBar* pScrollBar = new WMiniViewScrollBar(pTrackTableView); + pTrackTableView->setScrollBar(pScrollBar); + + connect(pTrackTableView, SIGNAL(loadTrack(TrackPointer)), + this, SIGNAL(loadTrack(TrackPointer))); + connect(pTrackTableView, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool)), + this, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool))); + connect(pTrackTableView, SIGNAL(trackSelected(TrackPointer)), + this, SIGNAL(trackSelected(TrackPointer))); + connect(pTrackTableView, SIGNAL(tableChanged()), + this, SLOT(restoreSaveButton())); + + connect(m_pLibrary, SIGNAL(setTrackTableFont(QFont)), + pTrackTableView, SLOT(setTrackTableFont(QFont))); + connect(m_pLibrary, SIGNAL(setTrackTableRowHeight(int)), + pTrackTableView, SLOT(setTrackTableRowHeight(int))); + + return pTrackTableView; +} + +QWidget* LibraryFeature::createInnerSidebarWidget(KeyboardEventFilter *pKeyboard) { + return createLibrarySidebarWidget(pKeyboard); +} + +WLibrarySidebar* LibraryFeature::createLibrarySidebarWidget(KeyboardEventFilter*) { + WLibrarySidebar* pSidebar = new WLibrarySidebar(nullptr); + QAbstractItemModel* pModel = getChildModel(); + pSidebar->setModel(pModel); + + // Set sidebar mini view + WMiniViewScrollBar* pMiniView = new WMiniViewScrollBar(pSidebar); + pMiniView->setTreeView(pSidebar); + pMiniView->setModel(pModel); + pSidebar->setVerticalScrollBar(pMiniView); + // invalidate probably stored QModelIndex + invalidateChild(); + + connect(pSidebar, SIGNAL(pressed(const QModelIndex&)), + this, SLOT(activateChild(const QModelIndex&))); + connect(pSidebar, SIGNAL(doubleClicked(const QModelIndex&)), + this, SLOT(onLazyChildExpandation(const QModelIndex&))); + connect(pSidebar, SIGNAL(rightClicked(const QPoint&, const QModelIndex&)), + this, SLOT(onRightClickChild(const QPoint&, const QModelIndex&))); + connect(pSidebar, SIGNAL(expanded(const QModelIndex&)), + this, SLOT(onLazyChildExpandation(const QModelIndex&))); + connect(this, SIGNAL(selectIndex(const QModelIndex&)), + pSidebar, SLOT(selectIndex(const QModelIndex&))); + + connect(pSidebar, SIGNAL(hovered()), + this, SLOT(slotSetHoveredSidebar())); + connect(pSidebar, SIGNAL(leaved()), + this, SLOT(slotResetHoveredSidebar())); + connect(pSidebar, SIGNAL(focusIn()), + this, SLOT(slotSetFocusedSidebar())); + connect(pSidebar, SIGNAL(focusOut()), + this, SLOT(slotResetFocusedSidebar())); + + return pSidebar; +} + +void LibraryFeature::showTrackModel(QAbstractItemModel *model) { + auto it = m_trackTablesByPaneId.constFind(m_featurePane); + if (it == m_trackTablesByPaneId.constEnd() || it->isNull()) { + return; + } + (*it)->loadTrackModel(model); + switchToFeature(); +} + +void LibraryFeature::switchToFeature() { + m_pLibrary->switchToFeature(this); +} + +void LibraryFeature::restoreSearch(const QString& search) { + m_pLibrary->restoreSearch(m_featurePane, search); +} + +void LibraryFeature::restoreSaveButton() { + if (m_featurePane >= 0) { + m_pLibrary->restoreSaveButton(m_featurePane); + } +} + +void LibraryFeature::showBreadCrumb(TreeItem *pTree) { + m_pLibrary->showBreadCrumb(m_featurePane, pTree); +} + +void LibraryFeature::showBreadCrumb(const QModelIndex& index) { + showBreadCrumb(index.data(AbstractRole::RoleBreadCrumb).toString(), + getIcon()); +} + +void LibraryFeature::showBreadCrumb(const QString &text, const QIcon& icon) { + m_pLibrary->showBreadCrumb(m_featurePane, text, icon); +} + +void LibraryFeature::showBreadCrumb() { + showBreadCrumb(title().toString(), getIcon()); +} +WTrackTableView* LibraryFeature::getFocusedTable() { + auto it = m_trackTablesByPaneId.constFind(m_featurePane); + if (it == m_trackTablesByPaneId.constEnd() || it->isNull()) { + return nullptr; + } + return *it; } QStringList LibraryFeature::getPlaylistFiles(QFileDialog::FileMode mode) { @@ -25,15 +294,17 @@ QStringList LibraryFeature::getPlaylistFiles(QFileDialog::FileMode mode) { ConfigKey("[Library]", "LastImportExportPlaylistDirectory"), QDesktopServices::storageLocation(QDesktopServices::MusicLocation)); - QFileDialog dialogg(NULL, - tr("Import Playlist"), - lastPlaylistDirectory, - tr("Playlist Files (*.m3u *.m3u8 *.pls *.csv)")); - dialogg.setAcceptMode(QFileDialog::AcceptOpen); - dialogg.setFileMode(mode); - dialogg.setModal(true); + QFileDialog dialog(nullptr, + tr("Import Playlist"), + lastPlaylistDirectory, + tr("Playlist Files (*.m3u *.m3u8 *.pls *.csv)")); + dialog.setAcceptMode(QFileDialog::AcceptOpen); + dialog.setFileMode(mode); + dialog.setModal(true); // If the user refuses return - if (! dialogg.exec()) return QStringList(); - return dialogg.selectedFiles(); + if (!dialog.exec()) { + return QStringList(); + } + return dialog.selectedFiles(); } diff --git a/src/library/libraryfeature.h b/src/library/libraryfeature.h index f750bf6cda1..5f38dfac1ca 100644 --- a/src/library/libraryfeature.h +++ b/src/library/libraryfeature.h @@ -4,113 +4,172 @@ #ifndef LIBRARYFEATURE_H #define LIBRARYFEATURE_H -#include -#include -#include -#include -#include -#include #include -#include -#include #include +#include +#include +#include +#include +#include "preferences/usersettings.h" +#include "library/dao/savedqueriesdao.h" #include "track/track.h" -#include "treeitemmodel.h" -#include "library/coverartcache.h" -#include "library/dao/trackdao.h" +class Library; +class KeyboardEventFilter; +class TrackCollection; class TrackModel; +class TreeItem; +class TreeItemModel; +class WBaseLibrary; +class WLibraryPane; class WLibrarySidebar; -class WLibrary; -class KeyboardEventFilter; +class WTrackTableView; -// pure virtual (abstract) class to provide an interface for libraryfeatures +// abstract class to provide an interface for library features class LibraryFeature : public QObject { - Q_OBJECT + Q_OBJECT public: - LibraryFeature(QObject* parent = NULL); + // The parent does not necessary be the Library LibraryFeature(UserSettingsPointer pConfig, - QObject* parent = NULL); + Library* pLibrary, TrackCollection* pTrackCollection, + QObject* parent = nullptr); + virtual ~LibraryFeature(); virtual QVariant title() = 0; - virtual QIcon getIcon() = 0; + virtual QString getIconPath() = 0; + + // This name must be unique for each feature + virtual QString getSettingsName() const; + virtual bool isSinglePane() const; - virtual bool dropAccept(QList urls, QObject* pSource) { - Q_UNUSED(urls); - Q_UNUSED(pSource); + QIcon getIcon(); + + virtual bool dropAccept(QList /* urls */, + QObject* /* pSource */) { return false; } virtual bool dropAcceptChild(const QModelIndex& index, - QList urls, QObject* pSource) { - Q_UNUSED(index); - Q_UNUSED(urls); - Q_UNUSED(pSource); - return false; - } - virtual bool dragMoveAccept(QUrl url) { - Q_UNUSED(url); - return false; - } - virtual bool dragMoveAcceptChild(const QModelIndex& index, QUrl url) { - Q_UNUSED(index); - Q_UNUSED(url); - return false; - } - - // Reimplement this to register custom views with the library widget. - virtual void bindWidget(WLibrary* /* libraryWidget */, - KeyboardEventFilter* /* keyboard */) {} + QList urls, + QObject* pSource); + virtual bool dragMoveAccept(QUrl url); + virtual bool dragMoveAcceptChild(const QModelIndex& index, + QUrl url); + + // Reimplement this to register custom views with the library widget + // at the right pane. + virtual QWidget* createPaneWidget(KeyboardEventFilter* pKeyboard, + int paneId); + + // Reimplement this to register custom views with the library widget, + // at the sidebar expanded pane + virtual QWidget* createSidebarWidget(KeyboardEventFilter* pKeyboard); + virtual TreeItemModel* getChildModel() = 0; - - protected: - inline QStringList getPlaylistFiles() { return getPlaylistFiles(QFileDialog::ExistingFiles); } - inline QString getPlaylistFile() { return getPlaylistFiles(QFileDialog::ExistingFile).first(); } - UserSettingsPointer m_pConfig; + + virtual void setFeaturePaneId(int paneId); + int getFeaturePaneId(); + + int getFocusedPane(); + void adoptPreselectedPane(); + + virtual SavedSearchQuery saveQuery(SavedSearchQuery sQuery); + virtual void restoreQuery(int id); + virtual QList getSavedQueries() const; public slots: // called when you single click on the root item virtual void activate() = 0; // called when you single click on a child item, e.g., a concrete playlist or crate - virtual void activateChild(const QModelIndex& index) { - Q_UNUSED(index); - } + virtual void activateChild(const QModelIndex&) {} + // called when the QModelIndex passed by activateChild() becomes invalid. + virtual void invalidateChild() {} // called when you right click on the root item - virtual void onRightClick(const QPoint& globalPos) { - Q_UNUSED(globalPos); - } + virtual void onRightClick(const QPoint&) {} // called when you right click on a child item, e.g., a concrete playlist or crate - virtual void onRightClickChild(const QPoint& globalPos, QModelIndex index) { - Q_UNUSED(globalPos); - Q_UNUSED(index); - } + virtual void onRightClickChild(const QPoint& /* globalPos */, + const QModelIndex& /* index */) {} // Only implement this, if using incremental or lazy childmodels, see BrowseFeature. // This method is executed whenever you **double** click child items - virtual void onLazyChildExpandation(const QModelIndex& index) { - Q_UNUSED(index); - } + virtual void onLazyChildExpandation(const QModelIndex&) {} + + virtual void onSearch(const QString&) {} + + void slotSetHoveredSidebar() { emit hovered(this); }; + void slotResetHoveredSidebar() { emit leaved(this); }; + void slotSetFocusedSidebar() { emit focusIn(this); }; + void slotResetFocusedSidebar() { emit focusOut(this); }; + signals: - void showTrackModel(QAbstractItemModel* model); - void switchToView(const QString& view); - void loadTrack(TrackPointer pTrack); + + void loadTrack(TrackPointer); void loadTrackToPlayer(TrackPointer pTrack, QString group, bool play = false); - void restoreSearch(const QString&); // emit this signal before you parse a large music collection, e.g., iTunes, Traktor. // The second arg indicates if the feature should be "selected" when loading starts void featureIsLoading(LibraryFeature*, bool selectFeature); // emit this signal if the foreign music collection has been imported/parsed. - void featureLoadingFinished(LibraryFeature*s); + void featureLoadingFinished(LibraryFeature*); // emit this signal to select pFeature void featureSelect(LibraryFeature* pFeature, const QModelIndex& index); + // emit this signal to select an index + void selectIndex(const QModelIndex& index); // emit this signal to enable/disable the cover art widget void enableCoverArtDisplay(bool); - void trackSelected(TrackPointer pTrack); + void trackSelected(TrackPointer); + + void hovered(LibraryFeature* pLibraryFeature); + void leaved(LibraryFeature* pLibraryFeature); + void focusIn(LibraryFeature* pLibraryFeature); + void focusOut(LibraryFeature* pLibraryFeature); + protected slots: + void restoreSaveButton(); + + protected: + inline QStringList getPlaylistFiles() { + return getPlaylistFiles(QFileDialog::ExistingFiles); + } + inline QString getPlaylistFile() { + QStringList files(getPlaylistFiles(QFileDialog::ExistingFile)); + if (files.isEmpty()) { + return QString(); + } + return files.first(); + } + + // Creates a table widget with no model + WTrackTableView* createTableWidget(int paneId); + + // Creates a WLibrarySidebar widget with the getChildModel() function as + // model + WLibrarySidebar* createLibrarySidebarWidget(KeyboardEventFilter*); + + // Override this function to create a custom inner widget for the sidebar, + // the default widget is a WLibrarySidebar widget + virtual QWidget* createInnerSidebarWidget(KeyboardEventFilter* pKeyboard); + + void showTrackModel(QAbstractItemModel* model); + void switchToFeature(); + void restoreSearch(const QString& search); + void showBreadCrumb(TreeItem* pTree); + void showBreadCrumb(const QModelIndex& index); + void showBreadCrumb(const QString& text, const QIcon &icon); + void showBreadCrumb(); + + WTrackTableView* getFocusedTable(); + + UserSettingsPointer m_pConfig; + Library* m_pLibrary; + TrackCollection* m_pTrackCollection; + SavedQueriesDAO& m_savedDAO; + + int m_featurePane; + private: QStringList getPlaylistFiles(QFileDialog::FileMode mode); - + QHash > m_trackTablesByPaneId; }; #endif /* LIBRARYFEATURE_H */ diff --git a/src/library/librarypanemanager.cpp b/src/library/librarypanemanager.cpp new file mode 100644 index 00000000000..8e5ad4ad5e2 --- /dev/null +++ b/src/library/librarypanemanager.cpp @@ -0,0 +1,231 @@ +#include + +#include "library/libraryfeature.h" +#include "library/library.h" +#include "library/librarypanemanager.h" +#include "util/assert.h" +#include "widget/wbuttonbar.h" +#include "widget/wlibrarybreadcrumb.h" +#include "widget/wtracktableview.h" + +LibraryPaneManager::LibraryPaneManager(int paneId, Library *pLibrary, QObject* parent) + : QObject(parent), + m_pPaneWidget(nullptr), + m_pBreadCrumb(nullptr), + m_paneId(paneId), + m_pLibrary(pLibrary) { +} + +LibraryPaneManager::~LibraryPaneManager() { +} + +void LibraryPaneManager::bindPaneWidget(WBaseLibrary* pPaneWidget, + KeyboardEventFilter* pKeyboard) { + //qDebug() << "LibraryPaneManager::bindLibraryWidget" << libraryWidget; + m_pPaneWidget = pPaneWidget; + + connect(pPaneWidget, SIGNAL(focused()), + this, SLOT(slotPaneFocused())); + connect(pPaneWidget, SIGNAL(collapsed()), + this, SLOT(slotPaneCollapsed())); + connect(pPaneWidget, SIGNAL(uncollapsed()), + this, SLOT(slotPaneUncollapsed())); + + if (qobject_cast(pPaneWidget) == nullptr) { + return; + } + for (LibraryFeature* f : m_features) { + //f->bindPaneWidget(pPaneWidget, pKeyboard, m_paneId); + + QWidget* pFeaturePaneWidget = f->createPaneWidget(pKeyboard, m_paneId); + if (pFeaturePaneWidget == nullptr) { + continue; + } + pFeaturePaneWidget->setParent(pPaneWidget); + pPaneWidget->registerView(f, pFeaturePaneWidget); + } +} + +void LibraryPaneManager::bindSearchBar(WSearchLineEdit* pSearchBar) { + pSearchBar->installEventFilter(this); + + connect(pSearchBar, SIGNAL(search(const QString&)), + this, SLOT(slotSearch(const QString&))); + connect(pSearchBar, SIGNAL(searchCleared()), + this, SLOT(slotSearchCleared())); + connect(pSearchBar, SIGNAL(searchStarting()), + this, SLOT(slotSearchStarting())); + connect(pSearchBar, SIGNAL(focused()), + this, SLOT(slotPaneFocused())); + connect(pSearchBar, SIGNAL(cancel()), + this, SLOT(slotSearchCancel())); + + m_pSearchBar = pSearchBar; +} + +void LibraryPaneManager::setBreadCrumb(WLibraryBreadCrumb* pBreadCrumb) { + m_pBreadCrumb = pBreadCrumb; + connect(m_pBreadCrumb, SIGNAL(preselected(bool)), + this, SLOT(slotPanePreselected(bool))); +} + +void LibraryPaneManager::addFeature(LibraryFeature* feature) { + DEBUG_ASSERT_AND_HANDLE(feature) { + return; + } + + m_features.append(feature); +} + +void LibraryPaneManager::addFeatures(const QList& features) { + m_features.append(features); +} + +WBaseLibrary* LibraryPaneManager::getPaneWidget() { + return m_pPaneWidget; +} + +void LibraryPaneManager::setCurrentFeature(LibraryFeature* pFeature) { + m_pCurrentFeature = pFeature; +} + +LibraryFeature *LibraryPaneManager::getCurrentFeature() const { + return m_pCurrentFeature; +} + +void LibraryPaneManager::setFocused(bool value) { + DEBUG_ASSERT_AND_HANDLE(m_pPaneWidget) { + return; + } + + m_pPaneWidget->setProperty("showFocus", (int) value); +} + +void LibraryPaneManager::switchToFeature(LibraryFeature* pFeature) { + m_pCurrentFeature = pFeature; + if (!m_pPaneWidget.isNull()) { + m_pPaneWidget->switchToFeature(pFeature); + } +} + +void LibraryPaneManager::restoreSearch(const QString& text) { + if (!m_pSearchBar.isNull()) { + m_pSearchBar->restoreSearch(text, m_pCurrentFeature); + } +} + +void LibraryPaneManager::restoreSaveButton() { + if (!m_pSearchBar.isNull()) { + m_pSearchBar->slotRestoreSaveButton(); + } +} + +void LibraryPaneManager::showBreadCrumb(TreeItem *pTree) { + DEBUG_ASSERT_AND_HANDLE(!m_pBreadCrumb.isNull()) { + return; + } + + m_pBreadCrumb->showBreadCrumb(pTree); +} + +void LibraryPaneManager::showBreadCrumb(const QString &text, const QIcon& icon) { + DEBUG_ASSERT_AND_HANDLE(!m_pBreadCrumb.isNull()) { + return; + } + m_pBreadCrumb->showBreadCrumb(text, icon); +} + +int LibraryPaneManager::getPaneId() const { + return m_paneId; +} + +void LibraryPaneManager::setPreselected(bool value) { + if (!m_pBreadCrumb.isNull()) { + m_pBreadCrumb->setPreselected(value); + } +} + +bool LibraryPaneManager::isPreselected() const { + if (!m_pBreadCrumb.isNull()) { + return m_pBreadCrumb->isPreselected(); + } + return false; +} + +void LibraryPaneManager::setPreviewed(bool value) { + if (!m_pBreadCrumb.isNull()) { + m_pBreadCrumb->setPreviewed(value); + } +} + +void LibraryPaneManager::slotPanePreselected(bool value) { + // Preselect button clicked + setPreselected(value); + m_pLibrary->panePreselected(this, value); +} + +void LibraryPaneManager::slotPaneCollapsed() { + m_pLibrary->paneCollapsed(m_paneId); +} + +void LibraryPaneManager::slotPaneUncollapsed() { + m_pLibrary->paneUncollapsed(m_paneId); +} + +void LibraryPaneManager::slotPaneFocused() { + m_pLibrary->paneFocused(this); +} + +void LibraryPaneManager::slotSearchCancel() { + if (!m_pPaneWidget.isNull()) { + QWidget* cw = m_pPaneWidget->currentWidget(); + if (cw != nullptr) { + cw->setFocus(); + } + } +} + +void LibraryPaneManager::slotSearch(const QString& text) { + DEBUG_ASSERT_AND_HANDLE(!m_pPaneWidget.isNull()) { + return; + } + m_pPaneWidget->search(text); + m_pCurrentFeature->onSearch(text); +} + +void LibraryPaneManager::slotSearchStarting() { + if (!m_pPaneWidget.isNull()) { + m_pPaneWidget->searchStarting(); + } +} + +void LibraryPaneManager::slotSearchCleared() { + if (!m_pPaneWidget.isNull()) { + m_pPaneWidget->searchCleared(); + } +} + +bool LibraryPaneManager::eventFilter(QObject*, QEvent* event) { + if (m_pPaneWidget.isNull() || m_pSearchBar.isNull()) { + return false; + } + + if (event->type() == QEvent::MouseButtonPress && + (m_pPaneWidget->underMouse() || m_pSearchBar->underMouse())) { + m_pLibrary->paneFocused(this); + } else if (event->type() == QEvent::FocusIn) { + m_pLibrary->paneFocused(this); + } + return false; +} + +bool LibraryPaneManager::focusSearch() { + if (m_pSearchBar.isNull()) { + return false; + } + if (!m_pSearchBar->isEnabled()) { + return false; + } + m_pSearchBar->setFocus(); + return true; +} diff --git a/src/library/librarypanemanager.h b/src/library/librarypanemanager.h new file mode 100644 index 00000000000..52cd0f9a8f0 --- /dev/null +++ b/src/library/librarypanemanager.h @@ -0,0 +1,94 @@ +#ifndef LIBRARYVIEWMANAGER_H +#define LIBRARYVIEWMANAGER_H + +#include +#include + +#include "../widget/wlibrarypane.h" +#include "widget/wsearchlineedit.h" +#include "widget/wtracktableview.h" + +class LibraryFeature; +class WButtonBar; +class WLibraryBreadCrumb; +class TreeItem; +class Library; + +class LibraryPaneManager : public QObject { + Q_OBJECT + + public: + + LibraryPaneManager(int paneId, Library* pLibrary, QObject* parent = nullptr); + + ~LibraryPaneManager(); + + bool initialize(); + + // All features must be added before adding a pane + virtual void bindPaneWidget(WBaseLibrary* pLibraryWidget, + KeyboardEventFilter* pKeyboard); + void bindSearchBar(WSearchLineEdit* pSearchBar); + void setBreadCrumb(WLibraryBreadCrumb* pBreadCrumb); + + void addFeature(LibraryFeature* feature); + void addFeatures(const QList& features); + + WBaseLibrary* getPaneWidget(); + + void setCurrentFeature(LibraryFeature* pFeature); + LibraryFeature* getCurrentFeature() const; + + void setFocused(bool value); + + void restoreSearch(const QString& text); + void restoreSaveButton(); + void switchToFeature(LibraryFeature* pFeature); + void showBreadCrumb(TreeItem* pTree); + void showBreadCrumb(const QString& text, const QIcon &icon); + + int getPaneId() const; + + void setPreselected(bool value); + bool isPreselected() const; + + void setPreviewed(bool value); + + bool focusSearch(); + + signals: + + void searchCleared(); + void searchStarting(); + + public slots: + + void slotPanePreselected(bool value); + void slotPaneCollapsed(); + void slotPaneUncollapsed(); + void slotPaneFocused(); + void slotSearch(const QString& text); + void slotSearchStarting(); + void slotSearchCleared(); + void slotSearchCancel(); + + protected: + + QPointer m_pPaneWidget; + QList m_features; + QPointer m_pBreadCrumb; + QPointer m_pSearchBar; + + private: + + LibraryFeature* m_pCurrentFeature; + int m_paneId; + Library* m_pLibrary; + + private slots: + + // Used to handle focus change + bool eventFilter(QObject*, QEvent* event); +}; + +#endif // LIBRARYVIEWMANAGER_H diff --git a/src/library/librarysidebarexpandedmanager.cpp b/src/library/librarysidebarexpandedmanager.cpp new file mode 100644 index 00000000000..60da9906c79 --- /dev/null +++ b/src/library/librarysidebarexpandedmanager.cpp @@ -0,0 +1,23 @@ +#include "librarysidebarexpandedmanager.h" +#include "library/libraryfeature.h" + +LibrarySidebarExpandedManager::LibrarySidebarExpandedManager(Library *pLibrary, + QObject* parent) + : LibraryPaneManager(-1, pLibrary, parent) { + +} + +void LibrarySidebarExpandedManager::bindPaneWidget(WBaseLibrary* sidebarWidget, + KeyboardEventFilter* pKeyboard) { + m_pPaneWidget = sidebarWidget; + + for (LibraryFeature* f : m_features) { + QWidget* pPane = f->createSidebarWidget(pKeyboard); + if (pPane == nullptr) { + continue; + } + pPane->setParent(sidebarWidget); + sidebarWidget->registerView(f, pPane); + } +} + diff --git a/src/library/librarysidebarexpandedmanager.h b/src/library/librarysidebarexpandedmanager.h new file mode 100644 index 00000000000..7a29eb9ada9 --- /dev/null +++ b/src/library/librarysidebarexpandedmanager.h @@ -0,0 +1,14 @@ +#ifndef LIBRARYSIDEBAREXPANDEDMANAGER_H +#define LIBRARYSIDEBAREXPANDEDMANAGER_H +#include "library/librarypanemanager.h" + +class LibrarySidebarExpandedManager : public LibraryPaneManager +{ + public: + LibrarySidebarExpandedManager(Library* pLibrary, QObject* parent = nullptr); + + void bindPaneWidget(WBaseLibrary* sidebarWidget, + KeyboardEventFilter* pKeyboard) override; +}; + +#endif // LIBRARYSIDEBAREXPANDEDMANAGER_H diff --git a/src/library/librarytablemodel.cpp b/src/library/librarytablemodel.cpp index 71616d510c5..7b1f8638613 100644 --- a/src/library/librarytablemodel.cpp +++ b/src/library/librarytablemodel.cpp @@ -82,6 +82,7 @@ bool LibraryTableModel::isColumnInternal(int column) { (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) || (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_CHANNELS)) || (column == fieldIndex(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_FSDELETED)) || + (column == fieldIndex(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_DIRECTORY)) || (PlayerManager::numPreviewDecks() == 0 && column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) || (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART_SOURCE)) || diff --git a/src/library/libraryview.h b/src/library/libraryview.h index 5ae34aee2fe..c40e6d8009d 100644 --- a/src/library/libraryview.h +++ b/src/library/libraryview.h @@ -7,22 +7,25 @@ #ifndef LIBRARYVIEW_H #define LIBRARYVIEW_H +#include #include class LibraryView { public: - virtual ~LibraryView() {}; + virtual ~LibraryView() {} virtual void onShow() = 0; // reimplement if LibraryView should be able to search - virtual void onSearch(const QString& text) {Q_UNUSED(text);} + virtual void onSearch(const QString&) {} + virtual void onSearchStarting() {} + virtual void onSearchCleared() {} // If applicable, requests that the LibraryView load the selected // track. Does nothing otherwise. - virtual void loadSelectedTrack() {}; + virtual void loadSelectedTrack() {} - virtual void slotSendToAutoDJ() {}; - virtual void slotSendToAutoDJTop() {}; + virtual void slotSendToAutoDJ() {} + virtual void slotSendToAutoDJTop() {} // If applicable, requests that the LibraryView load the selected track to // the specified group. Does nothing otherwise. diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp deleted file mode 100644 index 60271e3a38e..00000000000 --- a/src/library/mixxxlibraryfeature.cpp +++ /dev/null @@ -1,192 +0,0 @@ -// mixxxlibraryfeature.cpp -// Created 8/23/2009 by RJ Ryan (rryan@mit.edu) - -#include - -#include "library/mixxxlibraryfeature.h" - -#include "library/parser.h" -#include "library/library.h" -#include "library/basetrackcache.h" -#include "library/librarytablemodel.h" -#include "library/missingtablemodel.h" -#include "library/hiddentablemodel.h" -#include "library/queryutil.h" -#include "library/trackcollection.h" -#include "treeitem.h" -#include "sources/soundsourceproxy.h" -#include "widget/wlibrary.h" -#include "util/dnd.h" -#include "library/dlghidden.h" -#include "library/dlgmissing.h" - -MixxxLibraryFeature::MixxxLibraryFeature(Library* pLibrary, - TrackCollection* pTrackCollection, - UserSettingsPointer pConfig) - : LibraryFeature(pLibrary), - kMissingTitle(tr("Missing Tracks")), - kHiddenTitle(tr("Hidden Tracks")), - m_pLibrary(pLibrary), - m_pMissingView(NULL), - m_pHiddenView(NULL), - m_trackDao(pTrackCollection->getTrackDAO()), - m_pConfig(pConfig), - m_pTrackCollection(pTrackCollection) { - QStringList columns; - columns << "library." + LIBRARYTABLE_ID - << "library." + LIBRARYTABLE_PLAYED - << "library." + LIBRARYTABLE_TIMESPLAYED - //has to be up here otherwise Played and TimesPlayed are not show - << "library." + LIBRARYTABLE_ALBUMARTIST - << "library." + LIBRARYTABLE_ALBUM - << "library." + LIBRARYTABLE_ARTIST - << "library." + LIBRARYTABLE_TITLE - << "library." + LIBRARYTABLE_YEAR - << "library." + LIBRARYTABLE_RATING - << "library." + LIBRARYTABLE_GENRE - << "library." + LIBRARYTABLE_COMPOSER - << "library." + LIBRARYTABLE_GROUPING - << "library." + LIBRARYTABLE_TRACKNUMBER - << "library." + LIBRARYTABLE_KEY - << "library." + LIBRARYTABLE_KEY_ID - << "library." + LIBRARYTABLE_BPM - << "library." + LIBRARYTABLE_BPM_LOCK - << "library." + LIBRARYTABLE_DURATION - << "library." + LIBRARYTABLE_BITRATE - << "library." + LIBRARYTABLE_REPLAYGAIN - << "library." + LIBRARYTABLE_FILETYPE - << "library." + LIBRARYTABLE_DATETIMEADDED - << "track_locations.location" - << "track_locations.fs_deleted" - << "library." + LIBRARYTABLE_COMMENT - << "library." + LIBRARYTABLE_MIXXXDELETED - << "library." + LIBRARYTABLE_COVERART_SOURCE - << "library." + LIBRARYTABLE_COVERART_TYPE - << "library." + LIBRARYTABLE_COVERART_LOCATION - << "library." + LIBRARYTABLE_COVERART_HASH; - - QSqlQuery query(pTrackCollection->getDatabase()); - QString tableName = "library_cache_view"; - QString queryString = QString( - "CREATE TEMPORARY VIEW IF NOT EXISTS %1 AS " - "SELECT %2 FROM library " - "INNER JOIN track_locations ON library.location = track_locations.id") - .arg(tableName, columns.join(",")); - query.prepare(queryString); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } - - // Strip out library. and track_locations. - for (QStringList::iterator it = columns.begin(); - it != columns.end(); ++it) { - if (it->startsWith("library.")) { - *it = it->replace("library.", ""); - } else if (it->startsWith("track_locations.")) { - *it = it->replace("track_locations.", ""); - } - } - - BaseTrackCache* pBaseTrackCache = new BaseTrackCache( - pTrackCollection, tableName, LIBRARYTABLE_ID, columns, true); - connect(&m_trackDao, SIGNAL(trackDirty(TrackId)), - pBaseTrackCache, SLOT(slotTrackDirty(TrackId))); - connect(&m_trackDao, SIGNAL(trackClean(TrackId)), - pBaseTrackCache, SLOT(slotTrackClean(TrackId))); - connect(&m_trackDao, SIGNAL(trackChanged(TrackId)), - pBaseTrackCache, SLOT(slotTrackChanged(TrackId))); - connect(&m_trackDao, SIGNAL(tracksAdded(QSet)), - pBaseTrackCache, SLOT(slotTracksAdded(QSet))); - connect(&m_trackDao, SIGNAL(tracksRemoved(QSet)), - pBaseTrackCache, SLOT(slotTracksRemoved(QSet))); - connect(&m_trackDao, SIGNAL(dbTrackAdded(TrackPointer)), - pBaseTrackCache, SLOT(slotDbTrackAdded(TrackPointer))); - - m_pBaseTrackCache = QSharedPointer(pBaseTrackCache); - pTrackCollection->setTrackSource(m_pBaseTrackCache); - - // These rely on the 'default' track source being present. - m_pLibraryTableModel = new LibraryTableModel(this, pTrackCollection, "mixxx.db.model.library"); - - TreeItem* pRootItem = new TreeItem(); - TreeItem* pmissingChildItem = new TreeItem(kMissingTitle, kMissingTitle, - this, pRootItem); - TreeItem* phiddenChildItem = new TreeItem(kHiddenTitle, kHiddenTitle, - this, pRootItem); - pRootItem->appendChild(pmissingChildItem); - pRootItem->appendChild(phiddenChildItem); - - m_childModel.setRootItem(pRootItem); -} - -MixxxLibraryFeature::~MixxxLibraryFeature() { - delete m_pLibraryTableModel; -} - -void MixxxLibraryFeature::bindWidget(WLibrary* pLibraryWidget, - KeyboardEventFilter* pKeyboard) { - m_pHiddenView = new DlgHidden(pLibraryWidget, m_pConfig, m_pLibrary, - m_pTrackCollection, pKeyboard); - pLibraryWidget->registerView(kHiddenTitle, m_pHiddenView); - connect(m_pHiddenView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); - - m_pMissingView = new DlgMissing(pLibraryWidget, m_pConfig, m_pLibrary, - m_pTrackCollection, pKeyboard); - pLibraryWidget->registerView(kMissingTitle, m_pMissingView); - connect(m_pMissingView, SIGNAL(trackSelected(TrackPointer)), - this, SIGNAL(trackSelected(TrackPointer))); -} - -QVariant MixxxLibraryFeature::title() { - return tr("Library"); -} - -QIcon MixxxLibraryFeature::getIcon() { - return QIcon(":/images/library/ic_library_library.png"); -} - -TreeItemModel* MixxxLibraryFeature::getChildModel() { - return &m_childModel; -} - -void MixxxLibraryFeature::refreshLibraryModels() { - if (m_pLibraryTableModel) { - m_pLibraryTableModel->select(); - } - if (m_pMissingView) { - m_pMissingView->onShow(); - } - if (m_pHiddenView) { - m_pHiddenView->onShow(); - } -} - -void MixxxLibraryFeature::activate() { - qDebug() << "MixxxLibraryFeature::activate()"; - emit(showTrackModel(m_pLibraryTableModel)); - emit(enableCoverArtDisplay(true)); -} - -void MixxxLibraryFeature::activateChild(const QModelIndex& index) { - QString itemName = index.data().toString(); - emit(switchToView(itemName)); - emit(enableCoverArtDisplay(true)); -} - -bool MixxxLibraryFeature::dropAccept(QList urls, QObject* pSource) { - if (pSource) { - return false; - } else { - QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - - // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_trackDao.addMultipleTracks(files, true); - return trackIds.size() > 0; - } -} - -bool MixxxLibraryFeature::dragMoveAccept(QUrl url) { - return SoundSourceProxy::isUrlSupported(url) || - Parser::isPlaylistFilenameSupported(url.toLocalFile()); -} diff --git a/src/library/mixxxlibraryfeature.h b/src/library/mixxxlibraryfeature.h deleted file mode 100644 index 54d0a14f51e..00000000000 --- a/src/library/mixxxlibraryfeature.h +++ /dev/null @@ -1,64 +0,0 @@ -// mixxxlibraryfeature.h -// Created 8/23/2009 by RJ Ryan (rryan@mit.edu) - -#ifndef MIXXXLIBRARYFEATURE_H -#define MIXXXLIBRARYFEATURE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "library/libraryfeature.h" -#include "library/dao/trackdao.h" -#include "treeitemmodel.h" -#include "preferences/usersettings.h" - -class DlgHidden; -class DlgMissing; -class Library; -class BaseTrackCache; -class LibraryTableModel; -class TrackCollection; - -class MixxxLibraryFeature : public LibraryFeature { - Q_OBJECT - public: - MixxxLibraryFeature(Library* pLibrary, - TrackCollection* pTrackCollection, - UserSettingsPointer pConfig); - virtual ~MixxxLibraryFeature(); - - QVariant title(); - QIcon getIcon(); - bool dropAccept(QList urls, QObject* pSource); - bool dragMoveAccept(QUrl url); - TreeItemModel* getChildModel(); - void bindWidget(WLibrary* pLibrary, - KeyboardEventFilter* pKeyboard); - - public slots: - void activate(); - void activateChild(const QModelIndex& index); - void refreshLibraryModels(); - - private: - const QString kMissingTitle; - const QString kHiddenTitle; - Library* m_pLibrary; - QSharedPointer m_pBaseTrackCache; - LibraryTableModel* m_pLibraryTableModel; - DlgMissing* m_pMissingView; - DlgHidden* m_pHiddenView; - TreeItemModel m_childModel; - TrackDAO& m_trackDao; - UserSettingsPointer m_pConfig; - TrackCollection* m_pTrackCollection; -}; - -#endif /* MIXXXLIBRARYFEATURE_H */ diff --git a/src/library/previewbuttondelegate.cpp b/src/library/previewbuttondelegate.cpp index 6c09f594298..0a8c1129011 100644 --- a/src/library/previewbuttondelegate.cpp +++ b/src/library/previewbuttondelegate.cpp @@ -1,5 +1,7 @@ +#include #include #include +#include #include "library/previewbuttondelegate.h" #include "library/trackmodel.h" @@ -10,8 +12,8 @@ PreviewButtonDelegate::PreviewButtonDelegate(QObject *parent, int column) : QStyledItemDelegate(parent), - m_pTableView(NULL), - m_pButton(NULL), + m_pTableView(nullptr), + m_pButton(nullptr), m_isOneCellInEditMode(false), m_column(column) { m_pPreviewDeckPlay = new ControlProxy( @@ -31,6 +33,7 @@ PreviewButtonDelegate::PreviewButtonDelegate(QObject *parent, int column) m_pButton->setObjectName("LibraryPreviewButton"); m_pButton->setCheckable(true); m_pButton->setChecked(false); + m_pButton->setFocusPolicy(Qt::ClickFocus); m_pButton->hide(); connect(m_pTableView, SIGNAL(entered(QModelIndex)), this, SLOT(cellEntered(QModelIndex))); @@ -47,6 +50,7 @@ QWidget* PreviewButtonDelegate::createEditor(QWidget *parent, QPushButton* btn = new QPushButton(parent); btn->setObjectName("LibraryPreviewButton"); btn->setCheckable(true); + btn->setFocusPolicy(Qt::NoFocus); bool playing = m_pPreviewDeckPlay->toBool(); // Check-state is whether the track is loaded (index.data()) and whether // it's playing. @@ -72,10 +76,20 @@ void PreviewButtonDelegate::setModelData(QWidget *editor, Q_UNUSED(index); } -void PreviewButtonDelegate::paint(QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const { - // Let the editor paint in this case +void PreviewButtonDelegate::paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const { + + QStyleOptionViewItemV4 opt = option; + initStyleOption(&opt, index); + + // Draw original item without text as background + opt.text = QString(); + const QWidget *widget = opt.widget; + QStyle *style = widget ? widget->style() : QApplication::style(); + style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget); + + // Let the editor paint the button in this case if (index == m_currentEditedCellIndex) { return; } @@ -90,10 +104,6 @@ void PreviewButtonDelegate::paint(QPainter *painter, // it's playing. m_pButton->setChecked(index.data().toBool() && playing); - if (option.state == QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.base()); - } - painter->save(); // Render button at the desired position painter->translate(option.rect.topLeft()); diff --git a/src/library/recording/dlgrecording.h b/src/library/recording/dlgrecording.h deleted file mode 100644 index 53b515bf322..00000000000 --- a/src/library/recording/dlgrecording.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef DLGRECORDING_H -#define DLGRECORDING_H - -#include "preferences/usersettings.h" -#include "library/browse/browsetablemodel.h" -#include "library/libraryview.h" -#include "library/proxytrackmodel.h" -#include "library/library.h" -#include "library/trackcollection.h" -#include "controllers/keyboard/keyboardeventfilter.h" -#include "recording/recordingmanager.h" -#include "track/track.h" -#include "library/recording/ui_dlgrecording.h" - -class PlaylistTableModel; -class QSqlTableModel; -class WTrackTableView; - -class DlgRecording : public QWidget, public Ui::DlgRecording, public virtual LibraryView { - Q_OBJECT - public: - DlgRecording(QWidget *parent, UserSettingsPointer pConfig, - Library* pLibrary, TrackCollection* pTrackCollection, - RecordingManager* pRecManager, KeyboardEventFilter* pKeyboard); - virtual ~DlgRecording(); - - virtual void onSearch(const QString& text); - virtual void onShow(); - virtual void loadSelectedTrack(); - virtual void slotSendToAutoDJ(); - virtual void slotSendToAutoDJTop(); - virtual void loadSelectedTrackToGroup(QString group, bool play); - virtual void moveSelection(int delta); - inline const QString currentSearch() { return m_proxyModel.currentSearch(); } - - public slots: - void toggleRecording(bool toggle); - void slotRecordingEnabled(bool); - void slotBytesRecorded(int); - void refreshBrowseModel(); - void slotRestoreSearch(); - void slotDurationRecorded(QString durationRecorded); - void setTrackTableFont(const QFont& font); - void setTrackTableRowHeight(int rowHeight); - - signals: - void loadTrack(TrackPointer tio); - void loadTrackToPlayer(TrackPointer tio, QString group, bool play); - void restoreSearch(QString search); - - private: - UserSettingsPointer m_pConfig; - TrackCollection* m_pTrackCollection; - WTrackTableView* m_pTrackTableView; - BrowseTableModel m_browseModel; - ProxyTrackModel m_proxyModel; - QString m_recordingDir; - - void refreshLabel(); - QString m_bytesRecordedStr; - QString m_durationRecordedStr; - - RecordingManager* m_pRecordingManager; -}; - -#endif //DLGRECORDING_H diff --git a/src/library/recording/dlgrecording.ui b/src/library/recording/dlgrecording.ui deleted file mode 100644 index b9420593637..00000000000 --- a/src/library/recording/dlgrecording.ui +++ /dev/null @@ -1,92 +0,0 @@ - - - DlgRecording - - - - 0 - 0 - 582 - 399 - - - - Recordings - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Status: - - - - - - - Start Recording - - - true - - - - - - - - - true - - - - - - - - diff --git a/src/library/recording/recordingfeature.cpp b/src/library/recording/recordingfeature.cpp deleted file mode 100644 index e3a3c0d8c3a..00000000000 --- a/src/library/recording/recordingfeature.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// recordingfeature.cpp -// Created 03/26/2010 by Tobias Rafreider - -#include "library/recording/dlgrecording.h" -#include "track/track.h" -#include "library/treeitem.h" -#include "library/recording/recordingfeature.h" -#include "library/library.h" -#include "library/trackcollection.h" -#include "widget/wlibrary.h" -#include "controllers/keyboard/keyboardeventfilter.h" - -const QString RecordingFeature::m_sRecordingViewName = QString("Recording"); - -RecordingFeature::RecordingFeature(Library* pLibrary, - UserSettingsPointer pConfig, - TrackCollection* pTrackCollection, - RecordingManager* pRecordingManager) - : LibraryFeature(pLibrary), - m_pConfig(pConfig), - m_pLibrary(pLibrary), - m_pTrackCollection(pTrackCollection), - m_pRecordingManager(pRecordingManager) { -} - -RecordingFeature::~RecordingFeature() { - -} - -QVariant RecordingFeature::title() { - return QVariant(tr("Recordings")); -} - -QIcon RecordingFeature::getIcon() { - return QIcon(":/images/library/ic_library_recordings.png"); -} - -TreeItemModel* RecordingFeature::getChildModel() { - return &m_childModel; -} -void RecordingFeature::bindWidget(WLibrary* pLibraryWidget, - KeyboardEventFilter *keyboard) { - //The view will be deleted by LibraryWidget - DlgRecording* pRecordingView = new DlgRecording(pLibraryWidget, - m_pConfig, - m_pLibrary, - m_pTrackCollection, - m_pRecordingManager, - keyboard); - - pRecordingView->installEventFilter(keyboard); - pLibraryWidget->registerView(m_sRecordingViewName, pRecordingView); - connect(pRecordingView, SIGNAL(loadTrack(TrackPointer)), - this, SIGNAL(loadTrack(TrackPointer))); - connect(pRecordingView, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool)), - this, SIGNAL(loadTrackToPlayer(TrackPointer, QString, bool))); - connect(this, SIGNAL(refreshBrowseModel()), - pRecordingView, SLOT(refreshBrowseModel())); - connect(this, SIGNAL(requestRestoreSearch()), - pRecordingView, SLOT(slotRestoreSearch())); - connect(pRecordingView, SIGNAL(restoreSearch(QString)), - this, SIGNAL(restoreSearch(QString))); -} - - -void RecordingFeature::activate() { - emit(refreshBrowseModel()); - emit(switchToView(m_sRecordingViewName)); - // Ask the view to emit a restoreSearch signal. - emit(requestRestoreSearch()); - emit(enableCoverArtDisplay(false)); -} diff --git a/src/library/searchquery.cpp b/src/library/searchquery.cpp index d4e10b7b5cf..502fb2ed752 100644 --- a/src/library/searchquery.cpp +++ b/src/library/searchquery.cpp @@ -165,6 +165,17 @@ QString TextFilterNode::toSql() const { return concatSqlClauses(searchClauses, "OR"); } +QString ExactFilterNode::toSql() const { + FieldEscaper escaper(m_database); + QString escapedArgument = escaper.escapeString(m_argument); + + QStringList searchClauses; + for (const QString& sqlColumn : m_sqlColumns) { + searchClauses << QString("%1 GLOB %2").arg(sqlColumn, escapedArgument); + } + return concatSqlClauses(searchClauses, "OR"); +} + NumericFilterNode::NumericFilterNode(const QStringList& sqlColumns) : m_sqlColumns(sqlColumns), m_bOperatorQuery(false), diff --git a/src/library/searchquery.h b/src/library/searchquery.h index 222a4ae0c9e..5f1d3c5462d 100644 --- a/src/library/searchquery.h +++ b/src/library/searchquery.h @@ -84,12 +84,21 @@ class TextFilterNode : public QueryNode { bool match(const TrackPointer& pTrack) const override; QString toSql() const override; - private: + protected: QSqlDatabase m_database; QStringList m_sqlColumns; QString m_argument; }; +class ExactFilterNode : public TextFilterNode { + public: + ExactFilterNode(const QSqlDatabase& database, + const QStringList& sqlColumns, + const QString& argument) + : TextFilterNode(database, sqlColumns, argument) {} + QString toSql() const override; +}; + class NumericFilterNode : public QueryNode { public: NumericFilterNode(const QStringList& sqlColumns, const QString& argument); diff --git a/src/library/searchqueryparser.cpp b/src/library/searchqueryparser.cpp index f261088d805..b9b8a1209c9 100644 --- a/src/library/searchqueryparser.cpp +++ b/src/library/searchqueryparser.cpp @@ -15,7 +15,9 @@ SearchQueryParser::SearchQueryParser(QSqlDatabase& database) << "composer" << "grouping" << "comment" - << "location"; + << "location" + << "directory" + << "folder"; m_numericFilters << "year" << "track" << "bpm" @@ -24,10 +26,10 @@ SearchQueryParser::SearchQueryParser(QSqlDatabase& database) << "bitrate"; m_specialFilters << "key" << "duration" - << "added" - << "dateadded" - << "datetime_added" - << "date_added"; + << "added" + << "dateadded" + << "datetime_added" + << "date_added"; m_fieldToSqlColumns["artist"] << "artist" << "album_artist"; m_fieldToSqlColumns["album_artist"] << "album_artist"; @@ -47,6 +49,8 @@ SearchQueryParser::SearchQueryParser(QSqlDatabase& database) m_fieldToSqlColumns["played"] << "timesplayed"; m_fieldToSqlColumns["rating"] << "rating"; m_fieldToSqlColumns["location"] << "location"; + m_fieldToSqlColumns["directory"] << "directory"; + m_fieldToSqlColumns["folder"] << "directory"; m_fieldToSqlColumns["datetime_added"] << "datetime_added"; m_allFilters.append(m_textFilters); @@ -55,6 +59,7 @@ SearchQueryParser::SearchQueryParser(QSqlDatabase& database) m_fuzzyMatcher = QRegExp(QString("^~(%1)$").arg(m_allFilters.join("|"))); m_textFilterMatcher = QRegExp(QString("^-?(%1):(.*)$").arg(m_textFilters.join("|"))); + m_exactTextMatcher = QRegExp(QString("^-?(%1):=(.*)$").arg(m_textFilters.join("|"))); m_numericFilterMatcher = QRegExp(QString("^-?(%1):(.*)$").arg(m_numericFilters.join("|"))); m_specialFilterMatcher = QRegExp(QString("^[~-]?(%1):(.*)$").arg(m_specialFilters.join("|"))); } @@ -117,18 +122,36 @@ void SearchQueryParser::parseTokens(QStringList tokens, if (m_fuzzyMatcher.indexIn(token) != -1) { // TODO(XXX): implement this feature. } else if (m_textFilterMatcher.indexIn(token) != -1) { + bool exact = m_exactTextMatcher.indexIn(token) != -1; bool negate = token.startsWith(kNegatePrefix); - QString field = m_textFilterMatcher.cap(1); - QString argument = getTextArgument( - m_textFilterMatcher.cap(2), &tokens).trimmed(); + QString field = m_textFilterMatcher.cap(1); + QString argument; + if (exact) { + argument = getTextArgument( + m_exactTextMatcher.cap(2), &tokens).trimmed(); + } else { + argument = getTextArgument( + m_textFilterMatcher.cap(2), &tokens).trimmed(); + } if (!argument.isEmpty()) { - std::unique_ptr pNode(std::make_unique( - m_database, m_fieldToSqlColumns[field], argument)); + std::unique_ptr pNode; + + if (exact) { + pNode = std::make_unique( + m_database, m_fieldToSqlColumns[field], argument); + } else { + pNode = std::make_unique( + m_database, m_fieldToSqlColumns[field], argument); + } if (negate) { pNode = std::make_unique(std::move(pNode)); } pQuery->addNode(std::move(pNode)); + } else { + std::unique_ptr pNode(std::make_unique( + field + " IS NULL")); + pQuery->addNode(std::move(pNode)); } } else if (m_numericFilterMatcher.indexIn(token) != -1) { bool negate = token.startsWith(kNegatePrefix); diff --git a/src/library/searchqueryparser.h b/src/library/searchqueryparser.h index f505abcb335..3c830b941a0 100644 --- a/src/library/searchqueryparser.h +++ b/src/library/searchqueryparser.h @@ -36,6 +36,7 @@ class SearchQueryParser { QRegExp m_fuzzyMatcher; QRegExp m_textFilterMatcher; + QRegExp m_exactTextMatcher; QRegExp m_numericFilterMatcher; QRegExp m_specialFilterMatcher; diff --git a/src/library/setlogfeature.h b/src/library/setlogfeature.h deleted file mode 100644 index d02b25e4e6e..00000000000 --- a/src/library/setlogfeature.h +++ /dev/null @@ -1,54 +0,0 @@ -// setlogfeature.h - -#ifndef SETLOGFEATURE_H -#define SETLOGFEATURE_H - -#include -#include -#include - -#include "library/baseplaylistfeature.h" -#include "preferences/usersettings.h" - -class TrackCollection; -class TreeItem; - -class SetlogFeature : public BasePlaylistFeature { - Q_OBJECT -public: - SetlogFeature(QObject* parent, UserSettingsPointer pConfig, - TrackCollection* pTrackCollection); - virtual ~SetlogFeature(); - - QVariant title(); - QIcon getIcon(); - - virtual void bindWidget(WLibrary* libraryWidget, - KeyboardEventFilter* keyboard); - - public slots: - void onRightClick(const QPoint& globalPos); - void onRightClickChild(const QPoint& globalPos, QModelIndex index); - void slotJoinWithPrevious(); - void slotGetNewPlaylist(); - - protected: - void buildPlaylistList(); - void decorateChild(TreeItem *pChild, int playlist_id); - - private slots: - void slotPlayingTrackChanged(TrackPointer currentPlayingTrack); - void slotPlaylistTableChanged(int playlistId); - void slotPlaylistContentChanged(int playlistId); - void slotPlaylistTableRenamed(int playlistId, QString a_strName); - - private: - QString getRootViewHtml() const; - - QLinkedList m_recentTracks; - QAction* m_pJoinWithPreviousAction; - QAction* m_pGetNewPlaylist; - int m_playlistId; -}; - -#endif // SETLOGFEATURE_H diff --git a/src/library/sidebarmodel.cpp b/src/library/sidebarmodel.cpp deleted file mode 100644 index a87aaa44c60..00000000000 --- a/src/library/sidebarmodel.cpp +++ /dev/null @@ -1,427 +0,0 @@ -#include -#include -#include - -#include "library/libraryfeature.h" -#include "library/sidebarmodel.h" -#include "library/treeitem.h" -#include "library/browse/browsefeature.h" -#include "util/assert.h" - -SidebarModel::SidebarModel(QObject* parent) - : QAbstractItemModel(parent), - m_iDefaultSelectedIndex(0) { -} - -SidebarModel::~SidebarModel() { - -} - -void SidebarModel::addLibraryFeature(LibraryFeature* feature) { - m_sFeatures.push_back(feature); - connect(feature, SIGNAL(featureIsLoading(LibraryFeature*, bool)), - this, SLOT(slotFeatureIsLoading(LibraryFeature*, bool))); - connect(feature, SIGNAL(featureLoadingFinished(LibraryFeature*)), - this, SLOT(slotFeatureLoadingFinished(LibraryFeature*))); - connect(feature, SIGNAL(featureSelect(LibraryFeature*, const QModelIndex&)), - this, SLOT(slotFeatureSelect(LibraryFeature*, const QModelIndex&))); - - QAbstractItemModel* model = feature->getChildModel(); - - connect(model, SIGNAL(modelReset()), - this, SLOT(slotModelReset())); - connect(model, SIGNAL(dataChanged(const QModelIndex&,const QModelIndex&)), - this, SLOT(slotDataChanged(const QModelIndex&,const QModelIndex&))); - - connect(model, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)), - this, SLOT(slotRowsAboutToBeInserted(const QModelIndex&, int, int))); - connect(model, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), - this, SLOT(slotRowsAboutToBeRemoved(const QModelIndex&, int, int))); - connect(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)), - this, SLOT(slotRowsInserted(const QModelIndex&, int, int))); - connect(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)), - this, SLOT(slotRowsRemoved(const QModelIndex&, int, int))); - -} - -QModelIndex SidebarModel::getDefaultSelection() { - if (m_sFeatures.size() == 0) - return QModelIndex(); - return createIndex(m_iDefaultSelectedIndex, 0, (void*)this); -} - -void SidebarModel::setDefaultSelection(unsigned int index) { - m_iDefaultSelectedIndex = index; -} - -void SidebarModel::activateDefaultSelection() { - if (m_iDefaultSelectedIndex < - static_cast(m_sFeatures.size())) { - emit(selectIndex(getDefaultSelection())); - // Selecting an index does not activate it. - m_sFeatures[m_iDefaultSelectedIndex]->activate(); - } -} - -QModelIndex SidebarModel::index(int row, int column, - const QModelIndex& parent) const { - // qDebug() << "SidebarModel::index row=" << row - // << "column=" << column << "parent=" << parent.data(); - if (parent.isValid()) { - /* If we have selected the root of a library feature at position 'row' - * its internal pointer is the current sidebar object model - * we return its associated childmodel - */ - if (parent.internalPointer() == this) { - const QAbstractItemModel* childModel = m_sFeatures[parent.row()]->getChildModel(); - QModelIndex childIndex = childModel->index(row, column); - TreeItem* tree_item = (TreeItem*)childIndex.internalPointer(); - if (tree_item && childIndex.isValid()) { - return createIndex(childIndex.row(), childIndex.column(), (void*)tree_item); - } else { - return QModelIndex(); - } - } else { - // We have selected an item within the childmodel - // This item has always an internal pointer of (sub)type TreeItem - TreeItem* tree_item = (TreeItem*)parent.internalPointer(); - return createIndex(row, column, (void*) tree_item->child(row)); - } - } - return createIndex(row, column, (void*)this); -} - -QModelIndex SidebarModel::parent(const QModelIndex& index) const { - //qDebug() << "SidebarModel::parent index=" << index.data(); - if (index.isValid()) { - // If we have selected the root of a library feature - // its internal pointer is the current sidebar object model - // A root library feature has no parent and thus we return - // an invalid QModelIndex - if (index.internalPointer() == this) { - return QModelIndex(); - } else { - TreeItem* tree_item = (TreeItem*)index.internalPointer(); - if (tree_item == NULL) - return QModelIndex(); - TreeItem* tree_item_parent = tree_item->parent(); - // if we have selected an item at the first level of a childnode - - if (tree_item_parent) { - if (tree_item_parent->data() == "$root") { - LibraryFeature* feature = tree_item->getFeature(); - for (int i = 0; i < m_sFeatures.size(); ++i) { - if (feature == m_sFeatures[i]) { - // create a ModelIndex for parent 'this' having a - // library feature at position 'i' - return createIndex(i, 0, (void*)this); - } - } - } - // if we have selected an item at some deeper level of a childnode - return createIndex(tree_item_parent->row(), 0 , tree_item_parent); - } - } - } - return QModelIndex(); -} - -int SidebarModel::rowCount(const QModelIndex& parent) const { - //qDebug() << "SidebarModel::rowCount parent=" << parent.data(); - if (parent.isValid()) { - if (parent.internalPointer() == this) { - return m_sFeatures[parent.row()]->getChildModel()->rowCount(); - } else { - // We support tree models deeper than 1 level - TreeItem* tree_item = (TreeItem*)parent.internalPointer(); - if (tree_item) { - return tree_item->childCount(); - } - return 0; - } - } - return m_sFeatures.size(); -} - -int SidebarModel::columnCount(const QModelIndex& parent) const { - Q_UNUSED(parent); - //qDebug() << "SidebarModel::columnCount parent=" << parent; - // TODO(rryan) will we ever have columns? I don't think so. - return 1; -} - -bool SidebarModel::hasChildren(const QModelIndex& parent) const { - if (parent.isValid()) { - if (parent.internalPointer() == this) { - return QAbstractItemModel::hasChildren(parent); - } - else - { - TreeItem* tree_item = (TreeItem*)parent.internalPointer(); - if (tree_item) { - LibraryFeature* feature = tree_item->getFeature(); - return feature->getChildModel()->hasChildren(parent); - } - } - } - - return QAbstractItemModel::hasChildren(parent); -} - -QVariant SidebarModel::data(const QModelIndex& index, int role) const { - // qDebug("SidebarModel::data row=%d column=%d pointer=%8x, role=%d", - // index.row(), index.column(), index.internalPointer(), role); - if (!index.isValid()) { - return QVariant(); - } - - if (index.internalPointer() == this) { - //If it points to SidebarModel - if (role == Qt::DisplayRole) { - return m_sFeatures[index.row()]->title(); - } else if (role == Qt::DecorationRole) { - return m_sFeatures[index.row()]->getIcon(); - } - } - - if (index.internalPointer() != this) { - // If it points to a TreeItem - TreeItem* tree_item = (TreeItem*)index.internalPointer(); - if (tree_item) { - if (role == Qt::DisplayRole) { - return tree_item->data(); - } else if (role == Qt::ToolTipRole) { - // If it's the "Quick Links" node, display it's name - if (tree_item->dataPath() == QUICK_LINK_NODE) { - return tree_item->data(); - } else { - return tree_item->dataPath(); - } - } else if (role == TreeItemModel::kDataPathRole) { - // We use Qt::UserRole to ask for the datapath. - return tree_item->dataPath(); - } else if (role == Qt::FontRole) { - QFont font; - font.setBold(tree_item->isBold()); - return font; - } else if (role == Qt::DecorationRole) { - return tree_item->getIcon(); - } - } - } - - return QVariant(); -} - -void SidebarModel::clicked(const QModelIndex& index) { - //qDebug() << "SidebarModel::clicked() index=" << index; - - // We use clicked() for keyboard and mouse control, and the - // following code breaks that for us: - /*if (QApplication::mouseButtons() != Qt::LeftButton) { - return; - }*/ - - if (index.isValid()) { - if (index.internalPointer() == this) { - m_sFeatures[index.row()]->activate(); - } else { - TreeItem* tree_item = (TreeItem*)index.internalPointer(); - if (tree_item) { - LibraryFeature* feature = tree_item->getFeature(); - feature->activateChild(index); - } - } - } -} -void SidebarModel::doubleClicked(const QModelIndex& index) { - if (index.isValid()) { - if (index.internalPointer() == this) { - return; - } else { - TreeItem* tree_item = (TreeItem*)index.internalPointer(); - if (tree_item) { - LibraryFeature* feature = tree_item->getFeature(); - feature->onLazyChildExpandation(index); - } - } - } -} - -void SidebarModel::rightClicked(const QPoint& globalPos, const QModelIndex& index) { - //qDebug() << "SidebarModel::rightClicked() index=" << index; - if (index.isValid()) { - if (index.internalPointer() == this) { - m_sFeatures[index.row()]->activate(); - m_sFeatures[index.row()]->onRightClick(globalPos); - } - else - { - TreeItem* tree_item = (TreeItem*)index.internalPointer(); - if (tree_item) { - LibraryFeature* feature = tree_item->getFeature(); - feature->activateChild(index); - feature->onRightClickChild(globalPos, index); - } - } - } -} - -bool SidebarModel::dropAccept(const QModelIndex& index, QList urls, - QObject* pSource) { - //qDebug() << "SidebarModel::dropAccept() index=" << index << url; - bool result = false; - if (index.isValid()) { - if (index.internalPointer() == this) { - result = m_sFeatures[index.row()]->dropAccept(urls, pSource); - } else { - TreeItem* tree_item = (TreeItem*)index.internalPointer(); - if (tree_item) { - LibraryFeature* feature = tree_item->getFeature(); - result = feature->dropAcceptChild(index, urls, pSource); - } - } - } - return result; -} - -bool SidebarModel::dragMoveAccept(const QModelIndex& index, QUrl url) { - //qDebug() << "SidebarModel::dragMoveAccept() index=" << index << url; - bool result = false; - - if (index.isValid()) { - if (index.internalPointer() == this) { - result = m_sFeatures[index.row()]->dragMoveAccept(url); - } else { - TreeItem* tree_item = (TreeItem*)index.internalPointer(); - if (tree_item) { - LibraryFeature* feature = tree_item->getFeature(); - result = feature->dragMoveAcceptChild(index, url); - } - } - } - return result; -} - -// Translates an index from the child models to an index of the sidebar models -QModelIndex SidebarModel::translateSourceIndex(const QModelIndex& index) { - QModelIndex translatedIndex; - - /* These method is called from the slot functions below. - * QObject::sender() return the object which emitted the signal - * handled by the slot functions. - - * For child models, this always the child models itself - */ - - const QAbstractItemModel* model = dynamic_cast(sender()); - DEBUG_ASSERT_AND_HANDLE(model != NULL) { - return QModelIndex(); - } - - if (index.isValid()) { - TreeItem* item = (TreeItem*)index.internalPointer(); - translatedIndex = createIndex(index.row(), index.column(), item); - } - else - { - //Comment from Tobias Rafreider --> Dead Code???? - - for (int i = 0; i < m_sFeatures.size(); ++i) { - if (m_sFeatures[i]->getChildModel() == model) { - translatedIndex = createIndex(i, 0, (void*)this); - } - } - } - return translatedIndex; -} - -void SidebarModel::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - //qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight; - QModelIndex topLeftTranslated = translateSourceIndex(topLeft); - QModelIndex bottomRightTranslated = translateSourceIndex(bottomRight); - emit(dataChanged(topLeftTranslated, bottomRightTranslated)); -} - -void SidebarModel::slotRowsAboutToBeInserted(const QModelIndex& parent, int start, int end) { - //qDebug() << "slotRowsABoutToBeInserted" << parent << start << end; - - QModelIndex newParent = translateSourceIndex(parent); - beginInsertRows(newParent, start, end); -} - -void SidebarModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - //qDebug() << "slotRowsABoutToBeRemoved" << parent << start << end; - - QModelIndex newParent = translateSourceIndex(parent); - beginRemoveRows(newParent, start, end); -} - -void SidebarModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { - Q_UNUSED(parent); - Q_UNUSED(start); - Q_UNUSED(end); - //qDebug() << "slotRowsInserted" << parent << start << end; - //QModelIndex newParent = translateSourceIndex(parent); - endInsertRows(); -} - -void SidebarModel::slotRowsRemoved(const QModelIndex& parent, int start, int end) { - Q_UNUSED(parent); - Q_UNUSED(start); - Q_UNUSED(end); - //qDebug() << "slotRowsRemoved" << parent << start << end; - //QModelIndex newParent = translateSourceIndex(parent); - endRemoveRows(); -} - -void SidebarModel::slotModelReset() { - // If a child model is reset, we can't really do anything but reset(). This - // will close any open items. - reset(); -} - -/* - * Call this slot whenever the title of the feature has changed. - * See RhythmboxFeature for an example, in which the title becomes '(loading) Rhythmbox' - * If selectFeature is true, the feature is selected when the title change occurs. - */ -void SidebarModel::slotFeatureIsLoading(LibraryFeature * feature, bool selectFeature) { - featureRenamed(feature); - if (selectFeature) { - slotFeatureSelect(feature); - } -} - -/* Tobias: This slot is somewhat redundant but I decided - * to leave it for code readability reasons - */ -void SidebarModel::slotFeatureLoadingFinished(LibraryFeature * feature) { - featureRenamed(feature); - slotFeatureSelect(feature); -} - -void SidebarModel::featureRenamed(LibraryFeature* pFeature) { - for (int i=0; i < m_sFeatures.size(); ++i) { - if (m_sFeatures[i] == pFeature) { - QModelIndex ind = index(i, 0); - emit(dataChanged(ind, ind)); - } - } -} - -void SidebarModel::slotFeatureSelect(LibraryFeature* pFeature, const QModelIndex& featureIndex) { - QModelIndex ind; - if (featureIndex.isValid()) { - TreeItem* item = (TreeItem*)featureIndex.internalPointer(); - ind = createIndex(featureIndex.row(), featureIndex.column(), item); - } else { - for (int i=0; i < m_sFeatures.size(); ++i) { - if (m_sFeatures[i] == pFeature) { - ind = index(i, 0); - break; - } - } - } - emit(selectIndex(ind)); -} diff --git a/src/library/sidebarmodel.h b/src/library/sidebarmodel.h deleted file mode 100644 index ef234a95604..00000000000 --- a/src/library/sidebarmodel.h +++ /dev/null @@ -1,72 +0,0 @@ -// sidebarmodel.h -// Created 8/21/09 by RJ Ryan (rryan@mit.edu) - -#ifndef SIDEBARMODEL_H -#define SIDEBARMODEL_H - -#include -#include -#include -#include - -class LibraryFeature; - -class SidebarModel : public QAbstractItemModel { - Q_OBJECT - public: - explicit SidebarModel(QObject* parent = 0); - virtual ~SidebarModel(); - - void addLibraryFeature(LibraryFeature* feature); - QModelIndex getDefaultSelection(); - void setDefaultSelection(unsigned int index); - void activateDefaultSelection(); - - // Required for QAbstractItemModel - QModelIndex index(int row, int column, - const QModelIndex& parent = QModelIndex()) const; - QModelIndex parent(const QModelIndex& index) const; - int rowCount(const QModelIndex& parent = QModelIndex()) const; - int columnCount(const QModelIndex& parent = QModelIndex()) const; - QVariant data(const QModelIndex& index, - int role = Qt::DisplayRole) const; - bool dropAccept(const QModelIndex& index, QList urls, QObject* pSource); - bool dragMoveAccept(const QModelIndex& index, QUrl url); - virtual bool hasChildren(const QModelIndex& parent = QModelIndex()) const; - - public slots: - void clicked(const QModelIndex& index); - void doubleClicked(const QModelIndex& index); - void rightClicked(const QPoint& globalPos, const QModelIndex& index); - void slotFeatureSelect(LibraryFeature* pFeature, const QModelIndex& index = QModelIndex()); - - // Slots for every single QAbstractItemModel signal - // void slotColumnsAboutToBeInserted(const QModelIndex& parent, int start, int end); - // void slotColumnsAboutToBeRemoved(const QModelIndex& parent, int start, int end); - // void slotColumnsInserted(const QModelIndex& parent, int start, int end); - // void slotColumnsRemoved(const QModelIndex& parent, int start, int end); - void slotDataChanged(const QModelIndex& topLeft, const QModelIndex & bottomRight); - //void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last); - // void slotLayoutAboutToBeChanged(); - // void slotLayoutChanged(); - // void slotModelAboutToBeReset(); - // void slotModelReset(); - void slotRowsAboutToBeInserted(const QModelIndex& parent, int start, int end); - void slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); - void slotRowsInserted(const QModelIndex& parent, int start, int end); - void slotRowsRemoved(const QModelIndex& parent, int start, int end); - void slotModelReset(); - void slotFeatureIsLoading(LibraryFeature*, bool selectFeature); - void slotFeatureLoadingFinished(LibraryFeature*); - - signals: - void selectIndex(const QModelIndex& index); - - private: - QModelIndex translateSourceIndex(const QModelIndex& parent); - void featureRenamed(LibraryFeature*); - QList m_sFeatures; - unsigned int m_iDefaultSelectedIndex; /** Index of the item in the sidebar model to select at startup. */ -}; - -#endif /* SIDEBARMODEL_H */ diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index 1af6d52ebb2..13cecd067d5 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -14,7 +14,7 @@ #include "util/assert.h" // static -const int TrackCollection::kRequiredSchemaVersion = 27; +const int TrackCollection::kRequiredSchemaVersion = 28; TrackCollection::TrackCollection(UserSettingsPointer pConfig) : m_pConfig(pConfig), @@ -25,6 +25,7 @@ TrackCollection::TrackCollection(UserSettingsPointer pConfig) m_directoryDao(m_db), m_analysisDao(m_db, pConfig), m_libraryHashDao(m_db), + m_savedDao(m_db), m_trackDao(m_db, m_cueDao, m_playlistDao, m_crateDao, m_analysisDao, m_libraryHashDao, pConfig) { qDebug() << "Available QtSQL drivers:" << QSqlDatabase::drivers(); @@ -129,6 +130,7 @@ bool TrackCollection::checkForTables() { m_cueDao.initialize(); m_directoryDao.initialize(); m_libraryHashDao.initialize(); + m_savedDao.initialize(); return true; } @@ -152,6 +154,10 @@ DirectoryDAO& TrackCollection::getDirectoryDAO() { return m_directoryDao; } +SavedQueriesDAO &TrackCollection::getSavedQueriesDAO() { + return m_savedDao; +} + QSharedPointer TrackCollection::getTrackSource() { return m_defaultTrackSource; } diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index 936606802e1..87f551b0203 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -32,6 +32,7 @@ #include "library/dao/analysisdao.h" #include "library/dao/directorydao.h" #include "library/dao/libraryhashdao.h" +#include "library/dao/savedqueriesdao.h" #ifdef __SQLITE3__ typedef struct sqlite3_context sqlite3_context; @@ -63,6 +64,7 @@ class TrackCollection : public QObject { TrackDAO& getTrackDAO(); PlaylistDAO& getPlaylistDAO(); DirectoryDAO& getDirectoryDAO(); + SavedQueriesDAO& getSavedQueriesDAO(); AnalysisDao& getAnalysisDAO() { return m_analysisDao; } @@ -108,6 +110,7 @@ class TrackCollection : public QObject { DirectoryDAO m_directoryDao; AnalysisDao m_analysisDao; LibraryHashDAO m_libraryHashDao; + SavedQueriesDAO m_savedDao; TrackDAO m_trackDao; }; diff --git a/src/library/trackmodel.h b/src/library/trackmodel.h index f9bea8a1b37..08c146c1ce2 100644 --- a/src/library/trackmodel.h +++ b/src/library/trackmodel.h @@ -4,9 +4,11 @@ #include #include #include +#include #include #include "track/track.h" +#include "library/dao/savedqueriesdao.h" #include "library/dao/settingsdao.h" /** Pure virtual (abstract) class that provides an interface for data models which @@ -65,6 +67,8 @@ class TrackModel { bool isTrackModel() { return true;} virtual void search(const QString& searchText, const QString& extraFilter=QString()) = 0; + virtual void onSearchStarting() {} + virtual void onSearchCleared() {} virtual const QString currentSearch() const = 0; virtual bool isColumnInternal(int column) = 0; // if no header state exists, we may hide some columns so that the user can @@ -143,7 +147,26 @@ class TrackModel { virtual void select() { } + + virtual void saveSelection(const QModelIndexList&) { + } + + virtual QModelIndexList getSavedSelectionIndices() { + return QModelIndexList(); + } + virtual void restoreQuery(const SavedSearchQuery&) { + } + + virtual SavedSearchQuery saveQuery(const QModelIndexList& /* selected */, + SavedSearchQuery query = SavedSearchQuery()) const { + return query; + } + + virtual SavedSearchQuery saveQuery(SavedSearchQuery = SavedSearchQuery()) { + return SavedSearchQuery(); + } + private: QSqlDatabase m_db; QString m_settingsNamespace; diff --git a/src/library/treeitem.cpp b/src/library/treeitem.cpp index 2985373fcb9..a3ff61a528e 100644 --- a/src/library/treeitem.cpp +++ b/src/library/treeitem.cpp @@ -1,8 +1,10 @@ // TreeItem.cpp // Created 10/02/2010 by Tobias Rafreider +#include #include +#include "library/coverart.h" #include "library/treeitem.h" /* @@ -29,28 +31,54 @@ * - cratefeature.cpp * - *feature.cpp */ -TreeItem::TreeItem(const QString &data, const QString &data_path, - LibraryFeature* feature, TreeItem* parent) { - m_data = data; - m_dataPath = data_path; - m_parentItem = parent; - m_feature = feature; - m_bold = false; -} - -TreeItem::TreeItem() { - m_data = "$root"; - m_dataPath = "$root"; - m_parentItem = NULL; - m_feature = NULL; - m_bold = false; +TreeItem::TreeItem(const QVariant& data, const QVariant& dataPath, + LibraryFeature* pFeature, TreeItem* parent) + : m_data(data), + m_dataPath(dataPath), + m_pFeature(pFeature), + m_bold(false), + m_divider(false), + m_trackCount(-1), + m_pParent(parent) { +} + +TreeItem::TreeItem(LibraryFeature* pFeature) + : m_data("$root"), + m_dataPath("$root"), + m_pFeature(pFeature), + m_bold(false), + m_divider(false), + m_trackCount(-1), + m_pParent(nullptr) { + +} + +TreeItem::TreeItem(TreeItem* parent) + : m_data("$root"), + m_dataPath("$root"), + m_pFeature(nullptr), + m_bold(false), + m_divider(false), + m_trackCount(-1), + m_pParent(parent) { + +} + +TreeItem::TreeItem() + : m_data("$root"), + m_dataPath("$root"), + m_pFeature(nullptr), + m_bold(false), + m_divider(false), + m_trackCount(-1), + m_pParent(nullptr) { } TreeItem::~TreeItem() { qDeleteAll(m_childItems); } -void TreeItem::appendChild(TreeItem *item) { +void TreeItem::appendChild(TreeItem* item) { m_childItems.append(item); } @@ -58,7 +86,7 @@ void TreeItem::removeChild(int index) { m_childItems.removeAt(index); } -TreeItem *TreeItem::child(int row) { +TreeItem* TreeItem::child(int row) { return m_childItems.value(row); } @@ -67,6 +95,10 @@ int TreeItem::childCount() const { } QVariant TreeItem::data() const { + if (m_trackCount >= 0) { + return m_data.toString() + " (" + QString::number(m_trackCount) + ")"; + } + return m_data; } @@ -82,25 +114,50 @@ bool TreeItem::isFolder() const { return (m_childItems.count() != 0); } -TreeItem *TreeItem::parent() { - return m_parentItem; +TreeItem* TreeItem::parent() { + return m_pParent; +} + +void TreeItem::setParent(TreeItem* parent) { + m_pParent = parent; } int TreeItem::row() const { - if (m_parentItem) { - return m_parentItem->m_childItems.indexOf(const_cast(this)); + if (m_pParent) { + return m_pParent->m_childItems.indexOf(const_cast(this)); } return 0; } LibraryFeature* TreeItem::getFeature() { - return m_feature; + return m_pFeature; +} + +void TreeItem::setLibraryFeature(LibraryFeature* pFeature) { + m_pFeature = pFeature; } -bool TreeItem::insertChildren(QList &data, int position, int count) { - if (position < 0 || position > m_childItems.size()) +void TreeItem::setBold(bool bold) { + m_bold = bold; +} + +bool TreeItem::isBold() const { + return m_bold; +} + +void TreeItem::setDivider(bool divider) { + m_divider = divider; +} + +bool TreeItem::isDivider() const { + return m_divider; +} + +bool TreeItem::insertChildren(QList& data, int position, int count) { + if (position < 0 || position > m_childItems.size()) { return false; + } for (int row = 0; row < count; ++row) { TreeItem* item = data.at(row); @@ -111,27 +168,53 @@ bool TreeItem::insertChildren(QList &data, int position, int count) { } bool TreeItem::removeChildren(int position, int count) { - if (position < 0 || position + count > m_childItems.size()) + if (position < 0 || position + count > m_childItems.size()) { return false; + } for (int row = 0; row < count; ++row) { //Remove from list to avoid invalid pointers TreeItem* item = m_childItems.takeAt(position); - if(item) delete item; + if (item) { + delete item; + } } return true; } -bool TreeItem::setData(const QVariant &data, const QVariant &data_path) { - m_data = data.toString(); - m_dataPath = data_path.toString(); - return true; +void TreeItem::setData(const QVariant& data) { + m_data = data; +} + +void TreeItem::setData(const QVariant& data, const QVariant& dataPath) { + m_data = data; + m_dataPath = dataPath; +} + +void TreeItem::setDataPath(const QVariant& dataPath) { + m_dataPath = dataPath; } -QIcon TreeItem::getIcon() { +QIcon TreeItem::getIcon() const { return m_icon; } void TreeItem::setIcon(const QIcon& icon) { m_icon = icon; } + +void TreeItem::setCoverInfo(const CoverInfo &cover) { + m_cover = cover; +} + +const CoverInfo& TreeItem::getCoverInfo() const { + return m_cover; +} + +void TreeItem::setTrackCount(int count) { + m_trackCount = count; +} + +int TreeItem::getTrackCount() { + return m_trackCount; +} diff --git a/src/library/treeitem.h b/src/library/treeitem.h index d56c38cc243..d77baaed4b7 100644 --- a/src/library/treeitem.h +++ b/src/library/treeitem.h @@ -7,37 +7,46 @@ #include #include #include +#include #include "library/libraryfeature.h" +class CoverInfo; + class TreeItem { public: TreeItem(); //creates an invisible root item for the tree - TreeItem(const QString &data, - const QString &data_path, - LibraryFeature* feature, + TreeItem(const QVariant &data, + const QVariant &dataPath, + LibraryFeature* pFeature, TreeItem* parent); + TreeItem(LibraryFeature* pFeature); + TreeItem(TreeItem* parent); ~TreeItem(); /** appends a child item to this object **/ - void appendChild(TreeItem *child); + void appendChild(TreeItem* child); /** remove a child item at the given index **/ void removeChild(int index); - /** returns the tree item at position 'row' in the childlist **/ + /** returns the tree item at position 'row' in the child list **/ TreeItem *child(int row); /** returns the number of childs of this tree item **/ int childCount() const; /** Returns the position of this object within its parent **/ int row() const; /** returns the parent **/ - TreeItem *parent(); + TreeItem* parent(); + /** sets the parent **/ + void setParent(TreeItem* parent); /** for dynamic resizing models **/ - bool insertChildren(QList &data, int position, int count); + bool insertChildren(QList& data, int position, int count); /** Removes children from the child list starting at index **/ bool removeChildren(int position, int count); /** sets data **/ - bool setData(const QVariant &data, const QVariant &data_path); + void setData(const QVariant& data); + void setData(const QVariant& data, const QVariant& dataPath); + void setDataPath(const QVariant& dataPath); /** simple name of the playlist **/ QVariant data() const; /** Full path of the playlist **/ @@ -46,29 +55,38 @@ class TreeItem { bool isPlaylist() const; /** returns true if we have an inner node **/ bool isFolder() const; - /* Returns the Library feature object to which an item belongs to */ + // Returns the Library feature object to which an item belongs to LibraryFeature* getFeature(); + + void setLibraryFeature(LibraryFeature* pFeature); - void setBold(bool bold) { - m_bold = bold; - } - bool isBold() const { - return m_bold; - } + void setBold(bool bold); + bool isBold() const; + void setDivider(bool divider); + bool isDivider() const; void setIcon(const QIcon& icon); - QIcon getIcon(); + QIcon getIcon() const; + + void setCoverInfo(const CoverInfo& cover); + const CoverInfo& getCoverInfo() const; + + void setTrackCount(int count); + int getTrackCount(); private: QList m_childItems; - QString m_dataPath; - QString m_data; - LibraryFeature* m_feature; + QVariant m_data; + QVariant m_dataPath; + LibraryFeature* m_pFeature; bool m_bold; + bool m_divider; + int m_trackCount; - TreeItem *m_parentItem; + TreeItem* m_pParent; QIcon m_icon; + CoverInfo m_cover; }; #endif diff --git a/src/library/treeitemmodel.cpp b/src/library/treeitemmodel.cpp index cd9824c3a67..a57d8b0385c 100644 --- a/src/library/treeitemmodel.cpp +++ b/src/library/treeitemmodel.cpp @@ -1,6 +1,9 @@ +#include +#include + #include "library/treeitemmodel.h" -#include "library/treeitem.h" +#include "util/stringhelper.h" /* * Just a word about how the TreeItem objects and TreeItemModels are used in general: @@ -26,7 +29,7 @@ * - cratefeature.cpp * - *feature.cpp */ -TreeItemModel::TreeItemModel(QObject *parent) +TreeItemModel::TreeItemModel(QObject* parent) : QAbstractItemModel(parent), m_pRootItem(new TreeItem()) { } @@ -45,41 +48,66 @@ QVariant TreeItemModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); - if (role != Qt::DisplayRole && role != kDataPathRole && role != kBoldRole) + TreeItem* item = static_cast(index.internalPointer()); + if (item == nullptr) { return QVariant(); + } - TreeItem *item = static_cast(index.internalPointer()); - - // We use Qt::UserRole to ask for the datapath. - if (role == kDataPathRole) { - return item->dataPath(); - } else if (role == kBoldRole) { - return item->isBold(); + // We use Qt::UserRole to ask for the datapath. + switch(role) { + case Qt::DisplayRole: + return item->data(); + case Qt::SizeHintRole: + { + QIcon icon(item->getIcon()); + if (icon.isNull()) { + return QVariant(); + } + QSize size(getDefaultIconSize()); + size.setHeight(size.height() + 2); + return size; + } + case Qt::DecorationRole: + return item->getIcon(); + case AbstractRole::RoleDataPath: + return item->dataPath(); + case AbstractRole::RoleBold: + return item->isBold(); + case AbstractRole::RoleDivider: + return item->isDivider(); + case AbstractRole::RoleBreadCrumb: + return getBreadCrumbString(item); + case AbstractRole::RoleGroupingLetter: + return StringHelper::getFirstCharForGrouping(item->data().toString()); } - return item->data(); + + return QVariant(); } bool TreeItemModel::setData(const QModelIndex &a_rIndex, const QVariant &a_rValue, int a_iRole) { // Get the item referred to by this index. TreeItem *pItem = static_cast(a_rIndex.internalPointer()); - if (pItem == NULL) { + if (pItem == nullptr) { return false; } // Set the relevant data. switch (a_iRole) { - case Qt::DisplayRole: - pItem->setData(a_rValue, pItem->dataPath()); - break; - case kDataPathRole: - pItem->setData(pItem->data(), a_rValue); - break; - case kBoldRole: - pItem->setBold(a_rValue.toBool()); - break; - default: - return false; + case Qt::DisplayRole: + pItem->setData(a_rValue, pItem->dataPath()); + break; + case AbstractRole::RoleDataPath: + pItem->setData(pItem->data(), a_rValue); + break; + case AbstractRole::RoleBold: + pItem->setBold(a_rValue.toBool()); + break; + case AbstractRole::RoleDivider: + pItem->setDivider(a_rValue.toBool()); + break; + default: + return false; } emit(dataChanged(a_rIndex, a_rIndex)); @@ -89,8 +117,14 @@ bool TreeItemModel::setData(const QModelIndex &a_rIndex, Qt::ItemFlags TreeItemModel::flags(const QModelIndex &index) const { if (!index.isValid()) return 0; - - return Qt::ItemIsEnabled | Qt::ItemIsSelectable; + Qt::ItemFlags flags = Qt::ItemIsEnabled; + + bool divider = index.data(AbstractRole::RoleDivider).toBool(); + if (!divider) { + flags |= Qt::ItemIsSelectable; + } + + return flags; } QVariant TreeItemModel::headerData(int section, Qt::Orientation orientation, int role) const { @@ -165,7 +199,8 @@ void TreeItemModel::setRootItem(TreeItem *item) { * Before you can resize the data model dynamically by using 'insertRows' and 'removeRows' * make sure you have initialized */ -bool TreeItemModel::insertRows(QList &data, int position, int rows, const QModelIndex &parent) { +bool TreeItemModel::insertRows( + QList& data, int position, int rows, const QModelIndex &parent) { if (rows == 0) { return true; } @@ -204,3 +239,57 @@ void TreeItemModel::triggerRepaint() { QModelIndex right = index(rowCount() - 1, columnCount() - 1); emit(dataChanged(left, right)); } + +//static +QString TreeItemModel::getBreadCrumbString(TreeItem* pTree) { + // Base case + if (pTree == nullptr || pTree->getFeature() == nullptr) { + return QString(); + } + else if (pTree->parent() == nullptr) { + return pTree->getFeature()->title().toString(); + } + + // Recursive case + QString text = pTree->data().toString(); + QString next = getBreadCrumbString(pTree->parent()); + return next % QLatin1String(" > ") % text; +} + +//static +QSize TreeItemModel::getDefaultIconSize() { + return QSize(32, 32); +} + +void TreeItemModel::reloadTree() { + triggerRepaint(); +} + +bool TreeItemModel::dropAccept(const QModelIndex& index, QList urls, + QObject* pSource) { + //qDebug() << "TreeItemModel::dropAccept() index=" << index << urls; + LibraryFeature* pFeature = getFeatureFromIndex(index); + if (pFeature == nullptr) { + return false; + } + + return pFeature->dropAcceptChild(index, urls, pSource); +} + +bool TreeItemModel::dragMoveAccept(const QModelIndex& index, QUrl url) { + //qDebug() << "TreeItemModel::dragMoveAccept() index=" << index << url; + LibraryFeature* pFeature = getFeatureFromIndex(index); + if (pFeature == nullptr) { + return false; + } + + return pFeature->dragMoveAcceptChild(index, url); +} + +LibraryFeature* TreeItemModel::getFeatureFromIndex(const QModelIndex& index) const { + TreeItem* pTree = getItem(index); + if (pTree == nullptr) { + return nullptr; + } + return pTree->getFeature(); +} diff --git a/src/library/treeitemmodel.h b/src/library/treeitemmodel.h index 3aa28de27bf..2907186f924 100644 --- a/src/library/treeitemmodel.h +++ b/src/library/treeitemmodel.h @@ -1,20 +1,21 @@ #ifndef TREE_ITEM_MODEL_H #define TREE_ITEM_MODEL_H -#include #include #include #include +#include -class TreeItem; +#include "library/abstractmodelroles.h" +#include "library/treeitem.h" + +class LibraryFeature; class TreeItemModel : public QAbstractItemModel { Q_OBJECT + public: - static const int kDataPathRole = Qt::UserRole; - static const int kBoldRole = Qt::UserRole + 1; - - TreeItemModel(QObject *parent = 0); + TreeItemModel(QObject* parent = nullptr); virtual ~TreeItemModel(); virtual QVariant data(const QModelIndex &index, int role) const; @@ -34,11 +35,21 @@ class TreeItemModel : public QAbstractItemModel { // Return the underlying TreeItem. // If the index is invalid, the root item is returned. TreeItem* getItem(const QModelIndex &index) const; - + + bool dropAccept(const QModelIndex& index, QList urls, QObject* pSource); + bool dragMoveAccept(const QModelIndex& index, QUrl url); + + static QString getBreadCrumbString(TreeItem* pTree); + static QSize getDefaultIconSize(); + + public slots: + virtual void reloadTree(); void triggerRepaint(); - private: - TreeItem *m_pRootItem; + protected: + LibraryFeature* getFeatureFromIndex(const QModelIndex& index) const; + + TreeItem* m_pRootItem; }; #endif diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index d478fb3fb66..441e45d747f 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -246,13 +246,6 @@ void BaseTrackPlayerImpl::slotTrackLoaded(TrackPointer pNewTrack, m_pKey->set(m_pLoadedTrack->getKey()); setReplayGain(m_pLoadedTrack->getReplayGain().getRatio()); - // Clear loop - // It seems that the trick is to first clear the loop out point, and then - // the loop in point. If we first clear the loop in point, the loop out point - // does not get cleared. - m_pLoopOutPoint->set(-1); - m_pLoopInPoint->set(-1); - const QList trackCues(pNewTrack->getCuePoints()); QListIterator it(trackCues); while (it.hasNext()) { diff --git a/src/mixxx.cpp b/src/mixxx.cpp index d7511d7d478..3eaff44df35 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "analyzer/analyzerqueue.h" #include "dialog/dlgabout.h" @@ -263,10 +264,14 @@ void MixxxMainWindow::initialize(QApplication* pApp, const CmdlineArgs& args) { CoverArtCache::create(); // (long) - m_pLibrary = new Library(this, pConfig, + m_pLibrary = new Library(pConfig, m_pPlayerManager, m_pRecordingManager); m_pPlayerManager->bindToLibrary(m_pLibrary); + + new QShortcut( + QKeySequence(tr("Ctrl+F", "Search|Focus")), + this, SLOT(slotFocusSearch())); launchProgress(35); @@ -654,8 +659,9 @@ void MixxxMainWindow::initializeKeyboard() { // initialization into KeyboardEventFilter // Workaround for today: KeyboardEventFilter calls delete bool keyboardShortcutsEnabled = pConfig->getValueString( - ConfigKey("[Keyboard]", "Enabled")) == "1"; - m_pKeyboard = new KeyboardEventFilter(keyboardShortcutsEnabled ? m_pKbdConfig : m_pKbdConfigEmpty); + ConfigKey("[Keyboard]", "Enabled")) == "1"; + m_pKeyboard = new KeyboardEventFilter( + keyboardShortcutsEnabled ? m_pKbdConfig : m_pKbdConfigEmpty); } int MixxxMainWindow::noSoundDlg(void) { @@ -1043,6 +1049,7 @@ void MixxxMainWindow::rebootMixxxView() { // that need to be deleted -- otherwise we can't tell what features the skin // supports since the controls from the previous skin will be left over. m_pMenuBar->onNewSkinAboutToLoad(); + m_pLibrary->destroyInterface(); if (m_pWidgetParent) { m_pWidgetParent->hide(); @@ -1228,3 +1235,7 @@ void MixxxMainWindow::launchProgress(int progress) { m_pLaunchImage->progress(progress); qApp->processEvents(); } + +void MixxxMainWindow::slotFocusSearch() { + m_pLibrary->focusSearch(); +} diff --git a/src/mixxx.h b/src/mixxx.h index a1793d56b54..c4ba8feaa84 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -37,6 +37,7 @@ class EngineMaster; class GuiTick; class LaunchImage; class Library; +class LibraryPaneManager; class KeyboardEventFilter; class PlayerManager; class RecordingManager; @@ -93,6 +94,8 @@ class MixxxMainWindow : public QMainWindow { void slotNoDeckPassthroughInputConfigured(); void slotNoVinylControlInputConfigured(); + void slotFocusSearch(); + signals: void newSkinLoaded(); // used to uncheck the menu when the dialog of develeoper tools is closed diff --git a/src/preferences/dialog/dlgprefwaveform.cpp b/src/preferences/dialog/dlgprefwaveform.cpp index bf85e0f7f1d..25253122cb3 100644 --- a/src/preferences/dialog/dlgprefwaveform.cpp +++ b/src/preferences/dialog/dlgprefwaveform.cpp @@ -1,6 +1,7 @@ #include "preferences/dialog/dlgprefwaveform.h" #include "mixxx.h" +#include "library/trackcollection.h" #include "preferences/waveformsettings.h" #include "waveform/waveformwidgetfactory.h" #include "waveform/renderers/waveformwidgetrenderer.h" diff --git a/src/skin/colorschemeparser.cpp b/src/skin/colorschemeparser.cpp index 09f40564f21..46aa4ff991c 100644 --- a/src/skin/colorschemeparser.cpp +++ b/src/skin/colorschemeparser.cpp @@ -60,20 +60,20 @@ ImgSource* ColorSchemeParser::parseFilters(QDomNode filt) { return 0; } - ImgSource * ret = new ImgLoader(); + ImgSource* ret = new ImgLoader(); QDomNode f = filt.firstChild(); while (!f.isNull()) { QString name = f.nodeName().toLower(); if (name == "invert") { - ret = new ImgInvert(ret); + ret = new ImgInvert(QSharedPointer(ret)); } else if (name == "hueinv") { - ret = new ImgHueInv(ret); + ret = new ImgHueInv(QSharedPointer(ret)); } else if (name == "add") { - ret = new ImgAdd(ret, XmlParse::selectNodeInt(f, "Amount")); + ret = new ImgAdd(QSharedPointer(ret), XmlParse::selectNodeInt(f, "Amount")); } else if (name == "scalewhite") { - ret = new ImgScaleWhite(ret, XmlParse::selectNodeFloat(f, "Amount")); + ret = new ImgScaleWhite(QSharedPointer(ret), XmlParse::selectNodeFloat(f, "Amount")); } else if (name == "hsvtweak") { int hmin = 0; int hmax = 359; @@ -103,7 +103,7 @@ ImgSource* ColorSchemeParser::parseFilters(QDomNode filt) { if (!f.namedItem("SFact").isNull()) { sfact = XmlParse::selectNodeFloat(f, "SFact"); } if (!f.namedItem("VFact").isNull()) { vfact = XmlParse::selectNodeFloat(f, "VFact"); } - ret = new ImgHSVTweak(ret, hmin, hmax, smin, smax, vmin, vmax, hfact, hconst, + ret = new ImgHSVTweak(QSharedPointer(ret), hmin, hmax, smin, smax, vmin, vmax, hfact, hconst, sfact, sconst, vfact, vconst); } else { qDebug() << "Unknown image filter:" << name; diff --git a/src/skin/imgcolor.cpp b/src/skin/imgcolor.cpp index 5ddbe3d2b98..ac52643490b 100644 --- a/src/skin/imgcolor.cpp +++ b/src/skin/imgcolor.cpp @@ -30,7 +30,7 @@ QColor ImgScaleWhite::doColorCorrection(QColor c) { return c; } -ImgAdd::ImgAdd(ImgSource * parent, int amt) +ImgAdd::ImgAdd(const QSharedPointer &parent, int amt) : ImgColorProcessor(parent), m_amt(amt) { // Nothing left to do } @@ -48,7 +48,7 @@ QColor ImgAdd::doColorCorrection(QColor c) { return QColor(r, g, b, c.alpha()); } -ImgMax::ImgMax(ImgSource * parent, int amt) +ImgMax::ImgMax(const QSharedPointer &parent, int amt) : ImgColorProcessor(parent), m_amt(amt) { } @@ -88,3 +88,15 @@ QColor ImgHSVTweak::doColorCorrection(QColor c) { return c; } +ImgMonoColor::ImgMonoColor(const QSharedPointer &parent, const QColor& baseColor) + : ImgColorProcessor(parent), + m_baseColor(baseColor) { +} + +QColor ImgMonoColor::doColorCorrection(QColor c) { + // Get the hue color to do a monochrome image + int h, a, s, v; + c.getHsv(&h, &s, &v, &a); + c.setHsv(m_baseColor.hue(), s, v, a); + return c; +} diff --git a/src/skin/imgcolor.h b/src/skin/imgcolor.h index dca21076fb8..d29e04626c2 100644 --- a/src/skin/imgcolor.h +++ b/src/skin/imgcolor.h @@ -23,7 +23,7 @@ class ImgAdd : public ImgColorProcessor { public: - ImgAdd(ImgSource* parent, int amt); + ImgAdd(const QSharedPointer& parent, int amt); virtual QColor doColorCorrection(QColor c); private: @@ -33,17 +33,26 @@ class ImgAdd : public ImgColorProcessor { class ImgMax : public ImgColorProcessor { public: - ImgMax(ImgSource* parent, int amt); + ImgMax(const QSharedPointer& parent, int amt); virtual QColor doColorCorrection(QColor c); private: int m_amt; }; +class ImgMonoColor : public ImgColorProcessor { + + public: + ImgMonoColor(const QSharedPointer& parent, const QColor &baseColor); + virtual QColor doColorCorrection(QColor c); + private: + QColor m_baseColor; +}; + class ImgScaleWhite : public ImgColorProcessor { public: - inline ImgScaleWhite(ImgSource* parent, float amt) + inline ImgScaleWhite(const QSharedPointer& parent, float amt) : ImgColorProcessor(parent), m_amt(amt) {} virtual QColor doColorCorrection(QColor c); private: @@ -53,7 +62,7 @@ class ImgScaleWhite : public ImgColorProcessor { class ImgHueRot : public ImgColorProcessor { public: - inline ImgHueRot(ImgSource* parent, int amt) + inline ImgHueRot(QSharedPointer parent, int amt) : ImgColorProcessor(parent), m_amt(amt) {} virtual QColor doColorCorrection(QColor c); @@ -64,13 +73,13 @@ class ImgHueRot : public ImgColorProcessor { class ImgHueInv : public ImgColorProcessor { public: - inline ImgHueInv(ImgSource* parent) : ImgColorProcessor(parent) {} + inline ImgHueInv(const QSharedPointer& parent) : ImgColorProcessor(parent) {} virtual QColor doColorCorrection(QColor c); }; class ImgHSVTweak : public ImgColorProcessor { public: - inline ImgHSVTweak(ImgSource* parent, int hmin, int hmax, int smin, + inline ImgHSVTweak(const QSharedPointer& parent, int hmin, int hmax, int smin, int smax, int vmin, int vmax, float hfact, int hconst, float sfact, int sconst, float vfact, int vconst) : ImgColorProcessor(parent), diff --git a/src/skin/imginvert.h b/src/skin/imginvert.h index 2cf862609dc..9498f53f699 100644 --- a/src/skin/imginvert.h +++ b/src/skin/imginvert.h @@ -23,7 +23,7 @@ class ImgInvert : public ImgColorProcessor { public: - inline ImgInvert(ImgSource* parent) : ImgColorProcessor(parent) {} + inline ImgInvert(const QSharedPointer& parent) : ImgColorProcessor(parent) {} virtual QColor doColorCorrection(QColor c); }; diff --git a/src/skin/imgloader.cpp b/src/skin/imgloader.cpp index dc7366acbff..84125d5dbac 100644 --- a/src/skin/imgloader.cpp +++ b/src/skin/imgloader.cpp @@ -8,3 +8,6 @@ QImage * ImgLoader::getImage(QString img) { return new QImage(img); } +void ImgLoader::correctImageColors(QImage*) { +} + diff --git a/src/skin/imgloader.h b/src/skin/imgloader.h index 321a2339c06..81fd21066c1 100644 --- a/src/skin/imgloader.h +++ b/src/skin/imgloader.h @@ -24,7 +24,8 @@ class ImgLoader : public ImgSource { public: ImgLoader(); - virtual QImage* getImage(QString img); + QImage* getImage(QString img) override; + void correctImageColors(QImage*) override; }; #endif diff --git a/src/skin/imgsource.h b/src/skin/imgsource.h index 969107969f1..7c584dc4fc1 100644 --- a/src/skin/imgsource.h +++ b/src/skin/imgsource.h @@ -20,47 +20,55 @@ #include #include -#include +#include #include -#include +#include +#include class ImgSource { public: - virtual ~ImgSource() {}; + virtual ~ImgSource() {} virtual QImage* getImage(QString img) = 0; virtual inline QColor getCorrectColor(QColor c) { return c; } - virtual void correctImageColors(QImage* p) { (void)p; }; + virtual void correctImageColors(QImage*) = 0; + protected: + virtual void correctImageColorsInner(QImage*) {} }; class ImgProcessor : public ImgSource { public: - virtual ~ImgProcessor() {}; - inline ImgProcessor(ImgSource* parent) : m_parent(parent) {} - virtual QColor doColorCorrection(QColor c) = 0; - inline QColor getCorrectColor(QColor c) { + ~ImgProcessor() override {} + inline ImgProcessor(const QSharedPointer& parent) : m_parent(parent) {} + virtual QColor doColorCorrection(QColor) = 0; + QColor getCorrectColor(QColor c) override { return doColorCorrection(m_parent->getCorrectColor(c)); } - virtual void correctImageColors(QImage* p) { (void)p; }; - + void correctImageColors(QImage* pImg) override { + m_parent->correctImageColors(pImg); + correctImageColorsInner(pImg); + } protected: - ImgSource* m_parent; + void correctImageColorsInner(QImage*) override {}; + QSharedPointer m_parent; }; class ImgColorProcessor : public ImgProcessor { -public: - virtual ~ImgColorProcessor() {}; + public: + ~ImgColorProcessor() override {} - inline ImgColorProcessor(ImgSource* parent) : ImgProcessor(parent) {} + inline ImgColorProcessor(const QSharedPointer& parent) + : ImgProcessor(parent) {} - inline virtual QImage* getImage(QString img) { + virtual QImage* getImage(QString img) override { QImage* i = m_parent->getImage(img); - correctImageColors(i); + correctImageColorsInner(i); return i; } - virtual void correctImageColors(QImage* i) { + protected: + void correctImageColorsInner(QImage* i) override { if (i == NULL || i->isNull()) { return; } diff --git a/src/skin/legacyskinparser.cpp b/src/skin/legacyskinparser.cpp index 851d91fce1b..d4baffa6a4d 100644 --- a/src/skin/legacyskinparser.cpp +++ b/src/skin/legacyskinparser.cpp @@ -1,6 +1,7 @@ // legacyskinparser.cpp // Created 9/19/2010 by RJ Ryan (rryan@mit.edu) +#include #include "skin/legacyskinparser.h" #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -25,8 +27,10 @@ #include "controllers/controllermanager.h" #include "skin/colorschemeparser.h" -#include "skin/skincontext.h" +#include "skin/imgcolor.h" +#include "skin/imgloader.h" #include "skin/launchimage.h" +#include "skin/skincontext.h" #include "effects/effectsmanager.h" @@ -63,8 +67,9 @@ #include "widget/wwaveformviewer.h" #include "waveform/waveformwidgetfactory.h" #include "widget/wsearchlineedit.h" -#include "widget/wlibrary.h" #include "widget/wlibrarysidebar.h" +#include "widget/wlibrarybreadcrumb.h" +#include "widget/wbuttonbar.h" #include "widget/wskincolor.h" #include "widget/wpixmapstore.h" #include "widget/wwidgetstack.h" @@ -75,6 +80,7 @@ #include "widget/wcombobox.h" #include "widget/wsplitter.h" #include "widget/wsingletoncontainer.h" +#include "widget/wverticalscrollarea.h" #include "util/valuetransformer.h" #include "util/cmdlineargs.h" #include "util/timer.h" @@ -143,7 +149,8 @@ LegacySkinParser::LegacySkinParser() m_pVCManager(NULL), m_pEffectsManager(NULL), m_pParent(NULL), - m_pContext(NULL) { + m_pContext(NULL), + m_paneId(0) { } LegacySkinParser::LegacySkinParser(UserSettingsPointer pConfig, @@ -161,7 +168,8 @@ LegacySkinParser::LegacySkinParser(UserSettingsPointer pConfig, m_pVCManager(pVCMan), m_pEffectsManager(pEffectsManager), m_pParent(NULL), - m_pContext(NULL) { + m_pContext(NULL), + m_paneId(0) { } LegacySkinParser::~LegacySkinParser() { @@ -223,7 +231,7 @@ QList LegacySkinParser::getSchemeList(const QString& qSkinPath) { QDomElement docElem = openSkin(qSkinPath); QList schlist; - + QDomNode colsch = docElem.namedItem("Schemes"); if (!colsch.isNull() && colsch.isElement()) { QDomNode sch = colsch.firstChild(); @@ -363,7 +371,18 @@ QWidget* LegacySkinParser::parseSkin(const QString& skinPath, QWidget* pParent) } ColorSchemeParser::setupLegacyColorSchemes(skinDocument, m_pConfig); - + + // Setup Library Icons color + QString colorName = m_pContext->selectString(skinDocument, "LibraryIconsColor"); + if (!colorName.isEmpty()) { + QColor color(colorName); + QSharedPointer loader(new ImgLoader); + QSharedPointer mono(new ImgMonoColor(loader, color)); + WPixmapStore::setLibraryIconLoader(mono); + } else { + WPixmapStore::setLibraryIconLoader(QSharedPointer()); + } + QStringList skinPaths(skinPath); QDir::setSearchPaths("skin", skinPaths); @@ -452,7 +471,7 @@ QList LegacySkinParser::parseNode(const QDomElement& node) { qDebug() << "Skin is a" << (newStyle ? ">=1.12.0" : "<1.12.0") << "style skin."; - if (newStyle) { + if (newStyle) { // New style skins are just a WidgetGroup at the root. result.append(parseWidgetGroup(node)); } else { @@ -551,10 +570,18 @@ QList LegacySkinParser::parseNode(const QDomElement& node) { result = wrapWidget(parseLabelWidget(node)); } else if (nodeName == "Splitter") { result = wrapWidget(parseSplitter(node)); + } else if (nodeName == "LibrarySidebarButtons") { + result = wrapWidget(parseLibrarySidebarButtons(node)); } else if (nodeName == "LibrarySidebar") { result = wrapWidget(parseLibrarySidebar(node)); + } else if (nodeName == "LibrarySidebarExpanded") { + result = wrapWidget(parseLibrarySidebarExpanded(node)); + } else if (nodeName == "LibraryPane") { + result = wrapWidget(parseLibraryPane(node)); + } else if (nodeName == "LibraryBreadCrumb") { + result = wrapWidget(parseLibraryBreadCrumb(node)); } else if (nodeName == "Library") { - result = wrapWidget(parseLibrary(node)); + result = wrapWidget(parseLibrary(node)); } else if (nodeName == "Key") { result = wrapWidget(parseEngineKey(node)); } else if (nodeName == "Battery") { @@ -1135,21 +1162,22 @@ QWidget* LegacySkinParser::parseSpinny(const QDomElement& node) { } QWidget* LegacySkinParser::parseSearchBox(const QDomElement& node) { - WSearchLineEdit* pLineEditSearch = new WSearchLineEdit(m_pParent); - commonWidgetSetup(node, pLineEditSearch, false); - pLineEditSearch->setup(node, *m_pContext); - - // Connect search box signals to the library - connect(pLineEditSearch, SIGNAL(search(const QString&)), - m_pLibrary, SIGNAL(search(const QString&))); - connect(pLineEditSearch, SIGNAL(searchCleared()), - m_pLibrary, SIGNAL(searchCleared())); - connect(pLineEditSearch, SIGNAL(searchStarting()), - m_pLibrary, SIGNAL(searchStarting())); - connect(m_pLibrary, SIGNAL(restoreSearch(const QString&)), - pLineEditSearch, SLOT(restoreSearch(const QString&))); - - return pLineEditSearch; + int id = -1; + if (!m_pContext->hasNodeSelectInt(node, "Id", &id)) { + SKIN_WARNING(node, *m_pContext) << "SearchBox Id not found"; + return nullptr; + } + if (id < 0) { + SKIN_WARNING(node, *m_pContext) << "The SearchBox Id cannot be negative"; + return nullptr; + } + //qDebug() << "SearchBox ID:" << id; + WSearchLineEdit* pSearchLineEdit = new WSearchLineEdit(m_pParent); + m_pLibrary->bindSearchBar(pSearchLineEdit, id); + pSearchLineEdit->setup(node, *m_pContext); + commonWidgetSetup(node, pSearchLineEdit, false); + + return pSearchLineEdit; } QWidget* LegacySkinParser::parseCoverArt(const QDomElement& node) { @@ -1163,8 +1191,6 @@ QWidget* LegacySkinParser::parseCoverArt(const QDomElement& node) { // If no group was provided, hook the widget up to the Library. if (channel.isEmpty()) { // Connect cover art signals to the library - connect(m_pLibrary, SIGNAL(switchToView(const QString&)), - pCoverArt, SLOT(slotReset())); connect(m_pLibrary, SIGNAL(enableCoverArtDisplay(bool)), pCoverArt, SLOT(slotEnable(bool))); connect(m_pLibrary, SIGNAL(trackSelected(TrackPointer)), @@ -1237,31 +1263,135 @@ void LegacySkinParser::parseSingletonDefinition(const QDomElement& node) { pChild->hide(); } +QWidget* LegacySkinParser::parseLibraryPane(const QDomElement& node) { + int id = -1; + if (!m_pContext->hasNodeSelectInt(node, "Id", &id)) { + SKIN_WARNING(node, *m_pContext) << "Pane Id not found"; + return nullptr; + } + if (id < 0) { + SKIN_WARNING(node, *m_pContext) << "The pane Id cannot be negative"; + return nullptr; + } + //qDebug() << "LegacySkinParser::parseLibrary:ID" << id; + WLibraryPane* pLibraryPaneWidget = new WLibraryPane(m_pParent); + pLibraryPaneWidget->installEventFilter(m_pKeyboard); + pLibraryPaneWidget->installEventFilter( + m_pControllerManager->getControllerLearningEventFilter()); + + m_pLibrary->bindPaneWidget(pLibraryPaneWidget, m_pKeyboard, id); + + // This must come after the bindWidget or we will not style any of the + // LibraryView's because they have not been added yet. + commonWidgetSetup(node, pLibraryPaneWidget, false); + return pLibraryPaneWidget; +} + QWidget* LegacySkinParser::parseLibrary(const QDomElement& node) { - WLibrary* pLibraryWidget = new WLibrary(m_pParent); - pLibraryWidget->installEventFilter(m_pKeyboard); - pLibraryWidget->installEventFilter(m_pControllerManager->getControllerLearningEventFilter()); + // Must add both a SearchBox and a LibraryPane + QFrame* pContainer = new QFrame(m_pParent); + QVBoxLayout* pLayout = new QVBoxLayout(pContainer); + pContainer->setLayout(pLayout); + + WLibraryBreadCrumb* pBreadCrumb = new WLibraryBreadCrumb(pContainer); + m_pLibrary->bindBreadCrumb(pBreadCrumb, m_paneId); + setupWidget(node, pBreadCrumb); + pLayout->addWidget(pBreadCrumb); + + WSearchLineEdit* pSearchBox = new WSearchLineEdit(pContainer); + pSearchBox->setup(node, *m_pContext); + m_pLibrary->bindSearchBar(pSearchBox, m_paneId); + commonWidgetSetup(node, pSearchBox); + pLayout->addWidget(pSearchBox); + + WLibraryPane* pLibraryWidget = new WLibraryPane(pContainer); + pLibraryWidget->installEventFilter(m_pKeyboard); + pLibraryWidget->installEventFilter( + m_pControllerManager->getControllerLearningEventFilter()); + pLayout->addWidget(pLibraryWidget); + + m_pLibrary->bindPaneWidget(pLibraryWidget, m_pKeyboard, m_paneId); + commonWidgetSetup(node, pLibraryWidget, false); + qDebug() << "LegacySkinParser::parseLibrary"; + + ++m_paneId; + return pContainer; +} - // Connect Library search signals to the WLibrary - connect(m_pLibrary, SIGNAL(search(const QString&)), - pLibraryWidget, SLOT(search(const QString&))); - m_pLibrary->bindWidget(pLibraryWidget, m_pKeyboard); +QWidget* LegacySkinParser::parseLibrarySidebar(const QDomElement& node) { + // We must create both LibrarySidebarButtons and LibrarySidebarExpanded + // to allow support for old skins + QFrame* pContainer = new QFrame(m_pParent); + QHBoxLayout* pLayout = new QHBoxLayout(pContainer); + pContainer->setLayout(pLayout); + + // Create config object for WButtonBar + ConfigKey confKey("[Library]", "show_icon_text"); + controlFromConfigKey(confKey, true, nullptr); + m_pConfig->set(confKey, QString::number(1.0)); + + WVerticalScrollArea* scroll = new WVerticalScrollArea(pContainer); + scroll->installEventFilter(m_pKeyboard); + pLayout->addWidget(scroll); + + WButtonBar* pLibrarySidebar = new WButtonBar(scroll); + m_pLibrary->bindSidebarButtons(pLibrarySidebar); + scroll->setWidget(pLibrarySidebar); + connect(pLibrarySidebar, SIGNAL(ensureVisible(QWidget*)), + scroll, SLOT(slotEnsureVisible(QWidget*))); + + WBaseLibrary* pLibrarySidebarExpanded = new WBaseLibrary(pContainer); + pLibrarySidebarExpanded->installEventFilter(m_pKeyboard); + pLibrarySidebarExpanded->installEventFilter(m_pControllerManager->getControllerLearningEventFilter()); + m_pLibrary->bindSidebarExpanded(pLibrarySidebarExpanded, m_pKeyboard); + pLayout->addWidget(pLibrarySidebarExpanded); + + setupWidget(node, pLibrarySidebar); + commonWidgetSetup(node, pLibrarySidebarExpanded, false); + return pContainer; +} - // This must come after the bindWidget or we will not style any of the - // LibraryView's because they have not been added yet. - commonWidgetSetup(node, pLibraryWidget, false); +QWidget* LegacySkinParser::parseLibrarySidebarButtons(const QDomElement& node) { + WVerticalScrollArea* scroll = new WVerticalScrollArea(m_pParent); + scroll->installEventFilter(m_pKeyboard); + + WButtonBar* pLibrarySidebar = new WButtonBar(scroll); + pLibrarySidebar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); + m_pLibrary->bindSidebarButtons(pLibrarySidebar); + scroll->setWidget(pLibrarySidebar); + connect(pLibrarySidebar, SIGNAL(ensureVisible(QWidget*)), + scroll, SLOT(slotEnsureVisible(QWidget*))); + + setupWidget(node, scroll); + return scroll; +} - return pLibraryWidget; +QWidget *LegacySkinParser::parseLibrarySidebarExpanded(const QDomElement &node) { + WBaseLibrary* pLibrarySidebarExpanded = new WBaseLibrary(m_pParent); + pLibrarySidebarExpanded->installEventFilter(m_pKeyboard); + pLibrarySidebarExpanded->installEventFilter(m_pControllerManager->getControllerLearningEventFilter()); + m_pLibrary->bindSidebarExpanded(pLibrarySidebarExpanded, m_pKeyboard); + commonWidgetSetup(node, pLibrarySidebarExpanded, false); + return pLibrarySidebarExpanded; } -QWidget* LegacySkinParser::parseLibrarySidebar(const QDomElement& node) { - WLibrarySidebar* pLibrarySidebar = new WLibrarySidebar(m_pParent); - pLibrarySidebar->installEventFilter(m_pKeyboard); - pLibrarySidebar->installEventFilter(m_pControllerManager->getControllerLearningEventFilter()); - m_pLibrary->bindSidebarWidget(pLibrarySidebar); - commonWidgetSetup(node, pLibrarySidebar, false); - return pLibrarySidebar; +QWidget* LegacySkinParser::parseLibraryBreadCrumb(const QDomElement& node) { + int id = -1; + if (!m_pContext->hasNodeSelectInt(node, "Id", &id)) { + SKIN_WARNING(node, *m_pContext) << "BreadCrumb Id not found"; + return nullptr; + } + if (id < 0) { + SKIN_WARNING(node, *m_pContext) << "The BreadCrumb Id cannot be negative"; + return nullptr; + } + //qDebug() << "LegacySkinParser::parseLibrary:ID" << id; + WLibraryBreadCrumb* pLibraryBreacrumb = new WLibraryBreadCrumb(m_pParent); + m_pLibrary->bindBreadCrumb(pLibraryBreacrumb, id); + setupWidget(node, pLibraryBreacrumb); + + return pLibraryBreacrumb; } QWidget* LegacySkinParser::parseTableView(const QDomElement& node) { @@ -1289,7 +1419,7 @@ QWidget* LegacySkinParser::parseTableView(const QDomElement& node) { QWidget* oldParent = m_pParent; m_pParent = pSplitter; - QWidget* pLibraryWidget = parseLibrary(node); + QWidget* pLibraryWidget = parseLibraryPane(node); QWidget* pLibrarySidebarPage = new QWidget(pSplitter); m_pParent = pLibrarySidebarPage; diff --git a/src/skin/legacyskinparser.h b/src/skin/legacyskinparser.h index bd7d4b2eaa5..a05ef2e5264 100644 --- a/src/skin/legacyskinparser.h +++ b/src/skin/legacyskinparser.h @@ -15,6 +15,7 @@ class WBaseWidget; class Library; +class LibraryPaneManager; class KeyboardEventFilter; class PlayerManager; class EffectsManager; @@ -98,8 +99,12 @@ class LegacySkinParser : public QObject, public SkinParser { // Library widgets. QWidget* parseTableView(const QDomElement& node); QWidget* parseSearchBox(const QDomElement& node); - QWidget* parseLibrary(const QDomElement& node); + QWidget* parseLibraryPane(const QDomElement& node); QWidget* parseLibrarySidebar(const QDomElement& node); + QWidget* parseLibrarySidebarButtons(const QDomElement& node); + QWidget* parseLibrarySidebarExpanded(const QDomElement& node); + QWidget* parseLibraryBreadCrumb(const QDomElement& node); + QWidget* parseLibrary(const QDomElement& node); QWidget* parseBattery(const QDomElement& node); QWidget* parseCoverArt(const QDomElement& node); @@ -139,6 +144,7 @@ class LegacySkinParser : public QObject, public SkinParser { QHash m_templateCache; static QList s_channelStrs; static QMutex s_safeStringMutex; + int m_paneId; }; diff --git a/src/skin/skinloader.cpp b/src/skin/skinloader.cpp index 2cc4bb3bf1d..426a19dfe98 100644 --- a/src/skin/skinloader.cpp +++ b/src/skin/skinloader.cpp @@ -125,7 +125,7 @@ QWidget* SkinLoader::loadDefaultSkin(QWidget* pParent, } LegacySkinParser legacy(m_pConfig, pKeyboard, pPlayerManager, - pControllerManager, pLibrary, pVCMan, + pControllerManager, pLibrary, pVCMan, pEffectsManager); return legacy.parseSkin(skinPath, pParent); } diff --git a/src/skin/skinloader.h b/src/skin/skinloader.h index ac76b0c9978..0f594eb8052 100644 --- a/src/skin/skinloader.h +++ b/src/skin/skinloader.h @@ -11,6 +11,7 @@ class KeyboardEventFilter; class PlayerManager; class ControllerManager; class Library; +class LibraryPaneManager; class VinylControlManager; class EffectsManager; class LaunchImage; diff --git a/src/test/autodjprocessor_test.cpp b/src/test/autodjprocessor_test.cpp index f28718204a2..49aac9f556d 100644 --- a/src/test/autodjprocessor_test.cpp +++ b/src/test/autodjprocessor_test.cpp @@ -5,7 +5,7 @@ #include #include "test/librarytest.h" -#include "library/autodj/autodjprocessor.h" +#include "library/features/autodj/autodjprocessor.h" #include "control/controlpushbutton.h" #include "control/controlpotmeter.h" #include "control/controllinpotmeter.h" diff --git a/src/test/searchqueryparsertest.cpp b/src/test/searchqueryparsertest.cpp index 5400d69ac7b..6d0522221f9 100644 --- a/src/test/searchqueryparsertest.cpp +++ b/src/test/searchqueryparsertest.cpp @@ -189,17 +189,14 @@ TEST_F(SearchQueryParserTest, TextFilterEmpty) { searchColumns << "artist" << "album"; - // An empty argument should pass everything. + // An empty argument should pass "is null" elements. auto pQuery( m_parser.parseQuery("comment:", searchColumns, "")); TrackPointer pTrack(Track::newTemporary()); pTrack->setComment("test ASDF test"); - EXPECT_TRUE(pQuery->match(pTrack)); - - EXPECT_STREQ( - qPrintable(QString("")), - qPrintable(pQuery->toSql())); + EXPECT_TRUE(pQuery->match(pTrack)); + EXPECT_TRUE(pQuery->toSql().contains(QRegExp(".*IS NULL.*"))); } TEST_F(SearchQueryParserTest, TextFilterQuote) { diff --git a/src/test/stringhelpertest.cpp b/src/test/stringhelpertest.cpp new file mode 100644 index 00000000000..4d909b575ad --- /dev/null +++ b/src/test/stringhelpertest.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include + +#include + +#include "util/stringhelper.h" + +namespace { + +TEST(StringHelperTest, Ascii) { + QChar c = StringHelper::getFirstCharForGrouping("a"); + ASSERT_EQ(c, QChar('A')); +} + +TEST(StringHelperTest, Null) { + QChar c = StringHelper::getFirstCharForGrouping(""); + ASSERT_TRUE(c.isNull()); +} + +TEST(StringHelperTest, Change) { + QLocale prev; + QLocale::setDefault(QLocale::Catalan); + QString s1 = QString::fromUtf8("ç"); + QString s2 = QString::fromUtf8("C"); + QChar c1 = StringHelper::getFirstCharForGrouping(s1); + QChar c2 = s2.at(0); + ASSERT_EQ(QString::localeAwareCompare(c1, c2), 0) + << qPrintable(c1) << " " << qPrintable(c2); + QLocale::setDefault(prev); +} + +TEST(StringHelperTest, RemoveAccent) { + QLocale prev; + QLocale::setDefault(QLocale::Catalan); + QString s1 = QString::fromUtf8("à"); + QString s2 = QString::fromUtf8("A"); + QChar c1 = StringHelper::getFirstCharForGrouping(s1); + QChar c2 = s2.at(0); + ASSERT_EQ(QString::localeAwareCompare(c1, c2), 0) + << qPrintable(c1) << " " << qPrintable(c2); + QLocale::setDefault(prev); +} + +TEST(StringHelperTest, Finnish) { + QLocale prev; + QLocale::setDefault(QLocale::Finnish); + + QString s1 = QString::fromUtf8("å"); + QString s2 = QString::fromUtf8("Å"); + QChar c1 = StringHelper::getFirstCharForGrouping(s1); + QChar c2 = s2.at(0); +#if __LINUX__ + ASSERT_EQ(QString::localeAwareCompare(c1, c2), 0) + << qPrintable(c1) << " " << qPrintable(c2); +#endif + QLocale::setDefault(prev); +} + +TEST(StringHelperTest, Chinese) { + QLocale prev; + QLocale::setDefault(QLocale::Chinese); + + QString s1 = QString::fromUtf8("诶"); + QString s2 = QString::fromUtf8("诶"); + QChar c1 = StringHelper::getFirstCharForGrouping(s1); + QChar c2 = s2.at(0); + ASSERT_EQ(QString::localeAwareCompare(c1, c2), 0) + << qPrintable(c1) << " " << qPrintable(c2); + + QLocale::setDefault(prev); +} + +} diff --git a/src/util/dnd.h b/src/util/dnd.h index 542b0ecdf6d..8cab28e375c 100644 --- a/src/util/dnd.h +++ b/src/util/dnd.h @@ -26,6 +26,7 @@ class DragAndDropHelper { bool firstOnly, bool acceptPlaylists) { QList fileLocations; + qDebug() << urls; foreach (const QUrl& url, urls) { // XXX: Possible WTF alert - Previously we thought we needed diff --git a/src/util/stringhelper.cpp b/src/util/stringhelper.cpp new file mode 100644 index 00000000000..55d0fd59a41 --- /dev/null +++ b/src/util/stringhelper.cpp @@ -0,0 +1,44 @@ +#include +#include +#include + +#include "stringhelper.h" + +StringHelper::StringHelper() { + +} + +QChar StringHelper::getFirstCharForGrouping(const QString& text) { + if (text.size() <= 0) { + return QChar(); + } + + QChar c = text.at(0); + if (!c.isLetter()) { + return c; + } + c = c.toUpper(); + QString letter(c); + + // This removes the accents of the characters + QString s1 = letter.normalized(QString::NormalizationForm_KD); + QString normChar(s1.at(0)); + QString s1_1 = normChar + "d"; + QString s1_2 = normChar + "f"; + QString letter_1 = s1 + "e"; + if (s1.size() > 0) { + // We do the following checking: Ad < Äe < Af + // because locale aware compare takes in account the next character. + // This is due to Chinese letters or Finnish sorting. In Finnish the + // correct sorting is a-z, å, ä, ö and the user will expect these + // letters at the end and not like this a, ä, å, b-o, ö, p-z + + //qDebug() << "Checking " << s1_1 << "<" << letter_1 << "<" << s1_2; + if (s1_1.localeAwareCompare(letter_1) < 0 && + letter_1.localeAwareCompare(s1_2) < 0) { + c = s1.at(0).toUpper(); + } + } + return c; +} + diff --git a/src/util/stringhelper.h b/src/util/stringhelper.h new file mode 100644 index 00000000000..cdca88edd1f --- /dev/null +++ b/src/util/stringhelper.h @@ -0,0 +1,14 @@ +#ifndef STRINGHELPER_H +#define STRINGHELPER_H + +#include + +class StringHelper +{ + public: + StringHelper(); + + static QChar getFirstCharForGrouping(const QString &text); +}; + +#endif // STRINGHELPER_H diff --git a/src/widget/wbaselibrary.cpp b/src/widget/wbaselibrary.cpp new file mode 100644 index 00000000000..a5192ed9699 --- /dev/null +++ b/src/widget/wbaselibrary.cpp @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include + +#include "library/libraryfeature.h" +#include "widget/wbaselibrary.h" + +WBaseLibrary::WBaseLibrary(QWidget* parent) + : QStackedWidget(parent), + WBaseWidget(this), + m_pCurrentFeature(nullptr), + m_showFocus(0), + m_isCollapsed(false) { + +} + +bool WBaseLibrary::registerView(LibraryFeature* pFeature, QWidget* view) { + if (m_viewsByFeature.contains(pFeature)) { + return false; + } + + view->installEventFilter(this); + int index = addWidget(view); + setCurrentIndex(index); + m_pCurrentFeature = pFeature; + m_viewsByFeature.insert(pFeature, view); + return true; +} + +LibraryFeature* WBaseLibrary::getCurrentFeature() { + return m_pCurrentFeature; +} + +int WBaseLibrary::getShowFocus() { + return m_showFocus; +} + +void WBaseLibrary::setShowFocus(int sFocus) { + //qDebug() << "WBaseLibrary::setShowFocus" << sFocus << this; + m_showFocus = sFocus; + + style()->unpolish(this); + style()->polish(this); + update(); +} + +void WBaseLibrary::switchToFeature(LibraryFeature *pFeature) { + auto it = m_viewsByFeature.find(pFeature); + // Only change the current feature if it's not shown already + if (it != m_viewsByFeature.end() && currentWidget() != (*it)) { + m_pCurrentFeature = pFeature; + setCurrentWidget(*it); + } +} + +bool WBaseLibrary::eventFilter(QObject*, QEvent* pEvent) { + if (pEvent->type() == QEvent::FocusIn) { + //qDebug() << "WBaseLibrary::eventFilter FocusIn"; + emit(focused()); + } + return false; +} + +bool WBaseLibrary::event(QEvent* pEvent) { + if (pEvent->type() == QEvent::ToolTip) { + updateTooltip(); + } else if (pEvent->type() == QEvent::FocusIn) { + emit(focused()); + } + + return QStackedWidget::event(pEvent); +} + +void WBaseLibrary::resizeEvent(QResizeEvent *pEvent) { + // Detect whether the library is collapsed to change the focus behaviour + if (pEvent->size().isEmpty()) { + m_isCollapsed = true; + emit(collapsed()); + } else if (m_isCollapsed) { + m_isCollapsed = false; + emit(uncollapsed()); + } +} + diff --git a/src/widget/wbaselibrary.h b/src/widget/wbaselibrary.h new file mode 100644 index 00000000000..594f5fdf26f --- /dev/null +++ b/src/widget/wbaselibrary.h @@ -0,0 +1,55 @@ +#ifndef WLIBRARYSIDEBAREXPANDED_H +#define WLIBRARYSIDEBAREXPANDED_H +#include +#include +#include + +#include "widget/wbasewidget.h" + +class LibraryFeature; + +class WBaseLibrary : public QStackedWidget, public WBaseWidget +{ + Q_OBJECT + public: + + WBaseLibrary(QWidget* parent = nullptr); + + virtual bool registerView(LibraryFeature* pFeature, QWidget* view); + + LibraryFeature* getCurrentFeature(); + + Q_PROPERTY(int showFocus READ getShowFocus WRITE setShowFocus) + + int getShowFocus(); + + // Sets the widget to the focused state, it's not the same as Qt focus + void setShowFocus(int sFocus); + + virtual void switchToFeature(LibraryFeature* pFeature); + virtual void search(const QString&) {} + virtual void searchStarting() {} + virtual void searchCleared() {} + + signals: + + void focused(); + void collapsed(); + void uncollapsed(); + + protected: + + bool eventFilter(QObject*, QEvent* pEvent) override; + bool event(QEvent* pEvent) override; + void resizeEvent(QResizeEvent* pEvent) override; + + QHash m_viewsByFeature; + + private: + + LibraryFeature* m_pCurrentFeature; + int m_showFocus; + bool m_isCollapsed; +}; + +#endif // WLIBRARYSIDEBAREXPANDED_H diff --git a/src/widget/wbuttonbar.cpp b/src/widget/wbuttonbar.cpp new file mode 100644 index 00000000000..0c9e0affb75 --- /dev/null +++ b/src/widget/wbuttonbar.cpp @@ -0,0 +1,160 @@ +#include +#include +#include + +#include "wbuttonbar.h" +#include "library/libraryfeature.h" + +WButtonBar::WButtonBar(QWidget* parent) + : QFrame(parent), + m_focusItem(0), + m_focusFromButton(false) { + + QHBoxLayout* pHb = new QHBoxLayout(this); + pHb->setContentsMargins(0,0,0,0); + + QWidget* w1 = new QWidget(this); + + // QSizePolicy::Maximum -> treat the size hint as maximum. This protects us + // from growing to the scroll area size which includes the Scroll bar. + w1->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); + + pHb->addWidget(w1); + pHb->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, + QSizePolicy::Minimum)); + + m_pLayout = new QVBoxLayout(this); + m_pLayout->setContentsMargins(0,0,0,0); + m_pLayout->setSpacing(0); + m_pLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop); + + w1->setLayout(m_pLayout); + setLayout(pHb); + + setFocusPolicy(Qt::StrongFocus); + setAutoFillBackground(true); +} + +WFeatureClickButton* WButtonBar::addButton(LibraryFeature* pFeature) { + WFeatureClickButton* button = new WFeatureClickButton(pFeature, this); + m_pLayout->addWidget(button); + updateGeometry(); + return button; +} + +void WButtonBar::keyPressEvent(QKeyEvent* event) { + bool focusFound = false; + if (event->modifiers() == Qt::NoModifier) { + switch(event->key()) { + case Qt::Key_Up: + for (int i = m_pLayout->count() - 1; i >= 0; --i) { + QLayoutItem* item = m_pLayout->itemAt(i); + if (item) { + QWidget* widget = item->widget(); + if (widget) { + if (widget->hasFocus()) { + m_focusItem = i; + focusFound = true; + } else if (focusFound == true) { + widget->setFocus(); + emit ensureVisible(widget); + m_focusItem = i; + qDebug() << "Focus to" << i; + event->accept(); + return; + } + } + } + } + if (focusFound == false) { + QLayoutItem* item = m_pLayout->itemAt(m_focusItem); + if (item) { + QWidget* widget = item->widget(); + if (widget) { + widget->setFocus(); + emit ensureVisible(widget); + event->accept(); + return; + } + } + } + break; + case Qt::Key_Down: + for (int i = 0; i < m_pLayout->count(); ++i) { + QLayoutItem* item = m_pLayout->itemAt(i); + if (item) { + QWidget* widget = item->widget(); + if (widget) { + if (widget->hasFocus()) { + m_focusItem = i; + focusFound = true; + } else if (focusFound == true) { + widget->setFocus(); + emit ensureVisible(widget); + m_focusItem = i; + qDebug() << "Focus to" << i; + event->accept(); + return; + } + } + } + } + if (focusFound == false) { + QLayoutItem* item = m_pLayout->itemAt(m_focusItem); + if (item) { + QWidget* widget = item->widget(); + if (widget) { + widget->setFocus(); + emit ensureVisible(widget); + event->accept(); + return; + } + } + } + break; + } + } + QFrame::keyPressEvent(event); +} + +void WButtonBar::focusInEvent(QFocusEvent* event) { + QFrame::focusInEvent(event); + if (m_focusFromButton) { + // don't re-focus buttons, when the focus was just there before + focusPreviousChild(); + m_focusFromButton = false; + } else { + QLayoutItem* item = m_pLayout->itemAt(m_focusItem); + if (item) { + QWidget* widget = item->widget(); + if (widget) { + widget->setFocus(); + emit ensureVisible(widget); + } + } + } +} + +bool WButtonBar::focusNextPrevChild(bool next) { + // focus changing by keyboard + // Old item has still the focus, save it if it is one of our buttons + m_focusFromButton = false; + for (int i = 0; i < m_pLayout->count(); ++i) { + QLayoutItem* item = m_pLayout->itemAt(i); + if (item) { + QWidget* widget = item->widget(); + if (widget) { + if (widget->hasFocus()) { + m_focusItem = i; + if (!next) { + // WButtonBar::focusInEvent() is called short after. + m_focusFromButton = true; + } + break; + } + } + } + } + + return QFrame::focusNextPrevChild(next); +} diff --git a/src/widget/wbuttonbar.h b/src/widget/wbuttonbar.h new file mode 100644 index 00000000000..9372552519a --- /dev/null +++ b/src/widget/wbuttonbar.h @@ -0,0 +1,34 @@ +#ifndef WBUTTONBAR_H +#define WBUTTONBAR_H + +#include +#include +#include +#include +#include + +#include "widget/wfeatureclickbutton.h" + +class WButtonBar : public QFrame +{ + Q_OBJECT + public: + WButtonBar(QWidget* parent = nullptr); + + WFeatureClickButton* addButton(LibraryFeature *pFeature); + + signals: + void ensureVisible(QWidget* widget); + + protected: + void keyPressEvent(QKeyEvent* event) override; + void focusInEvent(QFocusEvent* event) override; + bool focusNextPrevChild(bool next) override; + + private: + QLayout* m_pLayout; + int m_focusItem; + bool m_focusFromButton; +}; + +#endif // WBUTTONBAR_H diff --git a/src/widget/wfeatureclickbutton.cpp b/src/widget/wfeatureclickbutton.cpp new file mode 100644 index 00000000000..f496ae41ab3 --- /dev/null +++ b/src/widget/wfeatureclickbutton.cpp @@ -0,0 +1,119 @@ +#include + +#include "wfeatureclickbutton.h" +#include "util/assert.h" + +const int WFeatureClickButton::kHoverTime = 250; // milliseconds + +WFeatureClickButton::WFeatureClickButton(LibraryFeature* pFeature, QWidget* parent) + : QToolButton(parent), + m_textControl(ConfigKey("[Library]", "show_icon_text"), this), + m_pFeature(pFeature) { + DEBUG_ASSERT_AND_HANDLE(pFeature != nullptr) { + return; + } + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); + setAcceptDrops(true); + connect(this, SIGNAL(clicked()), this, SLOT(slotClicked())); + + setIcon(m_pFeature->getIcon()); + m_textControl.connectValueChanged(SLOT(slotTextDisplayChanged(double))); + + if (m_textControl.valid()) { + slotTextDisplayChanged(m_textControl.get()); + } else { + slotTextDisplayChanged(1.0); + } + + setFocusPolicy(Qt::ClickFocus); +} + +void WFeatureClickButton::enterEvent(QEvent* pEvent) { + QToolButton::enterEvent(pEvent); + emit(hovered(m_pFeature)); +} + +void WFeatureClickButton::leaveEvent(QEvent* pEvent) { + QToolButton::leaveEvent(pEvent); + emit(leaved(m_pFeature)); +} + +void WFeatureClickButton::focusInEvent(QFocusEvent* pEvent) { + QToolButton::focusInEvent(pEvent); + emit(focusIn(m_pFeature)); +} + +void WFeatureClickButton::focusOutEvent(QFocusEvent* pEvent) { + QToolButton::focusOutEvent(pEvent); + emit(focusOut(m_pFeature)); +} + + +void WFeatureClickButton::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::RightButton) { + emit(rightClicked(event->globalPos())); + } + QToolButton::mousePressEvent(event); +} + +void WFeatureClickButton::dragEnterEvent(QDragEnterEvent* event) { + //qDebug() << "WFeatureClickButton::dragEnterEvent" << event; + if (!event->mimeData()->hasUrls() || event->source() == this) { + event->ignore(); + return; + } + if (m_pFeature->dragMoveAccept(event->mimeData()->urls().first())) { + event->acceptProposedAction(); + m_hoverTimer.start(kHoverTime, this); + } +} + +void WFeatureClickButton::dragLeaveEvent(QDragLeaveEvent*) { + m_hoverTimer.stop(); +} + +void WFeatureClickButton::dropEvent(QDropEvent* event) { + m_hoverTimer.stop(); + event->acceptProposedAction(); + if (!event->mimeData()->hasUrls() || event->source() == this) { + event->ignore(); + return; + } + + if (m_pFeature->dropAccept(event->mimeData()->urls(), event->source())) { + event->acceptProposedAction(); + } +} + +void WFeatureClickButton::timerEvent(QTimerEvent* event) { + if (event->timerId() != m_hoverTimer.timerId()) { + QToolButton::timerEvent(event); + return; + } + emit(hoverShow(m_pFeature)); +} + +void WFeatureClickButton::slotClicked() { + emit(clicked(m_pFeature)); +} + +void WFeatureClickButton::slotTextDisplayChanged(double value) { + if (value < 1.0) { + setText(""); + setToolButtonStyle(Qt::ToolButtonIconOnly); + } else { + setText(m_pFeature->title().toString()); + setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + } +} + +void WFeatureClickButton::keyPressEvent(QKeyEvent* event) { + switch(event->key()) { + case Qt::Key_Return: + emit(clicked(m_pFeature)); + break; + default: + QWidget::keyPressEvent(event); + break; + } +} diff --git a/src/widget/wfeatureclickbutton.h b/src/widget/wfeatureclickbutton.h new file mode 100644 index 00000000000..31d8a43f845 --- /dev/null +++ b/src/widget/wfeatureclickbutton.h @@ -0,0 +1,60 @@ +#ifndef WRIGHTCLICKBUTTON_H +#define WRIGHTCLICKBUTTON_H + +#include +#include +#include + +#include "library/libraryfeature.h" +#include "control/controlproxy.h" + +class WFeatureClickButton : public QToolButton +{ + Q_OBJECT + + public: + WFeatureClickButton(LibraryFeature* pFeature, QWidget* parent); + + signals: + + void clicked(LibraryFeature*); + void rightClicked(const QPoint&); + void hoverShow(LibraryFeature*); + + void hovered(LibraryFeature*); + void leaved(LibraryFeature*); + void focusIn(LibraryFeature*); + void focusOut(LibraryFeature*); + + protected: + + void enterEvent(QEvent*) override; + void leaveEvent(QEvent*) override; + void focusInEvent(QFocusEvent*) override; + void focusOutEvent(QFocusEvent*) override; + void mousePressEvent(QMouseEvent* event) override; + void keyPressEvent(QKeyEvent * event) override; + + void dragEnterEvent(QDragEnterEvent* event) override; + void dragLeaveEvent(QDragLeaveEvent*) override; + void dropEvent(QDropEvent* event) override; + + void timerEvent(QTimerEvent* event) override; + + + private slots: + + void slotClicked(); + void slotTextDisplayChanged(double value); + + private: + + static const int kHoverTime; + + ControlProxy m_textControl; + LibraryFeature* m_pFeature; + QBasicTimer m_hoverTimer; + bool m_mousEntered; +}; + +#endif // WRIGHTCLICKBUTTON_H diff --git a/src/widget/wlibrary.cpp b/src/widget/wlibrary.cpp deleted file mode 100644 index bcd01d52edd..00000000000 --- a/src/widget/wlibrary.cpp +++ /dev/null @@ -1,75 +0,0 @@ -// wlibrary.cpp -// Created 8/28/2009 by RJ Ryan (rryan@mit.edu) - -#include -#include - -#include "widget/wlibrary.h" -#include "library/libraryview.h" -#include "controllers/keyboard/keyboardeventfilter.h" - -WLibrary::WLibrary(QWidget* parent) - : QStackedWidget(parent), - WBaseWidget(this), - m_mutex(QMutex::Recursive) { -} - -bool WLibrary::registerView(QString name, QWidget* view) { - QMutexLocker lock(&m_mutex); - if (m_viewMap.contains(name)) { - return false; - } - if (dynamic_cast(view) == nullptr) { - qDebug() << "WARNING: Attempted to register a view with WLibrary " - << "that does not implement the LibraryView interface. " - << "Ignoring."; - return false; - } - addWidget(view); - m_viewMap[name] = view; - return true; -} - -void WLibrary::switchToView(const QString& name) { - QMutexLocker lock(&m_mutex); - //qDebug() << "WLibrary::switchToView" << name; - QWidget* widget = m_viewMap.value(name, nullptr); - if (widget != nullptr) { - LibraryView * lview = dynamic_cast(widget); - if (lview == nullptr) { - qDebug() << "WARNING: Attempted to register a view with WLibrary " - << "that does not implement the LibraryView interface. " - << "Ignoring."; - return; - } - if (currentWidget() != widget) { - //qDebug() << "WLibrary::setCurrentWidget" << name; - setCurrentWidget(widget); - lview->onShow(); - } - } -} - -void WLibrary::search(const QString& name) { - QMutexLocker lock(&m_mutex); - QWidget* current = currentWidget(); - LibraryView* view = dynamic_cast(current); - if (view == nullptr) { - qDebug() << "WARNING: Attempted to register a view with WLibrary " - << "that does not implement the LibraryView interface. Ignoring."; - return; - } - lock.unlock(); - view->onSearch(name); -} - -LibraryView* WLibrary::getActiveView() const { - return dynamic_cast(currentWidget()); -} - -bool WLibrary::event(QEvent* pEvent) { - if (pEvent->type() == QEvent::ToolTip) { - updateTooltip(); - } - return QStackedWidget::event(pEvent); -} diff --git a/src/widget/wlibrarybreadcrumb.cpp b/src/widget/wlibrarybreadcrumb.cpp new file mode 100644 index 00000000000..b9260fbf7f2 --- /dev/null +++ b/src/widget/wlibrarybreadcrumb.cpp @@ -0,0 +1,118 @@ +#include + +#include "widget/wlibrarybreadcrumb.h" + +#include "library/treeitemmodel.h" +#include "widget/wpixmapstore.h" +#include "widget/wtristatebutton.h" + +WLibraryBreadCrumb::WLibraryBreadCrumb(QWidget* parent) + : QWidget(parent), + m_pIcon(new QLabel(this)), + m_pText(new QLabel(this)), + m_pPreselectButton(new WTriStateButton(this)), + m_preselected(false) { + QLayout* layout = new QHBoxLayout(this); + + m_pIcon->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_pIcon); + layout->addWidget(m_pText); + + QPixmap preHovered(WPixmapStore::getLibraryPixmap( + ":/images/library/ic_library_preselect.png")); + QPixmap preOff(WPixmapStore::getLibraryPixmap( + ":/images/library/ic_library_notpreselect.png")); + QPixmap preLock(WPixmapStore::getLibraryPixmap( + ":/images/library/ic_library_lockpreselect.png")); + + m_preselectIcon.addPixmap(preLock, QIcon::Normal, QIcon::On); + m_preselectIcon.addPixmap(preOff, QIcon::Normal, QIcon::Off); + m_preselectIcon.addPixmap(preHovered, QIcon::Active, QIcon::Off); + m_pPreselectButton->setIcon(m_preselectIcon); + m_pPreselectButton->setChecked(m_preselected); + m_pPreselectButton->setFocusPolicy(Qt::ClickFocus); + + connect(m_pPreselectButton, SIGNAL(clicked(bool)), + this, SIGNAL(preselected(bool))); + + layout->addItem(new QSpacerItem(0,0, QSizePolicy::MinimumExpanding)); + layout->addWidget(m_pPreselectButton); + layout->setSpacing(2); + layout->setContentsMargins(0,0,0,0); + layout->setAlignment(Qt::AlignVCenter); + + setContentsMargins(0,0,0,0); + setLayout(layout); +} + +// Do not remove this, if the minimum size hint is not 0 width then when +// the user resizes a panel with the breadcrumb it never shows elided text and +// the minimum size becomes the text lenght +QSize WLibraryBreadCrumb::minimumSizeHint() const { + QSize min = m_pText->minimumSizeHint(); + return QSize(0, min.height()); +} + +void WLibraryBreadCrumb::setPreselected(bool value) { + m_preselected = value; + m_pPreselectButton->setChecked(value); + refreshWidth(); +} + +bool WLibraryBreadCrumb::isPreselected() const { + return m_preselected; +} + +void WLibraryBreadCrumb::setPreviewed(bool value) { + m_pPreselectButton->setHovered(value); +} + +void WLibraryBreadCrumb::showBreadCrumb(TreeItem* pTree) { + LibraryFeature* pFeature = pTree->getFeature(); + DEBUG_ASSERT_AND_HANDLE(pFeature != nullptr) { + return; + } + + setBreadIcon(pFeature->getIcon()); + setText(TreeItemModel::getBreadCrumbString(pTree)); +} + +void WLibraryBreadCrumb::showBreadCrumb(const QString& text, const QIcon& icon) { + setText(text); + setBreadIcon(icon); +} + +void WLibraryBreadCrumb::setBreadIcon(const QIcon& icon) { + // Get font height + int height = m_pText->fontMetrics().height(); + QPixmap pix = icon.pixmap(height); + m_pIcon->setPixmap(pix); +} + +void WLibraryBreadCrumb::resizeEvent(QResizeEvent* pEvent) { + QWidget::resizeEvent(pEvent); + refreshWidth(); +} + +void WLibraryBreadCrumb::setText(const QString &text) { + m_longText = text; + setToolTip(m_longText); + refreshWidth(); +} + +void WLibraryBreadCrumb::refreshWidth() { + QFontMetrics metrics(fontMetrics()); + + // Measure the text for the label width + int mLText, mRText, mLIcon, mRIcon, mLPLabel, mRPLabel; + m_pText->getContentsMargins(&mLText, nullptr, &mRText, nullptr); + m_pIcon->getContentsMargins(&mLIcon, nullptr, &mRIcon, nullptr); + m_pPreselectButton->getContentsMargins(&mLPLabel, nullptr, &mRPLabel, nullptr); + int margins = mLIcon + mRIcon + layout()->spacing() + + mLText + mRText + mLPLabel + mRPLabel; + + int newSize = width() - m_pIcon->width() - margins - + m_pPreselectButton->width(); + QString elidedText = metrics.elidedText(m_longText, Qt::ElideRight, newSize); + m_pText->setText(elidedText); +} diff --git a/src/widget/wlibrarybreadcrumb.h b/src/widget/wlibrarybreadcrumb.h new file mode 100644 index 00000000000..91d5fa5dd4f --- /dev/null +++ b/src/widget/wlibrarybreadcrumb.h @@ -0,0 +1,49 @@ +#ifndef SRC_WIDGET_WBREADCRUMB_H_ +#define SRC_WIDGET_WBREADCRUMB_H_ + +#include +#include +#include +#include + +class TreeItem; +class WTriStateButton; + +class WLibraryBreadCrumb : public QWidget { + Q_OBJECT + + public: + WLibraryBreadCrumb(QWidget* parent = nullptr); + + virtual QSize minimumSizeHint() const; + void setPreselected(bool value); + bool isPreselected() const; + + void setPreviewed(bool value); + + signals: + void preselected(bool); + + public slots: + void showBreadCrumb(TreeItem* pTree); + void showBreadCrumb(const QString& text, const QIcon& icon); + void setBreadIcon(const QIcon& icon); + + protected: + virtual void resizeEvent(QResizeEvent* pEvent); + + private: + void setText(const QString& text); + void refreshWidth(); + + QLabel* m_pIcon; + QLabel* m_pText; + WTriStateButton* m_pPreselectButton; + QIcon m_preselectIcon; + QIcon m_previewIcon; + bool m_preselected; + + QString m_longText; +}; + +#endif /* SRC_WIDGET_WBREADCRUMB_H_ */ diff --git a/src/widget/wlibrarypane.cpp b/src/widget/wlibrarypane.cpp new file mode 100644 index 00000000000..a032a2a3423 --- /dev/null +++ b/src/widget/wlibrarypane.cpp @@ -0,0 +1,75 @@ +// wlibrary.cpp +// Created 8/28/2009 by RJ Ryan (rryan@mit.edu) + +#include +#include +#include + +#include "library/libraryview.h" +#include "controllers/keyboard/keyboardeventfilter.h" + +namespace { +void showLibraryWarning() { + qDebug() << "WARNING: Attempted to register a view with WLibrary " + << "that does not implement the LibraryView interface. Ignoring."; +} +} + +WLibraryPane::WLibraryPane(QWidget* parent) + : WBaseLibrary(parent) { +} + +bool WLibraryPane::registerView(LibraryFeature* pFeature, QWidget* pView) { + if (pFeature == nullptr || dynamic_cast(pView) == nullptr) { + showLibraryWarning(); + return false; + } + return WBaseLibrary::registerView(pFeature, pView); +} + +LibraryView* WLibraryPane::getActiveView() const { + LibraryView* pView = dynamic_cast(currentWidget()); + if (pView == nullptr) { + showLibraryWarning(); + } + return pView; +} + + +void WLibraryPane::switchToFeature(LibraryFeature* pFeature) { + qDebug() << "switchToFeature" << pFeature; + auto it = m_viewsByFeature.find(pFeature); + if (it != m_viewsByFeature.end()) { + LibraryView* pView = dynamic_cast(*it); + if (pView == nullptr) { + showLibraryWarning(); + return; + } + WBaseLibrary::switchToFeature(pFeature); + pView->onShow(); + } +} + +void WLibraryPane::search(const QString& name) { + LibraryView* view = getActiveView(); + if (view == nullptr) { + return; + } + view->onSearch(name); +} + +void WLibraryPane::searchCleared() { + LibraryView* view = getActiveView(); + if (view == nullptr) { + return; + } + view->onSearchCleared(); +} + +void WLibraryPane::searchStarting() { + LibraryView* view = getActiveView(); + if (view == nullptr) { + return; + } + view->onSearchStarting(); +} diff --git a/src/widget/wlibrary.h b/src/widget/wlibrarypane.h similarity index 64% rename from src/widget/wlibrary.h rename to src/widget/wlibrarypane.h index 6c6ccb943ff..60049f8a809 100644 --- a/src/widget/wlibrary.h +++ b/src/widget/wlibrarypane.h @@ -11,14 +11,14 @@ #include #include "library/libraryview.h" -#include "widget/wbasewidget.h" +#include "widget/wbaselibrary.h" class KeyboardEventFilter; -class WLibrary : public QStackedWidget, public WBaseWidget { +class WLibraryPane : public WBaseLibrary { Q_OBJECT public: - explicit WLibrary(QWidget* parent); + explicit WLibraryPane(QWidget* parent); // registerView is used to add a view to the LibraryWidget which the widget // can disply on request via showView(). To switch to a given view, call @@ -26,24 +26,17 @@ class WLibrary : public QStackedWidget, public WBaseWidget { // the view and is in charge of deleting it. Returns whether or not the // registration was successful. Registered widget must implement the // LibraryView interface. - bool registerView(QString name, QWidget* view); + bool registerView(LibraryFeature* pFeature, QWidget* pView); LibraryView* getActiveView() const; - public slots: - // Show the view registered with the given name. Does nothing if the current + // Show the view registered with the given feature. Does nothing if the current // view is the specified view, or if the name does not specify any // registered view. - void switchToView(const QString& name); - - void search(const QString&); - - protected: - bool event(QEvent* pEvent) override; - - private: - QMutex m_mutex; - QMap m_viewMap; + void switchToFeature(LibraryFeature* pFeature) override; + void search(const QString& name) override; + void searchCleared() override; + void searchStarting() override; }; #endif /* WLIBRARY_H */ diff --git a/src/widget/wlibrarysidebar.cpp b/src/widget/wlibrarysidebar.cpp index 5a3c6b16c4f..8b26cb1feb8 100644 --- a/src/widget/wlibrarysidebar.cpp +++ b/src/widget/wlibrarysidebar.cpp @@ -1,19 +1,58 @@ #include "widget/wlibrarysidebar.h" +#include +#include #include #include -#include -#include #include +#include +#include +#include -#include "library/sidebarmodel.h" +#include "library/treeitemmodel.h" #include "util/dnd.h" const int expand_time = 250; + +WSidebarItemDelegate::WSidebarItemDelegate(QObject* parent) + : QStyledItemDelegate(parent) { + +} + +void WSidebarItemDelegate::paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const { + bool divider = index.data(AbstractRole::RoleDivider).toBool(); + if (!divider) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + QString text = index.data().toString(); + QRect rect(option.rect); + QFont font(option.font); + font.setBold(true); + // Set small padding left + rect.setLeft(rect.left() + 3); + + QFontMetrics fontMetrics(font); + QString elidedText = fontMetrics.elidedText(text, Qt::ElideRight, rect.width() - 3); + + // Draw the text + painter->setPen(option.palette.color(QPalette::Text)); + painter->setFont(font); + painter->drawText(rect, elidedText); + + // Draw line under text + painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); +} + WLibrarySidebar::WLibrarySidebar(QWidget* parent) : QTreeView(parent), WBaseWidget(this) { + setItemDelegate(new WSidebarItemDelegate(this)); + //Set some properties setHeaderHidden(true); setSelectionMode(QAbstractItemView::SingleSelection); @@ -25,8 +64,9 @@ WLibrarySidebar::WLibrarySidebar(QWidget* parent) setAutoScroll(true); setAttribute(Qt::WA_MacShowFocusRect, false); header()->setStretchLastSection(false); - header()->setResizeMode(QHeaderView::ResizeToContents); - header()->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + header()->setResizeMode(QHeaderView::Stretch); + + setFocusPolicy(Qt::StrongFocus); } void WLibrarySidebar::contextMenuEvent(QContextMenuEvent *event) { @@ -38,7 +78,7 @@ void WLibrarySidebar::contextMenuEvent(QContextMenuEvent *event) { // Drag enter event, happens when a dragged item enters the track sources view void WLibrarySidebar::dragEnterEvent(QDragEnterEvent * event) { - qDebug() << "WLibrarySidebar::dragEnterEvent" << event->mimeData()->formats(); + //qDebug() << "WLibrarySidebar::dragEnterEvent" << event->mimeData()->formats(); if (event->mimeData()->hasUrls()) { // We don't have a way to ask the LibraryFeatures whether to accept a // drag so for now we accept all drags. Since almost every @@ -77,19 +117,13 @@ void WLibrarySidebar::dragMoveEvent(QDragMoveEvent * event) { // Do nothing. event->ignore(); } else { - SidebarModel* sidebarModel = dynamic_cast(model()); bool accepted = true; - if (sidebarModel) { + TreeItemModel* treeModel = dynamic_cast(model()); + if (treeModel) { accepted = false; for (const QUrl& url : urls) { - QModelIndex destIndex = this->indexAt(event->pos()); - if (sidebarModel->dragMoveAccept(destIndex, url)) { - // We only need one URL to be valid for us - // to accept the whole drag... - // consider we have a long list of valid files, checking all will - // take a lot of time that stales Mixxx and this makes the drop feature useless - // Eg. you may have tried to drag two MP3's and an EXE, the drop is accepted here, - // but the EXE is sorted out later after dropping + QModelIndex destIndex = indexAt(event->pos()); + if (treeModel->dragMoveAccept(destIndex, url)) { accepted = true; break; } @@ -131,16 +165,14 @@ void WLibrarySidebar::dropEvent(QDropEvent * event) { event->ignore(); } else { //Reset the selected items (if you had anything highlighted, it clears it) - //this->selectionModel()->clear(); + //selectionModel()->clear(); //Drag-and-drop from an external application or the track table widget //eg. dragging a track from Windows Explorer onto the sidebar - SidebarModel* sidebarModel = dynamic_cast(model()); - if (sidebarModel) { + TreeItemModel* pTreeModel = dynamic_cast(model()); + if (pTreeModel) { QModelIndex destIndex = indexAt(event->pos()); - // event->source() will return NULL if something is droped from - // a different application QList urls(event->mimeData()->urls()); - if (sidebarModel->dropAccept(destIndex, urls, event->source())) { + if (pTreeModel->dropAccept(destIndex, urls, event->source())) { event->acceptProposedAction(); } else { event->ignore(); @@ -154,9 +186,8 @@ void WLibrarySidebar::dropEvent(QDropEvent * event) { } } - void WLibrarySidebar::toggleSelectedItem() { - QModelIndexList selectedIndices = this->selectionModel()->selectedRows(); + QModelIndexList selectedIndices = selectionModel()->selectedRows(); if (selectedIndices.size() > 0) { QModelIndex index = selectedIndices.at(0); // Activate the item so its content shows in the main library. @@ -166,29 +197,90 @@ void WLibrarySidebar::toggleSelectedItem() { } } +bool WLibrarySidebar::isDividerSelected() { + QModelIndex current = currentIndex(); + if (current.isValid()) { + return current.data(AbstractRole::RoleDivider).toBool(); + } + return false; +} + void WLibrarySidebar::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Return) { + qDebug() << "WLibrarySidebar::keyPressEvent" << event; + if (event == QKeySequence::Copy) { + event->ignore(); + } else if (event == QKeySequence::Paste) { + if (paste()) { + event->accept(); + } else { + event->ignore(); + } + } else if (event == QKeySequence::Cut) { + // TODO(XXX) allow delete by key but with a safety pop up + // or an undo feature + event->ignore(); + } else if (event == QKeySequence::SelectAll) { + event->ignore(); + } else if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && + event->modifiers() == Qt::NoModifier) { toggleSelectedItem(); - return; - } else if (event->key() == Qt::Key_Down || event->key() == Qt::Key_Up) { - // Let the tree view move up and down for us. + event->accept(); + } else if (event == QKeySequence::Delete) { + // TODO(XXX) allow delete by key but with a safety pop up + // or an undo feature + event->ignore(); + } else if (event->key() == Qt::Key_Down && + event->modifiers() == Qt::NoModifier) { QTreeView::keyPressEvent(event); + if (isDividerSelected()) { + QTreeView::keyPressEvent(event); + } + } else if (event->key() == Qt::Key_Up && + event->modifiers() == Qt::NoModifier) { + QTreeView::keyPressEvent(event); + if (isDividerSelected()) { + QTreeView::keyPressEvent(event); + } + } else { + // QTreeView::keyPressEvent(event) will consume all key events due to + // it's keyboardSearch feature. + // In Mixxx, we prefer that most keyboard mappings are working, so we + // pass only some basic keys to the base class + if (event->modifiers() == Qt::NoModifier) { + switch (event->key()) { + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Tab: + case Qt::Key_Backtab: + case Qt::Key_Space: + case Qt::Key_Select: + case Qt::Key_F2: + QTreeView::keyPressEvent(event); + break; - // But force the index to be activated/clicked after the selection - // changes. (Saves you from having to push "enter" after changing the - // selection.) - QModelIndexList selectedIndices = this->selectionModel()->selectedRows(); - - //Note: have to get the selected indices _after_ QTreeView::keyPressEvent() - if (selectedIndices.size() > 0) { - QModelIndex index = selectedIndices.at(0); - emit(pressed(index)); + // Ignored even though used in default QT: + case Qt::Key_Asterisk: + case Qt::Key_Plus: + case Qt::Key_Minus: + default: + event->ignore(); + } + } else if (event->modifiers() == Qt::SHIFT) { + switch (event->key()) { + case Qt::Key_Tab: + QTreeView::keyPressEvent(event); + break; + default: + event->ignore(); + } + } else { + event->ignore(); } - return; } - - // Fall through to deafult handler. - QTreeView::keyPressEvent(event); } void WLibrarySidebar::selectIndex(const QModelIndex& index) { @@ -212,3 +304,49 @@ bool WLibrarySidebar::event(QEvent* pEvent) { void WLibrarySidebar::slotSetFont(const QFont& font) { setFont(font); } + +bool WLibrarySidebar::paste() { + qDebug() << "WTrackTableView::paste()" + << QApplication::clipboard()->mimeData()->formats(); + + QModelIndex destIndex; + QModelIndexList indexes = selectionModel()->selectedRows(); + if (indexes.size() > 0) { + destIndex = indexes.at(0); + } else { + destIndex = currentIndex(); + } + + TreeItemModel* pTreeModel = qobject_cast(model()); + if (!pTreeModel) { + return false; + } + + const QMimeData* pMimeData = QApplication::clipboard()->mimeData(); + if (!pMimeData->hasUrls()) { + return false; + } + + return pTreeModel->dropAccept(destIndex, pMimeData->urls(), nullptr); +} + +void WLibrarySidebar::enterEvent(QEvent* pEvent) { + QTreeView::enterEvent(pEvent); + emit(hovered()); +} + +void WLibrarySidebar::leaveEvent(QEvent* pEvent) { + QTreeView::leaveEvent(pEvent); + emit(leaved()); +} + +void WLibrarySidebar::focusInEvent(QFocusEvent* pEvent) { + QTreeView::focusInEvent(pEvent); + emit(focusIn()); +} + +void WLibrarySidebar::focusOutEvent(QFocusEvent* pEvent) { + QTreeView::focusOutEvent(pEvent); + emit(focusOut()); +} + diff --git a/src/widget/wlibrarysidebar.h b/src/widget/wlibrarysidebar.h index 68490afa3d8..49071a26a33 100644 --- a/src/widget/wlibrarysidebar.h +++ b/src/widget/wlibrarysidebar.h @@ -11,8 +11,19 @@ #include #include #include +#include #include "widget/wbasewidget.h" +#include "library/libraryview.h" + +class WSidebarItemDelegate : public QStyledItemDelegate { + Q_OBJECT + public: + WSidebarItemDelegate(QObject* parent = nullptr); + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const; +}; class WLibrarySidebar : public QTreeView, public WBaseWidget { Q_OBJECT @@ -34,10 +45,22 @@ class WLibrarySidebar : public QTreeView, public WBaseWidget { signals: void rightClicked(const QPoint&, const QModelIndex&); + void hovered(); + void leaved(); + void focusIn(); + void focusOut(); + protected: bool event(QEvent* pEvent) override; + void enterEvent(QEvent*) override; + void leaveEvent(QEvent*) override; + void focusInEvent(QFocusEvent*) override; + void focusOutEvent(QFocusEvent*) override; private: + bool paste(); + bool isDividerSelected(); + QBasicTimer m_expandTimer; QModelIndex m_hoverIndex; }; diff --git a/src/widget/wlibrarystack.cpp b/src/widget/wlibrarystack.cpp new file mode 100644 index 00000000000..141bad38e87 --- /dev/null +++ b/src/widget/wlibrarystack.cpp @@ -0,0 +1,82 @@ +#include +#include + +#include "widget/wlibrarystack.h" + +WLibraryStack::WLibraryStack(QWidget* parent) + : QStackedWidget(parent) { + // TODO Auto-generated constructor stub + +} + +WLibraryStack::~WLibraryStack() { + // TODO Auto-generated destructor stub +} + +int WLibraryStack::addWidget(QWidget* w) { + //qDebug() << "WLibraryStack::addWidget" << w; + checkAndWarning(w); + w->installEventFilter(this); + return QStackedWidget::addWidget(w); +} + +int WLibraryStack::insertWidget(int index, QWidget* w) { + checkAndWarning(w); + w->installEventFilter(this); + return QStackedWidget::insertWidget(index, w); +} + +void WLibraryStack::onShow() { + LibraryView* pView = getCurrentView(); + if (pView) { + pView->onShow(); + } +} + +void WLibraryStack::onSearch(const QString& text) { + LibraryView* pView = getCurrentView(); + if (pView) { + pView->onSearch(text); + } +} + +void WLibraryStack::loadSelectedTrack() { + LibraryView* pView = getCurrentView(); + if (pView) { + pView->loadSelectedTrack(); + } +} + +void WLibraryStack::slotSendToAutoDJ() { + LibraryView* pView = getCurrentView(); + if (pView) { + pView->slotSendToAutoDJ(); + } +} + +void WLibraryStack::slotSendToAutoDJTop() { + LibraryView* pView = getCurrentView(); + if (pView) { + pView->slotSendToAutoDJTop(); + } +} + +bool WLibraryStack::eventFilter(QObject* o, QEvent* e) { + if (e->type() == QEvent::FocusIn) { + parent()->event(e); + } + return QStackedWidget::eventFilter(o, e); +} + +bool WLibraryStack::checkAndWarning(QWidget* w) { + if (!dynamic_cast(w)) { + qDebug() << "WARNING: Attempted to register a view with WLibraryStack" + << "that does not implement the LibraryView interface."; + return false; + } + return true; +} + +LibraryView *WLibraryStack::getCurrentView() { + return dynamic_cast(currentWidget()); +} diff --git a/src/widget/wlibrarystack.h b/src/widget/wlibrarystack.h new file mode 100644 index 00000000000..c8a7166bd7a --- /dev/null +++ b/src/widget/wlibrarystack.h @@ -0,0 +1,37 @@ +#ifndef WLIBRARYSTACK_H +#define WLIBRARYSTACK_H + +#include + +#include "library/libraryview.h" + +/* This is a stacked widget that contains LibraryViews inside to, it can be + * added to the WLibrary and allows compatibility + */ + +class WLibraryStack : public QStackedWidget, public LibraryView { + Q_OBJECT + + public: + WLibraryStack(QWidget* parent = nullptr); + ~WLibraryStack(); + + int addWidget(QWidget* w); + int insertWidget(int index, QWidget *w); + + void onShow(); + void onSearch(const QString& text); + + void loadSelectedTrack(); + void slotSendToAutoDJ(); + void slotSendToAutoDJTop(); + + bool eventFilter(QObject*o, QEvent* e); + + private: + + bool checkAndWarning(QWidget *w); + LibraryView* getCurrentView(); +}; + +#endif /* WLIBRARYSTACK_H */ diff --git a/src/widget/wlibrarytableview.cpp b/src/widget/wlibrarytableview.cpp index 7c121703b50..d3794ec43f5 100644 --- a/src/widget/wlibrarytableview.cpp +++ b/src/widget/wlibrarytableview.cpp @@ -16,7 +16,9 @@ WLibraryTableView::WLibraryTableView(QWidget* parent, ConfigKey vScrollBarPosKey) : QTableView(parent), m_pConfig(pConfig), - m_vScrollBarPosKey(vScrollBarPosKey) { + m_vScrollBarPosKey(vScrollBarPosKey), + m_savedSortColumn(-1), + m_savedSortOrder(Qt::AscendingOrder) { // Setup properties for table @@ -62,17 +64,20 @@ void WLibraryTableView::loadVScrollBarPosState() { m_iSavedVScrollBarPos = m_pConfig->getValueString(m_vScrollBarPosKey).toInt(); } -void WLibraryTableView::restoreVScrollBarPos() { - //Restore the scrollbar's position (scroll to that spot) - //when the search has been cleared +void WLibraryTableView::restoreView() { + // Restore the scrollbar's position (scroll to that spot), and sorting order + // when the search has been cleared updateGeometries(); verticalScrollBar()->setValue(m_iSavedVScrollBarPos); + horizontalHeader()->setSortIndicator(m_savedSortColumn, m_savedSortOrder); } -void WLibraryTableView::saveVScrollBarPos() { - //Save the scrollbar's position so we can return here after - //a search is cleared. +void WLibraryTableView::saveView() { + // Save the scrollbar's position and sorting order so we can return here + // after a search is cleared. m_iSavedVScrollBarPos = verticalScrollBar()->value(); + m_savedSortColumn = horizontalHeader()->sortIndicatorSection(); + m_savedSortOrder = horizontalHeader()->sortIndicatorOrder(); } @@ -112,6 +117,27 @@ void WLibraryTableView::moveSelection(int delta) { } } +void WLibraryTableView::restoreQuery(const SavedSearchQuery& query) { + verticalScrollBar()->setValue(query.vScrollBarPos); + + Qt::SortOrder order; + if (query.sortAscendingOrder) { + order = Qt::AscendingOrder; + } else { + order = Qt::DescendingOrder; + } + + horizontalHeader()->setSortIndicator(query.sortColumn, order); +} + +SavedSearchQuery WLibraryTableView::saveQuery(SavedSearchQuery query) const { + query.vScrollBarPos = verticalScrollBar()->value(); + query.sortColumn = horizontalHeader()->sortIndicatorSection(); + query.sortAscendingOrder = + horizontalHeader()->sortIndicatorOrder() == Qt::AscendingOrder; + return query; +} + void WLibraryTableView::setTrackTableFont(const QFont& font) { setFont(font); setTrackTableRowHeight(verticalHeader()->defaultSectionSize()); diff --git a/src/widget/wlibrarytableview.h b/src/widget/wlibrarytableview.h index b810bdee652..0dd67607a7a 100644 --- a/src/widget/wlibrarytableview.h +++ b/src/widget/wlibrarytableview.h @@ -12,7 +12,7 @@ #include "library/libraryview.h" #include "track/track.h" #include "library/coverartcache.h" - +#include "library/dao/savedqueriesdao.h" class WLibraryTableView : public QTableView, public virtual LibraryView { Q_OBJECT @@ -24,6 +24,9 @@ class WLibraryTableView : public QTableView, public virtual LibraryView { ~WLibraryTableView() override; void moveSelection(int delta) override; + virtual void restoreQuery(const SavedSearchQuery& query); + virtual SavedSearchQuery saveQuery(SavedSearchQuery query = SavedSearchQuery()) const; + signals: void loadTrack(TrackPointer pTrack); void loadTrackToPlayer(TrackPointer pTrack, QString group, @@ -31,10 +34,11 @@ class WLibraryTableView : public QTableView, public virtual LibraryView { void trackSelected(TrackPointer pTrack); void onlyCachedCoverArt(bool); void scrollValueChanged(int); + public slots: - void saveVScrollBarPos(); - void restoreVScrollBarPos(); + void saveView(); + void restoreView(); void setTrackTableFont(const QFont& font); void setTrackTableRowHeight(int rowHeight); @@ -47,6 +51,8 @@ class WLibraryTableView : public QTableView, public virtual LibraryView { // The position of the vertical scrollbar slider, eg. before a search is // executed int m_iSavedVScrollBarPos; + int m_savedSortColumn; + Qt::SortOrder m_savedSortOrder; }; diff --git a/src/widget/wmainmenubar.cpp b/src/widget/wmainmenubar.cpp index 61221c6ab7d..a9d7c628305 100644 --- a/src/widget/wmainmenubar.cpp +++ b/src/widget/wmainmenubar.cpp @@ -136,7 +136,7 @@ void WMainMenuBar::initialize() { connect(this, SIGNAL(internalLibraryScanActive(bool)), pLibraryRescan, SLOT(setDisabled(bool))); pLibraryMenu->addAction(pLibraryRescan); - + pLibraryMenu->addSeparator(); QString createPlaylistTitle = tr("Create &New Playlist"); @@ -260,6 +260,14 @@ void WMainMenuBar::initialize() { createVisibilityControl(pViewShowCoverArt, ConfigKey("[Library]", "show_coverart")); pViewMenu->addAction(pViewShowCoverArt); + QString showTextTitle = tr("Show text in sidebar icons"); + QString showTextText = tr("Shows the text below the icons in the sidebar"); + auto pLibraryText = new QAction(showTextTitle, this); + pLibraryText->setStatusTip(showTextText); + pLibraryText->setWhatsThis(buildWhatsThis(showTextTitle, showTextText)); + pLibraryText->setCheckable(true); + createVisibilityControl(pLibraryText, ConfigKey("[Library]", "show_icon_text")); + pViewMenu->addAction(pLibraryText); QString maximizeLibraryTitle = tr("Maximize Library"); QString maximizeLibraryText = tr("Maximize the track library to take up all the available screen space.") + diff --git a/src/widget/wminiviewscrollbar.cpp b/src/widget/wminiviewscrollbar.cpp new file mode 100644 index 00000000000..b4f3dd413e3 --- /dev/null +++ b/src/widget/wminiviewscrollbar.cpp @@ -0,0 +1,277 @@ +#include +#include +#include +#include + +#include "library/abstractmodelroles.h" + +#include "wminiviewscrollbar.h" + +namespace { +float interpolSize(float current, float max1, float max2) { + float res = current*(max2/max1); + return res; +} +} + +WMiniViewScrollBar::WMiniViewScrollBar(QWidget* parent) + : QScrollBar(parent), + m_showLetters(true) { + setMouseTracking(true); + + connect(this, SIGNAL(rangeChanged(int,int)), + this, SLOT(triggerUpdate())); +} + +void WMiniViewScrollBar::setShowLetters(bool show) { + m_showLetters = show; +} + +bool WMiniViewScrollBar::showLetters() const { + return m_showLetters; +} + +void WMiniViewScrollBar::setTreeView(QPointer pTreeView) { + m_pTreeView = pTreeView; +} + +QPointer WMiniViewScrollBar::getTreeView() { + return m_pTreeView; +} + +void WMiniViewScrollBar::setModel(QAbstractItemModel* model) { + m_pModel = model; + if (!m_pModel.isNull()) { + connect(m_pModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), + this, SLOT(triggerUpdate())); + connect(m_pModel, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(triggerUpdate())); + + triggerUpdate(); + } +} + +void WMiniViewScrollBar::paintEvent(QPaintEvent* event) { + if (!m_showLetters) { + QScrollBar::paintEvent(event); + return; + } + + QStylePainter painter(this); + QStyleOptionSlider opt(getStyleOptions()); + opt.subControls &= ~(QStyle::SC_ScrollBarSlider); + + painter.drawComplexControl(QStyle::CC_ScrollBar, opt); + + painter.setBrush(palette().color(QPalette::Text)); + int flags = Qt::AlignTop | Qt::AlignHCenter; + + // Get total size + int letterSize = fontMetrics().height(); + int w = width(); + const QPoint bottom(w, letterSize); + + // Draw each letter in its position + for (const CharPosition& p : m_computedPosition) { + if (p.position < 0 || p.character.isNull() || !p.character.isPrint()) { + continue; + } + QFont f(font()); + f.setBold(p.bold); + painter.setFont(f); + + QPoint topLeft = QPoint(0, p.position); + painter.drawText(QRect(topLeft, topLeft + bottom), flags, p.character); + } + + opt.subControls = QStyle::SC_ScrollBarSlider; + opt.rect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, + QStyle::SC_ScrollBarSlider, this); + //painter.drawComplexControl(QStyle::CC_ScrollBar, opt); + painter.drawControl(QStyle::CE_ScrollBarSlider, opt); +} + +void WMiniViewScrollBar::resizeEvent(QResizeEvent* pEvent) { + computeLettersSize(); + QScrollBar::resizeEvent(pEvent); +} + +void WMiniViewScrollBar::mouseMoveEvent(QMouseEvent* pEvent) { + // Check mouse hover condition + + bool found = false; + int letterSize = fontMetrics().height(); + int posVert = pEvent->pos().y(); + + for (CharPosition& c : m_computedPosition) { + if (posVert >= c.position && posVert < c.position + letterSize && !found) { + c.bold = true; + found = true; + } else { + c.bold = false; + } + } + update(); + QScrollBar::mouseMoveEvent(pEvent); +} + +void WMiniViewScrollBar::mousePressEvent(QMouseEvent* pEvent) { + QScrollBar::mousePressEvent(pEvent); + + bool found = false; + auto itL = m_letters.begin(); + auto itC = m_computedPosition.begin(); + int totalSum = 0; + + // When setting the slider position the correct position is the sum of all + // the previous elements of the selected element. + // In this scrollbar maximum = model.size(), minimum = 0 + while (!found && itL != m_letters.end() && itC != m_computedPosition.end()) { + if (itC->bold) { + setSliderPosition(totalSum); + return; + } + totalSum += itL->count; + ++itL; + ++itC; + } + + // If we haven't found any bold letter it means that the user is clicking + // on the blank space so take the relative position. + int posVert = pEvent->pos().y(); + QStyleOptionSlider opt(getStyleOptions()); + int maxHeight = style()->subControlRect(QStyle::CC_ScrollBar, &opt, + QStyle::SC_ScrollBarGroove, this).height(); + float size = interpolSize(posVert, maxHeight, totalSum); + setSliderPosition(size); +} + +void WMiniViewScrollBar::leaveEvent(QEvent* pEvent) { + for (CharPosition& c : m_computedPosition) { + c.bold = false; + } + QScrollBar::leaveEvent(pEvent); +} + +void WMiniViewScrollBar::refreshCharMap() { + if (m_pModel.isNull()) { + return; + } + + int size = m_pModel->rowCount(); + const QModelIndex& rootIndex = m_pModel->index(0, 0); + + m_letters.clear(); + for (int i = 0; i < size; ++i) { + const QModelIndex& index = rootIndex.sibling(i, 0); + + QChar c = index.data(AbstractRole::RoleGroupingLetter).toChar(); + int count = getVisibleChildCount(index); + + addToLastCharCount(c, count); + } +} + +void WMiniViewScrollBar::computeLettersSize() { + // Initialize each position with the times each character appears + m_computedPosition.resize(m_letters.size()); + for (int i = 0; i < m_letters.size(); ++i) { + m_computedPosition[i].bold = false; + m_computedPosition[i].position = m_letters[i].count; + m_computedPosition[i].character = m_letters[i].character; + } + + QStyleOptionSlider opt(getStyleOptions()); + QRect grooveRect = + style()->subControlRect(QStyle::CC_ScrollBar, &opt, + QStyle::SC_ScrollBarGroove, this); + + // Height of a letter + const int letterSize = fontMetrics().height(); + const int totalLinearSize = grooveRect.bottom(); + float nextAvailableScrollPosition = grooveRect.top(); + float optimalScrollPosition = grooveRect.top(); + + int totalCount = 0; + // Get the total count of letters appearance to make a linear interpolation + // with the current widget height + for (const CharCount& p : m_letters) { + totalCount += p.count; + } + + for (CharPosition& p : m_computedPosition) { + float size = interpolSize(p.position, totalCount, totalLinearSize); + float percentage = 100.0*p.position/float(totalCount); + + if ((optimalScrollPosition + size) < (nextAvailableScrollPosition + letterSize) || + percentage < 1.0 || + (optimalScrollPosition + letterSize) > totalLinearSize) { + // If a very small percentage letter takes the space for a letter + // with more high ocupacy percentage this can be annoying + + // A negative position won't paint the character + p.position = -1; + } else { + p.position = (int) optimalScrollPosition; + nextAvailableScrollPosition = optimalScrollPosition + (float)(letterSize); + } + optimalScrollPosition += size; + } +} + +void WMiniViewScrollBar::triggerUpdate() { + refreshCharMap(); + computeLettersSize(); + update(); +} + +QStyleOptionSlider WMiniViewScrollBar::getStyleOptions() { + QStyleOptionSlider opt; + opt.init(this); + opt.subControls = QStyle::SC_ScrollBarAddLine | QStyle::SC_ScrollBarSubLine | + QStyle::SC_ScrollBarFirst | QStyle::SC_ScrollBarLast | + QStyle::SC_ScrollBarGroove | QStyle::SC_ScrollBarSlider; + opt.orientation = orientation(); + opt.minimum = minimum(); + opt.maximum = maximum(); + opt.sliderPosition = sliderPosition(); + opt.sliderValue = value(); + opt.singleStep = singleStep(); + opt.pageStep = pageStep(); + + return opt; +} + +void WMiniViewScrollBar::addToLastCharCount(const QChar& c, int sum) { + if (m_letters.size() <= 0) { + m_letters.append({c, sum}); + } else { + CharCount& lastC = m_letters.last(); + if (lastC.character == c) { + lastC.count += sum; + } else { + m_letters.append({c, sum}); + } + } +} + +int WMiniViewScrollBar::getVisibleChildCount(const QModelIndex& index) { + if (!index.isValid()) { + return 0; + } + + if (m_pTreeView.isNull()) { + return 1; + } + + if (!m_pTreeView->isExpanded(index)) { + return 1; + } + + int total = 1; + int rowCount = m_pModel->rowCount(index); + for (int i = 0; i < rowCount; ++i) { + total += getVisibleChildCount(index.child(i, 0)); + } + return total; +} diff --git a/src/widget/wminiviewscrollbar.h b/src/widget/wminiviewscrollbar.h new file mode 100644 index 00000000000..90aa7e2eacc --- /dev/null +++ b/src/widget/wminiviewscrollbar.h @@ -0,0 +1,67 @@ +#ifndef WMINIVIEWSCROLLBAR_H +#define WMINIVIEWSCROLLBAR_H + +#include +#include +#include +#include + +#include "library/columncache.h" + +class WMiniViewScrollBar : public QScrollBar +{ + Q_OBJECT + public: + WMiniViewScrollBar(QWidget* parent = nullptr); + + void setShowLetters(bool show); + bool showLetters() const; + + void setTreeView(QPointer pTreeView); + QPointer getTreeView(); + + void setModel(QAbstractItemModel* model); + + public slots: + void triggerUpdate(); + + protected: + virtual void paintEvent(QPaintEvent* event); + virtual void resizeEvent(QResizeEvent* pEvent); + virtual void mouseMoveEvent(QMouseEvent* pEvent); + virtual void mousePressEvent(QMouseEvent* pEvent); + virtual void leaveEvent(QEvent*pEvent); + + private: + struct CharCount { + QChar character; + int count; + }; + + struct CharPosition { + QChar character; + int position; + bool bold; + }; + + private: + // The purpose of this function is to avoid computing all the sizes in the + // paintEvent function which can block the GUI thread + void refreshCharMap(); + void computeLettersSize(); + QStyleOptionSlider getStyleOptions(); + void addToLastCharCount(const QChar& c, int sum = 1); + int getVisibleChildCount(const QModelIndex &index); + + bool m_showLetters; + + // Contains the times each character appears in the model + QVector m_letters; + + // Contains each character's vertical position + QVector m_computedPosition; + QPointer m_pModel; + QPointer m_pTreeView; +}; + +#endif // WMINIVIEWSCROLLBAR_H diff --git a/src/widget/wpixmapstore.cpp b/src/widget/wpixmapstore.cpp index 393ad439173..ad2727fe4dd 100644 --- a/src/widget/wpixmapstore.cpp +++ b/src/widget/wpixmapstore.cpp @@ -17,6 +17,7 @@ #include "widget/wpixmapstore.h" +#include #include #include @@ -24,7 +25,8 @@ // static QHash WPixmapStore::m_paintableCache; -QSharedPointer WPixmapStore::m_loader = QSharedPointer(); +QSharedPointer WPixmapStore::m_pLoader = QSharedPointer(); +QSharedPointer WPixmapStore::m_pIconLoader = QSharedPointer(); // static Paintable::DrawMode Paintable::DrawModeFromString(const QString& str) { @@ -312,8 +314,8 @@ PaintablePointer WPixmapStore::getPaintable(PixmapSource source, // Otherwise, construct it with the pixmap loader. //qDebug() << "WPixmapStore Loading pixmap from file" << source.getPath(); - if (m_loader) { - QImage* pImage = m_loader->getImage(source.getPath()); + if (m_pLoader) { + QImage* pImage = m_pLoader->getImage(source.getPath()); pPaintable = PaintablePointer(new Paintable(pImage, mode)); } else { pPaintable = PaintablePointer(new Paintable(source, mode)); @@ -340,8 +342,8 @@ PaintablePointer WPixmapStore::getPaintable(PixmapSource source, // static QPixmap* WPixmapStore::getPixmapNoCache(const QString& fileName) { QPixmap* pPixmap = nullptr; - if (m_loader) { - QImage* img = m_loader->getImage(fileName); + if (m_pLoader) { + QImage* img = m_pLoader->getImage(fileName); #if QT_VERSION >= 0x040700 pPixmap = new QPixmap(); pPixmap->convertFromImage(*img); @@ -356,10 +358,35 @@ QPixmap* WPixmapStore::getPixmapNoCache(const QString& fileName) { } void WPixmapStore::setLoader(QSharedPointer ld) { - m_loader = ld; + m_pLoader = ld; // We shouldn't hand out pointers to existing pixmaps anymore since our // loader has changed. The pixmaps will get freed once all the widgets // referring to them are destroyed. m_paintableCache.clear(); } + +void WPixmapStore::setLibraryIconLoader(QSharedPointer pIconLoader) { + m_pIconLoader = pIconLoader; +} + +QIcon WPixmapStore::getLibraryIcon(const QString& fileName) { + return QIcon(getLibraryPixmap(fileName)); +} + +QPixmap WPixmapStore::getLibraryPixmap(const QString& fileName) { + if (m_pIconLoader.isNull()) { + return QPixmap(fileName); + } + + QImage* image = m_pIconLoader->getImage(fileName); + + if (!m_pLoader.isNull()) { + m_pLoader->correctImageColors(image); + + } + + QPixmap pixmap(QPixmap::fromImage(*image)); + delete image; + return pixmap; +} diff --git a/src/widget/wpixmapstore.h b/src/widget/wpixmapstore.h index 06a497b41e6..9d13ff3e88c 100644 --- a/src/widget/wpixmapstore.h +++ b/src/widget/wpixmapstore.h @@ -87,13 +87,18 @@ typedef QWeakPointer WeakPaintablePointer; class WPixmapStore { public: static PaintablePointer getPaintable(PixmapSource source, - Paintable::DrawMode mode); + Paintable::DrawMode mode); static QPixmap* getPixmapNoCache(const QString& fileName); static void setLoader(QSharedPointer ld); + + static void setLibraryIconLoader(QSharedPointer pIconLoader); + static QIcon getLibraryIcon(const QString& fileName); + static QPixmap getLibraryPixmap(const QString& fileName); private: static QHash m_paintableCache; - static QSharedPointer m_loader; + static QSharedPointer m_pIconLoader; + static QSharedPointer m_pLoader; }; #endif diff --git a/src/widget/wsearchlineedit.cpp b/src/widget/wsearchlineedit.cpp index fd239373588..1f4b3ec1326 100644 --- a/src/widget/wsearchlineedit.cpp +++ b/src/widget/wsearchlineedit.cpp @@ -1,33 +1,62 @@ -#include "wwidget.h" -#include "wskincolor.h" -#include "wsearchlineedit.h" - -#include -#include +#include +#include #include -#include +#include +#include +#include +#include +#include + +#include "dialog/savedqueries/dlgsavedquerieseditor.h" +#include "widget/wsearchlineedit.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" WSearchLineEdit::WSearchLineEdit(QWidget* pParent) : QLineEdit(pParent), WBaseWidget(this) { setAcceptDrops(false); - m_clearButton = new QToolButton(this); - QPixmap pixmap(":/skins/cross.png"); - m_clearButton->setIcon(QIcon(pixmap)); - m_clearButton->setIconSize(pixmap.size()); - m_clearButton->setCursor(Qt::ArrowCursor); - m_clearButton->setToolTip(tr("Clear input" , "Clear the search bar input field")); - m_clearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); - m_clearButton->hide(); + + QSize iconSize(height() / 2, height() / 2); + + m_pClearButton = new QToolButton(this); + m_pClearButton->setIcon(QIcon(":/skins/cross_2.png")); + m_pClearButton->setIconSize(iconSize); + m_pClearButton->setCursor(Qt::ArrowCursor); + m_pClearButton->setFocusPolicy(Qt::ClickFocus); + m_pClearButton->setToolTip(tr("Clear input" , "Clear the search bar input field")); + m_pClearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + m_pClearButton->hide(); + + m_pSaveButton = new QToolButton(this); + QIcon saveIcon; + saveIcon.addPixmap(QPixmap(":/skins/save.png"), QIcon::Active, QIcon::Off); + saveIcon.addPixmap(QPixmap(":/skins/save_disabled.png"), QIcon::Disabled, QIcon::Off); + saveIcon.addPixmap(QPixmap(":/skins/save.png"), QIcon::Active, QIcon::On); + saveIcon.addPixmap(QPixmap(":/skins/save_disabled.png"), QIcon::Disabled, QIcon::On); + + m_pSaveButton->setIcon(saveIcon); + m_pSaveButton->setIconSize(iconSize); + m_pSaveButton->setCursor(Qt::ArrowCursor); + m_pSaveButton->setFocusPolicy(Qt::ClickFocus); + m_pSaveButton->setToolTip(tr("Save query", "Save the current query for later use")); + m_pSaveButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + m_pSaveButton->hide(); + + m_pDropButton = new QToolButton(this); + m_pDropButton->setIcon(QIcon(":/skins/downArrow.png")); + m_pDropButton->setIconSize(iconSize); + m_pDropButton->setCursor(Qt::ArrowCursor); + m_pDropButton->setFocusPolicy(Qt::ClickFocus); + m_pDropButton->setToolTip(tr("Restore query", + "Restore the search with one of the selected queries")); + m_pDropButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + m_pDropButton->hide(); m_place = true; showPlaceholder(); setFocusPolicy(Qt::ClickFocus); - QShortcut *setFocusShortcut = new QShortcut( - QKeySequence(tr("Ctrl+F", "Search|Focus")), this); - connect(setFocusShortcut, SIGNAL(activated()), - this, SLOT(setFocus())); connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(slotTextChanged(const QString&))); @@ -45,21 +74,19 @@ WSearchLineEdit::WSearchLineEdit(QWidget* pParent) connect(this, SIGNAL(returnPressed()), this, SLOT(triggerSearch())); - connect(m_clearButton, SIGNAL(clicked()), + connect(m_pClearButton, SIGNAL(clicked()), this, SLOT(onSearchTextCleared())); // Forces immediate update of tracktable - connect(m_clearButton, SIGNAL(clicked()), + connect(m_pClearButton, SIGNAL(clicked()), this, SLOT(triggerSearch())); + + connect(m_pSaveButton, SIGNAL(clicked()), + this, SLOT(saveQuery())); + connect(m_pDropButton, SIGNAL(clicked()), + this, SLOT(restoreQuery())); connect(this, SIGNAL(textChanged(const QString&)), - this, SLOT(updateCloseButton(const QString&))); - - // The width of the frame for the widget based on the styling. - int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); - - // Ensures the text does not obscure the clear image. - setStyleSheet(QString("QLineEdit { padding-right: %1px; } "). - arg(m_clearButton->sizeHint().width() + frameWidth + 1)); + this, SLOT(updateButtons(const QString&))); } void WSearchLineEdit::setup(const QDomNode& node, const SkinContext& context) { @@ -85,17 +112,65 @@ void WSearchLineEdit::setup(const QDomNode& node, const SkinContext& context) { setPalette(pal); } -void WSearchLineEdit::resizeEvent(QResizeEvent* e) { - QLineEdit::resizeEvent(e); - QSize sz = m_clearButton->sizeHint(); +void WSearchLineEdit::setTrackCollection(TrackCollection* pTrackCollection) { + m_pTrackCollection = pTrackCollection; +} + +void WSearchLineEdit::slotRestoreSaveButton() { + m_pSaveButton->setEnabled(true); +} + +void WSearchLineEdit::resizeEvent(QResizeEvent* event) { + QLineEdit::resizeEvent(event); + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); - int height = (rect().bottom() + 1 - sz.height())/2; + + QStyleOption option; + option.initFrom(this); + const QRect contentsRect = + style()->subElementRect(QStyle::SE_LineEditContents, &option, this); + + const int Ydeviation = contentsRect.top(); + const int height = contentsRect.height(); + const int fontHeight = fontMetrics().height(); + QSize iconSize(fontHeight, fontHeight); + m_pDropButton->resize(iconSize); + m_pSaveButton->resize(iconSize); + m_pClearButton->resize(iconSize); + + m_pDropButton->setIconSize(iconSize); + m_pSaveButton->setIconSize(iconSize); + m_pClearButton->setIconSize(iconSize); + + const QSize sizeDrop = m_pDropButton->size(); + const QSize sizeSave = m_pSaveButton->size(); + const QSize sizeClear = m_pClearButton->size(); + const int space = 2; // 1px of space between items + + int posXDrop = frameWidth + 1; + int posXSave = posXDrop + sizeDrop.width() + space; + int posXClear = posXSave + sizeSave.width() + space; + + int posYDrop = (height - sizeDrop.height()) / 2 + Ydeviation; + int posYSave = (height - sizeSave.height()) / 2 + Ydeviation; + int posYClear = (height - sizeClear.height()) / 2 + Ydeviation; + if (layoutDirection() == Qt::LeftToRight) { - m_clearButton->move(rect().right() - frameWidth - sz.width() - 1, - height); + posXDrop += sizeDrop.width(); + posXSave += sizeDrop.width(); + posXClear += sizeDrop.width(); + + m_pDropButton->move(width() - posXDrop, posYDrop); + m_pSaveButton->move(width() - posXSave, posYSave); + m_pClearButton->move(width() - posXClear, posYClear); } else { - m_clearButton->move(frameWidth + 1, height); + m_pDropButton->move(posXDrop, posYDrop); + m_pSaveButton->move(posXSave, posYSave); + m_pClearButton->move(posXClear, posYClear); } + + // Ensures the text does not obscure the clear image. + setStyleSheet(QString("QLineEdit { padding-right: %1px; }").arg(posXClear)); } void WSearchLineEdit::focusInEvent(QFocusEvent* event) { @@ -103,8 +178,8 @@ void WSearchLineEdit::focusInEvent(QFocusEvent* event) { if (m_place) { // This gets rid of the blue mac highlight. setAttribute(Qt::WA_MacShowFocusRect, false); - //Must block signals here so that we don't emit a search() signal via - //textChanged(). + // Must block signals here so that we don't emit a search() signal via + // textChanged(). blockSignals(true); setText(""); blockSignals(false); @@ -127,17 +202,31 @@ void WSearchLineEdit::focusOutEvent(QFocusEvent* event) { } } -// slot -void WSearchLineEdit::restoreSearch(const QString& text) { +void WSearchLineEdit::keyPressEvent(QKeyEvent* event) { + switch(event->key()) + { + case Qt::Key_Escape: + emit cancel(); + break; + default: + QLineEdit::keyPressEvent(event); + } +} + +void WSearchLineEdit::restoreSearch(const QString& text, QPointer pFeature) { if(text.isNull()) { // disable setEnabled(false); blockSignals(true); setText("- - -"); blockSignals(false); + m_pSaveButton->hide(); + m_pDropButton->hide(); + m_pCurrentFeature = nullptr; return; } setEnabled(true); + m_pCurrentFeature = pFeature; qDebug() << "WSearchLineEdit::restoreSearch(" << text << ")"; blockSignals(true); setText(text); @@ -151,12 +240,10 @@ void WSearchLineEdit::restoreSearch(const QString& text) { setPalette(pal); m_place = false; } - updateCloseButton(text); + updateButtons(text); } -void WSearchLineEdit::slotSetupTimer(const QString& text) -{ - Q_UNUSED(text); +void WSearchLineEdit::slotSetupTimer(const QString&) { m_searchTimer.stop(); //300 milliseconds timeout m_searchTimer.start(300); @@ -169,8 +256,8 @@ void WSearchLineEdit::triggerSearch() } void WSearchLineEdit::showPlaceholder() { - //Must block signals here so that we don't emit a search() signal via - //textChanged(). + // Must block signals here so that we don't emit a search() signal via + // textChanged(). blockSignals(true); setText(tr("Search..." , "noun")); setToolTip(tr("Search" , "noun") + "\n" + tr("Enter a string to search for") + "\n\n" @@ -180,19 +267,21 @@ void WSearchLineEdit::showPlaceholder() { + tr("Esc") + " " + tr("Exit search" , "Exit search bar and leave focus") ); blockSignals(false); - QPalette pal = palette(); - pal.setColor(foregroundRole(), Qt::lightGray); - setPalette(pal); } -void WSearchLineEdit::updateCloseButton(const QString& text) -{ - m_clearButton->setVisible(!text.isEmpty() && !m_place); +void WSearchLineEdit::updateButtons(const QString& text) { + bool visible = !text.isEmpty() && !m_place; + m_pDropButton->setVisible(true); + m_pSaveButton->setVisible(true); + m_pSaveButton->setEnabled(true); + m_pClearButton->setVisible(visible); } bool WSearchLineEdit::event(QEvent* pEvent) { if (pEvent->type() == QEvent::ToolTip) { updateTooltip(); + } else if (pEvent->type() == QEvent::FocusIn) { + emit(focused()); } return QLineEdit::event(pEvent); } @@ -202,7 +291,95 @@ void WSearchLineEdit::onSearchTextCleared() { emit(searchCleared()); } +void WSearchLineEdit::saveQuery() { + if (m_pCurrentFeature.isNull()) { + return; + } + + SavedSearchQuery query; + query.title = query.query = text(); + + if (query.title.isEmpty()) { + // Request a title + bool ok = false; + query.title = + QInputDialog::getText(nullptr, + tr("Create search query"), + tr("Enter name for empty search query"), + QLineEdit::Normal, + tr("New query"), + &ok).trimmed(); + if (ok == false) { + return; + } + } + + m_pCurrentFeature->saveQuery(query); + m_pSaveButton->setEnabled(false); +} + +void WSearchLineEdit::restoreQuery() { + if (m_pCurrentFeature.isNull()) { + return; + } + + const QList& savedQueries = m_pCurrentFeature->getSavedQueries(); + + QMenu menu; + if (savedQueries.size() <= 0) { + QAction* action = menu.addAction(tr("No saved queries")); + action->setData(-1); + } + QActionGroup* group = new QActionGroup(&menu); + group->setExclusive(false); + + for (const SavedSearchQuery& sQuery : savedQueries) { + QAction* action = menu.addAction(sQuery.title); + if (sQuery.pinned) { + action->setActionGroup(group); + + action->setIcon(QIcon(":/images/ic_library_pinned.png")); + action->setIconVisibleInMenu(true); + action->setCheckable(true); + action->setChecked(true); + } + action->setData(sQuery.id); + } + + // There's no need to show the queries editor if there are not saved queries + if (savedQueries.size() > 0) { + menu.addSeparator(); + QAction* action = menu.addAction(tr("Queries editor")); + action->setData(-2); + } + + QPoint position = m_pDropButton->pos(); + position += QPoint(0, m_pDropButton->height()); + + QAction* selected = menu.exec(mapToGlobal(position)); + if (selected == nullptr) return; + + bool ok; + + // index >= 0 -> Normal data + // index == -1 -> No saved queries selected + // index == -2 -> Saved queries editor selected + int index = selected->data().toInt(&ok); + if (!ok) return; + + if (index >= 0) { + m_pCurrentFeature->restoreQuery(index); + } else if (index == -2 && !m_pTrackCollection.isNull()) { + // If we don't pass a nullptr as parent it uses the parent's style sheet + // and the table shown is weird + DlgSavedQueriesEditor editor(m_pCurrentFeature, + m_pTrackCollection, nullptr); + editor.exec(); + } +} + void WSearchLineEdit::slotTextChanged(const QString& text) { + setToolTip(text); if (text.isEmpty()) { triggerSearch(); emit(searchCleared()); diff --git a/src/widget/wsearchlineedit.h b/src/widget/wsearchlineedit.h index 81a64ca217a..17addb0b9ae 100644 --- a/src/widget/wsearchlineedit.h +++ b/src/widget/wsearchlineedit.h @@ -1,14 +1,18 @@ #ifndef WSEARCHLINEEDIT_H #define WSEARCHLINEEDIT_H -#include -#include -#include -#include -#include #include +#include #include +#include +#include +#include +#include +#include +#include "library/dao/savedqueriesdao.h" +#include "library/libraryfeature.h" +#include "library/trackcollection.h" #include "preferences/usersettings.h" #include "skin/skincontext.h" #include "widget/wbasewidget.h" @@ -16,38 +20,49 @@ class WSearchLineEdit : public QLineEdit, public WBaseWidget { Q_OBJECT public: - explicit WSearchLineEdit(QWidget* pParent); + explicit WSearchLineEdit(QWidget* pParent = nullptr); void setup(const QDomNode& node, const SkinContext& context); + void restoreSearch(const QString& text, + QPointer pFeature = nullptr); + void slotRestoreSaveButton(); + void setTrackCollection(TrackCollection* pTrackCollection); protected: - void resizeEvent(QResizeEvent* /*unused*/) override; - void focusInEvent(QFocusEvent* /*unused*/) override; - void focusOutEvent(QFocusEvent* /*unused*/) override; + void resizeEvent(QResizeEvent* event) override; + void focusInEvent(QFocusEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; bool event(QEvent* pEvent) override; signals: void search(const QString& text); void searchCleared(); void searchStarting(); - - public slots: - void restoreSearch(const QString& text); - void slotTextChanged(const QString& text); + void focused(); + void cancel(); private slots: - void updateCloseButton(const QString& text); - void slotSetupTimer(const QString& text); + void slotTextChanged(const QString& text); + void updateButtons(const QString& text); + void slotSetupTimer(const QString&); void triggerSearch(); void onSearchTextCleared(); + + void saveQuery(); + void restoreQuery(); private: void showPlaceholder(); + QPointer m_pCurrentFeature; QTimer m_searchTimer; - QToolButton* m_clearButton; + QToolButton* m_pClearButton; + QToolButton* m_pSaveButton; + QToolButton* m_pDropButton; bool m_place; QColor m_fgc; //Foreground color + QPointer m_pTrackCollection; }; #endif diff --git a/src/widget/wsingletoncontainer.cpp b/src/widget/wsingletoncontainer.cpp index 11f50737396..ee3e7805689 100644 --- a/src/widget/wsingletoncontainer.cpp +++ b/src/widget/wsingletoncontainer.cpp @@ -1,10 +1,10 @@ +#include #include "widget/wsingletoncontainer.h" #include #include "util/assert.h" #include "skin/skincontext.h" -#include "widget/wlibrary.h" WSingletonContainer::WSingletonContainer(QWidget* pParent) diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index a5e753e3e87..27a3d50a9ae 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include "widget/wtracktableview.h" @@ -78,9 +78,8 @@ WTrackTableView::WTrackTableView(QWidget * parent, connect(m_pCoverMenu, SIGNAL(reloadCoverArt()), this, SLOT(slotReloadCoverArt())); - // Disable editing - //setEditTriggers(QAbstractItemView::NoEditTriggers); + setEditTriggers(QAbstractItemView::NoEditTriggers); // Create all the context m_pMenu->actions (stuff that shows up when you //right-click) @@ -101,10 +100,8 @@ WTrackTableView::WTrackTableView(QWidget * parent, connect(this, SIGNAL(scrollValueChanged(int)), this, SLOT(slotScrollValueChanged(int))); - QShortcut *setFocusShortcut = new QShortcut( - QKeySequence(tr("ESC", "Focus")), this); - connect(setFocusShortcut, SIGNAL(activated()), - this, SLOT(setFocus())); + QScrollBar* pScroll = verticalScrollBar(); + connect(pScroll, SIGNAL(valueChanged(int)), this, SIGNAL(tableChanged())); } WTrackTableView::~WTrackTableView() { @@ -211,8 +208,8 @@ void WTrackTableView::loadTrackModel(QAbstractItemModel *model) { * this will cause a small GUI freeze */ if (getTrackModel() == trackModel) { - // Re-sort the table even if the track model is the same. This triggers - // a select() if the table is dirty. + // Re-sort the table even if the track model is the same. + trackModel->select(); doSortByColumn(horizontalHeader()->sortIndicatorSection()); return; } @@ -272,7 +269,6 @@ void WTrackTableView::loadTrackModel(QAbstractItemModel *model) { setHorizontalHeader(header); header->setMovable(true); header->setClickable(true); - header->setHighlightSections(true); header->setSortIndicatorShown(m_sorting); header->setDefaultAlignment(Qt::AlignLeft); @@ -355,6 +351,11 @@ void WTrackTableView::loadTrackModel(QAbstractItemModel *model) { // target though, so my hax above may not be completely unjustified. setVisible(true); + trackModel->select(); + + if (!m_pScrollBar.isNull()) { + m_pScrollBar->setModel(model); + } } void WTrackTableView::createActions() { @@ -505,6 +506,7 @@ void WTrackTableView::slotRemove() { void WTrackTableView::slotPurge() { QModelIndexList indices = selectionModel()->selectedRows(); + indices = selectionModel()->selectedIndexes(); if (indices.size() > 0) { TrackModel* trackModel = getTrackModel(); if (trackModel) { @@ -563,6 +565,8 @@ void WTrackTableView::slotHide() { } void WTrackTableView::slotUnhide() { + qDebug() << "WTrackTableView::slotUnhide" << selectionModel(); + QModelIndexList indices = selectionModel()->selectedRows(); if (indices.size() > 0) { @@ -975,13 +979,18 @@ void WTrackTableView::onSearch(const QString& text) { } void WTrackTableView::onSearchStarting() { - saveVScrollBarPos(); + saveView(); + TrackModel* trackModel = getTrackModel(); + if (trackModel) { + trackModel->onSearchStarting(); + } } void WTrackTableView::onSearchCleared() { - restoreVScrollBarPos(); + restoreView(); TrackModel* trackModel = getTrackModel(); if (trackModel) { + trackModel->onSearchCleared(); trackModel->search(""); } } @@ -1029,7 +1038,7 @@ void WTrackTableView::dragEnterEvent(QDragEnterEvent * event) { return; } } else if (DragAndDropHelper::dragEnterAccept(*event->mimeData(), - "library", true, true)) { + "", true, true)) { event->acceptProposedAction(); return; } @@ -1063,26 +1072,6 @@ void WTrackTableView::dragMoveEvent(QDragMoveEvent * event) { // Drag-and-drop "drop" event. Occurs when something is dropped onto the track table view void WTrackTableView::dropEvent(QDropEvent * event) { - TrackModel* trackModel = getTrackModel(); - - // We only do things to the TrackModel in this method so if we don't have - // one we should just bail. - if (!trackModel) { - return; - } - - if (!event->mimeData()->hasUrls() || trackModel->isLocked()) { - event->ignore(); - return; - } - - // Save the vertical scrollbar position. Adding new tracks and moving tracks in - // the SQL data models causes a select() (ie. generation of a new result set), - // which causes view to reset itself. A view reset causes the widget to scroll back - // up to the top, which is confusing when you're dragging and dropping. :) - saveVScrollBarPos(); - - // Calculate the model index where the track or tracks are destined to go. // (the "drop" position in a drag-and-drop) // The user usually drops on the seam between two rows. @@ -1094,163 +1083,26 @@ void WTrackTableView::dropEvent(QDropEvent * event) { //qDebug() << "destIndex.row() is" << destIndex.row(); - // Drag and drop within this widget (track reordering) - if (event->source() == this && modelHasCapabilities(TrackModel::TRACKMODELCAPS_REORDER)) { - // Note the above code hides an ambiguous case when a - // playlist is empty. For that reason, we can't factor that - // code out to be common for both internal reordering - // and external drag-and-drop. With internal reordering, - // you can't have an empty playlist. :) - - //qDebug() << "track reordering" << __FILE__ << __LINE__; - - // Save a list of row (just plain ints) so we don't get screwed over - // when the QModelIndexes all become invalid (eg. after moveTrack() - // or addTrack()) - QModelIndexList indices = selectionModel()->selectedRows(); - - QList selectedRows; - for (const QModelIndex& idx : indices) { - selectedRows.append(idx.row()); - } - - // Note: The biggest subtlety in the way I've done this track reordering code - // is that as soon as we've moved ANY track, all of our QModelIndexes probably - // get screwed up. The starting point for the logic below is to say screw it to - // the QModelIndexes, and just keep a list of row numbers to work from. That - // ends up making the logic simpler and the behavior totally predictable, - // which lets us do nice things like "restore" the selection model. - - // The model indices are sorted so that we remove the tracks from the table - // in ascending order. This is necessary because if track A is above track B in - // the table, and you remove track A, the model index for track B will change. - // Sorting the indices first means we don't have to worry about this. - //qSort(m_selectedIndices); - //qSort(m_selectedIndices.begin(), m_selectedIndices.end(), qGreater()); - qSort(selectedRows); - int maxRow = 0; - int minRow = 0; - if (!selectedRows.isEmpty()) { - maxRow = selectedRows.last(); - minRow = selectedRows.first(); - } - - // Destination row, if destIndex is invalid we set it to last row + 1 - int destRow = destIndex.row() < 0 ? model()->rowCount() : destIndex.row(); - - int selectedRowCount = selectedRows.count(); - int selectionRestoreStartRow = destRow; - // Adjust first row of new selection - if (destRow >= minRow && destRow <= maxRow) { - // If you drag a contiguous selection of multiple tracks and drop - // them somewhere inside that same selection, do nothing. + // Drag and drop within this widget (track reordering) + if (event->source() == this && + modelHasCapabilities(TrackModel::TRACKMODELCAPS_REORDER)) { + if (!move(event->mimeData(), destIndex)) { + event->ignore(); return; - } else { - if (destRow < minRow) { - // If we're moving the tracks _up_, - // then reverse the order of the row selection - // to make the algorithm below work as it is - qSort(selectedRows.begin(), - selectedRows.end(), - qGreater()); - } else { - if (destRow > maxRow) { - // If we're moving the tracks _down_, - // adjust the first row to reselect - selectionRestoreStartRow = - selectionRestoreStartRow - selectedRowCount; - } - } - } - - // For each row that needs to be moved... - while (!selectedRows.isEmpty()) { - int movedRow = selectedRows.takeFirst(); // Remember it's row index - // Move it - trackModel->moveTrack(model()->index(movedRow, 0), destIndex); - - // Move the row indices for rows that got bumped up - // into the void we left, or down because of the new spot - // we're taking. - for (int i = 0; i < selectedRows.count(); i++) { - if ((selectedRows[i] > movedRow) && ( - (destRow > selectedRows[i]) )) { - selectedRows[i] = selectedRows[i] - 1; - } else if ((selectedRows[i] < movedRow) && - (destRow < selectedRows[i])) { - selectedRows[i] = selectedRows[i] + 1; - } - } - } - - - // Highlight the moved rows again (restoring the selection) - //QModelIndex newSelectedIndex = destIndex; - for (int i = 0; i < selectedRowCount; i++) { - this->selectionModel()->select(model()->index(selectionRestoreStartRow + i, 0), - QItemSelectionModel::Select | QItemSelectionModel::Rows); } } else { // Drag and drop inside Mixxx is only for few rows, bulks happen here - // Reset the selected tracks (if you had any tracks highlighted, it - // clears them) - this->selectionModel()->clear(); - - // Add all the dropped URLs/tracks to the track model (playlist/crate) - QList fileList = DragAndDropHelper::supportedTracksFromUrls( - event->mimeData()->urls(), false, true); - - QList fileLocationList; - for (const QFileInfo& fileInfo : fileList) { - // TODO(uklotzde): Replace with TrackRef::location() - fileLocationList.append(fileInfo.absoluteFilePath()); - } - - // Drag-and-drop from an external application - // eg. dragging a track from Windows Explorer onto the track table. - int numNewRows = fileLocationList.count(); - - // Have to do this here because the index is invalid after - // addTrack - int selectionStartRow = destIndex.row(); - - // Make a new selection starting from where the first track was - // dropped, and select all the dropped tracks - - // If the track was dropped into an empty playlist, start at row - // 0 not -1 :) - if ((destIndex.row() == -1) && (model()->rowCount() == 0)) { - selectionStartRow = 0; - } else if ((destIndex.row() == -1) && (model()->rowCount() > 0)) { - // If the track was dropped beyond the end of a playlist, then - // we need to fudge the destination a bit... - //qDebug() << "Beyond end of playlist"; - //qDebug() << "rowcount is:" << model()->rowCount(); - selectionStartRow = model()->rowCount(); - } - - // calling the addTracks returns number of failed additions - int tracksAdded = trackModel->addTracks(destIndex, fileLocationList); - - // Decrement # of rows to select if some were skipped - numNewRows -= (fileLocationList.size() - tracksAdded); - - // Create the selection, but only if the track model supports - // reordering. (eg. crates don't support reordering/indexes) - if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_REORDER)) { - for (int i = selectionStartRow; i < selectionStartRow + numNewRows; i++) { - this->selectionModel()->select(model()->index(i, 0), - QItemSelectionModel::Select | - QItemSelectionModel::Rows); - } + if (!insert(event->mimeData(), destIndex)) { + event->ignore(); + return; } } event->acceptProposedAction(); - restoreVScrollBarPos(); + restoreView(); } -TrackModel* WTrackTableView::getTrackModel() { +TrackModel* WTrackTableView::getTrackModel() const { TrackModel* trackModel = dynamic_cast(model()); return trackModel; } @@ -1262,13 +1114,75 @@ bool WTrackTableView::modelHasCapabilities(TrackModel::CapabilitiesFlags capabil } void WTrackTableView::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Return) { - // It is not a good idea if 'key_return' - // causes a track to load since we allow in-line editing - // of table items in general - return; + qDebug() << "WTrackTableView::keyPressEvent" << event; + if (event == QKeySequence::Copy) { + copy(); + event->accept(); + } else if (event == QKeySequence::Paste) { + paste(); + event->accept(); + } else if (event == QKeySequence::Cut) { + cut(); + event->accept(); + } else if (event == QKeySequence::SelectAll) { + selectAll(); + event->accept(); + } else if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && + event->modifiers() == Qt::NoModifier) { + loadSelectedTrack(); + event->accept(); + } else if (event == QKeySequence::Delete) { + slotRemove(); + event->accept(); + } else if (event->key() == Qt::Key_Down && event->modifiers() == Qt::NoModifier) { + // If there is no row selected, select the first + // Qt fails to do it for us when there is only a single row + QModelIndexList indexes = selectionModel()->selectedRows(); + if (indexes.size() == 0) { + selectionModel()->select(model()->index(0, 0), + QItemSelectionModel::Select | QItemSelectionModel::Rows); + event->accept(); + } else { + QTableView::keyPressEvent(event); + } } else { - QTableView::keyPressEvent(event); + // QTableView::keyPressEvent(event) will consume all key events due to + // it's keyboardSearch feature. + // In Mixxx, we prefer that most keyboard mappings are working, so we + // pass only some basic keys to the base class + if (event->modifiers() == Qt::NoModifier) { + switch (event->key()) { + case Qt::Key_Down: + case Qt::Key_Up: + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Tab: + case Qt::Key_Backtab: + case Qt::Key_Space: + case Qt::Key_Select: + case Qt::Key_F2: + QTableView::keyPressEvent(event); + break; + default: + event->ignore(); + } + } else if (event->modifiers() == Qt::SHIFT) { + switch (event->key()) { + case Qt::Key_Tab: + case Qt::Key_Down: + case Qt::Key_Up: + QTableView::keyPressEvent(event); + break; + default: + event->ignore(); + } + } else { + event->ignore(); + } } } @@ -1283,6 +1197,51 @@ void WTrackTableView::loadSelectedTrackToGroup(QString group, bool play) { loadSelectionToGroup(group, play); } +void WTrackTableView::setSorting(bool sorting) { + m_sorting = sorting; +} + +void WTrackTableView::setScrollBar(WMiniViewScrollBar *pScrollbar) { + m_pScrollBar = pScrollbar; + connect(pScrollbar, SIGNAL(valueChanged(int)), this, SIGNAL(tableChanged())); + setVerticalScrollBar(pScrollbar); +} + +void WTrackTableView::restoreQuery(const SavedSearchQuery& query) { + + TrackModel* trackModel = getTrackModel(); + if (trackModel == nullptr) { + return; + } + + trackModel->restoreQuery(query); + QModelIndexList selectedIndeces = trackModel->getSavedSelectionIndices(); + QItemSelectionModel* selectionM = selectionModel(); + selectionM->clearSelection(); + for (const QModelIndex& index : selectedIndeces) { + selectionM->select(index, QItemSelectionModel::Rows | + QItemSelectionModel::Select); + } + + // First of all the track model must be set in order to get the correct + // table size for the WLibraryTableView parameters that are going to be + // restored + WLibraryTableView::restoreQuery(query); +} + +SavedSearchQuery WTrackTableView::saveQuery(SavedSearchQuery query) const { + query = WLibraryTableView::saveQuery(query); + + TrackModel* trackModel = getTrackModel(); + if (trackModel == nullptr) { + return query; + } + + QModelIndexList rowsSelected = selectionModel()->selectedRows(); + query = trackModel->saveQuery(rowsSelected, query); + return query; +} + void WTrackTableView::slotSendToAutoDJ() { // append to auto DJ sendToAutoDJ(false); // bTop = false @@ -1382,7 +1341,7 @@ void WTrackTableView::addSelectionToPlaylist(int iPlaylistId) { trackIds.append(trackId); } } - if (iPlaylistId == -1) { // i.e. a new playlist is suppose to be created + if (iPlaylistId == -1) { // i.e. a new playlist is suppose to be created QString name; bool validNameGiven = false; @@ -1417,7 +1376,7 @@ void WTrackTableView::addSelectionToPlaylist(int iPlaylistId) { +name); return; } - } + } if (trackIds.size() > 0) { // TODO(XXX): Care whether the append succeeded. m_pTrackCollection->getTrackDAO().unhideTracks(trackIds); @@ -1491,65 +1450,37 @@ void WTrackTableView::addSelectionToCrate(int iCrateId) { void WTrackTableView::doSortByColumn(int headerSection) { TrackModel* trackModel = getTrackModel(); - QAbstractItemModel* itemModel = model(); - if (trackModel == nullptr || itemModel == nullptr || !m_sorting) { + if (trackModel == nullptr || !m_sorting) { return; } - // Save the selection - QModelIndexList selection = selectionModel()->selectedRows(); - QSet trackIds; - for (const auto& index: selection) { - trackIds.insert(trackModel->getTrackId(index)); - } - + trackModel->saveSelection(selectionModel()->selectedRows()); sortByColumn(headerSection); QItemSelectionModel* currentSelection = selectionModel(); - - // Find a visible column - int visibleColumn = 0; - while (isColumnHidden(visibleColumn) && visibleColumn < itemModel->columnCount()) { - visibleColumn++; - } - currentSelection->reset(); // remove current selection - - QMap selectedRows; - for (const auto& trackId : trackIds) { - - // TODO(rryan) slowly fixing the issues with BaseSqlTableModel. This - // code is broken for playlists because it assumes each trackid is in - // the table once. This will erroneously select all instances of the - // track for playlists, but it works fine for every other view. The way - // to fix this that we should do is to delegate the selection saving to - // the TrackModel. This will allow the playlist table model to use the - // table index as the unique id instead of this code stupidly using - // trackid. - QLinkedList rows = trackModel->getTrackRows(trackId); - for (int row : rows) { - // Restore sort order by rows, so the following commands will act as expected - selectedRows.insert(row,0); - } - } - + + QModelIndexList savedSelection = trackModel->getSavedSelectionIndices(); QModelIndex first; - QMapIterator i(selectedRows); - while (i.hasNext()) { - i.next(); - QModelIndex tl = itemModel->index(i.key(), visibleColumn); - currentSelection->select(tl, QItemSelectionModel::Rows | QItemSelectionModel::Select); - + for (const QModelIndex& index : savedSelection) { + currentSelection->select(index, + QItemSelectionModel::Rows | + QItemSelectionModel::Select); if (!first.isValid()) { - first = tl; + first = index.sibling(index.row(), getVisibleColumn()); } } - + if (first.isValid()) { scrollTo(first, QAbstractItemView::EnsureVisible); - //scrollTo(first, QAbstractItemView::PositionAtCenter); } + + if (!m_pScrollBar.isNull()) { + m_pScrollBar->triggerUpdate(); + } + + emit(tableChanged()); } void WTrackTableView::slotLockBpm() { @@ -1594,6 +1525,19 @@ void WTrackTableView::lockBpm(bool lock) { } } +int WTrackTableView::getVisibleColumn() { + QAbstractItemModel* itemModel = model(); + if (itemModel == nullptr) { + return -1; + } + + int column = 0; + while (isColumnHidden(column) && column < itemModel->columnCount()) { + column++; + } + return column; +} + void WTrackTableView::slotClearBeats() { TrackModel* trackModel = getTrackModel(); if (trackModel == nullptr) { @@ -1658,3 +1602,263 @@ void WTrackTableView::slotReloadCoverArt() { pCache->requestGuessCovers(selectedTracks); } } + +void WTrackTableView::cut() { + copy(); + slotRemove(); +} + +void WTrackTableView::paste() { + qDebug() << "WTrackTableView::paste()" + << QApplication::clipboard()->mimeData()->formats(); + + QModelIndex destIndex; + QModelIndexList indexes = selectionModel()->selectedRows(); + if (indexes.size() > 0) { + destIndex = indexes.at(0); + } else { + destIndex = currentIndex(); + } + + insert(QApplication::clipboard()->mimeData(), destIndex); +} + +void WTrackTableView::copy(){ + qDebug() << "QKeySequence::Copy"; + + TrackModel* trackModel = getTrackModel(); + if (!trackModel) { + return; + } + + QList locationUrls; + QString locations; + QByteArray gnomeFormat = QByteArray("copy"); + QModelIndexList indices = selectionModel()->selectedRows(); + QList trackIds; + for (const QModelIndex& index : indices) { + TrackPointer pTrack = trackModel->getTrack(index); + if (!pTrack) { + continue; + } + if (!locations.isEmpty()) { + locations += "\n"; + } + locations += pTrack->getLocation(); + QUrl fileUrl = QUrl::fromLocalFile(pTrack->getLocation()); + locationUrls.append(fileUrl); + gnomeFormat += "\n" + fileUrl.toEncoded(); + } + + if (locationUrls.isEmpty()) { + return; + } + + QMimeData* mimeData = new QMimeData(); + mimeData->setUrls(locationUrls); + // According to RFC 6657 this should be "US-ASCII" + // However this is used by Gedit and Utf8 character are accepted + mimeData->setText(locations); + // This is the correct way to pass unicode strings (not used by Gedit) + mimeData->setData("text/plain;charset=utf-8", locations.toUtf8()); + // This is accepted by Nautilus and Co + mimeData->setData("x-special/gnome-copied-files", gnomeFormat); + + QApplication::clipboard()->setMimeData(mimeData); +} + +bool WTrackTableView::insert( + const QMimeData* pMimeData, const QModelIndex& destIndex) { + TrackModel* trackModel = getTrackModel(); + + // We only do things to the TrackModel in this method so if we don't have + // one we should just bail. + if (!trackModel) { + return false; + } + + if (!pMimeData->hasUrls() || trackModel->isLocked()) { + return false; + } + + // Save the vertical scrollbar position. Adding new tracks and moving tracks in + // the SQL data models causes a select() (ie. generation of a new result set), + // which causes view to reset itself. A view reset causes the widget to scroll back + // up to the top, which is confusing when you're dragging and dropping. :) + saveView(); + + // Reset the selected tracks (if you had any tracks highlighted, it + // clears them) + selectionModel()->clear(); + + // Add all the dropped URLs/tracks to the track model (playlist/crate) + QList fileList = DragAndDropHelper::supportedTracksFromUrls( + pMimeData->urls(), false, true); + + QList fileLocationList; + for (const QFileInfo& fileInfo : fileList) { + // TODO(uklotzde): Replace with TrackRef::location() + fileLocationList.append(fileInfo.absoluteFilePath()); + } + + int numNewRows = fileLocationList.count(); + + // Have to do this here because the index is invalid after + // addTrack + int selectionStartRow = destIndex.row(); + + // Make a new selection starting from where the first track was + // dropped, and select all the dropped tracks + + // If the track was dropped into an empty playlist, start at row + // 0 not -1 :) + if ((destIndex.row() == -1) && (model()->rowCount() == 0)) { + selectionStartRow = 0; + } else if ((destIndex.row() == -1) && (model()->rowCount() > 0)) { + // If the track was dropped beyond the end of a playlist, then + // we need to fudge the destination a bit... + //qDebug() << "Beyond end of playlist"; + //qDebug() << "rowcount is:" << model()->rowCount(); + selectionStartRow = model()->rowCount(); + } + + // calling the addTracks returns number of failed additions + int tracksAdded = trackModel->addTracks(destIndex, fileLocationList); + + // Decrement # of rows to select if some were skipped + numNewRows -= (fileLocationList.size() - tracksAdded); + + // Create the selection, but only if the track model supports + // reordering. (eg. crates don't support reordering/indexes) + if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_REORDER)) { + for (int i = selectionStartRow; i < selectionStartRow + numNewRows; i++) { + this->selectionModel()->select(model()->index(i, 0), + QItemSelectionModel::Select | + QItemSelectionModel::Rows); + } + } + + restoreView(); + + return true; +} + + +bool WTrackTableView::move( + const QMimeData* pMimeData, const QModelIndex& destIndex) { + + TrackModel* trackModel = getTrackModel(); + + // We only do things to the TrackModel in this method so if we don't have + // one we should just bail. + if (!trackModel) { + return false; + } + + if (!pMimeData->hasUrls() || trackModel->isLocked()) { + return false; + } + + // Save the vertical scrollbar position. Adding new tracks and moving tracks in + // the SQL data models causes a select() (ie. generation of a new result set), + // which causes view to reset itself. A view reset causes the widget to scroll back + // up to the top, which is confusing when you're dragging and dropping. :) + saveView(); + + // Note the above code hides an ambiguous case when a + // playlist is empty. For that reason, we can't factor that + // code out to be common for both internal reordering + // and external drag-and-drop. With internal reordering, + // you can't have an empty playlist. :) + + //qDebug() << "track reordering" << __FILE__ << __LINE__; + + // Save a list of row (just plain ints) so we don't get screwed over + // when the QModelIndexes all become invalid (eg. after moveTrack() + // or addTrack()) + QModelIndexList indices = selectionModel()->selectedRows(); + + QList selectedRows; + for (const QModelIndex& idx : indices) { + selectedRows.append(idx.row()); + } + + // Note: The biggest subtlety in the way I've done this track reordering code + // is that as soon as we've moved ANY track, all of our QModelIndexes probably + // get screwed up. The starting point for the logic below is to say screw it to + // the QModelIndexes, and just keep a list of row numbers to work from. That + // ends up making the logic simpler and the behavior totally predictable, + // which lets us do nice things like "restore" the selection model. + + // The model indices are sorted so that we remove the tracks from the table + // in ascending order. This is necessary because if track A is above track B in + // the table, and you remove track A, the model index for track B will change. + // Sorting the indices first means we don't have to worry about this. + //qSort(m_selectedIndices); + //qSort(m_selectedIndices.begin(), m_selectedIndices.end(), qGreater()); + qSort(selectedRows); + int maxRow = 0; + int minRow = 0; + if (!selectedRows.isEmpty()) { + maxRow = selectedRows.last(); + minRow = selectedRows.first(); + } + + // Destination row, if destIndex is invalid we set it to last row + 1 + int destRow = destIndex.row() < 0 ? model()->rowCount() : destIndex.row(); + + int selectedRowCount = selectedRows.count(); + int selectionRestoreStartRow = destRow; + + // Adjust first row of new selection + if (destRow >= minRow && destRow <= maxRow) { + // If you drag a contiguous selection of multiple tracks and drop + // them somewhere inside that same selection, do nothing. + return true; + } else { + if (destRow < minRow) { + // If we're moving the tracks _up_, + // then reverse the order of the row selection + // to make the algorithm below work as it is + qSort(selectedRows.begin(), + selectedRows.end(), + qGreater()); + } else { + if (destRow > maxRow) { + // If we're moving the tracks _down_, + // adjust the first row to reselect + selectionRestoreStartRow = + selectionRestoreStartRow - selectedRowCount; + } + } + } + + // For each row that needs to be moved... + while (!selectedRows.isEmpty()) { + int movedRow = selectedRows.takeFirst(); // Remember it's row index + // Move it + trackModel->moveTrack(model()->index(movedRow, 0), destIndex); + + // Move the row indices for rows that got bumped up + // into the void we left, or down because of the new spot + // we're taking. + for (int i = 0; i < selectedRows.count(); i++) { + if ((selectedRows[i] > movedRow) && ( + (destRow > selectedRows[i]) )) { + selectedRows[i] = selectedRows[i] - 1; + } else if ((selectedRows[i] < movedRow) && + (destRow < selectedRows[i])) { + selectedRows[i] = selectedRows[i] + 1; + } + } + } + + + // Highlight the moved rows again (restoring the selection) + //QModelIndex newSelectedIndex = destIndex; + for (int i = 0; i < selectedRowCount; i++) { + this->selectionModel()->select(model()->index(selectionRestoreStartRow + i, 0), + QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + return true; +} diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index 5836006ef87..28f23e09c22 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -13,6 +13,7 @@ #include "track/track.h" #include "util/duration.h" #include "widget/wlibrarytableview.h" +#include "widget/wminiviewscrollbar.h" class ControlProxy; class DlgTrackInfo; @@ -35,16 +36,23 @@ class WTrackTableView : public WLibraryTableView { void keyPressEvent(QKeyEvent* event) override; void loadSelectedTrack() override; void loadSelectedTrackToGroup(QString group, bool play) override; + void setSorting(bool sorting); + void setScrollBar(WMiniViewScrollBar* pScrollbar); + void restoreQuery(const SavedSearchQuery& query) override; + SavedSearchQuery saveQuery(SavedSearchQuery query = SavedSearchQuery()) const override; public slots: void loadTrackModel(QAbstractItemModel* model); void slotMouseDoubleClicked(const QModelIndex &); void slotUnhide(); void slotPurge(); - void onSearchStarting(); - void onSearchCleared(); + void onSearchStarting() override; + void onSearchCleared() override; void slotSendToAutoDJ() override; void slotSendToAutoDJTop() override; + + signals: + void tableChanged(); private slots: void slotRemove(); @@ -85,7 +93,11 @@ class WTrackTableView : public WLibraryTableView { void dragMoveEvent(QDragMoveEvent * event) override; void dragEnterEvent(QDragEnterEvent * event) override; void dropEvent(QDropEvent * event) override; + void cut(); + void paste(); + void copy(); void lockBpm(bool lock); + int getVisibleColumn(); void enableCachedOnly(); void selectionChanged(const QItemSelection &selected, @@ -96,9 +108,12 @@ class WTrackTableView : public WLibraryTableView { void mouseMoveEvent(QMouseEvent *pEvent) override; // Returns the current TrackModel, or returns NULL if none is set. - TrackModel* getTrackModel(); + TrackModel* getTrackModel() const; bool modelHasCapabilities(TrackModel::CapabilitiesFlags capabilities); + bool insert(const QMimeData* pMimeData, const QModelIndex& destIndex); + bool move(const QMimeData* pMimeData, const QModelIndex& destIndex); + UserSettingsPointer m_pConfig; TrackCollection* m_pTrackCollection; @@ -157,6 +172,7 @@ class WTrackTableView : public WLibraryTableView { // Replay Gain feature QAction *m_pReplayGainResetAction; + QPointer m_pScrollBar; bool m_sorting; diff --git a/src/widget/wtristatebutton.cpp b/src/widget/wtristatebutton.cpp new file mode 100644 index 00000000000..24541ff6d2c --- /dev/null +++ b/src/widget/wtristatebutton.cpp @@ -0,0 +1,60 @@ +#include "wtristatebutton.h" +#include + +WTriStateButton::WTriStateButton(QWidget* parent) + : QToolButton(parent), + m_state(State::Unactive) { + setCheckable(true); +} + +void WTriStateButton::setChecked(bool value) { + //qDebug() << this << "setChecked" << value; + // This omits the Hovered state + State state = value ? State::Active : State::Unactive; + setState(state); +} + +bool WTriStateButton::isChecked() const { + return m_state == State::Active; +} + +void WTriStateButton::setState(State state) { + if (state == m_state) return; + m_state = state; + updateButton(); +} + +WTriStateButton::State WTriStateButton::getState() const { + return m_state; +} + +void WTriStateButton::setHovered(bool value) { + //qDebug() << this << "setHovered" << value; + State state = value ? State::Hovered : State::Unactive; + setState(state); +} + +void WTriStateButton::setIcon(const QIcon& icon) { + m_icon = icon; + updateButton(); +} + +void WTriStateButton::updateButton() { + QPixmap pix; + switch (m_state) { + case State::Unactive: + pix = m_icon.pixmap(height(), QIcon::Normal, QIcon::Off); + QToolButton::setChecked(false); + break; + case State::Active: + pix = m_icon.pixmap(height(), QIcon::Normal, QIcon::On); + QToolButton::setChecked(true); + break; + case State::Hovered: + pix = m_icon.pixmap(height(), QIcon::Active, QIcon::Off); + QToolButton::setChecked(false); + break; + } + + QToolButton::setIcon(QIcon(pix)); +} diff --git a/src/widget/wtristatebutton.h b/src/widget/wtristatebutton.h new file mode 100644 index 00000000000..ba0f9ec2b1d --- /dev/null +++ b/src/widget/wtristatebutton.h @@ -0,0 +1,34 @@ +#ifndef WTRISTATEBUTTON_H +#define WTRISTATEBUTTON_H + +#include + +class WTriStateButton : public QToolButton +{ + public: + enum State { + Unactive, + Active, + Hovered + }; + + WTriStateButton(QWidget* parent = nullptr); + + void setChecked(bool value); + bool isChecked() const; + + void setState(State state); + State getState() const; + + void setHovered(bool value); + + void setIcon(const QIcon& icon); + + private: + void updateButton(); + + QIcon m_icon; + State m_state; +}; + +#endif // WTRISTATEBUTTON_H diff --git a/src/widget/wverticalscrollarea.cpp b/src/widget/wverticalscrollarea.cpp new file mode 100644 index 00000000000..32e1e5e9eaa --- /dev/null +++ b/src/widget/wverticalscrollarea.cpp @@ -0,0 +1,52 @@ +#include +#include +#include + +#include "wverticalscrollarea.h" + +WVerticalScrollArea::WVerticalScrollArea(QWidget* parent) + : QScrollArea(parent) { + setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setAlignment(Qt::AlignTop); + setWidgetResizable(true); + setFocusPolicy(Qt::NoFocus); +} + +void WVerticalScrollArea::setWidget(QWidget* widget) { + widget->installEventFilter(this); + QScrollArea::setWidget(widget); +} + +bool WVerticalScrollArea::eventFilter(QObject* o, QEvent* e) { + if (o == widget() && e->type() == QEvent::Resize) { + calcSize(); + } + return false; +} + +void WVerticalScrollArea::resizeEvent(QResizeEvent *e) { + QScrollArea::resizeEvent(e); + calcSize(); +} + +void WVerticalScrollArea::calcSize() { + int width = widget()->minimumSizeHint().width(); + int vScrollWidth = 0; + + if (height() <= widget()->minimumSizeHint().height()) { + vScrollWidth = verticalScrollBar()->width(); + } + setFixedWidth(width + frameWidth() * 2 + vScrollWidth); +} + +void WVerticalScrollArea::slotEnsureVisible(QWidget* widget) { + //qDebug() << "WVerticalScrollArea::slotEnsureVisible"; + ensureWidgetVisible(widget, 0, 0); +} + +bool WVerticalScrollArea::focusNextPrevChild(bool next) { + // QScrollArea::focusNextPrevChild scrolls to center + // of focused child. Sip scrolling here. + return QWidget::focusNextPrevChild(next); +} diff --git a/src/widget/wverticalscrollarea.h b/src/widget/wverticalscrollarea.h new file mode 100644 index 00000000000..65b68634aa6 --- /dev/null +++ b/src/widget/wverticalscrollarea.h @@ -0,0 +1,26 @@ +#ifndef WVERTICALSCROLLAREA_H +#define WVERTICALSCROLLAREA_H + +#include + +class WVerticalScrollArea : public QScrollArea +{ + Q_OBJECT + public: + WVerticalScrollArea(QWidget* parent = nullptr); + + void setWidget(QWidget* widget); + + public slots: + void slotEnsureVisible(QWidget* widget); + + protected: + bool eventFilter(QObject* o, QEvent* e) override; + void resizeEvent(QResizeEvent* e) override; + bool focusNextPrevChild(bool next) override; + + private: + void calcSize(); +}; + +#endif // WVERTICALSCROLLAREA_H