Implement disk caching of album art (#360)

* Implement disk caching of album art

This includes a button to clear the cache in the settings, as
requested.

Closes #358

* Make the cache size defaults match

* Implement the review by jonaski

* Fix more problems with the PR
This commit is contained in:
Gavin D. Howard
2020-02-07 15:18:18 -07:00
committed by GitHub
parent ab7b65a30b
commit 691f5d99ca
10 changed files with 254 additions and 16 deletions

View File

@@ -158,6 +158,7 @@ void SCollection::ResumeWatcher() { watcher_->SetRescanPausedAsync(false); }
void SCollection::ReloadSettings() { void SCollection::ReloadSettings() {
watcher_->ReloadSettingsAsync(); watcher_->ReloadSettingsAsync();
model_->ReloadSettings();
} }

View File

@@ -899,6 +899,10 @@ SongList CollectionBackend::GetCompilationSongs(const QString &album, const Quer
} }
Song::Source CollectionBackend::Source() const {
return source_;
}
void CollectionBackend::UpdateCompilations() { void CollectionBackend::UpdateCompilations() {
QMutexLocker l(db_->Mutex()); QMutexLocker l(db_->Mutex());

View File

@@ -188,6 +188,8 @@ class CollectionBackend : public CollectionBackendInterface {
SongList GetSongsBySongId(const QList<int> &song_ids); SongList GetSongsBySongId(const QList<int> &song_ids);
SongList GetSongsBySongId(const QStringList &song_ids); SongList GetSongsBySongId(const QStringList &song_ids);
Song::Source Source() const;
public slots: public slots:
void Exit(); void Exit();
void LoadDirectories(); void LoadDirectories();

View File

@@ -46,6 +46,7 @@
#include <QImage> #include <QImage>
#include <QPixmapCache> #include <QPixmapCache>
#include <QSettings> #include <QSettings>
#include <QStandardPaths>
#include <QtDebug> #include <QtDebug>
#include "core/application.h" #include "core/application.h"
@@ -63,6 +64,7 @@
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "playlist/songmimedata.h" #include "playlist/songmimedata.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "settings/collectionsettingspage.h"
using std::bind; using std::bind;
using std::sort; using std::sort;
@@ -71,7 +73,9 @@ using std::placeholders::_2;
const char *CollectionModel::kSavedGroupingsSettingsGroup = "SavedGroupings"; const char *CollectionModel::kSavedGroupingsSettingsGroup = "SavedGroupings";
const int CollectionModel::kPrettyCoverSize = 32; const int CollectionModel::kPrettyCoverSize = 32;
const int CollectionModel::kPixmapCacheLimit = QPixmapCache::cacheLimit() * 8; const char *CollectionModel::kPixmapDiskCacheDir = "/pixmapcache";
QNetworkDiskCache *CollectionModel::sIconCache = nullptr;
static bool IsArtistGroupBy(const CollectionModel::GroupBy by) { static bool IsArtistGroupBy(const CollectionModel::GroupBy by) {
return by == CollectionModel::GroupBy_Artist || by == CollectionModel::GroupBy_AlbumArtist; return by == CollectionModel::GroupBy_Artist || by == CollectionModel::GroupBy_AlbumArtist;
@@ -96,7 +100,8 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
playlist_icon_(IconLoader::Load("albums")), playlist_icon_(IconLoader::Load("albums")),
init_task_id_(-1), init_task_id_(-1),
use_pretty_covers_(false), use_pretty_covers_(false),
show_dividers_(true) { show_dividers_(true),
use_disk_cache_(false) {
root_->lazy_loaded = true; root_->lazy_loaded = true;
@@ -115,6 +120,11 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
//no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); //no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
// When running under gdb, all calls to this constructor came from the same thread.
// If this ever changes, these two lines might need to be protected by a mutex.
if (sIconCache == nullptr)
sIconCache = new QNetworkDiskCache(this);
connect(backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList))); connect(backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
connect(backend_, SIGNAL(SongsDeleted(SongList)), SLOT(SongsDeleted(SongList))); connect(backend_, SIGNAL(SongsDeleted(SongList)), SLOT(SongsDeleted(SongList)));
connect(backend_, SIGNAL(DatabaseReset()), SLOT(Reset())); connect(backend_, SIGNAL(DatabaseReset()), SLOT(Reset()));
@@ -127,7 +137,9 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
backend_->UpdateTotalArtistCountAsync(); backend_->UpdateTotalArtistCountAsync();
backend_->UpdateTotalAlbumCountAsync(); backend_->UpdateTotalAlbumCountAsync();
QPixmapCache::setCacheLimit(kPixmapCacheLimit); connect(app_, SIGNAL(ClearPixmapDiskCache()), SLOT(ClearDiskCache()));
ReloadSettings();
} }
@@ -165,6 +177,26 @@ void CollectionModel::SaveGrouping(QString name) {
} }
void CollectionModel::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
use_disk_cache_ = s.value(CollectionSettingsPage::kSettingsDiskCacheEnable, false).toBool();
if (!use_disk_cache_) {
sIconCache->clear();
}
sIconCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + kPixmapDiskCacheDir);
sIconCache->setMaximumCacheSize(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit));
QPixmapCache::setCacheLimit(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsCacheSize, CollectionSettingsPage::kSettingsCacheSizeUnit) / 1024);
s.endGroup();
}
void CollectionModel::Init(bool async) { void CollectionModel::Init(bool async) {
if (async) { if (async) {
@@ -476,6 +508,7 @@ void CollectionModel::SongsDeleted(const SongList &songs) {
// Remove from pixmap cache // Remove from pixmap cache
const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(node)); const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(node));
QPixmapCache::remove(cache_key); QPixmapCache::remove(cache_key);
if (use_disk_cache_) sIconCache->remove(QUrl(cache_key));
if (pending_cache_keys_.contains(cache_key)) { if (pending_cache_keys_.contains(cache_key)) {
pending_cache_keys_.remove(cache_key); pending_cache_keys_.remove(cache_key);
} }
@@ -532,7 +565,7 @@ QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const {
idx_copy = idx_copy.parent(); idx_copy = idx_copy.parent();
} }
return "collectionart:" + path.join("/"); return Song::TextForSource(backend_->Source()) + path.join("/");
} }
@@ -549,6 +582,18 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
return cached_pixmap; return cached_pixmap;
} }
// Try to load it from the disk cache
if (use_disk_cache_) {
std::unique_ptr<QIODevice> cache(sIconCache->data(QUrl(cache_key)));
if (cache) {
QImage cached_pixmap;
if (cached_pixmap.load(cache.get(), "XPM")) {
QPixmapCache::insert(cache_key, QPixmap::fromImage(cached_pixmap));
return QPixmap::fromImage(cached_pixmap);
}
}
}
// Maybe we're loading a pixmap already? // Maybe we're loading a pixmap already?
if (pending_cache_keys_.contains(cache_key)) { if (pending_cache_keys_.contains(cache_key)) {
return no_cover_icon_; return no_cover_icon_;
@@ -591,6 +636,21 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url,
QPixmapCache::insert(cache_key, image_pixmap); QPixmapCache::insert(cache_key, image_pixmap);
} }
// If we have a valid cover not already in the disk cache
if (use_disk_cache_) {
std::unique_ptr<QIODevice> cached_img(sIconCache->data(QUrl(cache_key)));
if (!cached_img && !image.isNull()) {
QNetworkCacheMetaData item_metadata;
item_metadata.setSaveToDisk(true);
item_metadata.setUrl(QUrl(cache_key));
QIODevice* cache = sIconCache->prepare(item_metadata);
if (cache) {
image.save(cache, "XPM");
sIconCache->insert(cache);
}
}
}
const QModelIndex idx = ItemToIndex(item); const QModelIndex idx = ItemToIndex(item);
if (!idx.isValid()) return; if (!idx.isValid()) return;
@@ -1479,6 +1539,19 @@ bool CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem
} }
int CollectionModel::MaximumCacheSize(QSettings *s, const char *size_id, const char *size_unit_id) const {
int size = s->value(size_id, 80).toInt();
int unit = s->value(size_unit_id, CollectionSettingsPage::CacheSizeUnit::CacheSizeUnit_MB).toInt() + 1;
do {
size *= 1024;
unit -= 1;
} while (unit > 0);
return size;
}
void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const { void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const {
switch (item->type) { switch (item->type) {
@@ -1606,6 +1679,10 @@ void CollectionModel::TotalAlbumCountUpdatedSlot(int count) {
} }
void CollectionModel::ClearDiskCache() {
sIconCache->clear();
}
QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping &g) { QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping &g) {
s << quint32(g.first) << quint32(g.second) << quint32(g.third); s << quint32(g.first) << quint32(g.second) << quint32(g.third);
return s; return s;

View File

@@ -43,6 +43,7 @@
#include <QImage> #include <QImage>
#include <QIcon> #include <QIcon>
#include <QPixmap> #include <QPixmap>
#include <QNetworkDiskCache>
#include <QSettings> #include <QSettings>
#include "core/simpletreemodel.h" #include "core/simpletreemodel.h"
@@ -69,7 +70,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
static const char *kSavedGroupingsSettingsGroup; static const char *kSavedGroupingsSettingsGroup;
static const int kPrettyCoverSize; static const int kPrettyCoverSize;
static const int kPixmapCacheLimit; static const char *kPixmapDiskCacheDir;
enum Role { enum Role {
Role_Type = Qt::UserRole + 1, Role_Type = Qt::UserRole + 1,
@@ -162,6 +163,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// Save the current grouping // Save the current grouping
void SaveGrouping(QString name); void SaveGrouping(QString name);
// Reload settings.
void ReloadSettings();
// Utility functions for manipulating text // Utility functions for manipulating text
static QString TextOrUnknown(const QString &text); static QString TextOrUnknown(const QString &text);
static QString PrettyYearAlbum(const int year, const QString &album); static QString PrettyYearAlbum(const int year, const QString &album);
@@ -203,6 +207,7 @@ signals:
void TotalSongCountUpdatedSlot(int count); void TotalSongCountUpdatedSlot(int count);
void TotalArtistCountUpdatedSlot(int count); void TotalArtistCountUpdatedSlot(int count);
void TotalAlbumCountUpdatedSlot(int count); void TotalAlbumCountUpdatedSlot(int count);
void ClearDiskCache();
// Called after ResetAsync // Called after ResetAsync
void ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult> future); void ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult> future);
@@ -244,6 +249,7 @@ signals:
QVariant AlbumIcon(const QModelIndex &idx); QVariant AlbumIcon(const QModelIndex &idx);
QVariant data(const CollectionItem *item, int role) const; QVariant data(const CollectionItem *item, int role) const;
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const; bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
int MaximumCacheSize(QSettings *s, const char *size_id, const char *size_unit_id) const;
private: private:
CollectionBackend *backend_; CollectionBackend *backend_;
@@ -274,10 +280,13 @@ signals:
QIcon playlists_dir_icon_; QIcon playlists_dir_icon_;
QIcon playlist_icon_; QIcon playlist_icon_;
static QNetworkDiskCache *sIconCache;
int init_task_id_; int init_task_id_;
bool use_pretty_covers_; bool use_pretty_covers_;
bool show_dividers_; bool show_dividers_;
bool use_disk_cache_;
AlbumCoverLoaderOptions cover_loader_options_; AlbumCoverLoaderOptions cover_loader_options_;

View File

@@ -126,6 +126,7 @@ signals:
void SettingsChanged(); void SettingsChanged();
void SettingsDialogRequested(SettingsDialog::Page page); void SettingsDialogRequested(SettingsDialog::Page page);
void ExitFinished(); void ExitFinished();
void ClearPixmapDiskCache();
private: private:
std::unique_ptr<ApplicationImpl> p_; std::unique_ptr<ApplicationImpl> p_;

View File

@@ -469,17 +469,17 @@ Song::Source Song::SourceFromURL(const QUrl &url) {
QString Song::TextForSource(Source source) { QString Song::TextForSource(Source source) {
switch (source) { switch (source) {
case Song::Source_LocalFile: return QObject::tr("File"); case Song::Source_LocalFile: return "file";
case Song::Source_Collection: return QObject::tr("Collection"); case Song::Source_Collection: return "collection";
case Song::Source_CDDA: return QObject::tr("CD"); case Song::Source_CDDA: return "cd";
case Song::Source_Device: return QObject::tr("Device"); case Song::Source_Device: return "device";
case Song::Source_Stream: return QObject::tr("Stream"); case Song::Source_Stream: return "stream";
case Song::Source_Tidal: return QObject::tr("Tidal"); case Song::Source_Tidal: return "tidal";
case Song::Source_Subsonic: return QObject::tr("subsonic"); case Song::Source_Subsonic: return "subsonic";
case Song::Source_Qobuz: return QObject::tr("qobuz"); case Song::Source_Qobuz: return "qobuz";
case Song::Source_Unknown: return QObject::tr("Unknown"); case Song::Source_Unknown: return "unknown";
} }
return QObject::tr("Unknown"); return "unknown";
} }

View File

@@ -34,6 +34,7 @@
#include <QPushButton> #include <QPushButton>
#include <QSettings> #include <QSettings>
#include "core/application.h"
#include "core/iconloader.h" #include "core/iconloader.h"
#include "collection/collectiondirectorymodel.h" #include "collection/collectiondirectorymodel.h"
#include "collectionsettingspage.h" #include "collectionsettingspage.h"
@@ -43,6 +44,13 @@
#include "ui_collectionsettingspage.h" #include "ui_collectionsettingspage.h"
const char *CollectionSettingsPage::kSettingsGroup = "Collection"; const char *CollectionSettingsPage::kSettingsGroup = "Collection";
const char *CollectionSettingsPage::kSettingsCacheSize = "cache_size";
const char *CollectionSettingsPage::kSettingsCacheSizeUnit = "cache_size_unit";
const char *CollectionSettingsPage::kSettingsDiskCacheEnable = "disk_cache_enable";
const char *CollectionSettingsPage::kSettingsDiskCacheSize = "disk_cache_size";
const char *CollectionSettingsPage::kSettingsDiskCacheSizeUnit = "disk_cache_size_unit";
const QStringList CollectionSettingsPage::cacheUnitNames = { "KB", "MB", "GB", "TB" };
CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog) CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog)
: SettingsPage(dialog), : SettingsPage(dialog),
@@ -91,6 +99,14 @@ void CollectionSettingsPage::CurrentRowChanged(const QModelIndex& index) {
ui_->remove->setEnabled(index.isValid()); ui_->remove->setEnabled(index.isValid());
} }
void CollectionSettingsPage::DiskCacheEnable(int state) {
bool checked = state == Qt::Checked;
ui_->button_disk_cache->setEnabled(checked);
ui_->label_disk_cache_size->setEnabled(checked);
ui_->spinbox_disk_cache_size->setEnabled(checked);
ui_->combobox_disk_cache_size->setEnabled(checked);
}
void CollectionSettingsPage::Load() { void CollectionSettingsPage::Load() {
if (!initialised_model_) { if (!initialised_model_) {
@@ -130,6 +146,20 @@ void CollectionSettingsPage::Load() {
ui_->checkbox_cover_lowercase->setChecked(s.value("cover_lowercase", true).toBool()); ui_->checkbox_cover_lowercase->setChecked(s.value("cover_lowercase", true).toBool());
ui_->checkbox_cover_replace_spaces->setChecked(s.value("cover_replace_spaces", true).toBool()); ui_->checkbox_cover_replace_spaces->setChecked(s.value("cover_replace_spaces", true).toBool());
ui_->spinbox_cache_size->setValue(s.value(kSettingsCacheSize, 80).toInt());
ui_->combobox_cache_size->addItems(cacheUnitNames);
ui_->combobox_cache_size->setCurrentIndex(s.value(kSettingsCacheSizeUnit, (int) CacheSizeUnit_MB).toInt());
ui_->checkbox_disk_cache->setChecked(s.value(kSettingsDiskCacheEnable, false).toBool());
ui_->label_disk_cache_size->setEnabled(ui_->checkbox_disk_cache->isChecked());
ui_->spinbox_disk_cache_size->setEnabled(ui_->checkbox_disk_cache->isChecked());
ui_->spinbox_disk_cache_size->setValue(s.value(kSettingsDiskCacheSize, 80).toInt());
ui_->combobox_disk_cache_size->setEnabled(ui_->checkbox_disk_cache->isChecked());
ui_->combobox_disk_cache_size->addItems(cacheUnitNames);
ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, (int) CacheSizeUnit_MB).toInt());
connect(ui_->checkbox_disk_cache, SIGNAL(stateChanged(int)), SLOT(DiskCacheEnable(int)));
connect(ui_->button_disk_cache, SIGNAL(clicked()), dialog()->app(), SIGNAL(ClearPixmapDiskCache()));
s.endGroup(); s.endGroup();
} }
@@ -161,6 +191,12 @@ void CollectionSettingsPage::Save() {
s.setValue("cover_lowercase", ui_->checkbox_cover_lowercase->isChecked()); s.setValue("cover_lowercase", ui_->checkbox_cover_lowercase->isChecked());
s.setValue("cover_replace_spaces", ui_->checkbox_cover_replace_spaces->isChecked()); s.setValue("cover_replace_spaces", ui_->checkbox_cover_replace_spaces->isChecked());
s.setValue(kSettingsCacheSize, ui_->spinbox_cache_size->value());
s.setValue(kSettingsCacheSizeUnit, ui_->combobox_cache_size->currentIndex());
s.setValue(kSettingsDiskCacheEnable, ui_->checkbox_disk_cache->isChecked());
s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value());
s.setValue(kSettingsDiskCacheSizeUnit, ui_->combobox_disk_cache_size->currentIndex());
s.endGroup(); s.endGroup();
} }

