--- a/src/container.h +++ b/src/container.h @@ -89,7 +89,7 @@ struct : Ui::MainWindow { class ScrobbleLabel* scrobbleLabel; - class RestStateWidget* restStateWidget; + class SearchWidget* searchWidget; class MetaDataWidget* metaDataWidget; class SideBarTree* sidebar; --- /dev/null +++ b/src/search_win.ui @@ -0,0 +1,437 @@ + + SearchWidget + + + + 0 + 0 + 561 + 623 + + + + Form + + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 16 + 22 + + + + + + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 22 + 16 + + + + + + + + 0 + + + + + + + + 5 + + + + + + + + + 0 + 0 + + + + + artist + + + + + tag + + + + + + + + Search + + + true + + + + + + + Play + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + 5 + + + + + + 0 + 0 + + + + Show + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + + 0 + 0 + + + + 1 + + + 100 + + + 1 + + + 50 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 2 + 5 + + + + + + + + + 0 + 0 + + + + 50 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + percent of matches + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + 5 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 16 + 6 + + + + + + + + + 0 + 0 + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 22 + 16 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 16 + 22 + + + + + + + + + DragLabel + QLabel +
draglabel.h
+
+ + SpinnerLabel + QLabel +
SpinnerLabel.h
+
+
+ + + + matchPercentSlider + sliderMoved(int) + matchPercentLabel + setNum(int) + + + 274 + 87 + + + 405 + 81 + + + + + matchPercentSlider + valueChanged(int) + matchPercentLabel + setNum(int) + + + 262 + 87 + + + 405 + 81 + + + + +
--- /dev/null +++ b/src/SearchWidget.cpp @@ -0,0 +1,480 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2007 by * + * Christian Muehlhaeuser, Last.fm Ltd * + * Erik Jaelevik, Last.fm Ltd * + * Copyright (C) 2007 by * + * John Stamp * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include +#include +#include "logger.h" +#include "Radio.h" +#include "SearchWidget.h" +#include "watermarkwidget.h" +#include "WebService.h" +#include "WebService/Request.h" +#include "SpinnerLabel.h" + + +// These are pixel sizes +#ifdef Q_WS_MAC +static const int k_fontMin = 13; +#else +static const int k_fontMin = 11; +#endif +static const int k_fontMax = 24; +static const int k_lineHeight = 29; + +static const QColor k_tagFontColour = QColor( 0x6d, 0x83, 0xa2 ); + + +SearchWidget::SearchWidget() : + m_type( 0 ), // set artist search as default, despite presenting the toptags + m_currentCloudType( 1 ), // tags in cloud + m_trialStatus( 0 ), + m_topTags( false ) +{ + Q_DEBUG_BLOCK; + + /* + On the Mac, we need to put a spacer above the scroll area to reduce the + ugliness of the scrollbar not going all the way up to the line. On + Windows we just set the spacer to 0. + */ + + // The widget inside the scroll area + WatermarkWidget* searchExtWidget = new WatermarkWidget( this ); + searchExtWidget->setWatermark( MooseUtils::dataPath( "watermark.png" ) ); + + ui.setupUi( searchExtWidget ); + + requestTopTags(); + + QPalette palette( QTabWidget().palette() ); + palette.setColor( QPalette::Base, palette.window().color() ); + + // Tag cloud + ui.resultBrowser->setPalette( palette ); + ui.resultBrowser->setItems( QStringList() ); + ui.resultBrowser->setUniformLineHeight( k_lineHeight ); + ui.resultBrowser->setItemsSelectable( true ); + ui.resultBrowser->setAlignment( Qt::AlignLeft | Qt::AlignBottom ); + ui.resultBrowser->setJustified( true ); + + // Top search widgets + ui.label3->hide(); + ui.searchType->setCurrentIndex( m_type ); + ui.searchType->setEnabled( false ); + ui.searchButton->setEnabled( false ); + ui.playButton->setEnabled( false ); + + ui.searchEdit->installEventFilter( this ); + + #ifdef Q_WS_MAC + // Hack some minsizes for the buttons as Qt is being gay on the Mac + ui.searchButton->setFixedWidth( ui.searchButton->sizeHint().width() + 0 ); + ui.playButton->setFixedWidth( ui.playButton->sizeHint().width() + 0 ); + #endif + + // Scroll area + m_scrollArea = new QScrollArea( this ); + + QPalette p = m_scrollArea->palette(); + p.setColor( QPalette::Window, Qt::white ); + m_scrollArea->setPalette( p ); + m_scrollArea->setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded ); + m_scrollArea->setVerticalScrollBarPolicy( Qt::ScrollBarAsNeeded ); + m_scrollArea->setFrameStyle( QFrame::NoFrame ); + m_scrollArea->setWidgetResizable( true ); + m_scrollArea->setWidget( searchExtWidget ); + + // Create a vertical layout onto which we'll add a spacer (for Mac) and the + // scroll area. + QVBoxLayout* vbl = new QVBoxLayout( this ); + vbl->setMargin( 0 ); + vbl->setSpacing( 0 ); + #ifdef Q_WS_MAC + // Add space to the spacer above the scroll area and remove the same + // amount from the one below. + QSpacerItem* macSpace = new QSpacerItem( 1, 30, QSizePolicy::Fixed, QSizePolicy::Minimum ); + vbl->addItem( macSpace ); +// ui.spacerItem->changeSize( 1, 0, QSizePolicy::Fixed, QSizePolicy::Minimum ); + #endif + vbl->addWidget( m_scrollArea ); + + connect( ui.searchEdit, SIGNAL(returnPressed()), ui.searchButton, SLOT(animateClick()) ); + connect( ui.searchEdit, SIGNAL(textChanged( QString )), SLOT(searchFieldChanged()) ); + connect( ui.searchButton, SIGNAL(clicked()), SLOT(search()) ); + connect( ui.playButton, SIGNAL(clicked() ), SLOT(play()) ); + connect( ui.resultBrowser, SIGNAL(clicked( int )), SLOT(itemClicked( int )) ); + connect( ui.matchPercentSlider, SIGNAL(valueChanged( int )), SLOT(matchingTags()) ); + connect( The::webService(), SIGNAL( handshakeResult( Handshake* ) ), SLOT( onHandshaken( Handshake* ) ) ); +} + + +void +SearchWidget::onHandshaken( Handshake* handshake ) +{ + m_trialStatus = handshake->freeTrial(); + setFreeTrialStatus( m_trialStatus ); +} + + +void +SearchWidget::setFreeTrialStatus( int status ) +{ + ui.label3->setEnabled( true ); + + if (status == 2) + { + ui.searchType->setEnabled( false ); + ui.searchButton->setEnabled( false ); + ui.searchEdit->setEnabled( false ); + ui.playButton->setEnabled( false ); + ui.matchPercentSlider->setEnabled( false ); + ui.resultBrowser->setEnabled( false ); + ui.label3->setText( tr( "

Did you enjoy it?

Your free trial has expired. Subscribe to keep
listening to non-stop, personalised radio!" ) ); + ui.label3->show(); + } + else + { + if (status == 1) + { + ui.label3->setText( tr("Try out Last.fm radio with your free trial.") ); + ui.label3->show(); + } + else + ui.label3->hide(); + ui.searchType->setEnabled( true ); + ui.searchButton->setEnabled( true ); + ui.searchEdit->setEnabled( true ); + ui.playButton->setEnabled( true ); + ui.matchPercentSlider->setEnabled( true ); + ui.resultBrowser->setEnabled( true ); + } +} + + +bool +SearchWidget::eventFilter( QObject* o, QEvent* e ) +{ + if (o == ui.searchEdit) + { + switch (e->type()) + { + case QEvent::FocusOut: + case QEvent::FocusIn: + ui.searchEdit->update(); + break; + + case QEvent::Paint: + if (!ui.searchEdit->hasFocus() && ui.searchEdit->text().isEmpty()) + { + ui.searchEdit->event( e ); + + QRect r = ui.searchEdit->rect().adjusted( 5, 2, -5, 0 ); + QPainter p( ui.searchEdit ); + p.setPen( Qt::gray ); + p.setFont( ui.searchEdit->font() ); + p.drawText( r, Qt::AlignVCenter, tr( "Find a station by" ) ); + ui.searchEdit->setMinimumWidth( p.fontMetrics().width( tr( "Find a station by" ) ) + 12 ); + + return true; //eat event + } + break; + + default: + break; + } + } + + return QWidget::eventFilter( o, e ); +} + +void +SearchWidget::requestTopTags() +{ + ui.spinnerLabel->setVisible( true ); + + TopTagsRequest *tags = new TopTagsRequest; + connect( tags, SIGNAL(result( Request* )), SLOT(searchResults( Request* )) ); + tags->start(); + + ui.statusLabel->setText( tr( "Generating popular tags..." ) ); +} + +void +SearchWidget::searchFieldChanged() +{ + ui.playButton->setEnabled( !ui.searchEdit->text().isEmpty() ); +} + +void +SearchWidget::search() +{ + if ( m_type != ui.searchType->currentIndex() ) + { + // search mode changed, wipe selections + clearSelection(); + ui.resultBrowser->clear(); + m_type = ui.searchType->currentIndex(); + } + + const QString searchText = ui.searchEdit->text().trimmed(); + if ( searchText.isEmpty() ) + { + requestTopTags(); + return; + } + + if ( searchText.startsWith( "lastfm://" ) ) + { + play(); + return; + } + + ui.searchButton->setEnabled( false ); + ui.searchType->setEnabled( false ); + ui.resultBrowser->setItemsSelectable( false ); + ui.spinnerLabel->setVisible( true ); + + Request *request; + switch( m_type ) + { + case 0: + request = new SimilarArtistsRequest( searchText ); + + ui.statusLabel->setText( tr( "Generating similar artists..." ) ); + ui.resultBrowser->setItemType( UnicornEnums::ItemArtist ); + break; + + case 1: + request = new SimilarTagsRequest( searchText ); + + ui.statusLabel->setText( tr( "Generating similar tags..." ) ); + ui.resultBrowser->setItemType( UnicornEnums::ItemTag ); + break; + } + + connect( request, SIGNAL(result( Request* )), SLOT(searchResults( Request* )) ); + request->start(); + + m_searching = true; +} + +int +SearchWidget::matchingTags() +{ + int total = int( ui.matchPercentSlider->value() * m_currentItems.size() / 100.0f + 0.5f ); + + // OK this cheats a bit. In case a search only returns one item: itself, + // we want to make sure that it's still there when the slider is below 50%. + if ( total == 0 && m_currentItems.size() > 0 ) + total = 1; + + if ( !m_topTags ) + { + m_currentCloudType = m_type; + } + + WeightedStringList alpha_sorted = m_currentItems.mid( 0, total ); + + // They are still sorted by weight when they get here, + // so element 0 will have the biggest weighting + int const max = alpha_sorted.isEmpty() ? 0 : alpha_sorted.first().weighting(); + int const min = alpha_sorted.size() < 2 ? 0 : alpha_sorted.last().weighting(); + + // this will sort them alphabetically + qSort( alpha_sorted.begin(), alpha_sorted.end() ); + + ui.resultBrowser->clear(); + for ( int i = 0; i < alpha_sorted.count(); i++ ) + { + QString name = alpha_sorted[i]; + ui.resultBrowser->append( name ); + + QString tooltipText = QString::number( alpha_sorted[i].weighting() ); + if ( m_type == 0 && !m_topTags ) + tooltipText = tr( "%1% similarity").arg( tooltipText ); + else + tooltipText = tr( "%1 matches" ).arg( tooltipText ); + ui.resultBrowser->setItemTooltip( i, tooltipText ); + + int w = alpha_sorted[i].weighting(); + // float normalised_w = (float) (w - min) / qMax( 1, max - min ); + // Keep the font sizes consistent as the match percentage changes + float normalised_w = (float) (w) / qMax( 100, max ); + int size = int(normalised_w * (k_fontMax - k_fontMin) + k_fontMin + 0.5f); + + QFont f( "Arial" ); + f.setBold( true ); + + // we qBound because we don't check that maxWeight is valid, and it is + // sometimes the case that it in fact isn't, thus we get a crazy font sizes + // tag cloud! :) --mxcl + f.setPixelSize( qBound( k_fontMin, size, k_fontMax ) ); + + ui.resultBrowser->setItemFont( i, f ); + ui.resultBrowser->setItemColor( i, k_tagFontColour ); + + // why on earth use a QHash? --mxcl + QHash data; + data.insert( "artist", name ); // eh? + ui.resultBrowser->setItemData( i, data ); + } + return alpha_sorted.count(); +} + + +void +SearchWidget::searchResults( Request *r ) +{ + Q_DEBUG_BLOCK; + + QString searchToken; + m_topTags = false; + + switch (r->type()) { + case TypeSimilarArtists: + m_currentItems = static_cast(r)->artists(); + searchToken = static_cast(r)->artist(); + break; + case TypeTopTags: + m_topTags = true; + case TypeSimilarTags: + m_currentItems = static_cast(r)->tags(); + break; + + default: + qWarning() << "Unhandled case" << __PRETTY_FUNCTION__; + } + +////// + + int sortCount = matchingTags(); + + ui.searchButton->setEnabled( true ); + ui.searchType->setEnabled( true ); + ui.resultBrowser->setItemsSelectable( true ); + ui.statusLabel->clear(); + ui.spinnerLabel->setVisible( false ); + + if ( m_currentItems.count() ) + { + switch ( m_currentCloudType ) + { + case 0: + ui.resultBrowser->setJustified( false ); + break; + + case 1: + ui.resultBrowser->setJustified( true ); + break; + } + } + else + { + ui.playButton->setEnabled( false ); + + ui.statusLabel->setText( tr( "Sorry, your search didn't return any results." ) ); + } + + if ( ui.searchEdit->text().trimmed().isEmpty() || sortCount == 0 ) + { + ui.searchButton->setDefault( true ); + ui.playButton->setDefault( false ); + ui.searchButton->setFocus(); + + if ( sortCount == 0 ) + ui.searchEdit->setFocus(); + } + else + { + ui.searchButton->setDefault( false ); + ui.playButton->setDefault( true ); + ui.playButton->setFocus(); + } + + m_searching = false; + setFreeTrialStatus( m_trialStatus ); +} + + +StationUrl +SearchWidget::stationUrl() +{ + QString url; + + if ( ui.searchEdit->text().startsWith( "lastfm://" ) ) + url = QString( "%1" ).arg( ui.searchEdit->text() ); + + else if ( ui.searchEdit->text().startsWith ( "http://" ) ) + url = ""; + + else if ( m_type == 0 ) + { + QString artist = ui.searchEdit->text(); + artist = artist.trimmed(); + url = QString( "lastfm://artist/%1/similarartists" ).arg( UnicornUtils::urlEncodeItem( artist ) ); + } + else + { + QString tag = ui.searchEdit->text(); + tag = tag.trimmed(); + url = QString( "lastfm://globaltags/%1" ).arg( UnicornUtils::urlEncodeItem( tag ) ); + } + + return StationUrl( url ); +} + + +void +SearchWidget::itemClicked( int index ) +{ + if (m_searching) + return; + + QString item = ui.resultBrowser->items().at( index ); + + if ( m_currentCloudType != ui.searchType->currentIndex() ) + { + ui.searchType->setCurrentIndex( m_currentCloudType ); + } + + ui.searchEdit->setText( item ); + search(); +} + + +void +SearchWidget::play() +{ + ui.searchButton->setDefault( true ); + ui.playButton->setDefault( false ); + ui.searchButton->setFocus(); + + The::radio().playStation( stationUrl() ); +} + + +void +SearchWidget::clearSelection() +{ + ui.resultBrowser->clearSelections(); +} --- /dev/null +++ b/src/SearchWidget.h @@ -0,0 +1,79 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2007 by * + * Christian Muehlhaeuser, Last.fm Ltd * + * Erik Jaelevik, Last.fm Ltd * + * Copyright (C) 2007 by * + * John Stamp * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef SEARCH_EXTENSION_H +#define SEARCH_EXTENSION_H + +#include "StationUrl.h" + +#include +#include + +#ifdef Q_WS_MAC +# include "ui_search_mac.h" +#else +# include "ui_search_win.h" +#endif + +#include "WeightedStringList.h" + + +class SearchWidget : public QWidget +{ + Q_OBJECT + + public: + SearchWidget(); + void setFreeTrialStatus( int status ); + + public slots: + void clearSelection(); + + private slots: + void search(); + void play(); + void searchFieldChanged(); + void requestTopTags(); + void searchResults( class Request* ); + void itemClicked( int index ); + int matchingTags(); + void onHandshaken( class Handshake* handshake ); + + private: + StationUrl stationUrl(); + virtual bool eventFilter( QObject* watched, QEvent* event ); + + private: + Ui::SearchWidget ui; + QScrollArea* m_scrollArea; + + int m_type; + int m_currentCloudType; + int m_trialStatus; + WeightedStringList m_currentItems; + + bool m_topTags; + bool m_searching; +}; + +#endif --- a/src/MetaDataWidget.cpp +++ b/src/MetaDataWidget.cpp @@ -488,6 +488,8 @@ void MetaDataWidget::displayNotListening() { + m_tuning_in_timer->stop(); + ui_notPlaying.spinnerLabel->setVisible( false ); ui_notPlaying.messageLabel->setText( tr( "Start listening in your media player\nor tune in to free radio" ) ); --- a/src/src.pro +++ b/src/src.pro @@ -58,10 +58,10 @@ playcontrols.ui \ failedlogindialog.ui \ tagdialog.ui \ + search_win.ui \ ShareDialog.ui \ MetaDataWidget.ui \ MetaDataWidgetTuningIn.ui \ - RestStateWidget.ui \ DiagnosticsDialog.ui \ BootstrapSelectorWidget.ui \ majorupdate.ui @@ -124,8 +124,8 @@ TrackProgressFrame.h \ DiagnosticsDialog.h \ User.h \ - RestStateWidget.h \ RestStateMessage.h \ + SearchWidget.h \ SpinnerLabel.h \ ProxyOutput.h \ Bootstrapper/AbstractBootstrapper.h \ @@ -188,7 +188,7 @@ MetaDataWidget.cpp \ TagListWidget.cpp \ TrackProgressFrame.cpp \ - RestStateWidget.cpp \ + SearchWidget.cpp \ DiagnosticsDialog.cpp \ User.cpp \ RestStateMessage.cpp \ --- a/src/container.cpp +++ b/src/container.cpp @@ -52,7 +52,7 @@ #include "SideBarView.h" #include "systray.h" #include "tagdialog.h" -#include "RestStateWidget.h" +#include "SearchWidget.h" #include "updatewizard.h" #include "User.h" #include "toolbarvolumeslider.h" @@ -142,7 +142,7 @@ centralWidget()->setPalette( p ); ////// Main Widgets - ui.restStateWidget = new RestStateWidget( this ); + ui.searchWidget = new SearchWidget; ui.metaDataWidget = new MetaDataWidget( this ); ////// SideBar @@ -155,7 +155,7 @@ ////// ui.stack ui.stack->setBackgroundRole( QPalette::Base ); - ui.stack->addWidget( ui.restStateWidget ); + ui.stack->addWidget( ui.searchWidget ); ui.stack->addWidget( ui.metaDataWidget ); #ifdef HIDE_RADIO @@ -171,13 +171,13 @@ ui.actionVolumeDown->setVisible( false ); ui.actionMute->setVisible( false ); - ui.stack->removeWidget( ui.restStateWidget ); + ui.stack->removeWidget( ui.searchWidget ); #endif if ( qApp->arguments().contains( "--debug" ) ) ui.menuHelp->addAction( "kr4sh pls, kthxbai", this, SLOT( crash() ) ); - ui.restStateWidget->setFocus(); + ui.searchWidget->setFocus(); } @@ -850,7 +850,7 @@ case Radio_FreeTrialExpired: { - ui.restStateWidget->setFreeTrialStatus( 2 ); + ui.searchWidget->setFreeTrialStatus( 2 ); int const r = LastMessageBox::information( tr("Did you enjoy it?"), @@ -910,7 +910,6 @@ Container::getPlugin() { ConfigWizard( this, ConfigWizard::Plugin ).exec(); - ui.restStateWidget->updatePlayerNames(); } @@ -1722,8 +1721,7 @@ m_sidebarEnabled = !The::user().settings().sidebarEnabled(); toggleSidebar(); - ui.restStateWidget->clear(); - ui.restStateWidget->updatePlayerNames(); + ui.searchWidget->clearSelection(); // this call is redundant. Settings's userSettingsChanged will be emitted when switching the user! // updateUserStuff( The::user().settings() ); @@ -1736,7 +1734,6 @@ case Event::UserHandshaken: { ui.actionToggleDiscoveryMode->setEnabled( The::user().isSubscriber() ); - ui.restStateWidget->setPlayEnabled( true ); #ifndef HIDE_RADIO statusBar()->showMessage( tr( "Radio service initialised" ) ); @@ -1757,7 +1754,7 @@ showMetaDataWidget(); ui.metaDataWidget->displayTuningIn(); - ui.restStateWidget->clear(); + ui.searchWidget->clearSelection(); if ( The::radio().stationUrl().isPlaylist() ) ui.stationTimeBar->setText( tr( "Connecting to playlist..." ) ); --- a/src/libUnicorn/WebService/SimilarArtistsRequest.cpp +++ b/src/libUnicorn/WebService/SimilarArtistsRequest.cpp @@ -56,7 +56,7 @@ QDomNode match = n.namedItem( "match" ); QDomNode image = n.namedItem( "image_small" ); - m_artists += WeightedString( item.toElement().text(), match.toElement().text().toInt() ); + m_artists += WeightedString( item.toElement().text(), int( match.toElement().text().toDouble() * 100 + 0.5f ) ); m_images += image.toElement().text(); } }