/* * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome * Copyright 2020-2021, Jonas Kvinge * * Strawberry 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 3 of the License, or * (at your option) any later version. * * Strawberry 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 Strawberry. If not, see . * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/logging.h" #include "core/shared_ptr.h" #include "core/iconloader.h" #include "core/mimedata.h" #include "core/settings.h" #include "widgets/favoritewidget.h" #include "widgets/renametablineedit.h" #include "playlist.h" #include "playlistmanager.h" #include "playlisttabbar.h" const char *PlaylistTabBar::kSettingsGroup = "PlaylistTabBar"; PlaylistTabBar::PlaylistTabBar(QWidget *parent) : QTabBar(parent), manager_(nullptr), menu_(new QMenu(this)), menu_index_(-1), action_star_(nullptr), action_close_(nullptr), action_rename_(nullptr), action_save_(nullptr), action_new_(nullptr), drag_hover_tab_(0), suppress_current_changed_(false), initialized_(false), rename_editor_(new RenameTabLineEdit(this)) { setAcceptDrops(true); setElideMode(Qt::ElideRight); setUsesScrollButtons(true); setTabsClosable(true); action_star_ = menu_->addAction(IconLoader::Load(QStringLiteral("star")), tr("Star playlist"), this, &PlaylistTabBar::StarSlot); action_close_ = menu_->addAction(IconLoader::Load(QStringLiteral("list-remove")), tr("Close playlist"), this, &PlaylistTabBar::CloseSlot); action_rename_ = menu_->addAction(IconLoader::Load(QStringLiteral("edit-rename")), tr("Rename playlist..."), this, &PlaylistTabBar::RenameSlot); action_save_ = menu_->addAction(IconLoader::Load(QStringLiteral("document-save")), tr("Save playlist..."), this, &PlaylistTabBar::SaveSlot); menu_->addSeparator(); rename_editor_->setVisible(false); QObject::connect(rename_editor_, &RenameTabLineEdit::editingFinished, this, &PlaylistTabBar::RenameInline); QObject::connect(rename_editor_, &RenameTabLineEdit::EditingCanceled, this, &PlaylistTabBar::HideEditor); QObject::connect(this, &PlaylistTabBar::currentChanged, this, &PlaylistTabBar::CurrentIndexChanged); QObject::connect(this, &PlaylistTabBar::tabMoved, this, &PlaylistTabBar::TabMoved); // We can't just emit Close signal, we need to extract the playlist id first QObject::connect(this, &PlaylistTabBar::tabCloseRequested, this, &PlaylistTabBar::CloseFromTabIndex); } void PlaylistTabBar::SetActions(QAction *new_playlist, QAction *load_playlist) { menu_->insertAction(nullptr, new_playlist); menu_->insertAction(nullptr, load_playlist); action_new_ = new_playlist; } void PlaylistTabBar::SetManager(SharedPtr manager) { manager_ = manager; QObject::connect(&*manager_, &PlaylistManager::PlaylistFavorited, this, &PlaylistTabBar::PlaylistFavoritedSlot); QObject::connect(&*manager_, &PlaylistManager::PlaylistManagerInitialized, this, &PlaylistTabBar::PlaylistManagerInitialized); } void PlaylistTabBar::PlaylistManagerInitialized() { // Signal that we are done loading and thus further changes should be committed to the db. initialized_ = true; QObject::disconnect(&*manager_, &PlaylistManager::PlaylistManagerInitialized, this, &PlaylistTabBar::PlaylistManagerInitialized); } void PlaylistTabBar::contextMenuEvent(QContextMenuEvent *e) { // We need to finish the renaming action before showing context menu if (rename_editor_->isVisible()) { //discard any change HideEditor(); } menu_index_ = tabAt(e->pos()); action_star_->setEnabled(menu_index_ != -1 && count() > 1); action_close_->setEnabled(menu_index_ != -1 && count() > 1); action_rename_->setEnabled(menu_index_ != -1); action_save_->setEnabled(menu_index_ != -1); menu_->popup(e->globalPos()); } void PlaylistTabBar::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::MiddleButton) { // Update menu index menu_index_ = tabAt(e->pos()); CloseSlot(); } QTabBar::mouseReleaseEvent(e); } void PlaylistTabBar::mouseDoubleClickEvent(QMouseEvent *e) { int index = tabAt(e->pos()); // Discard a double click with the middle button if (e->button() != Qt::MiddleButton) { if (index == -1) { action_new_->activate(QAction::Trigger); } else { menu_index_ = index; QString new_text = tabText(index); new_text = new_text.replace(QLatin1String("&&"), QLatin1String("&")); rename_editor_->setGeometry(tabRect(index)); rename_editor_->setText(new_text); rename_editor_->setVisible(true); rename_editor_->setFocus(); } } QTabBar::mouseDoubleClickEvent(e); } void PlaylistTabBar::RenameSlot() { if (menu_index_ == -1) return; QString name = tabText(menu_index_); name = name.replace(QLatin1String("&&"), QLatin1String("&")); QString new_name = QInputDialog::getText(this, tr("Rename playlist"), tr("Enter a new name for this playlist"), QLineEdit::Normal, name); if (new_name.isEmpty()) return; emit Rename(tabData(menu_index_).toInt(), new_name); } void PlaylistTabBar::RenameInline() { emit Rename(tabData(menu_index_).toInt(), rename_editor_->text()); HideEditor(); } void PlaylistTabBar::HideEditor() { // editingFinished() will be called twice due to Qt bug #40, so we reuse the same instance, don't delete it rename_editor_->setVisible(false); // Hack to give back focus to playlist view manager_->SetCurrentPlaylist(manager_->current()->id()); } void PlaylistTabBar::StarSlot() { if (menu_index_ == -1) return; FavoriteWidget *favorite_widget = qobject_cast(tabButton(menu_index_, QTabBar::LeftSide)); if (favorite_widget) { favorite_widget->SetFavorite(!favorite_widget->IsFavorite()); } } void PlaylistTabBar::CloseSlot() { if (menu_index_ == -1) return; const int playlist_id = tabData(menu_index_).toInt(); Settings s; s.beginGroup(kSettingsGroup); const bool ask_for_delete = s.value("warn_close_playlist", true).toBool(); if (ask_for_delete && !manager_->IsPlaylistFavorite(playlist_id) && !manager_->playlist(playlist_id)->GetAllSongs().empty()) { QMessageBox confirmation_box; confirmation_box.setWindowIcon(QIcon(QStringLiteral(":/icons/64x64/strawberry.png"))); confirmation_box.setWindowTitle(tr("Remove playlist")); confirmation_box.setIcon(QMessageBox::Question); confirmation_box.setText( tr("You are about to remove a playlist which is not part of your " "favorite playlists: " "the playlist will be deleted (this action cannot be undone). \n" "Are you sure you want to continue?")); confirmation_box.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); QCheckBox dont_prompt_again(tr("Warn me when closing a playlist tab"), &confirmation_box); dont_prompt_again.setChecked(ask_for_delete); dont_prompt_again.blockSignals(true); dont_prompt_again.setToolTip(tr("This option can be changed in the \"Behavior\" preferences")); QGridLayout *grid = qobject_cast(confirmation_box.layout()); QDialogButtonBox *buttons = confirmation_box.findChild(); if (grid && buttons) { const int index = grid->indexOf(buttons); int row = 0, column = 0, row_span = 0, column_span = 0; grid->getItemPosition(index, &row, &column, &row_span, &column_span); QLayoutItem *buttonsItem = grid->takeAt(index); grid->addWidget(&dont_prompt_again, row, column, row_span, column_span, Qt::AlignLeft | Qt::AlignTop); grid->addItem(buttonsItem, ++row, column, row_span, column_span); } else { confirmation_box.addButton(&dont_prompt_again, QMessageBox::ActionRole); } if (confirmation_box.exec() != QMessageBox::Yes) { return; } // If user changed the pref, save the new one if (dont_prompt_again.isChecked() != ask_for_delete) { s.setValue("warn_close_playlist", dont_prompt_again.isChecked()); } } // Close the playlist. If the playlist is not a favorite playlist, it will be deleted, as it will not be visible after being closed. // Otherwise, the tab is closed but the playlist still exists and can be resurrected from the "Playlists" tab. emit Close(playlist_id); // Select the nearest tab. if (menu_index_ > 1) { setCurrentIndex(menu_index_ - 1); } // Update playlist tab order/visibility TabMoved(); } void PlaylistTabBar::CloseFromTabIndex(int index) { // Update the global index menu_index_ = index; CloseSlot(); } void PlaylistTabBar::SaveSlot() { if (menu_index_ == -1) return; emit Save(tabData(menu_index_).toInt()); } int PlaylistTabBar::current_id() const { if (currentIndex() == -1) return -1; return tabData(currentIndex()).toInt(); } int PlaylistTabBar::index_of(const int id) const { for (int i = 0; i < count(); ++i) { if (tabData(i).toInt() == id) { return i; } } return -1; } void PlaylistTabBar::set_current_id(const int id) { setCurrentIndex(index_of(id)); } int PlaylistTabBar::id_of(const int index) const { if (index < 0 || index >= count()) { qLog(Warning) << "Playlist tab index requested is out of bounds!"; return 0; } return tabData(index).toInt(); } void PlaylistTabBar::set_icon_by_id(const int id, const QIcon &icon) { setTabIcon(index_of(id), icon); } void PlaylistTabBar::RemoveTab(const int id) { removeTab(index_of(id)); } void PlaylistTabBar::set_text_by_id(const int id, const QString &text) { QString new_text = text; new_text = new_text.replace(QLatin1String("&"), QLatin1String("&&")); setTabText(index_of(id), new_text); setTabToolTip(index_of(id), text); } void PlaylistTabBar::CurrentIndexChanged(const int index) { if (!suppress_current_changed_) emit CurrentIdChanged(tabData(index).toInt()); } void PlaylistTabBar::InsertTab(const int id, const int index, const QString &text, const bool favorite) { QString new_text = text; if (new_text.contains(QLatin1Char('&'))) { new_text = new_text.replace(QLatin1Char('&'), QLatin1String("&&")); } suppress_current_changed_ = true; insertTab(index, new_text); setTabData(index, id); setTabToolTip(index, text); FavoriteWidget *widget = new FavoriteWidget(id, favorite); widget->setToolTip(tr("Double-click here to favorite this playlist so it will be saved and remain accessible through the \"Playlists\" panel on the left side bar")); QObject::connect(widget, &FavoriteWidget::FavoriteStateChanged, this, &PlaylistTabBar::PlaylistFavorited); setTabButton(index, QTabBar::LeftSide, widget); suppress_current_changed_ = false; // If we are still starting up, we don't need to do this, as the tab ordering after startup will be the same as was already in the db. if (initialized_) { if (currentIndex() == index) emit CurrentIdChanged(id); // Update playlist tab order/visibility TabMoved(); } } void PlaylistTabBar::TabMoved() { QList ids; ids.reserve(count()); for (int i = 0; i < count(); ++i) { ids << tabData(i).toInt(); } emit PlaylistOrderChanged(ids); } void PlaylistTabBar::dragEnterEvent(QDragEnterEvent *e) { if (e->mimeData()->hasUrls() || e->mimeData()->hasFormat(QString::fromLatin1(Playlist::kRowsMimetype)) || qobject_cast(e->mimeData())) { e->acceptProposedAction(); } } void PlaylistTabBar::dragMoveEvent(QDragMoveEvent *e) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) drag_hover_tab_ = tabAt(e->position().toPoint()); #else drag_hover_tab_ = tabAt(e->pos()); #endif if (drag_hover_tab_ != -1) { e->setDropAction(Qt::CopyAction); e->accept(tabRect(drag_hover_tab_)); if (!drag_hover_timer_.isActive()) { drag_hover_timer_.start(kDragHoverTimeout, this); } } else { drag_hover_timer_.stop(); } } void PlaylistTabBar::dragLeaveEvent(QDragLeaveEvent*) { drag_hover_timer_.stop(); } void PlaylistTabBar::timerEvent(QTimerEvent *e) { QTabBar::timerEvent(e); if (e->timerId() == drag_hover_timer_.timerId()) { drag_hover_timer_.stop(); if (drag_hover_tab_ != -1) setCurrentIndex(drag_hover_tab_); } } void PlaylistTabBar::dropEvent(QDropEvent *e) { if (drag_hover_tab_ == -1) { const MimeData *mime_data = qobject_cast(e->mimeData()); if (mime_data && !mime_data->name_for_new_playlist_.isEmpty()) { manager_->New(mime_data->name_for_new_playlist_); } else { manager_->New(tr("Playlist")); } setCurrentIndex(count() - 1); } else { setCurrentIndex(drag_hover_tab_); } manager_->current()->dropMimeData(e->mimeData(), e->proposedAction(), -1, 0, QModelIndex()); } bool PlaylistTabBar::event(QEvent *e) { switch (e->type()) { case QEvent::ToolTip:{ QHelpEvent *he = static_cast(e); QSize real_tab = tabSizeHint(tabAt(he->pos())); QRect displayed_tab = tabRect(tabAt(he->pos())); // Check whether the tab is elided or not bool is_elided = displayed_tab.width() < real_tab.width(); if (!is_elided) { // If it's not elided, don't show the tooltip QToolTip::hideText(); } else { QToolTip::showText(he->globalPos(), tabToolTip(tabAt(he->pos()))); } return true; } default: return QTabBar::event(e); } } void PlaylistTabBar::PlaylistFavoritedSlot(const int id, const bool favorite) { const int index = index_of(id); FavoriteWidget *favorite_widget = qobject_cast(tabButton(index, QTabBar::LeftSide)); if (favorite_widget) { favorite_widget->SetFavorite(favorite); } }