diff --git a/src/SvgHandler.cpp b/src/SvgHandler.cpp index 9107030f95..e30a141357 100644 --- a/src/SvgHandler.cpp +++ b/src/SvgHandler.cpp @@ -1,523 +1,520 @@ /**************************************************************************************** * Copyright (c) 2008 Nikolaj Hald Nielsen * * Copyright (c) 2008 Jeff Mitchell * * Copyright (c) 2009-2013 Mark Kretschmann * * * * 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, see . * ****************************************************************************************/ #define DEBUG_PREFIX "SvgHandler" #include "SvgHandler.h" #include "App.h" #include "EngineController.h" #include "MainWindow.h" #include "PaletteHandler.h" #include "SvgTinter.h" #include "core/meta/Meta.h" #include "core/support/Debug.h" #include "covermanager/CoverCache.h" #include "moodbar/MoodbarManager.h" #include #include #include #include #include #include #include #include #include #include namespace The { static SvgHandler* s_SvgHandler_instance = 0; SvgHandler* svgHandler() { if( !s_SvgHandler_instance ) s_SvgHandler_instance = new SvgHandler(); return s_SvgHandler_instance; } } SvgHandler::SvgHandler( QObject* parent ) : QObject( parent ) , m_cache( new KImageCache( "Amarok-pixmaps", 20 * 1024 ) ) , m_themeFile( "amarok/images/default-theme-clean.svg" ) // //use default theme , m_customTheme( false ) { DEBUG_BLOCK connect( The::paletteHandler(), &PaletteHandler::newPalette, this, &SvgHandler::discardCache ); } SvgHandler::~SvgHandler() { DEBUG_BLOCK delete m_cache; qDeleteAll( m_renderers ); m_renderers.clear(); The::s_SvgHandler_instance = 0; } bool SvgHandler::loadSvg( const QString& name, bool forceCustomTheme ) { const QString &svgFilename = m_customTheme || forceCustomTheme ? name : QStandardPaths::locate( QStandardPaths::GenericDataLocation, name ); QSvgRenderer *renderer = new QSvgRenderer( The::svgTinter()->tint( svgFilename ) ); if ( !renderer->isValid() ) { debug() << "Bluddy 'ell mateys, aye canna' load ya Ess Vee Gee at " << svgFilename; delete renderer; return false; } QWriteLocker writeLocker( &m_lock ); if( m_renderers[name] ) delete m_renderers[name]; m_renderers[name] = renderer; return true; } QSvgRenderer* SvgHandler::getRenderer( const QString& name ) { QReadLocker readLocker( &m_lock ); if( ! m_renderers[name] ) { readLocker.unlock(); if( !loadSvg( name ) ) { QWriteLocker writeLocker( &m_lock ); m_renderers[name] = new QSvgRenderer(); } readLocker.relock(); } return m_renderers[name]; } QSvgRenderer * SvgHandler::getRenderer() { return getRenderer( m_themeFile ); } QPixmap SvgHandler::renderSvg( const QString &name, const QString& keyname, int width, int height, const QString& element, bool skipCache, const qreal opacity ) { QString key; if( !skipCache ) { key = QStringLiteral("%1:%2x%3") .arg( keyname ) .arg( width ) .arg( height ); } QPixmap pixmap; if( skipCache || !m_cache->findPixmap( key, &pixmap ) ) { pixmap = QPixmap( width, height ); pixmap.fill( Qt::transparent ); QReadLocker readLocker( &m_lock ); if( ! m_renderers[name] ) { readLocker.unlock(); if( !loadSvg( name ) ) { return pixmap; } readLocker.relock(); } QPainter pt( &pixmap ); pt.setOpacity( opacity ); if ( element.isEmpty() ) m_renderers[name]->render( &pt, QRectF( 0, 0, width, height ) ); else m_renderers[name]->render( &pt, element, QRectF( 0, 0, width, height ) ); if( !skipCache ) m_cache->insertPixmap( key, pixmap ); } return pixmap; } QPixmap SvgHandler::renderSvg( const QUrl& url, const QString& keyname, int width, int height, const QString& element, bool skipCache, const qreal opacity ) { - if( !url.isLocalFile() ) - return QPixmap(); - QString key; if( !skipCache ) { key = QStringLiteral("%1:%2x%3") .arg( keyname ) .arg( width ) .arg( height ); } QPixmap pixmap; if( skipCache || !m_cache->findPixmap( key, &pixmap ) ) { pixmap = QPixmap( width, height ); pixmap.fill( Qt::transparent ); - QString name = url.toLocalFile(); + QString name = url.isLocalFile() ? url.toLocalFile() : ":" + url.path(); QReadLocker readLocker( &m_lock ); if( ! m_renderers[name] ) { readLocker.unlock(); if( !loadSvg( name, true ) ) { return pixmap; } readLocker.relock(); } QPainter pt( &pixmap ); pt.setOpacity( opacity ); if ( element.isEmpty() ) m_renderers[name]->render( &pt, QRectF( 0, 0, width, height ) ); else m_renderers[name]->render( &pt, element, QRectF( 0, 0, width, height ) ); if( !skipCache ) m_cache->insertPixmap( key, pixmap ); } return pixmap; } QPixmap SvgHandler::renderSvg(const QString & keyname, int width, int height, const QString & element, bool skipCache, const qreal opacity ) { return renderSvg( m_themeFile, keyname, width, height, element, skipCache, opacity ); } QPixmap SvgHandler::renderSvgWithDividers(const QString & keyname, int width, int height, const QString & element) { const QString key = QStringLiteral("%1:%2x%3-div") .arg( keyname ) .arg( width ) .arg( height ); QPixmap pixmap; if ( !m_cache->findPixmap( key, &pixmap ) ) { // debug() << QStringLiteral("svg %1 not in cache...").arg( key ); pixmap = QPixmap( width, height ); pixmap.fill( Qt::transparent ); QString name = m_themeFile; QReadLocker readLocker( &m_lock ); if( ! m_renderers[name] ) { readLocker.unlock(); if( ! loadSvg( name ) ) { return pixmap; } readLocker.relock(); } QPainter pt( &pixmap ); if ( element.isEmpty() ) m_renderers[name]->render( &pt, QRectF( 0, 0, width, height ) ); else m_renderers[name]->render( &pt, element, QRectF( 0, 0, width, height ) ); //add dividers. 5% spacing on each side int margin = width / 20; m_renderers[name]->render( &pt, "divider_top", QRectF( margin, 0 , width - 1 * margin, 1 ) ); m_renderers[name]->render( &pt, "divider_bottom", QRectF( margin, height - 1 , width - 2 * margin, 1 ) ); m_cache->insertPixmap( key, pixmap ); } return pixmap; } void SvgHandler::reTint() { The::svgTinter()->init(); if ( !loadSvg( m_themeFile )) warning() << "Unable to load theme file: " << m_themeFile; Q_EMIT retinted(); } QString SvgHandler::themeFile() { return m_themeFile; } void SvgHandler::setThemeFile( const QString & themeFile ) { DEBUG_BLOCK debug() << "got new theme file: " << themeFile; m_themeFile = themeFile; m_customTheme = true; discardCache(); } void SvgHandler::discardCache() { //redraw entire app.... reTint(); m_cache->clear(); if( auto window = pApp->mainWindow() ) window->update(); } QPixmap SvgHandler::imageWithBorder( Meta::AlbumPtr album, int size, int borderWidth ) { const int imageSize = size - ( borderWidth * 2 ); const QString &loc = album->imageLocation( imageSize ).url(); const QString &key = !loc.isEmpty() ? loc : album->name(); return addBordersToPixmap( The::coverCache()->getCover( album, imageSize ), borderWidth, key ); } QPixmap SvgHandler::addBordersToPixmap( const QPixmap &orgPixmap, int borderWidth, const QString &name, bool skipCache ) { int newWidth = orgPixmap.width() + borderWidth * 2; int newHeight = orgPixmap.height() + borderWidth *2; QString key; if( !skipCache ) { key = QStringLiteral("%1:%2x%3b%4") .arg( name ) .arg( newWidth ) .arg( newHeight ) .arg( borderWidth ); } QPixmap pixmap; if( skipCache || !m_cache->findPixmap( key, &pixmap ) ) { // Cache miss! We need to create the pixmap // if skipCache is true, we might actually already have fetched the image, including borders from the cache.... // so we really need to create a blank pixmap here as well, to not pollute the cached pixmap pixmap = QPixmap( newWidth, newHeight ); pixmap.fill( Qt::transparent ); QReadLocker readLocker( &m_lock ); if( !m_renderers[m_themeFile] ) { readLocker.unlock(); if( !loadSvg( m_themeFile ) ) { return pixmap; } readLocker.relock(); } QPainter pt( &pixmap ); pt.drawPixmap( borderWidth, borderWidth, orgPixmap.width(), orgPixmap.height(), orgPixmap ); m_renderers[m_themeFile]->render( &pt, "cover_border_topleft", QRectF( 0, 0, borderWidth, borderWidth ) ); m_renderers[m_themeFile]->render( &pt, "cover_border_top", QRectF( borderWidth, 0, orgPixmap.width(), borderWidth ) ); m_renderers[m_themeFile]->render( &pt, "cover_border_topright", QRectF( newWidth - borderWidth , 0, borderWidth, borderWidth ) ); m_renderers[m_themeFile]->render( &pt, "cover_border_right", QRectF( newWidth - borderWidth, borderWidth, borderWidth, orgPixmap.height() ) ); m_renderers[m_themeFile]->render( &pt, "cover_border_bottomright", QRectF( newWidth - borderWidth, newHeight - borderWidth, borderWidth, borderWidth ) ); m_renderers[m_themeFile]->render( &pt, "cover_border_bottom", QRectF( borderWidth, newHeight - borderWidth, orgPixmap.width(), borderWidth ) ); m_renderers[m_themeFile]->render( &pt, "cover_border_bottomleft", QRectF( 0, newHeight - borderWidth, borderWidth, borderWidth ) ); m_renderers[m_themeFile]->render( &pt, "cover_border_left", QRectF( 0, borderWidth, borderWidth, orgPixmap.height() ) ); if( !skipCache ) m_cache->insertPixmap( key, pixmap ); } return pixmap; } #if 0 void SvgHandler::paintCustomSlider( QPainter *p, int x, int y, int width, int height, qreal percentage, bool active ) { int knobSize = height - 4; int sliderRange = width - ( knobSize + 4 ); int knobRelPos = x + sliderRange * percentage + 2; int knobY = y + ( height - knobSize ) / 2 + 1; int sliderY = y + ( height / 2 ) - 1; //first draw the played part p->drawPixmap( x, sliderY, renderSvg( "new_slider_top_played", width, 2, "new_slider_top_played" ), 0, 0, knobRelPos - x, 2 ); //and then the unplayed part p->drawPixmap( knobRelPos + 1, sliderY, renderSvg( "new_slider_top", width, 2, "new_slider_top" ), knobRelPos + 1 - x, 0, -1, 2 ); //and then the bottom p->drawPixmap( x, sliderY + 2, renderSvg( "new_slider_bottom", width, 2, "new_slider_bottom" ) ); //draw end markers p->drawPixmap( x, y, renderSvg( "new_slider_end", 2, height, "new_slider_end" ) ); p->drawPixmap( x + width - 2, y, renderSvg( "new_slider_end", 2, height, "new_slider_endr" ) ); if ( active ) p->drawPixmap( knobRelPos, knobY, renderSvg( "new_slider_knob_active", knobSize, knobSize, "new_slider_knob_active" ) ); else p->drawPixmap( knobRelPos, knobY, renderSvg( "new_slider_knob", knobSize, knobSize, "new_slider_knob" ) ); } #endif QRect SvgHandler::sliderKnobRect( const QRect &slider, qreal percent, bool inverse ) const { if ( inverse ) percent = 1.0 - percent; const int knobSize = slider.height() - 4; QRect ret( 0, 0, knobSize, knobSize ); ret.moveTo( slider.x() + qRound( ( slider.width() - knobSize ) * percent ), slider.y() + 1 ); return ret; } // Experimental, using a mockup from Nuno Pinheiro (new_slider_nuno) void SvgHandler::paintCustomSlider( QPainter *p, QStyleOptionSlider *slider, qreal percentage, bool paintMoodbar ) { int sliderHeight = slider->rect.height() - 6; const bool inverse = ( slider->orientation == Qt::Vertical ) ? slider->upsideDown : ( (slider->direction == Qt::RightToLeft) != slider->upsideDown ); QRect knob = sliderKnobRect( slider->rect, percentage, inverse ); QPoint pt = slider->rect.topLeft() + QPoint( 0, 2 ); //debug() << "rel: " << knobRelPos << ", width: " << width << ", height:" << height << ", %: " << percentage; //if we should paint moodbar, paint this as the bottom layer bool moodbarPainted = false; if ( paintMoodbar ) { Meta::TrackPtr currentTrack = The::engineController()->currentTrack(); if ( currentTrack ) { if( The::moodbarManager()->hasMoodbar( currentTrack ) ) { QPixmap moodbar = The::moodbarManager()->getMoodbar( currentTrack, slider->rect.width() - sliderHeight, sliderHeight, inverse ); p->drawPixmap( pt, renderSvg( "moodbar_end_left", sliderHeight / 2, sliderHeight, "moodbar_end_left" ) ); pt.rx() += sliderHeight / 2; p->drawPixmap( pt, moodbar ); pt.rx() += slider->rect.width() - sliderHeight; p->drawPixmap( pt, renderSvg( "moodbar_end_right", sliderHeight / 2, sliderHeight, "moodbar_end_right" ) ); moodbarPainted = true; } } } if( !moodbarPainted ) { // Draw the slider background in 3 parts p->drawPixmap( pt, renderSvg( "progress_slider_left", sliderHeight, sliderHeight, "progress_slider_left" ) ); pt.rx() += sliderHeight; QRect midRect(pt, QSize(slider->rect.width() - sliderHeight * 2, sliderHeight) ); p->drawTiledPixmap( midRect, renderSvg( "progress_slider_mid", 32, sliderHeight, "progress_slider_mid" ) ); pt = midRect.topRight() + QPoint( 1, 0 ); p->drawPixmap( pt, renderSvg( "progress_slider_right", sliderHeight, sliderHeight, "progress_slider_right" ) ); //draw the played background. int playedBarHeight = sliderHeight - 6; int sizeOfLeftPlayed = qBound( 0, inverse ? slider->rect.right() - knob.right() + 2 : knob.x() - 2, playedBarHeight ); if( sizeOfLeftPlayed > 0 ) { QPoint tl, br; if ( inverse ) { tl = knob.topRight() + QPoint( -5, 5 ); // 5px x padding to avoid a "gap" between it and the top and bottom of the round knob. br = slider->rect.topRight() + QPoint( -3, 5 + playedBarHeight - 1 ); QPixmap rightEnd = renderSvg( "progress_slider_played_right", playedBarHeight, playedBarHeight, "progress_slider_played_right" ); p->drawPixmap( br.x() - rightEnd.width() + 1, tl.y(), rightEnd, qMax(0, rightEnd.width() - (sizeOfLeftPlayed + 3)), 0, sizeOfLeftPlayed + 3, playedBarHeight ); br.rx() -= playedBarHeight; } else { tl = slider->rect.topLeft() + QPoint( 3, 5 ); br = QPoint( knob.x() + 5, tl.y() + playedBarHeight - 1 ); QPixmap leftEnd = renderSvg( "progress_slider_played_left", playedBarHeight, playedBarHeight, "progress_slider_played_left" ); p->drawPixmap( tl.x(), tl.y(), leftEnd, 0, 0, sizeOfLeftPlayed + 3, playedBarHeight ); tl.rx() += playedBarHeight; } if ( sizeOfLeftPlayed == playedBarHeight ) p->drawTiledPixmap( QRect(tl, br), renderSvg( "progress_slider_played_mid", 32, playedBarHeight, "progress_slider_played_mid" ) ); } } if ( slider->state & QStyle::State_Enabled ) { // Draw the knob (handle) const char *string = ( slider->activeSubControls & QStyle::SC_SliderHandle ) ? "slider_knob_200911_active" : "slider_knob_200911"; p->drawPixmap( knob.topLeft(), renderSvg( string, knob.width(), knob.height(), string ) ); } } diff --git a/src/SvgTinter.cpp b/src/SvgTinter.cpp index d85b66b4b1..e766c5a0cb 100644 --- a/src/SvgTinter.cpp +++ b/src/SvgTinter.cpp @@ -1,140 +1,140 @@ /**************************************************************************************** * Copyright (c) 2007 Nikolaj Hald Nielsen * * Copyright (c) 2007 Mark Kretschmann * * * * 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, see . * ****************************************************************************************/ #include "SvgTinter.h" #include "App.h" #include "core/support/Debug.h" #include #include #include SvgTinter * SvgTinter::s_instance = nullptr; SvgTinter::SvgTinter() : m_firstRun( true ) { init(); m_firstRun = false; } SvgTinter::~SvgTinter() {} QByteArray -SvgTinter::tint( const QString &filename) +SvgTinter::tint( const QString &filename ) { QFile file( filename ); if ( !file.open( QIODevice::ReadOnly ) ) { error() << "Unable to open file: " << filename; return QByteArray(); } QByteArray svg_source( file.readAll() ); // Copied from KSvgrenderer.cpp as we don't load it directly. if (!svg_source.startsWith("open(QIODevice::ReadOnly)) { delete flt; return QByteArray(); } svg_source = flt->readAll(); delete flt; } // QString svg_string( svg_source ); QHashIterator tintIter( m_tintMap ); while( tintIter.hasNext() ) { tintIter.next(); svg_source.replace( tintIter.key(), tintIter.value().toLocal8Bit() ); } return svg_source; } void SvgTinter::init() { if ( m_lastPalette != pApp->palette() || m_firstRun ) { m_tintMap.insert( "#666765", pApp->palette().window().color().name() ); //insert a color for bright ( highlight color ) m_tintMap.insert( "#66ffff", pApp->palette().highlight().color().name() ); //a slightly lighter than window color: m_tintMap.insert( "#e8e8e8", blendColors( pApp->palette().window().color(), "#ffffff", 90 ).name() ); //a slightly darker than window color: m_tintMap.insert( "#565755", blendColors( pApp->palette().window().color(), "#000000", 90 ).name() ); //list background: #ifdef Q_WS_MAC m_tintMap.insert( "#f0f0f0", blendColors( pApp->palette().window().color(), "#000000", 90 ).name() ); m_tintMap.insert( "#ffffff", blendColors( pApp->palette().window().color(), "#000000", 98 ).name() ); #else m_tintMap.insert( "#f0f0f0", pApp->palette().base().color().name() ); #endif //alternate list background: m_tintMap.insert( "#e0e0e0", pApp->palette().alternateBase().color().name() ); //highlight/window mix: m_tintMap.insert( "#123456", blendColors( pApp->palette().window().color(), pApp->palette().highlight().color().name(), 80 ).name() ); //text color, useful for adding contrast m_tintMap.insert( "#010101", pApp->palette().text().color().name() ); m_lastPalette = pApp->palette(); } } QColor SvgTinter::blendColors( const QColor& color1, const QColor& color2, int percent ) { const float factor1 = ( float ) percent / 100; const float factor2 = ( 100 - ( float ) percent ) / 100; const int r = static_cast( color1.red() * factor1 + color2.red() * factor2 ); const int g = static_cast( color1.green() * factor1 + color2.green() * factor2 ); const int b = static_cast( color1.blue() * factor1 + color2.blue() * factor2 ); QColor result; result.setRgb( r, g, b ); return result; } namespace The { SvgTinter* svgTinter() { if ( SvgTinter::s_instance == nullptr ) SvgTinter::s_instance = new SvgTinter(); return SvgTinter::s_instance; } } diff --git a/src/context/AppletModel.cpp b/src/context/AppletModel.cpp index 6cba62fe70..0ee18c13d6 100644 --- a/src/context/AppletModel.cpp +++ b/src/context/AppletModel.cpp @@ -1,366 +1,362 @@ /**************************************************************************************** * Copyright (c) 2017 Malte Veerman * * * * 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, see . * ****************************************************************************************/ #define DEBUG_PREFIX "AppletModel" #include "AppletModel.h" #include "AmarokContextPackageStructure.h" #include "AppletLoader.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include #include #include #include #include using namespace Context; class Context::AppletPackage : public KPackage::Package { public: AppletPackage(const KPackage::Package &package) : KPackage::Package(package) {} bool operator==(const AppletPackage &p) { return metadata() == p.metadata(); } }; AppletModel::AppletModel(AppletLoader *loader, QObject* parent) : QAbstractListModel(parent) , m_loader(loader) { newApplets(loader->applets()); connect(loader, &AppletLoader::finished, this, &AppletModel::newApplets); } AppletModel::~AppletModel() { } int AppletModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent) return m_packages.size(); } void AppletModel::newApplets(const QList& applets) { DEBUG_BLOCK beginResetModel(); m_packages.clear(); for (const auto &applet : applets) { auto loader = KPackage::PackageLoader::self(); auto structure = new AmarokContextPackageStructure; loader->addKnownPackageStructure(QStringLiteral("Amarok/Context"), structure); auto package = loader->loadPackage(QStringLiteral("Amarok/Context"), applet.pluginId()); if (package.isValid()) { m_packages << package; } else error() << "Error loading package:" << applet.pluginId(); } //Sort applets by name std::sort(m_packages.begin(), m_packages.end(), [] (const AppletPackage &p1, const AppletPackage &p2) { return p1.metadata().name() < p2.metadata().name(); }); endResetModel(); } QVariant AppletModel::data(const QModelIndex& index, int role) const { int row = index.row(); if (row >= m_packages.size()) return QVariant(); const auto &package = m_packages.at(row); switch (role) { case Name: return package.metadata().name(); case Id: return package.metadata().pluginId(); case Icon: return package.fileUrl("icon"); case Mainscript: return package.fileUrl("mainscript"); case Collapsed: return Amarok::config(QStringLiteral("Context")).readEntry(package.metadata().pluginId() + "_collapsed", false); case ContentHeight: return Amarok::config(QStringLiteral("Context")).readEntry(package.metadata().pluginId() + "_contentHeight", 300); - - case PackagePath: - return QVariant(package.path() + "contents/"); } return QVariant(); } bool Context::AppletModel::setData(const QModelIndex& index, const QVariant& value, int role) { int row = index.row(); if (row >= m_packages.size()) return false; const auto &package = m_packages.at(row); switch (role) { case Collapsed: { Amarok::config(QStringLiteral("Context")).writeEntry(package.metadata().pluginId() + "_collapsed", value.toBool()); Q_EMIT dataChanged(index, index, QVector{role}); return true; } case ContentHeight: { Amarok::config(QStringLiteral("Context")).writeEntry(package.metadata().pluginId() + "_contentHeight", value.toReal()); Q_EMIT dataChanged(index, index, QVector{role}); return true; } default: warning() << (Role) role << "is read-only."; } return false; } QHash< int, QByteArray > AppletModel::roleNames() const { QHash roles; roles.insert(Name, "name"); roles.insert(Id, "appletId"); roles.insert(Icon, "icon"); roles.insert(Mainscript, "mainscript"); roles.insert(Collapsed, "collapsed"); - roles.insert(PackagePath, "packagePath"); roles.insert(ContentHeight, "contentHeight"); return roles; } void AppletModel::setAppletCollapsed(const QString& id, bool collapsed) { DEBUG_BLOCK debug() << "Set collapsed for applet:" << id << "to:" << collapsed; auto package = findPackage(id); if (package.isValid()) { Amarok::config(QStringLiteral("Context")).writeEntry(id + "_collapsed", collapsed); int row = m_packages.indexOf(package); auto index = createIndex(row, 0); Q_EMIT dataChanged(index, index, QVector{Collapsed}); } } void Context::AppletModel::setAppletContentHeight(const QString& id, qreal height) { DEBUG_BLOCK debug() << "Set content height for applet:" << id << "to:" << height; auto package = findPackage(id); if (package.isValid()) { Amarok::config(QStringLiteral("Context")).writeEntry(id + "_contentHeight", height); int row = m_packages.indexOf(package); auto index = createIndex(row, 0); Q_EMIT dataChanged(index, index, QVector{ContentHeight}); } } QUrl Context::AppletModel::imageUrl(const QString& id, const QString& imageName) { auto package = findPackage(id); if (package.isValid()) return package.fileUrl("images", imageName); return QUrl(); } AppletPackage AppletModel::findPackage(const QString& id) { for (const auto &package : m_packages) { auto metadata = package.metadata(); if (metadata.pluginId() == id) return package; } warning() << "Applet with id:" << id << "not found."; return AppletPackage(KPackage::Package()); } AppletProxyModel::AppletProxyModel(AppletModel* appletModel, QObject *parent) : QSortFilterProxyModel(parent) , m_appletModel(appletModel) { setSourceModel(appletModel); setDynamicSortFilter(true); sort(0); connect(m_appletModel->loader(), &AppletLoader::finished, this, &AppletProxyModel::enabledAppletsChanged); } AppletProxyModel::~AppletProxyModel() { } QStringList AppletProxyModel::enabledApplets() const { QStringList list; for (const auto &applet : m_appletModel->loader()->enabledApplets()) { list << applet.pluginId(); } std::sort(list.begin(), list.end(), [] (const QString &a, const QString &b) { QStringList ae = Amarok::config(QStringLiteral("Context")).readEntry("enabledApplets", QStringList()); return ae.indexOf(a) < ae.indexOf(b); } ); return list; } void AppletProxyModel::setAppletEnabled(const QString& id, bool enabled, int place) { DEBUG_BLOCK debug() << "Set enabled for applet:" << id << "to:" << enabled; if (enabled == appletEnabled(id)) return; QStringList ea = enabledApplets(); if (enabled) { if (place <= -1) place = ea.size(); debug() << "Applet's new place is" << place; ea.insert(place, id); } else { ea.removeAll(id); } Amarok::config(QStringLiteral("Context")).writeEntry("enabledApplets", ea); debug() << "New enabled applets:" << ea; invalidateFilter(); Q_EMIT enabledAppletsChanged(); } void AppletProxyModel::setAppletPlace(const QString& id, int place) { DEBUG_BLOCK debug() << "Set place for applet:" << id << "to:" << place; int currentPlace = appletPlace(id); debug() << "Current place is" << currentPlace; if (currentPlace == place) return; if (place <= -1) { setAppletEnabled(id, false); return; } if (currentPlace <= -1) setAppletEnabled(id, true, place); QStringList ea = enabledApplets(); place = qMin(place, ea.size() - 1); bool forward = place > currentPlace; beginMoveRows(QModelIndex(), currentPlace, currentPlace, QModelIndex(), forward ? place + 1 : place); ea.move(currentPlace, place); Amarok::config(QStringLiteral("Context")).writeEntry("enabledApplets", ea); endMoveRows(); debug() << "New enabled applets:" << ea; } int AppletProxyModel::appletPlace(const QString& id) const { return enabledApplets().indexOf(id); } bool AppletProxyModel::appletEnabled(const QString& id) const { return enabledApplets().contains(id); } void Context::AppletProxyModel::clear() { for( const auto &applet : enabledApplets() ) { setAppletEnabled( applet, false ); } } bool AppletProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const { int placeLeft = appletPlace(source_left.data(AppletModel::Id).toString()); int placeRight = appletPlace(source_right.data(AppletModel::Id).toString()); return placeLeft < placeRight; } bool AppletProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { QModelIndex index = sourceModel()->index(source_row, 0, source_parent); return appletEnabled(index.data(AppletModel::Id).toString()); } diff --git a/src/context/AppletModel.h b/src/context/AppletModel.h index 37a7fe9f59..ca7cfb3219 100644 --- a/src/context/AppletModel.h +++ b/src/context/AppletModel.h @@ -1,146 +1,145 @@ /**************************************************************************************** * Copyright (c) 2017 Malte Veerman * * * * 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, see . * ****************************************************************************************/ #ifndef APPLETMODEL_H #define APPLETMODEL_H #include #include class KPluginMetaData; namespace Context { class AppletLoader; class AppletPackage; class AppletModel : public QAbstractListModel { Q_OBJECT public: enum Role { Name, Id, Icon, Mainscript, Collapsed, - PackagePath, ContentHeight }; Q_ENUM(Role) explicit AppletModel(AppletLoader *loader, QObject *parent = nullptr); ~AppletModel() override; int rowCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; QHash< int, QByteArray > roleNames() const override; AppletLoader* loader() const { return m_loader; } Q_INVOKABLE void setAppletCollapsed(const QString &id, bool collapsed); Q_INVOKABLE void setAppletContentHeight(const QString& id, qreal height); Q_INVOKABLE QUrl imageUrl(const QString &id, const QString &imageName); public Q_SLOTS: void newApplets(const QList &applets); protected: AppletPackage findPackage(const QString &id); private: QList m_packages; AppletLoader *const m_loader; }; class AppletProxyModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QStringList enabledApplets READ enabledApplets NOTIFY enabledAppletsChanged) public: explicit AppletProxyModel(AppletModel *appletModel, QObject *parent = nullptr); ~AppletProxyModel() override; /** * @returns QStringList with ids of all enabled applets. * Sorted in ascending order of place of applets. */ QStringList enabledApplets() const; /** * Set an applet to be enabled or disabled. Does nothing if id is invalid. * Disabling an applet sets its place to -1. * * @param id Id of the applet. * @param enabled Set to true if applet should be enabled and false for disabled. * @param place The place of the applet after enabling. -1 appends the applet to the end of the list. * Irrelevant if applet is to be disabled. */ Q_INVOKABLE void setAppletEnabled(const QString &id, bool enabled, int place = -1); /** * Set an applet's place. Does nothing if id is invalid. * Enables the applet if it is disabled. * * @param id Id of the applet. * @param place The new place of the applet. Negative values disable the applet. */ Q_INVOKABLE void setAppletPlace(const QString &id, int place); /** * Find out an applet's place. * * @returns an integer with the applet's place. -1 if the applet is disabled. * * @param id Id of applet's place to be returned. */ Q_INVOKABLE int appletPlace(const QString &id) const; /** * Find out if an applet is enabled or disabled. * * @returns true if applet is enabled. Returns false if not. * * @param id Id of the applet. */ Q_INVOKABLE bool appletEnabled(const QString &id) const; /** * Clear the context area by disabling all applets */ Q_INVOKABLE void clear(); Q_SIGNALS: void enabledAppletsChanged(); protected: bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; private: AppletModel *m_appletModel; }; } #endif // APPLETMODEL_H diff --git a/src/context/applets/currenttrack/package/contents/ui/main.qml b/src/context/applets/currenttrack/package/contents/ui/main.qml index bd25c24aaa..239492f9a1 100644 --- a/src/context/applets/currenttrack/package/contents/ui/main.qml +++ b/src/context/applets/currenttrack/package/contents/ui/main.qml @@ -1,109 +1,109 @@ /**************************************************************************************** * Copyright (c) 2017 Malte Veerman * * * * 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, see . * ****************************************************************************************/ import QtQuick 2.4 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.0 as Kirigami import org.kde.amarok.qml 1.0 as AmarokQml import org.kde.amarok.currenttrack 1.0 AmarokQml.Applet { id: applet //album art Loader { id: cover height: parent.height width: height sourceComponent: CurrentTrackEngine.hasValidCover ? coverComponent : emptyComponent } ColumnLayout { anchors { left: cover.right leftMargin: applet.spacing right: parent.right top: parent.top bottom: parent.bottom } RowLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.alignment: Qt.AlignTop InfoItem { id: infoItem Layout.fillHeight: true Layout.fillWidth: true Layout.alignment: Qt.AlignLeft } AmarokQml.RatingItem { id: ratingItem Layout.preferredWidth: height * 6 Layout.preferredHeight: parent.height / 5 Layout.alignment: Qt.AlignTop | Qt.AlignRight rating: CurrentTrackEngine.rating onClicked: CurrentTrackEngine.rating = newRating } } StatsItem { id: statsItem Layout.fillWidth: true Layout.alignment: Qt.AlignBottom Layout.preferredHeight: parent.height / 5 } } Component { id: coverComponent Rectangle { id: cover color: "white" radius: Kirigami.Units.smallSpacing / 2 border.width: 1 border.color: applet.palette.light AmarokQml.PixmapItem { id: iconItem anchors.fill: parent anchors.margins: parent.radius source: CurrentTrackEngine.cover } } } Component { id: emptyComponent AmarokQml.PixmapItem { - source: Svg.renderSvg("file://" + applet.packagePath + "images/amarok-currenttrack.svg", + source: Svg.renderSvg(applet.imageUrl("amarok-currenttrack.svg"), "CurrentTrack", width, height, "album_old"); } } } diff --git a/src/context/context_qml_package/contents/ui/main.qml b/src/context/context_qml_package/contents/ui/main.qml index c707e575e4..9d9412f5a1 100644 --- a/src/context/context_qml_package/contents/ui/main.qml +++ b/src/context/context_qml_package/contents/ui/main.qml @@ -1,117 +1,128 @@ /**************************************************************************************** * Copyright (c) 2017 Malte Veerman * * * * 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, see . * ****************************************************************************************/ import QtQuick 2.4 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import org.kde.kirigami 2.0 as Kirigami import "toolbar" Item { id: root Component.onCompleted: Context.debug("Context created") ColumnLayout { anchors.fill: parent - ListView { - id: appletListView + Flickable { + id: appletFlickable signal scrollToApplet(string id) Layout.alignment: Qt.AlignTop Layout.fillHeight: true Layout.fillWidth: true - spacing: Kirigami.Units.smallSpacing - displayMarginEnd: 100000 - displayMarginBeginning: 100000 - clip: true + contentWidth: scrollBar.visible ? width - scrollBar.width : width + contentHeight: appletColumn.height + flickableDirection: Flickable.VerticalFlick + focus: true - model: AppletProxyModel + Keys.onUpPressed: scrollBar.decrease() + Keys.onDownPressed: scrollBar.increase() ScrollBar.vertical: ScrollBar { id: scrollBar } - delegate: Loader { - width: scrollBar.visible ? parent.width - scrollBar.width : parent.width - active: true - asynchronous: true - - function initialize() { - setSource(mainscript, { - "name": name, - "appletId": appletId, - "iconSource": icon, - "collapsed": collapsed, - "contentHeight": contentHeight, - "packagePath": packagePath, - "configEnabled": Qt.binding(function() { return appletToolbar.configEnabled; } ) - }); - } + Column { + id: appletColumn + + width: parent.width + spacing: Kirigami.Units.smallSpacing + + Repeater { + model: AppletProxyModel + + delegate: Loader { + width: appletColumn.width + active: true + asynchronous: false + + function initialize() { + setSource(mainscript, { + "name": name, + "appletId": appletId, + "iconSource": icon, + "collapsed": collapsed, + "contentHeight": contentHeight, + "configEnabled": Qt.binding(function() { return appletToolbar.configEnabled; } ) + }); + } - Component.onCompleted: initialize() + Component.onCompleted: initialize() - onStatusChanged: { - if (status == Loader.Error) { - Context.error("Error loading applet: " + appletId); - Context.error(sourceComponent.errorString()); - } - if (status == Loader.Ready) { - Context.debug("Applet loaded: " + appletId); - } - } + onStatusChanged: { + if (status == Loader.Error) { + Context.error("Error loading applet: " + appletId); + Context.error(sourceComponent.errorString()); + } + if (status == Loader.Ready) { + Context.debug("Applet loaded: " + appletId); + } + } - Connections { - target: AppletProxyModel + Connections { + target: AppletProxyModel - onDataChanged: { - if (!!mainscript && mainscript != source) { - Context.debug("Data changed for applet " + appletId); - initialize(); + onDataChanged: { + if (!!mainscript && mainscript != source) { + Context.debug("Data changed for applet " + appletId); + initialize(); + } + } } - } - } - Connections { - target: appletListView - - onScrollToApplet: { - if (id == appletId) { - appletListView.positionViewAtIndex(index, ListView.Beginning); - Context.debug("Scroll to applet: " + appletId); + Connections { + target: appletFlickable + + onScrollToApplet: { + if (id == appletId) { + appletFlickable.contentY = y; + Context.debug("Scroll to applet: " + appletId); + } + } } } } } } AppletToolbarAddItem { id: appletToolbarAddItem Layout.fillWidth: true height: Kirigami.Units.iconSizes.enormous visible: appletToolbar.configEnabled } AppletToolbar { id: appletToolbar contextRoot: root addItem: appletToolbarAddItem - listView: appletListView + flickable: appletFlickable Layout.alignment: Qt.AlignBottom Layout.fillWidth: true } } } diff --git a/src/context/context_qml_package/contents/ui/toolbar/AppletToolbar.qml b/src/context/context_qml_package/contents/ui/toolbar/AppletToolbar.qml index a5ba81f102..f6eb77f0dc 100644 --- a/src/context/context_qml_package/contents/ui/toolbar/AppletToolbar.qml +++ b/src/context/context_qml_package/contents/ui/toolbar/AppletToolbar.qml @@ -1,134 +1,134 @@ /**************************************************************************************** * Copyright (c) 2017 Malte Veerman * * * * 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, see . * ****************************************************************************************/ import QtQuick 2.4 import QtQuick.Controls 1.4 import QtQml.Models 2.1 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.0 as Kirigami Rectangle { id: root readonly property alias configEnabled: configureButton.checked property var addItem - property var listView + property var flickable property var contextRoot function resizeApplets() { var items = []; var children = toolbarAppletRow.contentItem.visibleChildren; for (var i=0; i 0) { var space = toolbarAppletRow.width - (items.length - 1) * toolbarAppletRow.spacing; var threshold = space / items.length; var smallApplets = []; var largeApplets = []; for (var i=0; i 0 && space > 0) { var countSmallApplets = smallApplets.length; smallApplets.forEach(function (applet, index) { if (applet.implicitWidth >= threshold) { largeApplets.push(applet); applet.width = applet.implicitWidth; space -= applet.width; } }); smallApplets = smallApplets.filter(function (applet) { return largeApplets.indexOf(applet) == -1 }); if (countSmallApplets == smallApplets.length) { smallApplets.forEach(function (applet, index) { applet.width = space / countSmallApplets; }); return; } threshold = space / smallApplets.length; } if (smallApplets.length == 0) return; for (var i=0; i * * * * 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, see . * ****************************************************************************************/ import QtQuick 2.6 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.0 as Kirigami MouseArea { id: root property alias name: label.text property string appletId property var toolbar - property var listView + property var flickable property bool configEnabled: !!toolbar ? toolbar.configEnabled : false property bool held: false height: held ? toolbar.height : toolbar.height - Kirigami.Units.smallSpacing anchors.verticalCenter: parent.verticalCenter implicitWidth: label.implicitWidth + Kirigami.Units.smallSpacing * 2 acceptedButtons: Qt.LeftButton hoverEnabled: true drag.target: held ? content : undefined drag.axis: Drag.XAxis cursorShape: configEnabled ? held ? Qt.ClosedHandCursor : Qt.PointingHandCursor : Qt.ArrowCursor onPressAndHold: if (configEnabled) held = true onReleased: held = false - onPressed: if (!configEnabled) listView.scrollToApplet(appletId) + onPressed: if (!configEnabled) flickable.scrollToApplet(appletId) onImplicitWidthChanged: toolbar.resizeApplets() DropArea { anchors { fill: parent leftMargin: Kirigami.Units.smallSpacing rightMargin: Kirigami.Units.smallSpacing } onEntered: { AppletProxyModel.setAppletPlace(drag.source.appletId, AppletProxyModel.appletPlace(root.appletId)); } } Rectangle { id: content readonly property string appletId: root.appletId border.width: 1 color: root.pressed ? palette.highlight : palette.button border.color: root.containsMouse ? palette.highlight : palette.buttonText radius: Kirigami.Units.smallSpacing / 2 anchors { horizontalCenter: root.horizontalCenter verticalCenter: root.verticalCenter } width: root.width height: root.height Drag.active: root.held Drag.source: root Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 states: State { when: root.held ParentChange { target: content parent: contextRoot } AnchorChanges { target: content anchors { horizontalCenter: undefined verticalCenter: undefined } } } Label { id: label anchors { fill: parent leftMargin: Kirigami.Units.smallSpacing rightMargin: Kirigami.Units.smallSpacing } horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter clip: true } } SystemPalette { id: palette } } diff --git a/src/context/qml_plugin/Applet.qml b/src/context/qml_plugin/Applet.qml index 2879a1f889..0691ade432 100644 --- a/src/context/qml_plugin/Applet.qml +++ b/src/context/qml_plugin/Applet.qml @@ -1,106 +1,104 @@ /**************************************************************************************** * Copyright (c) 2017 Malte Veerman * * * * 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, see . * ****************************************************************************************/ import QtQuick 2.4 import QtQuick.Dialogs 1.2 import org.kde.kirigami 2.0 as Kirigami -import org.kde.kirigami 2.0 as Kirigami Rectangle { id: root default property alias contents: content.data property alias title: header.title property string name: "Nameless Applet" property string appletId - property string packagePath property url iconSource property bool collapsed: false property bool configEnabled: false property real spacing: Kirigami.Units.smallSpacing property real padding: spacing property real contentHeight: content.childrenRect.height property Dialog configDialog: null readonly property AppletHeader appletHeader: header readonly property SystemPalette palette: palette radius: Kirigami.Units.smallSpacing border.width: 2 border.color: palette.mid color: "transparent" clip: true height: content.height + header.height + 2 * padding + !collapsed * spacing function imageUrl(filename) { return AppletModel.imageUrl(root.appletId, filename); } onCollapsedChanged: AppletModel.setAppletCollapsed(appletId, collapsed) onContentHeightChanged: AppletModel.setAppletContentHeight(appletId, contentHeight) AppletHeader { id: header title: root.name iconSource: root.iconSource } Item { id: content anchors { top: header.bottom left: root.left right: root.right topMargin: root.spacing leftMargin: root.padding rightMargin: root.padding } height: root.collapsed ? 0 : root.contentHeight clip: true Behavior on height { enabled: !resizeMouseArea.pressed NumberAnimation { duration: 350 } } } MouseArea { id: resizeMouseArea anchors { bottom: parent.bottom left: parent.left right: parent.right } height: root.padding enabled: root.configEnabled cursorShape: enabled ? Qt.SizeVerCursor : Qt.ArrowCursor acceptedButtons: Qt.LeftButton preventStealing: true onMouseYChanged: { if(pressed) { root.contentHeight = Math.max(Kirigami.Units.largeSpacing, root.contentHeight + mouseY); } } } SystemPalette { id: palette } }