feat: basic lyrics support

This commit is contained in:
Gary Wang 2024-09-20 20:59:40 +08:00
parent f4374a0768
commit 2a92f4ea7f
No known key found for this signature in database
GPG Key ID: 5D30A4F15EA78760
11 changed files with 484 additions and 57 deletions

View File

@ -26,6 +26,8 @@ set (PMUSIC_CPP_FILES
seekableslider.cpp
playlistmanager.cpp
singleapplicationmanager.cpp
lrcbar.cpp
lyricsmanager.cpp
)
set (PMUSIC_HEADER_FILES
@ -33,6 +35,8 @@ set (PMUSIC_HEADER_FILES
seekableslider.h
playlistmanager.h
singleapplicationmanager.h
lrcbar.h
lyricsmanager.h
)
set (PMUSIC_UI_FILES

View File

@ -7,6 +7,7 @@ Since **I** just need a simple player which *just works* right now, so I did man
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
- No music library management support and there won't be one!
- It'll auto-load music files in the same folder of the file that you attempted to play, so organize your music files on a folder-basis.
## Build

View File

@ -1,71 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>LrcBar</name>
<message>
<location filename="../lrcbar.cpp" line="88"/>
<source>(Interlude...)</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.cpp" line="71"/>
<location filename="../mainwindow.cpp" line="93"/>
<source>Mono</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="73"/>
<location filename="../mainwindow.cpp" line="95"/>
<source>Stereo</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="75"/>
<location filename="../mainwindow.cpp" line="97"/>
<source>%1 Channels</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="200"/>
<location filename="../mainwindow.cpp" line="241"/>
<source>Select songs to play</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="202"/>
<location filename="../mainwindow.cpp" line="243"/>
<source>Audio Files</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="576"/>
<source>Select image as background skin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="578"/>
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="23"/>
<source>Pineapple Player</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="231"/>
<source>^</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="297"/>
<source>No song loaded...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="304"/>
<source>Drag and drop file to load</source>
<location filename="../lrcbar.cpp" line="89"/>
<source>Pineapple Music</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="313"/>
<location filename="../mainwindow.ui" line="320"/>
<source>0:00</source>
<source>No song loaded...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="81"/>
<location filename="../mainwindow.ui" line="325"/>
<source>Drag and drop file to load</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="338"/>
<source>Lrc</source>
<comment>Lyrics</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="103"/>
<source>Sample Rate: %1 Hz</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="86"/>
<location filename="../mainwindow.cpp" line="108"/>
<source>Bitrate: %1 Kbps</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="91"/>
<location filename="../mainwindow.cpp" line="113"/>
<source>Channel Count: %1</source>
<translation type="unfinished"></translation>
</message>

View File

@ -1,71 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>LrcBar</name>
<message>
<location filename="../lrcbar.cpp" line="88"/>
<source>(Interlude...)</source>
<translation></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.cpp" line="71"/>
<location filename="../mainwindow.cpp" line="93"/>
<source>Mono</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="73"/>
<location filename="../mainwindow.cpp" line="95"/>
<source>Stereo</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="75"/>
<location filename="../mainwindow.cpp" line="97"/>
<source>%1 Channels</source>
<translation>%1 </translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="200"/>
<location filename="../mainwindow.cpp" line="241"/>
<source>Select songs to play</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="202"/>
<location filename="../mainwindow.cpp" line="243"/>
<source>Audio Files</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="576"/>
<source>Select image as background skin</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="578"/>
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
<translation> (*.jpg *.jpeg *.png *.gif)</translation>
</message>
<message>
<location filename="../mainwindow.ui" line="23"/>
<source>Pineapple Player</source>
<translation type="unfinished"></translation>
<location filename="../lrcbar.cpp" line="89"/>
<source>Pineapple Music</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="231"/>
<source>^</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="297"/>
<location filename="../mainwindow.ui" line="313"/>
<source>No song loaded...</source>
<translation>...</translation>
</message>
<message>
<location filename="../mainwindow.ui" line="304"/>
<location filename="../mainwindow.ui" line="325"/>
<source>Drag and drop file to load</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="313"/>
<location filename="../mainwindow.ui" line="320"/>
<source>0:00</source>
<translation type="unfinished"></translation>
<location filename="../mainwindow.ui" line="338"/>
<source>Lrc</source>
<comment>Lyrics</comment>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="81"/>
<location filename="../mainwindow.cpp" line="103"/>
<source>Sample Rate: %1 Hz</source>
<translation>: %1 Hz</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="86"/>
<location filename="../mainwindow.cpp" line="108"/>
<source>Bitrate: %1 Kbps</source>
<translation>: %1 Kbps</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="91"/>
<location filename="../mainwindow.cpp" line="113"/>
<source>Channel Count: %1</source>
<translation>: %1</translation>
</message>
@ -75,7 +89,7 @@
<message>
<location filename="../main.cpp" line="28"/>
<source>File list.</source>
<translation></translation>
<translation></translation>
</message>
</context>
</TS>

128
lrcbar.cpp Normal file
View File

@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "lrcbar.h"
#include "lyricsmanager.h"
#include <QMouseEvent>
#include <QPainter>
#include <QWindow>
LrcBar::LrcBar(QWidget *parent)
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
, m_lrcMgr(new LyricsManager(this))
{
m_font.setPointSize(30);
m_font.setStyleStrategy(QFont::PreferAntialias);
QSize windowSize(sizeHint());
QFontMetrics fm(m_font);
m_baseLinearGradient.setColorAt(0, QColor(20, 100, 200));
m_baseLinearGradient.setColorAt(1, QColor(0, 80, 255));
m_baseLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
m_baseLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
m_maskLinearGradient.setColorAt(0, QColor(255, 128, 0));
m_maskLinearGradient.setColorAt(0.5, QColor(255, 255, 0));
m_maskLinearGradient.setColorAt(1, QColor(255, 128, 0));
m_maskLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
m_maskLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
setAttribute(Qt::WA_TranslucentBackground);
setMouseTracking(true);
setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2,
qApp->primaryScreen()->geometry().height() - windowSize.height() - 50),
windowSize));
}
LrcBar::~LrcBar()
{
}
bool LrcBar::loadLyrics(QString filepath)
{
m_currentTimeMs = 0;
return m_lrcMgr->loadLyrics(filepath);
}
void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs)
{
if (!isVisible()) return;
m_currentTimeMs = timestampMs;
m_lrcMgr->updateCurrentTimeMs(timestampMs, totalTimeMs);
update();
}
QSize LrcBar::sizeHint() const
{
return QSize(1000, 88);
}
void LrcBar::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
window()->windowHandle()->startSystemMove();
event->accept();
}
return QWidget::mouseMoveEvent(event);
}
void LrcBar::paintEvent(QPaintEvent *)
{
QPainter painter(this);
if (underMouse()) {
painter.setBrush(QBrush(QColor(255, 255, 255, 120)));
painter.setPen(Qt::NoPen);
painter.drawRect(0, 0, width(), height());
}
painter.setFont(m_font);
painter.setRenderHint(QPainter::Antialiasing, true);
QString curLrc(m_lrcMgr->lyrics().trimmed());
if (curLrc.isEmpty()) {
curLrc = m_lrcMgr->hasLyrics() ? tr("(Interlude...)")
: QCoreApplication::translate("MainWindow", "Pineapple Music", nullptr);
}
QFontMetrics fm(m_font);
int lrcWidth = fm.horizontalAdvance(curLrc);
int maskWidth = lrcWidth * m_lrcMgr->maskPercent(m_currentTimeMs);
int startOffsetX = 0;
if (fm.horizontalAdvance(curLrc) < width()) {
startOffsetX = (width() - lrcWidth) / 2;
} else {
if (maskWidth < width() / 2) {
startOffsetX = 0;
} else if (lrcWidth - maskWidth < width() / 2) {
startOffsetX = width() - lrcWidth;
} else {
startOffsetX = 0 - (maskWidth - width() / 2);
}
}
// shadow
painter.setPen(QColor(0, 0, 0, 80));
painter.drawText(startOffsetX + 2, 2, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
// text itself
painter.setPen(QPen(m_baseLinearGradient, 0));
painter.drawText(startOffsetX, 0, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
// mask
painter.setPen(QPen(m_maskLinearGradient, 0));
painter.drawText(startOffsetX, 0, maskWidth, this->height(), Qt::AlignVCenter, curLrc);
}
void LrcBar::enterEvent(QEnterEvent *)
{
update();
}
void LrcBar::leaveEvent(QEvent *)
{
update();
}

36
lrcbar.h Normal file
View File

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QLinearGradient>
#include <QMediaPlayer>
#include <QWidget>
class LyricsManager;
class LrcBar : public QWidget
{
Q_OBJECT
public:
explicit LrcBar(QWidget *parent);
~LrcBar();
bool loadLyrics(QString filepath);
void playbackPositionChanged(int timestampMs, int totalTimeMs);
protected:
QSize sizeHint() const override;
void mouseMoveEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *) override;
void enterEvent(QEnterEvent *) override;
void leaveEvent(QEvent *) override;
private:
int m_currentTimeMs = 0;
LyricsManager * m_lrcMgr;
QFont m_font;
QLinearGradient m_baseLinearGradient;
QLinearGradient m_maskLinearGradient;
bool m_underMouse = false;
};

143
lyricsmanager.cpp Normal file
View File

@ -0,0 +1,143 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "lyricsmanager.h"
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
LyricsManager::LyricsManager(QObject *parent)
: QObject(parent)
{
}
LyricsManager::~LyricsManager()
{
}
bool LyricsManager::loadLyrics(QString filepath)
{
// reset state
reset();
// check and load file
QFileInfo fileInfo(filepath);
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
}
if (!fileInfo.exists()) return false;
QFile file(fileInfo.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
QTextStream stream(&file);
QStringList lines = QString(stream.readAll()).split('\n');
file.close();
// parse lyrics timestamp
QRegularExpression tagRegex(R"regex(\[(ti|ar|al|au|length|by|offset|tool|re|ve|#):\s?([^\]]*)\]$)regex");
QRegularExpression lrcRegex(R"regex(\[(\d{2,3}:\d{2}\.\d{2})\](.*))regex");
bool tagSectionPassed = false;
for (QString line : std::as_const(lines)) {
line = line.trimmed();
if (line.isEmpty()) continue;
if (!tagSectionPassed) {
QRegularExpressionMatch tagMatch = tagRegex.match(line);
if (tagMatch.hasMatch()) {
QString tag(tagMatch.captured(1));
if (tag == QLatin1String("offset")) {
// The value is prefixed with either + or -, with + causing lyrics to appear sooner
m_timeOffset = -tagMatch.captured(2).toInt();
qDebug() << m_timeOffset;
}
qDebug() << "[tag]" << tagMatch.captured(1) << tagMatch.captured(2);
continue;
}
}
QRegularExpressionMatch match = lrcRegex.match(line);
if (match.hasMatch()) {
tagSectionPassed = true;
QTime timestamp(QTime::fromString(match.captured(1), "m:s.zz"));
m_lyricsMap.insert(timestamp.msecsSinceStartOfDay(), match.captured(2));
qDebug() << "[lrc]" << match.captured(1) << match.captured(2);
}
}
if (!m_lyricsMap.isEmpty()) {
m_timestampList = m_lyricsMap.keys();
std::sort(m_timestampList.begin(), m_timestampList.end());
return true;
}
return false;
}
bool LyricsManager::hasLyrics() const
{
return !m_lyricsMap.isEmpty();
}
void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
{
if (!hasLyrics()) return;
// TODO: we don't need to find from the top everytime the time is updated
auto iter = std::find_if(m_timestampList.begin(), m_timestampList.end() + 1, [&curTimeMs, this](int timestamp) -> bool {
return (timestamp + m_timeOffset) > curTimeMs;
});
m_nextLyricsTime = (iter == m_timestampList.end() + 1) ? totalTimeMs : *iter;
if (iter != m_timestampList.begin() && iter != (m_timestampList.end() + 1)) {
iter--;
}
m_currentLyricsTime = *iter;
}
QString LyricsManager::lyrics(int lineOffset) const
{
if (!hasLyrics()) return QString();
int index = m_timestampList.indexOf(m_currentLyricsTime) + lineOffset;
if (index >= 0 && index < m_timestampList.count()) {
int timestamp = m_timestampList.at(index);
return m_lyricsMap.value(timestamp);
} else {
return QString();
}
}
double LyricsManager::maskPercent(int curTimeMs)
{
if (!hasLyrics()) return 0;
if (curTimeMs <= currentLyricsTime()) return 0;
if (curTimeMs >= nextLyricsTime()) return 1;
if (m_nextLyricsTime == currentLyricsTime()) return 1;
return (double)(curTimeMs - currentLyricsTime()) / (m_nextLyricsTime - m_currentLyricsTime);
}
void LyricsManager::reset()
{
m_currentLyricsTime = 0;
m_nextLyricsTime = 0;
m_timeOffset = 0;
m_lyricsMap.clear();
m_timestampList.clear();
}
int LyricsManager::currentLyricsTime() const
{
return m_currentLyricsTime + m_timeOffset;
}
int LyricsManager::nextLyricsTime() const
{
return m_nextLyricsTime + m_timeOffset;
}

37
lyricsmanager.h Normal file
View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QHash>
#include <QList>
#include <QObject>
class LyricsManager : public QObject
{
Q_OBJECT
public:
explicit LyricsManager(QObject *parent);
~LyricsManager();
bool loadLyrics(QString filepath);
bool hasLyrics() const;
void updateCurrentTimeMs(int curTimeMs, int totalTimeMs);
QString lyrics(int lineOffset = 0) const;
double maskPercent(int curTimeMs);
protected:
private:
void reset();
int currentLyricsTime() const;
int nextLyricsTime() const;
QHash<int, QString> m_lyricsMap;
QList<int> m_timestampList;
int m_currentLyricsTime = 0;
int m_nextLyricsTime = 0;
int m_timeOffset = 0;
};

View File

@ -6,6 +6,7 @@
#include "./ui_mainwindow.h"
#include "playlistmanager.h"
#include "lrcbar.h"
// taglib
#ifndef NO_TAGLIB
@ -38,6 +39,7 @@ MainWindow::MainWindow(QWidget *parent)
, m_mediaDevices(new QMediaDevices(this))
, m_mediaPlayer(new QMediaPlayer(this))
, m_audioOutput(new QAudioOutput(this))
, m_lrcbar(new LrcBar(nullptr))
, m_playlistManager(new PlaylistManager(this))
{
ui->setupUi(this);
@ -48,8 +50,8 @@ MainWindow::MainWindow(QWidget *parent)
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
ui->playlistView->setModel(m_playlistManager->model());
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
this->setAttribute(Qt::WA_TranslucentBackground, true);
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
setAttribute(Qt::WA_TranslucentBackground, true);
loadSkinData();
initConnections();
@ -60,6 +62,7 @@ MainWindow::MainWindow(QWidget *parent)
MainWindow::~MainWindow()
{
delete m_lrcbar;
delete ui;
}
@ -215,6 +218,11 @@ void MainWindow::dropEvent(QDropEvent *e)
return;
}
if (fileName.endsWith(".lrc")) {
m_lrcbar->loadLyrics(fileName);
return;
}
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
if (modelIndex.isValid()) {
loadByModelIndex(modelIndex);
@ -238,11 +246,13 @@ void MainWindow::loadFile()
m_playlistManager->loadPlaylist(urlList);
m_mediaPlayer->setSource(urlList.first());
m_lrcbar->loadLyrics(urlList.first().toLocalFile());
}
void MainWindow::loadByModelIndex(const QModelIndex & index)
{
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
m_lrcbar->loadLyrics(m_playlistManager->localFileByIndex(index));
}
void MainWindow::play()
@ -441,6 +451,7 @@ void MainWindow::initConnections()
if (m_mediaPlayer->duration() != 0) {
ui->playbackSlider->setSliderPosition(ui->playbackSlider->maximum() * pos / m_mediaPlayer->duration());
}
m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration());
});
connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) {
@ -578,3 +589,13 @@ void MainWindow::on_playlistView_activated(const QModelIndex &index)
loadByModelIndex(index);
play();
}
void MainWindow::on_lrcBtn_clicked()
{
if (m_lrcbar->isVisible()) {
m_lrcbar->hide();
} else {
m_lrcbar->show();
}
}

