From 4777b3eab1e66c2e1c628f4187fc90bd41ac73f1 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Mon, 18 Jan 2010 02:23:55 +0000 Subject: [PATCH] SomaFM streams --- TODO | 3 + src/lastfmservice.cpp | 9 +- src/lastfmservice.h | 1 + src/mainwindow.cpp | 4 +- src/multiloadingindicator.cpp | 3 + src/playlist.cpp | 2 +- src/playlistview.cpp | 27 +----- src/playlistview.h | 8 -- src/radioitem.cpp | 4 + src/radioitem.h | 2 + src/radioloadingindicator.ui | 56 ------------ src/radiomodel.cpp | 6 +- src/radiomodel.h | 4 +- src/radioplaylistitem.cpp | 10 ++- src/radioplaylistitem.h | 4 +- src/radioservice.cpp | 16 ++++ src/radioservice.h | 12 +-- src/somafmservice.cpp | 164 ++++++++++++++++++++++++++++++++++ src/somafmservice.h | 48 ++++++++++ src/src.pro | 28 ++++-- src/trackslider.ui | 3 + 21 files changed, 296 insertions(+), 118 deletions(-) delete mode 100644 src/radioloadingindicator.ui create mode 100644 src/somafmservice.cpp create mode 100644 src/somafmservice.h diff --git a/TODO b/TODO index 703ef3221..3b7507d15 100644 --- a/TODO +++ b/TODO @@ -10,12 +10,15 @@ - osd - Save and load playlists from files - Shuffle and repeat playlist +- Autocompletion from library when editing tags - Edit tags in playlist view - Disabled fields in tag editor Long-term: - iPod +- Automatic tagging from musicbrainz +- Album cover fetching from last.fm Windows: - Playlist delegates diff --git a/src/lastfmservice.cpp b/src/lastfmservice.cpp index 35058a96c..ff4f96084 100644 --- a/src/lastfmservice.cpp +++ b/src/lastfmservice.cpp @@ -14,6 +14,7 @@ const char* LastFMService::kServiceName = "Last.fm"; const char* LastFMService::kSettingsGroup = "Last.fm"; +const char* LastFMService::kLoadingText = "Loading Last.fm radio"; const char* LastFMService::kAudioscrobblerClientId = "tng"; const char* LastFMService::kApiKey = "75d20fb472be99275392aefa2760ea09"; const char* LastFMService::kSecret = "d3072b60ae626be12be69448f5c46e70"; @@ -80,7 +81,7 @@ void LastFMService::ScrobblingEnabledChangedSlot(bool value) { } RadioItem* LastFMService::CreateRootItem(RadioItem* parent) { - RadioItem* item = new RadioItem(this, RadioItem::Type_Service, "Last.fm", parent); + RadioItem* item = new RadioItem(this, RadioItem::Type_Service, kServiceName, parent); item->icon = QIcon(":last.fm/as.png"); return item; } @@ -252,7 +253,7 @@ void LastFMService::StartLoading(const QUrl& url) { if (!IsAuthenticated()) return; - emit LoadingStarted(); + emit TaskStarted(kLoadingText); delete tuner_; @@ -284,7 +285,7 @@ void LastFMService::TunerError(lastfm::ws::Error error) { if (!initial_tune_) return; - emit LoadingFinished(); + emit TaskFinished(kLoadingText); if (error == lastfm::ws::NotEnoughContent) { emit StreamFinished(); @@ -325,7 +326,7 @@ QString LastFMService::ErrorString(lastfm::ws::Error error) const { void LastFMService::TunerTrackAvailable() { if (initial_tune_) { - emit LoadingFinished(); + emit TaskFinished(kLoadingText); LoadNext(last_url_); initial_tune_ = false; diff --git a/src/lastfmservice.h b/src/lastfmservice.h index 36f1b4282..e29453f9a 100644 --- a/src/lastfmservice.h +++ b/src/lastfmservice.h @@ -21,6 +21,7 @@ class LastFMService : public RadioService { static const char* kServiceName; static const char* kSettingsGroup; + static const char* kLoadingText; static const char* kAudioscrobblerClientId; static const char* kApiKey; static const char* kSecret; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f8ff41f3b..43c91e372 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -183,8 +183,8 @@ MainWindow::MainWindow(QWidget *parent) playlist_menu_->addAction(ui_.action_clear_playlist); // Radio connections - connect(radio_model_, SIGNAL(LoadingStarted()), ui_.playlist, SLOT(StartRadioLoading())); - connect(radio_model_, SIGNAL(LoadingFinished()), ui_.playlist, SLOT(StopRadioLoading())); + connect(radio_model_, SIGNAL(TaskStarted(QString)), multi_loading_indicator_, SLOT(TaskStarted(QString))); + connect(radio_model_, SIGNAL(TaskFinished(QString)), multi_loading_indicator_, SLOT(TaskFinished(QString))); connect(radio_model_, SIGNAL(StreamError(QString)), SLOT(ReportError(QString))); connect(radio_model_, SIGNAL(StreamFinished()), player_, SLOT(Next())); connect(radio_model_, SIGNAL(StreamReady(QUrl,QUrl)), player_, SLOT(StreamReady(QUrl,QUrl))); diff --git a/src/multiloadingindicator.cpp b/src/multiloadingindicator.cpp index f1b0f508a..bb4fdf072 100644 --- a/src/multiloadingindicator.cpp +++ b/src/multiloadingindicator.cpp @@ -7,6 +7,9 @@ MultiLoadingIndicator::MultiLoadingIndicator(QWidget *parent) } void MultiLoadingIndicator::TaskStarted(const QString &name) { + if (tasks_.contains(name)) + return; + tasks_ << name; UpdateText(); diff --git a/src/playlist.cpp b/src/playlist.cpp index 5437bbbcf..4fbb0bdea 100644 --- a/src/playlist.cpp +++ b/src/playlist.cpp @@ -272,7 +272,7 @@ QModelIndex Playlist::InsertRadioStations(const QList& items, int af if (!item->playable) continue; - playlist_items << new RadioPlaylistItem(item->service, item->Url(), item->Title()); + playlist_items << new RadioPlaylistItem(item->service, item->Url(), item->Title(), item->Artist()); } return InsertItems(playlist_items, after); } diff --git a/src/playlistview.cpp b/src/playlistview.cpp index 7fa99c3b4..65d80205a 100644 --- a/src/playlistview.cpp +++ b/src/playlistview.cpp @@ -1,7 +1,6 @@ #include "playlistview.h" #include "playlist.h" #include "playlistheader.h" -#include "radioloadingindicator.h" #include "trackslider.h" #include @@ -125,8 +124,7 @@ PlaylistView::PlaylistView(QWidget *parent) glow_intensity_step_(0), row_height_(-1), currenttrack_play_(":currenttrack_play.png"), - currenttrack_pause_(":currenttrack_pause.png"), - radio_loading_(new RadioLoadingIndicator(this)) + currenttrack_pause_(":currenttrack_pause.png") { setItemDelegate(new PlaylistDelegateBase(this)); setItemDelegateForColumn(Playlist::Column_Length, new LengthItemDelegate(this)); @@ -140,8 +138,6 @@ PlaylistView::PlaylistView(QWidget *parent) glow_timer_->setInterval(1500 / kGlowIntensitySteps); connect(glow_timer_, SIGNAL(timeout()), SLOT(GlowIntensityChanged())); - - radio_loading_->hide(); } void PlaylistView::setModel(QAbstractItemModel *model) { @@ -326,24 +322,3 @@ void PlaylistView::contextMenuEvent(QContextMenuEvent* e) { emit RightClicked(e->globalPos(), indexAt(e->pos())); e->accept(); } - -void PlaylistView::resizeEvent(QResizeEvent *event) { - const QPoint kPadding(5,5); - - QPoint pos(viewport()->mapTo(this, viewport()->rect().bottomRight()) - - kPadding - QPoint(radio_loading_->sizeHint().width(), - radio_loading_->sizeHint().height())); - - radio_loading_->move(pos); - radio_loading_->resize(radio_loading_->sizeHint()); - - QTreeView::resizeEvent(event); -} - -void PlaylistView::StartRadioLoading() { - radio_loading_->show(); -} - -void PlaylistView::StopRadioLoading() { - radio_loading_->hide(); -} diff --git a/src/playlistview.h b/src/playlistview.h index b27d498c5..6b185f705 100644 --- a/src/playlistview.h +++ b/src/playlistview.h @@ -37,9 +37,6 @@ class PlaylistView : public QTreeView { public: PlaylistView(QWidget* parent = 0); - // QWidget - void resizeEvent(QResizeEvent *event); - // QTreeView void setModel(QAbstractItemModel *model); void drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; @@ -52,9 +49,6 @@ class PlaylistView : public QTreeView { void StopGlowing(); void StartGlowing(); - void StartRadioLoading(); - void StopRadioLoading(); - signals: void PlayPauseItem(const QModelIndex& index); void RightClicked(const QPoint& global_pos, const QModelIndex& index); @@ -88,8 +82,6 @@ class PlaylistView : public QTreeView { QList currenttrack_bar_right_; QPixmap currenttrack_play_; QPixmap currenttrack_pause_; - - RadioLoadingIndicator* radio_loading_; }; #endif // PLAYLISTVIEW_H diff --git a/src/radioitem.cpp b/src/radioitem.cpp index 7c1309a17..397073663 100644 --- a/src/radioitem.cpp +++ b/src/radioitem.cpp @@ -22,3 +22,7 @@ QUrl RadioItem::Url() const { QString RadioItem::Title() const { return service->TitleForItem(this); } + +QString RadioItem::Artist() const { + return service->ArtistForItem(this); +} diff --git a/src/radioitem.h b/src/radioitem.h index e41368e58..8bb00b42c 100644 --- a/src/radioitem.h +++ b/src/radioitem.h @@ -21,8 +21,10 @@ class RadioItem : public SimpleTreeItem { QUrl Url() const; QString Title() const; + QString Artist() const; QIcon icon; + QString artist; RadioService* service; bool playable; }; diff --git a/src/radioloadingindicator.ui b/src/radioloadingindicator.ui deleted file mode 100644 index 67ba1b5da..000000000 --- a/src/radioloadingindicator.ui +++ /dev/null @@ -1,56 +0,0 @@ - - - RadioLoadingIndicator - - - - 0 - 0 - 191 - 24 - - - - Form - - - - 6 - - - 6 - - - 4 - - - 6 - - - 4 - - - - - - - - - 0 - 0 - - - - - - - - - BusyIndicator - QLabel -
busyindicator.h
-
-
- - -
diff --git a/src/radiomodel.cpp b/src/radiomodel.cpp index 0ba81ff2d..96083acba 100644 --- a/src/radiomodel.cpp +++ b/src/radiomodel.cpp @@ -1,6 +1,7 @@ #include "radiomodel.h" #include "radioservice.h" #include "lastfmservice.h" +#include "somafmservice.h" #include "radiomimedata.h" #include @@ -16,14 +17,15 @@ RadioModel::RadioModel(QObject* parent) root_->lazy_loaded = true; AddService(new LastFMService(this)); + AddService(new SomaFMService(this)); } void RadioModel::AddService(RadioService *service) { sServices[service->name()] = service; service->CreateRootItem(root_); - connect(service, SIGNAL(LoadingStarted()), SIGNAL(LoadingStarted())); - connect(service, SIGNAL(LoadingFinished()), SIGNAL(LoadingFinished())); + connect(service, SIGNAL(TaskStarted(QString)), SIGNAL(TaskStarted(QString))); + connect(service, SIGNAL(TaskFinished(QString)), SIGNAL(TaskFinished(QString))); connect(service, SIGNAL(StreamReady(QUrl,QUrl)), SIGNAL(StreamReady(QUrl,QUrl))); connect(service, SIGNAL(StreamFinished()), SIGNAL(StreamFinished())); connect(service, SIGNAL(StreamError(QString)), SIGNAL(StreamError(QString))); diff --git a/src/radiomodel.h b/src/radiomodel.h index cbd4fd9a0..d2ac82e4b 100644 --- a/src/radiomodel.h +++ b/src/radiomodel.h @@ -35,8 +35,8 @@ class RadioModel : public SimpleTreeModel { void ShowContextMenu(RadioItem* item, const QPoint& global_pos); signals: - void LoadingStarted(); - void LoadingFinished(); + void TaskStarted(const QString&); + void TaskFinished(const QString&); void StreamReady(const QUrl& original_url, const QUrl& media_url); void StreamFinished(); void StreamError(const QString& message); diff --git a/src/radioplaylistitem.cpp b/src/radioplaylistitem.cpp index 6fae07e25..08fd71ddb 100644 --- a/src/radioplaylistitem.cpp +++ b/src/radioplaylistitem.cpp @@ -10,10 +10,11 @@ RadioPlaylistItem::RadioPlaylistItem() } RadioPlaylistItem::RadioPlaylistItem(RadioService* service, const QUrl& url, - const QString& title) + const QString& title, const QString& artist) : service_(service), url_(url), - title_(title) + title_(title), + artist_(artist) { InitMetadata(); } @@ -22,12 +23,15 @@ void RadioPlaylistItem::Save(QSettings& settings) const { settings.setValue("service", service_->name()); settings.setValue("url", url_.toString()); settings.setValue("title", title_); + if (!artist_.isEmpty()) + settings.setValue("artist", artist_); } void RadioPlaylistItem::Restore(const QSettings& settings) { service_ = RadioModel::ServiceByName(settings.value("service").toString()); url_ = settings.value("url").toString(); title_ = settings.value("title").toString(); + artist_ = settings.value("artist").toString(); InitMetadata(); } @@ -39,6 +43,8 @@ void RadioPlaylistItem::InitMetadata() { metadata_.set_title(title_); else metadata_.set_title(url_.toString()); + + metadata_.set_artist(artist_); } Song RadioPlaylistItem::Metadata() const { diff --git a/src/radioplaylistitem.h b/src/radioplaylistitem.h index aea501300..c869880da 100644 --- a/src/radioplaylistitem.h +++ b/src/radioplaylistitem.h @@ -11,7 +11,8 @@ class RadioService; class RadioPlaylistItem : public PlaylistItem { public: RadioPlaylistItem(); - RadioPlaylistItem(RadioService* service, const QUrl& url, const QString& title); + RadioPlaylistItem(RadioService* service, const QUrl& url, + const QString& title, const QString& artist); Type type() const { return Type_Radio; } Options options() const; @@ -36,6 +37,7 @@ class RadioPlaylistItem : public PlaylistItem { RadioService* service_; QUrl url_; QString title_; + QString artist_; Song metadata_; Song temp_metadata_; diff --git a/src/radioservice.cpp b/src/radioservice.cpp index b3560725f..174410a72 100644 --- a/src/radioservice.cpp +++ b/src/radioservice.cpp @@ -5,3 +5,19 @@ RadioService::RadioService(const QString& name, QObject *parent) name_(name) { } + +QUrl RadioService::UrlForItem(const RadioItem* item) const { + return item->key; +} + +QString RadioService::TitleForItem(const RadioItem* item) const { + return item->DisplayText(); +} + +QString RadioService::ArtistForItem(const RadioItem* item) const { + return item->artist; +} + +void RadioService::LoadNext(const QUrl&) { + emit StreamFinished(); +} diff --git a/src/radioservice.h b/src/radioservice.h index 864228dbb..57bd4d0dc 100644 --- a/src/radioservice.h +++ b/src/radioservice.h @@ -21,21 +21,23 @@ class RadioService : public QObject { virtual RadioItem* CreateRootItem(RadioItem* parent) = 0; virtual void LazyPopulate(RadioItem* item) = 0; - virtual QUrl UrlForItem(const RadioItem* item) const = 0; - virtual QString TitleForItem(const RadioItem* item) const = 0; + virtual QUrl UrlForItem(const RadioItem* item) const; + virtual QString TitleForItem(const RadioItem* item) const; + virtual QString ArtistForItem(const RadioItem* item) const; virtual void ShowContextMenu(RadioItem* item, const QPoint& global_pos) { Q_UNUSED(item); Q_UNUSED(global_pos); } virtual void StartLoading(const QUrl& url) = 0; - virtual void LoadNext(const QUrl& url) = 0; + virtual void LoadNext(const QUrl& url); virtual bool IsPauseAllowed() const { return true; } virtual bool ShowLastFmControls() const { return false; } signals: - void LoadingStarted(); - void LoadingFinished(); + void TaskStarted(const QString& name); + void TaskFinished(const QString& name); + void StreamReady(const QUrl& original_url, const QUrl& media_url); void StreamFinished(); void StreamError(const QString& message); diff --git a/src/somafmservice.cpp b/src/somafmservice.cpp new file mode 100644 index 000000000..68f39cc23 --- /dev/null +++ b/src/somafmservice.cpp @@ -0,0 +1,164 @@ +#include "somafmservice.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +const char* SomaFMService::kServiceName = "SomaFM"; +const char* SomaFMService::kLoadingChannelsText = "Getting channels"; +const char* SomaFMService::kLoadingStreamText = "Loading stream"; +const char* SomaFMService::kChannelListUrl = "http://somafm.com/channels.xml"; + +SomaFMService::SomaFMService(QObject* parent) + : RadioService(kServiceName, parent), + root_(NULL), + network_(new QNetworkAccessManager(this)) +{ +} + +RadioItem* SomaFMService::CreateRootItem(RadioItem* parent) { + root_ = new RadioItem(this, RadioItem::Type_Service, kServiceName, parent); + return root_; +} + +void SomaFMService::LazyPopulate(RadioItem* item) { + switch (item->type) { + case RadioItem::Type_Service: + RefreshChannels(); + break; + + default: + break; + } + + item->lazy_loaded = true; +} + +void SomaFMService::ShowContextMenu(RadioItem* item, const QPoint& global_pos) { + +} + +void SomaFMService::StartLoading(const QUrl& url) { + // Load the playlist + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader("User-Agent", QString("%1 %2").arg( + QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8()); + + QNetworkReply* reply = network_->get(request); + connect(reply, SIGNAL(finished()), SLOT(LoadPlaylistFinished())); + + emit TaskStarted(kLoadingStreamText); +} + +void SomaFMService::LoadPlaylistFinished() { + QNetworkReply* reply = qobject_cast(sender()); + emit TaskFinished(kLoadingStreamText); + + if (reply->error() != QNetworkReply::NoError) { + // TODO: Error handling + qDebug() << reply->errorString(); + return; + } + + // TODO: Replace with some more robust .pls parsing :( + QTemporaryFile temp_file; + temp_file.open(); + temp_file.write(reply->readAll()); + temp_file.flush(); + + QSettings s(temp_file.fileName(), QSettings::IniFormat); + s.beginGroup("playlist"); + + emit StreamReady(reply->url().toString(), s.value("File1").toString()); +} + +void SomaFMService::RefreshChannels() { + QNetworkRequest request = QNetworkRequest(QUrl(kChannelListUrl)); + request.setRawHeader("User-Agent", QString("%1 %2").arg( + QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8()); + + QNetworkReply* reply = network_->get(request); + connect(reply, SIGNAL(finished()), SLOT(RefreshChannelsFinished())); + + emit TaskStarted(kLoadingChannelsText); +} + +void SomaFMService::RefreshChannelsFinished() { + QNetworkReply* reply = qobject_cast(sender()); + emit TaskFinished(kLoadingChannelsText); + + if (reply->error() != QNetworkReply::NoError) { + // TODO: Error handling + qDebug() << reply->errorString(); + return; + } + + QXmlStreamReader reader(reply); + while (!reader.atEnd()) { + reader.readNext(); + + if (reader.tokenType() == QXmlStreamReader::StartElement && + reader.name() == "channel") { + ReadChannel(reader); + } + } +} + +void SomaFMService::ReadChannel(QXmlStreamReader& reader) { + RadioItem* item = new RadioItem(this, Type_Stream, QString::null); + item->lazy_loaded = true; + item->playable = true; + + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::EndElement: + if (item->key.isNull()) { + // Didn't find a URL + delete item; + } else { + item->InsertNotify(root_); + } + return; + + case QXmlStreamReader::StartElement: + if (reader.name() == "title") { + item->display_text = reader.readElementText(); + } else if (reader.name() == "dj") { + item->artist = reader.readElementText(); + } else if (reader.name() == "fastpls" && reader.attributes().value("format") == "mp3") { + item->key = reader.readElementText(); + } else { + ConsumeElement(reader); + } + break; + + default: + break; + } + } + + delete item; +} + +void SomaFMService::ConsumeElement(QXmlStreamReader& reader) { + int level = 1; + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: level++; break; + case QXmlStreamReader::EndElement: level--; break; + default: break; + } + + if (level == 0) + return; + } +} + +QString SomaFMService::TitleForItem(const RadioItem* item) const { + return "SomaFM " + item->display_text; +} diff --git a/src/somafmservice.h b/src/somafmservice.h new file mode 100644 index 000000000..2ffa425d0 --- /dev/null +++ b/src/somafmservice.h @@ -0,0 +1,48 @@ +#ifndef SOMAFMSERVICE_H +#define SOMAFMSERVICE_H + +#include "radioservice.h" + +class QNetworkAccessManager; +class QXmlStreamReader; + +class SomaFMService : public RadioService { + Q_OBJECT + + public: + SomaFMService(QObject* parent = 0); + + enum ItemType { + Type_Stream = 2000, + }; + + static const char* kServiceName; + static const char* kLoadingChannelsText; + static const char* kLoadingStreamText; + static const char* kChannelListUrl; + + RadioItem* CreateRootItem(RadioItem* parent); + void LazyPopulate(RadioItem* item); + + QString TitleForItem(const RadioItem* item) const; + + void ShowContextMenu(RadioItem* item, const QPoint& global_pos); + + void StartLoading(const QUrl& url); + + private slots: + void RefreshChannelsFinished(); + void LoadPlaylistFinished(); + + private: + void RefreshChannels(); + void ReadChannel(QXmlStreamReader& reader); + void ConsumeElement(QXmlStreamReader& reader); + + private: + RadioItem* root_; + + QNetworkAccessManager* network_; +}; + +#endif // SOMAFMSERVICE_H diff --git a/src/src.pro b/src/src.pro index 66bff6e8a..f085cbe3c 100644 --- a/src/src.pro +++ b/src/src.pro @@ -1,11 +1,10 @@ # Change this line to install Clementine somewhere else install_prefix = /usr - VERSION = 0.1 QT += sql \ network \ - opengl \ - xml + xml \ + opengl TARGET = clementine TEMPLATE = app SOURCES += main.cpp \ @@ -49,7 +48,8 @@ SOURCES += main.cpp \ trackslider.cpp \ edittagdialog.cpp \ lineedit.cpp \ - multiloadingindicator.cpp + multiloadingindicator.cpp \ + somafmservice.cpp HEADERS += mainwindow.h \ player.h \ library.h \ @@ -98,12 +98,12 @@ HEADERS += mainwindow.h \ trackslider.h \ edittagdialog.h \ lineedit.h \ - multiloadingindicator.h + multiloadingindicator.h \ + somafmservice.h FORMS += mainwindow.ui \ libraryconfig.ui \ fileview.ui \ lastfmconfig.ui \ - radioloadingindicator.ui \ lastfmstationdialog.ui \ trackslider.ui \ edittagdialog.ui \ @@ -158,6 +158,16 @@ win32:SOURCES += ../3rdparty/qtsingleapplication/qtlockedfile_win32.cpp # Installs target.path = $${install_prefix}/bin/ desktop.path = dummy -desktop.extra = xdg-icon-resource install --size 64 ../dist/clementine_64.png application-x-clementine ; \ - xdg-desktop-menu install --novendor ../dist/clementine.desktop -INSTALLS += target desktop +desktop.extra = xdg-icon-resource \ + install \ + --size \ + 64 \ + ../dist/clementine_64.png \ + application-x-clementine \ + ; \ + xdg-desktop-menu \ + install \ + --novendor \ + ../dist/clementine.desktop +INSTALLS += target \ + desktop diff --git a/src/trackslider.ui b/src/trackslider.ui index 687e8aecd..2fe56ca0b 100644 --- a/src/trackslider.ui +++ b/src/trackslider.ui @@ -29,6 +29,9 @@ + + 10 + Qt::Horizontal