+#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();
}
}