View File

@ -17,6 +17,7 @@ class QAudioOutput;
class QPropertyAnimation;
QT_END_NAMESPACE
class LrcBar;
class PlaylistManager;
class MainWindow : public QMainWindow
{
@ -73,6 +74,7 @@ private slots:
void on_setSkinBtn_clicked();
void on_playListBtn_clicked();
void on_playlistView_activated(const QModelIndex &index);
void on_lrcBtn_clicked();
signals:
void playbackModeChanged(enum PlaybackMode mode);
@ -89,6 +91,7 @@ private:
QMediaDevices *m_mediaDevices;
QMediaPlayer *m_mediaPlayer;
QAudioOutput *m_audioOutput;
LrcBar *m_lrcbar;
QPropertyAnimation *m_fadeOutAnimation;
PlaylistManager *m_playlistManager;

View File

@ -20,7 +20,7 @@
<bool>true</bool>
</property>
<property name="windowTitle">
<string>Pineapple Player</string>
<string>Pineapple Music</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
@ -315,25 +315,51 @@ QListView {
</widget>
</item>
<item>
<widget class="QLabel" name="propLabel">
<property name="text">
<string>Drag and drop file to load</string>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
</widget>
<item>
<widget class="QLabel" name="propLabel">
<property name="text">
<string>Drag and drop file to load</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="lrcBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string comment="Lyrics">Lrc</string>
</property>
<property name="iconSize">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="playbackTimeLayout">
<item>
<widget class="QLabel" name="nowTimeLabel">
<property name="text">
<string>0:00</string>
<string notr="true">0:00</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="totalTimeLabel">
<property name="text">
<string>0:00</string>
<string notr="true">0:00</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>