View File

@@ -39,6 +39,18 @@ public:
~CollectionSettingsPage(); ~CollectionSettingsPage();
static const char *kSettingsGroup; static const char *kSettingsGroup;
static const char *kSettingsCacheSize;
static const char *kSettingsCacheSizeUnit;
static const char *kSettingsDiskCacheEnable;
static const char *kSettingsDiskCacheSize;
static const char *kSettingsDiskCacheSizeUnit;
enum CacheSizeUnit {
CacheSizeUnit_KB,
CacheSizeUnit_MB,
CacheSizeUnit_GB,
CacheSizeUnit_TB,
};
enum SaveCover { enum SaveCover {
SaveCover_Hash = 1, SaveCover_Hash = 1,
@@ -53,11 +65,14 @@ private slots:
void Remove(); void Remove();
void CurrentRowChanged(const QModelIndex &index); void CurrentRowChanged(const QModelIndex &index);
void DiskCacheEnable(int state);
void CoverSaveInAlbumDirChanged(); void CoverSaveInAlbumDirChanged();
private: private:
Ui_CollectionSettingsPage *ui_; Ui_CollectionSettingsPage *ui_;
bool initialised_model_; bool initialised_model_;
static const QStringList cacheUnitNames;
}; };
#endif // COLLECTIONSETTINGSPAGE_H #endif // COLLECTIONSETTINGSPAGE_H

View File

@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>509</width> <width>509</width>
<height>746</height> <height>913</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -249,6 +249,99 @@ If there are no matches then it will use the largest image in the directory.</st
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="albumArtGroupBox">
<property name="title">
<string>Album art cache</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_cache_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinbox_cache_size">
<property name="maximum">
<number>1048576</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combobox_cache_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="checkbox_disk_cache">
<property name="text">
<string>Enable Disk Cache</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_disk_cache">
<property name="text">
<string>Clear Disk Cache</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_disk_cache_size">
<property name="text">
<string>Disk Cache Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinbox_disk_cache_size">
<property name="maximum">
<number>1048576</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combobox_disk_cache_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>