1 /****************************************************************************************
2  * Copyright (c) 2007 Ian Monroe <ian@monroe.nu>                                        *
3  * Copyright (c) 2009 Leo Franchi <lfranchi@kde.org>                                    *
4  *                                                                                      *
5  * This program is free software; you can redistribute it and/or modify it under        *
6  * the terms of the GNU General Public License as published by the Free Software        *
7  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
8  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
9  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
10  * version 3 of the license.                                                            *
11  *                                                                                      *
12  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
13  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
14  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
15  *                                                                                      *
16  * You should have received a copy of the GNU General Public License along with         *
17  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
18  ****************************************************************************************/
19 
20 #define DEBUG_PREFIX "PlaylistDock"
21 
22 #include "PlaylistDock.h"
23 
24 #include "ActionClasses.h"
25 #include "App.h"
26 #include "MainWindow.h"
27 #include "PaletteHandler.h"
28 #include "amarokconfig.h"
29 #include "amarokurls/AmarokUrl.h"
30 #include "core/support/Debug.h"
31 #include "playlist/PlaylistActions.h"
32 #include "playlist/PlaylistController.h"
33 #include "playlist/PlaylistDefines.h"
34 #include "playlist/PlaylistInfoWidget.h"
35 #include "playlist/PlaylistModelStack.h"
36 #include "playlist/PlaylistQueueEditor.h"
37 #include "playlist/PlaylistToolBar.h"
38 #include "playlist/ProgressiveSearchWidget.h"
39 #include "playlist/layouts/LayoutManager.h"
40 #include "playlist/navigators/NavigatorConfigAction.h"
41 #include "playlistmanager/PlaylistManager.h"
42 #include "core-impl/playlists/providers/user/UserPlaylistProvider.h"
43 #include "widgets/HorizontalDivider.h"
44 #include "widgets/BoxWidget.h"
45 
46 #include <QLabel>
47 #include <QStandardPaths>
48 #include <QToolBar>
49 
50 #include <KActionMenu>
51 #include <KToolBarSpacerAction>
52 
53 static const QString s_dynMode( QStringLiteral("dynamic_mode") );
54 static const QString s_repopulate( QStringLiteral("repopulate") );
55 static const QString s_turnOff( QStringLiteral("turn_off") );
56 
Dock(QWidget * parent)57 Playlist::Dock::Dock( QWidget* parent )
58     : AmarokDockWidget( i18n( "&Playlist" ), parent )
59     , m_barBox( 0 )
60 {
61     setObjectName( QStringLiteral("Playlist dock") );
62     setAllowedAreas( Qt::AllDockWidgetAreas );
63 }
64 
65 Playlist::PrettyListView *
currentView()66 Playlist::Dock::currentView()
67 {
68     ensurePolish();
69     return m_playlistView;
70 }
71 
72 Playlist::SortWidget *
sortWidget()73 Playlist::Dock::sortWidget()
74 {
75     ensurePolish();
76     return m_sortWidget;
77 }
78 
79 Playlist::ProgressiveSearchWidget *
searchWidget()80 Playlist::Dock::searchWidget()
81 {
82     ensurePolish();
83     return m_searchWidget;
84 }
85 
86 void
polish()87 Playlist::Dock::polish()
88 {
89     m_mainWidget = new BoxWidget( true, this );
90     setWidget( m_mainWidget );
91     m_mainWidget->setContentsMargins( 0, 0, 0, 0 );
92     m_mainWidget->setFrameShape( QFrame::NoFrame );
93     m_mainWidget->setMinimumWidth( 200 );
94     m_mainWidget->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Ignored );
95     m_mainWidget->setFocus( Qt::ActiveWindowFocusReason );
96 
97     m_sortWidget = new Playlist::SortWidget( m_mainWidget );
98     new HorizontalDivider( m_mainWidget );
99 
100     m_searchWidget = new Playlist::ProgressiveSearchWidget( m_mainWidget );
101 
102     // show visual indication of dynamic playlists  being enabled
103     connect( The::playlistActions(), &Playlist::Actions::navigatorChanged,
104              this, &Playlist::Dock::showDynamicHint );
105     m_dynamicHintWidget = new QLabel( i18n( "<a href='%1'>Dynamic Mode</a> Enabled. "
106         "<a href='%2'>Repopulate</a> | <a href='%3'>Turn off</a>", s_dynMode,
107         s_repopulate, s_turnOff ), m_mainWidget );
108     m_dynamicHintWidget->setAlignment( Qt::AlignCenter );
109     m_dynamicHintWidget->setTextInteractionFlags( Qt::LinksAccessibleByKeyboard | Qt::LinksAccessibleByMouse );
110     m_dynamicHintWidget->setMinimumSize( 1, 1 ); // so that it doesn't prevent playlist from shrinking
111     connect( m_dynamicHintWidget, &QLabel::linkActivated, this, &Dock::slotDynamicHintLinkActivated );
112 
113     QFont dynamicHintWidgetFont = m_dynamicHintWidget->font();
114     dynamicHintWidgetFont.setPointSize( dynamicHintWidgetFont.pointSize() + 1 );
115     m_dynamicHintWidget->setFont( dynamicHintWidgetFont );
116 
117     showDynamicHint();
118 
119     paletteChanged( pApp->palette() );
120     connect( The::paletteHandler(), &PaletteHandler::newPalette,
121              this, &Playlist::Dock::paletteChanged );
122 
123     QWidget * layoutHolder = new QWidget( m_mainWidget );
124 
125     QVBoxLayout* mainPlaylistlayout = new QVBoxLayout( layoutHolder );
126     mainPlaylistlayout->setContentsMargins( 0, 0, 0, 0 );
127 
128     m_playlistView = new PrettyListView();
129     m_playlistView->show();
130 
131     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::filterChanged,
132              m_playlistView, &Playlist::PrettyListView::find );
133     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::next,
134              m_playlistView, &Playlist::PrettyListView::findNext );
135     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::previous,
136              m_playlistView, &Playlist::PrettyListView::findPrevious );
137     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::filterCleared,
138              m_playlistView, &Playlist::PrettyListView::clearSearchTerm );
139     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::showOnlyMatches,
140              m_playlistView, &Playlist::PrettyListView::showOnlyMatches );
141     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::activateFilterResult,
142              m_playlistView, &Playlist::PrettyListView::playFirstSelected );
143     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::downPressed, m_playlistView, &Playlist::PrettyListView::downOneTrack );
144     connect( m_searchWidget, &Playlist::ProgressiveSearchWidget::upPressed, m_playlistView, &Playlist::PrettyListView::upOneTrack );
145 
146     connect( The::mainWindow(), &MainWindow::switchQueueStateShortcut,
147              m_playlistView, &Playlist::PrettyListView::switchQueueState );
148 
149     KConfigGroup searchConfig = Amarok::config(QStringLiteral("Playlist Search"));
150     m_playlistView->showOnlyMatches( searchConfig.readEntry( "ShowOnlyMatches", false ) );
151 
152     connect( m_playlistView, &Playlist::PrettyListView::found, m_searchWidget, &Playlist::ProgressiveSearchWidget::match );
153     connect( m_playlistView, &Playlist::PrettyListView::notFound, m_searchWidget, &Playlist::ProgressiveSearchWidget::noMatch );
154 
155     connect( LayoutManager::instance(), &LayoutManager::activeLayoutChanged,
156              m_playlistView, &Playlist::PrettyListView::reset );
157 
158     mainPlaylistlayout->setSpacing( 0 );
159     mainPlaylistlayout->addWidget( m_playlistView );
160 
161     ModelStack::instance(); //This also creates the Controller.
162 
163     { // START: Playlist toolbar
164         // action toolbar
165         m_barBox = new BoxWidget( false, m_mainWidget );
166         m_barBox->setObjectName( QStringLiteral("PlaylistBarBox") );
167         m_barBox->setContentsMargins( 0, 0, 4, 0 );
168         m_barBox->setFixedHeight( 36 );
169 
170         // Use QToolBar instead of KToolBar, see bug 228390
171         Playlist::ToolBar *plBar = new Playlist::ToolBar( m_barBox );
172         plBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Preferred );
173         plBar->setMovable( false );
174 
175         QActionGroup *playlistActions = new QActionGroup( m_mainWidget );
176         playlistActions->addAction( Amarok::actionCollection()->action( QStringLiteral("playlist_clear") ) );
177 
178         m_savePlaylistMenu = new KActionMenu( QIcon::fromTheme( QStringLiteral("document-save-amarok") ),
179                                               i18n("&Save Current Playlist"), m_mainWidget );
180         m_savePlaylistMenu->addAction( Amarok::actionCollection()->action( QStringLiteral("playlist_export") ) );
181 
182         m_saveActions = new KActionCollection( m_mainWidget );
183 
184         connect( m_savePlaylistMenu, &KActionMenu::triggered,
185                  this, &Dock::slotSaveCurrentPlaylist );
186         foreach( Playlists::PlaylistProvider *provider, The::playlistManager()->providersForCategory(
187                             PlaylistManager::UserPlaylist ) )
188         {
189             playlistProviderAdded( provider, PlaylistManager::UserPlaylist );
190         }
191 
192         connect( The::playlistManager(), &PlaylistManager::providerAdded,
193                  this, &Dock::playlistProviderAdded );
194         connect( The::playlistManager(), &PlaylistManager::providerRemoved,
195                  this, &Dock::playlistProviderRemoved );
196 
197         playlistActions->addAction( m_savePlaylistMenu );
198 
199         playlistActions->addAction( Amarok::actionCollection()->action( QStringLiteral("playlist_undo") ) );
200         //redo action can be accessed from menu > Playlist
201 
202         playlistActions->addAction( Amarok::actionCollection()->action( QStringLiteral("show_active_track") ) );
203 
204         plBar->addCollapsibleActions( playlistActions );
205 
206         NavigatorConfigAction *navigatorConfig = new NavigatorConfigAction( m_mainWidget );
207         plBar->addAction( navigatorConfig );
208 
209         QToolButton *toolButton =
210                 qobject_cast<QToolButton *>(plBar->widgetForAction( navigatorConfig ) );
211         if( toolButton )
212             toolButton->setPopupMode( QToolButton::InstantPopup );
213 
214         plBar->addAction( new KToolBarSpacerAction( m_mainWidget ) );
215 
216         // label widget
217         new PlaylistInfoWidget( m_barBox );
218     } // END Playlist Toolbar
219 
220     //set correct colors
221     paletteChanged( QApplication::palette() );
222 
223     // If it is active, clear the search filter before replacing the playlist. Fixes Bug #200709.
224     connect( The::playlistController(), &Playlist::Controller::replacingPlaylist,
225              this, &Playlist::Dock::clearFilterIfActive );
226 
227 }
228 
229 QSize
sizeHint() const230 Playlist::Dock::sizeHint() const
231 {
232     return QSize( static_cast<QWidget*>( parent() )->size().width() / 4 , 300 );
233 }
234 
235 void
paletteChanged(const QPalette & palette)236 Playlist::Dock::paletteChanged( const QPalette &palette )
237 {
238     const QString backgroundColor = palette.color( QPalette::Active, QPalette::Mid ).name();
239     const QString textColor = palette.color( QPalette::Active, QPalette::HighlightedText ).name();
240     const QString linkColor = palette.color( QPalette::Active, QPalette::Link ).name();
241     const QString ridgeColor = palette.color( QPalette::Active, QPalette::Window ).name();
242 
243     QString hintStyle( "QLabel { background-color: %1; color: %2; border-radius: 3px; } "
244                        "a { color: %3; }" );
245     hintStyle = hintStyle.arg( backgroundColor, textColor, linkColor );
246 
247     QString barStyle( "QFrame#PlaylistBarBox { border: 1px ridge %1; background-color: %2; "
248                                              " color: %3; border-radius: 3px; } "
249                       "QLabel { color: %4; }" );
250     barStyle = barStyle.arg( ridgeColor, backgroundColor, textColor, textColor );
251 
252     m_dynamicHintWidget->setStyleSheet( hintStyle );
253     if( m_barBox )
254         m_barBox->setStyleSheet( barStyle );
255 
256 }
257 
258 void
playlistProviderAdded(Playlists::PlaylistProvider * provider,int category)259 Playlist::Dock::playlistProviderAdded( Playlists::PlaylistProvider *provider, int category )
260 {
261     if( category != PlaylistManager::UserPlaylist )
262         return;
263 
264     debug() << "Adding provider: " << provider->prettyName();
265     Playlists::UserPlaylistProvider *userProvider =
266             dynamic_cast<Playlists::UserPlaylistProvider *>(provider);
267     if( userProvider == 0 )
268         return;
269     QAction *action = new QAction( userProvider->icon(),
270                                    i18n("&Save playlist to \"%1\"", provider->prettyName() ),
271                                    this );
272     action->setData( QVariant::fromValue( QPointer<Playlists::UserPlaylistProvider>( userProvider ) ) );
273     m_saveActions->addAction( QString::number( (qlonglong) userProvider ), action );
274 
275     // insert the playlist provider actions before "export"
276     QAction* exportAction = Amarok::actionCollection()->action( QStringLiteral("playlist_export") );
277     m_savePlaylistMenu->insertAction( exportAction, action );
278     connect( action, &QAction::triggered, this, &Playlist::Dock::slotSaveCurrentPlaylist );
279 }
280 
281 void
playlistProviderRemoved(Playlists::PlaylistProvider * provider,int category)282 Playlist::Dock::playlistProviderRemoved( Playlists::PlaylistProvider *provider, int category )
283 {
284     if( category != PlaylistManager::UserPlaylist )
285         return;
286 
287     QAction *action = m_saveActions->action( QString::number( (qlonglong) provider ) );
288     if( action )
289         m_savePlaylistMenu->removeAction( action );
290     else
291         warning() << __PRETTY_FUNCTION__ << ": no save action for provider" << provider->prettyName();
292 }
293 
294 void
slotSaveCurrentPlaylist()295 Playlist::Dock::slotSaveCurrentPlaylist()
296 {
297     DEBUG_BLOCK
298 
299     QAction *action = qobject_cast<QAction *>( QObject::sender() );
300     if( action == 0 )
301         return;
302 
303     QWeakPointer<Playlists::UserPlaylistProvider> pointer =
304             action->data().value< QWeakPointer<Playlists::UserPlaylistProvider> >();
305     Playlists::UserPlaylistProvider* provider = pointer.data();
306 
307     const Meta::TrackList tracks = The::playlist()->tracks();
308     The::playlistManager()->save( tracks, Amarok::generatePlaylistName( tracks ), provider );
309 }
310 
311 void
slotEditQueue()312 Playlist::Dock::slotEditQueue()
313 {
314     if( m_playlistQueueEditor ) {
315         m_playlistQueueEditor->raise();
316         return;
317     }
318     m_playlistQueueEditor = new PlaylistQueueEditor;
319     m_playlistQueueEditor->setAttribute( Qt::WA_DeleteOnClose );
320     m_playlistQueueEditor->show();
321 }
322 
323 void
showActiveTrack()324 Playlist::Dock::showActiveTrack()
325 {
326     ensurePolish();
327     m_playlistView->scrollToActiveTrack();
328 }
329 
330 void
editTrackInfo()331 Playlist::Dock::editTrackInfo()
332 {
333     m_playlistView->editTrackInformation();
334 }
335 
336 void
showDynamicHint()337 Playlist::Dock::showDynamicHint() // slot
338 {
339     DEBUG_BLOCK
340 
341     if( AmarokConfig::dynamicMode() )
342         m_dynamicHintWidget->show();
343     else
344         m_dynamicHintWidget->hide();
345 }
346 
347 void
clearFilterIfActive()348 Playlist::Dock::clearFilterIfActive() // slot
349 {
350     DEBUG_BLOCK
351     KConfigGroup config = Amarok::config( QStringLiteral("Playlist Search") );
352     bool filterActive = config.readEntry( "ShowOnlyMatches", true );
353 
354     if( filterActive )
355         m_searchWidget->slotFilterClear();
356 }
357 
358 void
slotDynamicHintLinkActivated(const QString & href)359 Playlist::Dock::slotDynamicHintLinkActivated( const QString &href )
360 {
361     if( href == s_dynMode )
362         AmarokUrl( QStringLiteral("amarok://navigate/playlists/dynamic category") ).run();
363     else if( href == s_repopulate )
364         The::playlistActions()->repopulateDynamicPlaylist();
365     else if( href == s_turnOff )
366         The::playlistActions()->enableDynamicMode( false );
367 }
368