[network/ruqola] src/widgets/room: Fix room header layout at varying widths
Till Adam <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit eab181365433e3691d69ef1c78ecf4a15c094ed2 by Till Adam.
Committed on 28/07/2026 at 14:42.
Pushed by tilladam into branch 'master'.
Fix room header layout at varying widths
Place the room topic next to the channel name while preserving wrapping and responsive header sizing. Keep controls aligned with the first line, prevent the header from clipping the message view, preserve valid rich text while collapsing long topics, and tighten the show-more and show-less spacing. Includes regression coverage for compact, expanded, narrow, and rich-text layouts.
M +74 -0 src/widgets/room/autotests/roomheaderlabeltest.cpp
M +4 -0 src/widgets/room/autotests/roomheaderlabeltest.h
M +92 -1 src/widgets/room/autotests/roomheaderwidgettest.cpp
M +3 -0 src/widgets/room/autotests/roomheaderwidgettest.h
M +2 -0 src/widgets/room/autotests/roomwidgettest.cpp
M +63 -47 src/widgets/room/roomheaderlabel.cpp
M +0 -1 src/widgets/room/roomheaderlabel.h
M +53 -27 src/widgets/room/roomheaderwidget.cpp
M +1 -0 src/widgets/room/roomheaderwidget.h
M +2 -1 src/widgets/room/roomwidget.cpp
https://invent.kde.org/network/ruqola/-/commit/eab181365433e3691d69ef1c78ecf4a15c094ed2
diff --git a/src/widgets/room/autotests/roomheaderlabeltest.cpp b/src/widgets/room/autotests/roomheaderlabeltest.cpp
index 54fbec2439..9f62d54406 100644
--- a/src/widgets/room/autotests/roomheaderlabeltest.cpp
+++ b/src/widgets/room/autotests/roomheaderlabeltest.cpp
@@ -7,7 +7,11 @@
#include "roomheaderlabeltest.h"
#include "room/roomheaderlabel.h"
#include <QTest>
+#include <QTextBlock>
+#include <QTextDocument>
+#include <QTextFragment>
QTEST_MAIN(RoomHeaderLabelTest)
+using namespace Qt::Literals::StringLiterals;
RoomHeaderLabelTest::RoomHeaderLabelTest(QObject *parent)
: QObject(parent)
{
@@ -20,4 +24,74 @@ void RoomHeaderLabelTest::shouldHaveDefaultValues()
// TODO
}
+void RoomHeaderLabelTest::shouldKeepShowMoreAtNormalLineSpacing()
+{
+ RoomHeaderLabel w;
+ w.resize(1000, 200);
+ w.setRoomTopic(u"<qt><p>Main topic</p>\n<p>Second line</p>\n<p>Third line</p>\n<p>Fourth line</p></qt>"_s);
+
+ QVERIFY(w.text().contains(u"<br />"_s));
+ QVERIFY(w.text().contains(u"href=\"showmoretext\""_s));
+
+ Q_EMIT w.linkActivated(u"showmoretext"_s);
+ QVERIFY(w.text().contains(u"<br />"_s));
+ QVERIFY(w.text().contains(u"href=\"showlesstext\""_s));
+}
+
+void RoomHeaderLabelTest::shouldCollapseWrappedSingleParagraph()
+{
+ RoomHeaderLabel w;
+ w.resize(240, 200);
+ w.setRoomTopic(
+ u"This is a long single-paragraph topic that wraps over many visual lines at the label's actual width and must remain collapsed by default."_s);
+
+ QVERIFY(w.text().contains(u"href=\"showmoretext\""_s));
+}
+
+void RoomHeaderLabelTest::shouldUseFullWidthBeforeShowMore()
+{
+ RoomHeaderLabel w;
+ w.resize(420, 200);
+
+ const int availableWidth = w.width() - 10;
+ const int showMoreWidth = w.fontMetrics().horizontalAdvance(u"(Show More Info…)"_s);
+ QString firstLine;
+ const QString word = u"topic "_s;
+ while (w.fontMetrics().horizontalAdvance(firstLine + word) < availableWidth - 10) {
+ firstLine += word;
+ }
+ firstLine = firstLine.trimmed();
+ QVERIFY(w.fontMetrics().horizontalAdvance(firstLine) > availableWidth - showMoreWidth);
+
+ w.setRoomTopic(firstLine + u" tail tail tail tail tail tail<br>Second line<br>Third line<br>Fourth line"_s);
+
+ QTextDocument document;
+ document.setHtml(w.text());
+ QVERIFY(document.toPlainText().section(u'\n', 0, 0).startsWith(firstLine));
+}
+
+void RoomHeaderLabelTest::shouldPreserveRichTextWhenCollapsing()
+{
+ RoomHeaderLabel w;
+ w.resize(300, 200);
+ const QString originalUrl = u"https://example.com/"_s + QString(300, u'a');
+ w.setRoomTopic(
+ u"<p>See <a href=\"%1\">the link text that continues across the first visual line and beyond it</a> followed by more text.</p>"
+ "<p>Second paragraph</p><p>Third paragraph</p><p>Fourth paragraph</p>"_s.arg(originalUrl));
+
+ QStringList anchors;
+ QTextDocument document;
+ document.setHtml(w.text());
+ for (QTextBlock block = document.begin(); block.isValid(); block = block.next()) {
+ for (QTextBlock::iterator it = block.begin(); !it.atEnd(); ++it) {
+ const QTextFragment fragment = it.fragment();
+ if (fragment.isValid() && fragment.charFormat().isAnchor()) {
+ anchors.append(fragment.charFormat().anchorHref());
+ }
+ }
+ }
+ QVERIFY(anchors.contains(originalUrl));
+ QVERIFY(anchors.contains(u"showmoretext"_s));
+}
+
#include "moc_roomheaderlabeltest.cpp"
diff --git a/src/widgets/room/autotests/roomheaderlabeltest.h b/src/widgets/room/autotests/roomheaderlabeltest.h
index 740e67fc0f..cdad6be62a 100644
--- a/src/widgets/room/autotests/roomheaderlabeltest.h
+++ b/src/widgets/room/autotests/roomheaderlabeltest.h
@@ -15,4 +15,8 @@ public:
~RoomHeaderLabelTest() override = default;
private Q_SLOTS:
void shouldHaveDefaultValues();
+ void shouldKeepShowMoreAtNormalLineSpacing();
+ void shouldCollapseWrappedSingleParagraph();
+ void shouldUseFullWidthBeforeShowMore();
+ void shouldPreserveRichTextWhenCollapsing();
};
diff --git a/src/widgets/room/autotests/roomheaderwidgettest.cpp b/src/widgets/room/autotests/roomheaderwidgettest.cpp
index d2a041a2cc..5e7e5e71de 100644
--- a/src/widgets/room/autotests/roomheaderwidgettest.cpp
+++ b/src/widgets/room/autotests/roomheaderwidgettest.cpp
@@ -25,10 +25,15 @@ RoomHeaderWidgetTest::RoomHeaderWidgetTest(QObject *parent)
void RoomHeaderWidgetTest::shouldHaveDefaultValues()
{
RoomHeaderWidget w;
+ QVERIFY(w.sizePolicy().hasHeightForWidth());
auto mainLayout = w.findChild<QVBoxLayout *>(u"mainLayout"_s);
QVERIFY(mainLayout);
QCOMPARE(mainLayout->contentsMargins(), QMargins{});
+ auto headerLayout = w.findChild<QHBoxLayout *>(u"headerLayout"_s);
+ QVERIFY(headerLayout);
+ QCOMPARE(headerLayout->contentsMargins(), QMargins(0, 4, 0, 4));
+
auto mRoomName = w.findChild<QLabel *>(u"mRoomName"_s);
QVERIFY(mRoomName);
QVERIFY(mRoomName->text().isEmpty());
@@ -37,7 +42,9 @@ void RoomHeaderWidgetTest::shouldHaveDefaultValues()
auto roomNameLayout = w.findChild<QHBoxLayout *>(u"roomNameLayout"_s);
QVERIFY(roomNameLayout);
- QCOMPARE(roomNameLayout->contentsMargins(), QMargins{});
+ QCOMPARE(roomNameLayout->contentsMargins().left(), 0);
+ QCOMPARE(roomNameLayout->contentsMargins().right(), 0);
+ QCOMPARE(roomNameLayout->contentsMargins().bottom(), 0);
auto mTeamName = w.findChild<TeamNameLabel *>(u"mTeamName"_s);
QVERIFY(mTeamName);
@@ -115,6 +122,90 @@ void RoomHeaderWidgetTest::shouldHaveDefaultValues()
QVERIFY(!mTeamChannelsButton->toolTip().isEmpty());
}
+void RoomHeaderWidgetTest::shouldCenterCollapsedHeaderItems()
+{
+ RoomHeaderWidget w;
+ w.setRoomName(u"#yggdrasil"_s);
+
+ auto roomIcon = w.findChild<QLabel *>(u"mRoomIcon"_s);
+ QVERIFY(roomIcon);
+ roomIcon->setPixmap(QPixmap(24, 24));
+
+ w.resize(600, 100);
+ w.show();
+ QCoreApplication::processEvents();
+
+ const auto roomName = w.findChild<QLabel *>(u"mRoomName"_s);
+ const auto favoriteButton = w.findChild<QToolButton *>(u"mFavoriteButton"_s);
+ const auto callButton = w.findChild<QToolButton *>(u"mCallButton"_s);
+ const int centerY = favoriteButton->geometry().center().y();
+ // Allow 2px: two independently centre-aligned widgets of different heights can differ by up to
+ // that much from the integer truncation in geometry().center() (== y() + height()/2), and the
+ // exact heights depend on the platform's default font (1px passes here but not on every distro).
+ QVERIFY(qAbs(roomIcon->geometry().center().y() - centerY) <= 2);
+ QVERIFY(qAbs(roomName->geometry().center().y() - centerY) <= 2);
+ QVERIFY(qAbs(callButton->geometry().center().y() - centerY) <= 2);
+}
+
+void RoomHeaderWidgetTest::shouldAlignExpandedHeaderItemsWithFirstRow()
+{
+ RoomHeaderWidget w;
+ w.setRoomName(u"#ruqola"_s);
+ w.setRoomTopic(u"First line\nSecond line\nThird line\nFourth line\nFifth line"_s);
+
+ auto roomIcon = w.findChild<QLabel *>(u"mRoomIcon"_s);
+ QVERIFY(roomIcon);
+ roomIcon->setPixmap(QPixmap(24, 24));
+
+ const auto roomHeaderLabel = w.findChild<RoomHeaderLabel *>(u"mRoomHeaderLabel"_s);
+ Q_EMIT roomHeaderLabel->linkActivated(u"showmoretext"_s);
+
+ w.resize(600, 300);
+ w.show();
+ QCoreApplication::processEvents();
+
+ const auto roomName = w.findChild<QLabel *>(u"mRoomName"_s);
+ const auto favoriteButton = w.findChild<QToolButton *>(u"mFavoriteButton"_s);
+ const auto callButton = w.findChild<QToolButton *>(u"mCallButton"_s);
+ QVERIFY(roomHeaderLabel->height() > favoriteButton->height());
+
+ const int firstRowCenterY = roomName->geometry().center().y();
+ // See shouldCenterCollapsedHeaderItems(): 2px tolerance for integer rounding in
+ // geometry().center() across platform fonts.
+ QVERIFY(qAbs(favoriteButton->geometry().center().y() - firstRowCenterY) <= 2);
+ QVERIFY(qAbs(roomIcon->geometry().center().y() - firstRowCenterY) <= 2);
+ QVERIFY(qAbs(callButton->geometry().center().y() - firstRowCenterY) <= 2);
+}
+
+void RoomHeaderWidgetTest::shouldGrowExpandedHeaderForNarrowWidth()
+{
+ QWidget container;
+ auto outerLayout = new QVBoxLayout(&container);
+ outerLayout->setContentsMargins({});
+ outerLayout->setSpacing(0);
+ auto constrainedHeader = new RoomHeaderWidget(&container);
+ constrainedHeader->setRoomName(u"#ruqola"_s);
+ constrainedHeader->setRoomTopic(
+ u"First line with enough text to wrap at narrow widths\nSecond line with enough text to wrap at narrow widths\nThird line with enough text to "
+ u"wrap at narrow widths\nFourth line with enough text to wrap at narrow widths\nFifth line with enough text to wrap at narrow widths"_s);
+ const auto constrainedLabel = constrainedHeader->findChild<RoomHeaderLabel *>(u"mRoomHeaderLabel"_s);
+ Q_EMIT constrainedLabel->linkActivated(u"showmoretext"_s);
+ outerLayout->addWidget(constrainedHeader);
+ auto body = new QWidget(&container);
+ body->setMinimumHeight(20);
+ body->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+ outerLayout->addWidget(body);
+ container.resize(400, 300);
+ container.show();
+ QCoreApplication::processEvents();
+
+ const int requiredHeaderHeight = constrainedHeader->heightForWidth(constrainedHeader->width());
+ QVERIFY(requiredHeaderHeight + body->minimumHeight() <= container.height());
+ QCOMPARE(constrainedHeader->geometry().top(), 0);
+ QVERIFY(constrainedHeader->height() >= requiredHeaderHeight);
+ QVERIFY(constrainedLabel->height() >= constrainedLabel->heightForWidth(constrainedLabel->width()));
+}
+
void RoomHeaderWidgetTest::shouldShowHideIcon()
{
RoomHeaderWidget w;
diff --git a/src/widgets/room/autotests/roomheaderwidgettest.h b/src/widgets/room/autotests/roomheaderwidgettest.h
index 1f3e5fcd46..de6351d714 100644
--- a/src/widgets/room/autotests/roomheaderwidgettest.h
+++ b/src/widgets/room/autotests/roomheaderwidgettest.h
@@ -16,6 +16,9 @@ public:
~RoomHeaderWidgetTest() override = default;
private Q_SLOTS:
void shouldHaveDefaultValues();
+ void shouldCenterCollapsedHeaderItems();
+ void shouldAlignExpandedHeaderItemsWithFirstRow();
+ void shouldGrowExpandedHeaderForNarrowWidth();
void shouldShowHideIcon();
void shouldEmitSignal();
};
diff --git a/src/widgets/room/autotests/roomwidgettest.cpp b/src/widgets/room/autotests/roomwidgettest.cpp
index 4f86f8a22e..05ec055ac2 100644
--- a/src/widgets/room/autotests/roomwidgettest.cpp
+++ b/src/widgets/room/autotests/roomwidgettest.cpp
@@ -43,6 +43,8 @@ void RoomWidgetTest::shouldHaveDefaultValues()
auto mainLayout = w.findChild<QVBoxLayout *>(u"mainLayout"_s);
QVERIFY(mainLayout);
QCOMPARE(mainLayout->contentsMargins(), QMargins{});
+ QCOMPARE(mainLayout->spacing(), 0);
+ QCOMPARE(mainLayout->stretch(1), 1);
auto mRoomHeaderWidget = w.findChild<RoomHeaderWidget *>(u"mRoomHeaderWidget"_s);
QVERIFY(mRoomHeaderWidget);
diff --git a/src/widgets/room/roomheaderlabel.cpp b/src/widgets/room/roomheaderlabel.cpp
index abeb4a36ff..8e41fafa5f 100644
--- a/src/widgets/room/roomheaderlabel.cpp
+++ b/src/widgets/room/roomheaderlabel.cpp
@@ -8,9 +8,49 @@
#include "ruqolautils.h"
#include <KLocalizedString>
+#include <QTextBlock>
+#include <QTextCursor>
#include <QTextDocument>
+#include <QTextDocumentFragment>
+#include <QTextLayout>
using namespace Qt::Literals::StringLiterals;
+namespace
+{
+int lineCount(const QTextDocument &document)
+{
+ int count = 0;
+ for (QTextBlock block = document.begin(); block.isValid(); block = block.next()) {
+ count += block.layout()->lineCount();
+ }
+ return count;
+}
+
+int firstVisualLineEnd(const QTextDocument &document)
+{
+ for (QTextBlock block = document.begin(); block.isValid(); block = block.next()) {
+ const QTextLayout *layout = block.layout();
+ if (layout->lineCount() > 0) {
+ const QTextLine line = layout->lineAt(0);
+ int end = block.position() + line.textStart() + line.textLength();
+ while (end > block.position() && document.characterAt(end - 1).isSpace()) {
+ --end;
+ }
+ return end;
+ }
+ }
+ return 0;
+}
+
+QString appendInfoLink(QTextDocument &document, const QString &href, const QString &linkText)
+{
+ QTextCursor cursor(&document);
+ cursor.movePosition(QTextCursor::End);
+ cursor.insertHtml(u"<br><a href=\"%1\"> %2</a>"_s.arg(href, linkText.toHtmlEscaped()));
+ return document.toHtml();
+}
+}
+
RoomHeaderLabel::RoomHeaderLabel(QWidget *parent)
: QLabel(parent)
{
@@ -51,7 +91,7 @@ void RoomHeaderLabel::updateSqueezedText()
}
setVisible(true);
const QString text = rPixelSqueeze(mFullText, width() - 10);
- QLabel::setText("<qt>"_L1 + text + "</qt>"_L1);
+ QLabel::setText(text);
if (mFullText != text && !mExpandTopic) {
setToolTip(mFullText);
}
@@ -73,56 +113,32 @@ void RoomHeaderLabel::slotMoreInfo(const QString &content)
}
QString RoomHeaderLabel::rPixelSqueeze(const QString &text, int maxPixels) const
-{
- const auto tSize = textSize(text);
- const int tHeight = tSize.height();
- if (tHeight > (3 * fontMetrics().ascent() + fontMetrics().descent())) {
- QString tmp = text;
- if (!mExpandTopic) {
- int tw = tSize.width();
- const QString showMoreText = i18n("(Show More Info…)");
- if (tw > maxPixels) {
- int em = fontMetrics().maxWidth();
- maxPixels -= fontMetrics().horizontalAdvance(showMoreText);
-
- // On some MacOS system, maxWidth may return 0
- if (em == 0) {
- for (const QChar c : text) {
- em = qMax(em, fontMetrics().horizontalAdvance(c));
- }
- }
- while ((tw > maxPixels) && !tmp.isEmpty()) {
- const int len = tmp.length();
- int delta = (em == 0) ? 0 : (tw - maxPixels) / em;
- delta = qBound(1, delta, len);
-
- tmp.remove(len - delta, delta);
- tw = textSize(tmp).width();
- }
- } else {
- tmp = tmp.split(u"\n"_s).at(0);
- }
- if (!tmp.endsWith(u'\n')) {
- tmp.append(u'\n');
- }
- return tmp.append(u"<a href=\"showmoretext\"> %1</a>"_s.arg(showMoreText));
- } else {
- if (!tmp.endsWith(u'\n')) {
- tmp.append(u'\n');
- }
- return tmp.append(u"<a href=\"showlesstext\"> %1</a>"_s.arg(i18n("(Show Less Info…)")));
- }
- }
- return text;
-}
-
-QSize RoomHeaderLabel::textSize(const QString &text) const
{
QTextDocument document;
document.setDefaultFont(font());
- document.setHtml("<qt>"_L1 + text + "</qt>"_L1);
+ document.setDocumentMargin(0);
+ document.setHtml(text);
+ document.setTextWidth(qMax(1, maxPixels));
+ document.size(); // Force the width-constrained layout before inspecting its lines.
+
+ if (lineCount(document) <= 3) {
+ return text;
+ }
+
+ if (mExpandTopic) {
+ return appendInfoLink(document, u"showlesstext"_s, i18n("(Show Less Info…)"));
+ }
+
+ QTextCursor selection(&document);
+ selection.setPosition(0);
+ selection.setPosition(firstVisualLineEnd(document), QTextCursor::KeepAnchor);
- return document.size().toSize();
+ QTextDocument collapsedDocument;
+ collapsedDocument.setDefaultFont(font());
+ collapsedDocument.setDocumentMargin(0);
+ QTextCursor collapsedCursor(&collapsedDocument);
+ collapsedCursor.insertFragment(selection.selection());
+ return appendInfoLink(collapsedDocument, u"showmoretext"_s, i18n("(Show More Info…)"));
}
const QString &RoomHeaderLabel::fullText() const
diff --git a/src/widgets/room/roomheaderlabel.h b/src/widgets/room/roomheaderlabel.h
index 53a8849633..bdd6d69da8 100644
--- a/src/widgets/room/roomheaderlabel.h
+++ b/src/widgets/room/roomheaderlabel.h
@@ -32,7 +32,6 @@ private:
LIBRUQOLAWIDGETS_NO_EXPORT void updateSqueezedText();
LIBRUQOLAWIDGETS_NO_EXPORT void updateHeaderText();
[[nodiscard]] LIBRUQOLAWIDGETS_NO_EXPORT QString rPixelSqueeze(const QString &text, int maxPixels) const;
- [[nodiscard]] LIBRUQOLAWIDGETS_NO_EXPORT QSize textSize(const QString &text) const;
QString mFullText;
QString mTopic;
QString mAnnouncement;
diff --git a/src/widgets/room/roomheaderwidget.cpp b/src/widgets/room/roomheaderwidget.cpp
index 00aed262d0..acb1542e49 100644
--- a/src/widgets/room/roomheaderwidget.cpp
+++ b/src/widgets/room/roomheaderwidget.cpp
@@ -50,15 +50,27 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
return left->order() < right->order();
});
}
+ // The header height depends on the width available to the wrapping topic.
+ QSizePolicy headerSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+ headerSizePolicy.setHeightForWidth(true);
+ setSizePolicy(headerSizePolicy);
+
auto mainLayout = new QVBoxLayout(this);
mainLayout->setObjectName(u"mainLayout"_s);
mainLayout->setContentsMargins({});
auto headerLayout = new QHBoxLayout;
headerLayout->setObjectName(u"headerLayout"_s);
- headerLayout->setContentsMargins({});
+ headerLayout->setContentsMargins(0, 4, 0, 4);
mainLayout->addLayout(headerLayout);
+ // Keep the controls in a top-anchored first-row band when the topic expands below it.
+ auto leftButtonLayout = new QHBoxLayout;
+ leftButtonLayout->setObjectName(u"leftButtonLayout"_s);
+ leftButtonLayout->setContentsMargins({});
+ headerLayout->addLayout(leftButtonLayout);
+ headerLayout->setAlignment(leftButtonLayout, Qt::AlignTop);
+
mFavoriteButton->setAutoRaise(true);
mFavoriteButton->setObjectName(u"mFavoriteButton"_s);
mFavoriteButton->setIcon(QIcon::fromTheme(u"favorite"_s));
@@ -67,14 +79,14 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
#ifndef QT_NO_ACCESSIBILITY
mFavoriteButton->setAccessibleName(i18n("Favorite"));
#endif
- headerLayout->addWidget(mFavoriteButton, 0, Qt::AlignTop);
+ leftButtonLayout->addWidget(mFavoriteButton, 0, Qt::AlignVCenter);
connect(mFavoriteButton, &QToolButton::clicked, this, &RoomHeaderWidget::favoriteChanged);
mDiscussionBackButton->setAutoRaise(true);
mDiscussionBackButton->setObjectName(u"mDiscussionBackButton"_s);
mDiscussionBackButton->setIcon(QIcon::fromTheme(u"draw-arrow-back"_s));
mDiscussionBackButton->setCheckable(false);
- headerLayout->addWidget(mDiscussionBackButton, 0, Qt::AlignTop);
+ leftButtonLayout->addWidget(mDiscussionBackButton, 0, Qt::AlignVCenter);
connect(mDiscussionBackButton, &QToolButton::clicked, this, &RoomHeaderWidget::goBackToRoom);
mDiscussionBackButton->setVisible(false);
@@ -88,7 +100,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
mEncryptedButton->setCheckable(true);
mEncryptedButton->setVisible(false);
- headerLayout->addWidget(mEncryptedButton, 0, Qt::AlignTop);
+ leftButtonLayout->addWidget(mEncryptedButton, 0, Qt::AlignVCenter);
connect(mEncryptedButton, &QToolButton::clicked, this, [this](bool checked) {
if (!checked) {
slotDisabledEncryption();
@@ -97,46 +109,57 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
}
});
+ mRoomIcon->setObjectName(u"mRoomIcon"_s);
mRoomIcon->setMargin(1);
- headerLayout->addWidget(mRoomIcon, 0, Qt::AlignTop);
-
- auto infoLayout = new QVBoxLayout;
- infoLayout->setObjectName(u"infoLayout"_s);
- infoLayout->setContentsMargins({});
- infoLayout->setSpacing(0);
- headerLayout->addLayout(infoLayout);
+ leftButtonLayout->addWidget(mRoomIcon, 0, Qt::AlignVCenter);
auto roomNameLayout = new QHBoxLayout;
roomNameLayout->setObjectName(u"roomNameLayout"_s);
roomNameLayout->setContentsMargins({});
- roomNameLayout->setSpacing(0);
- infoLayout->addLayout(roomNameLayout);
+ roomNameLayout->setSpacing(6);
+ headerLayout->addLayout(roomNameLayout, 1);
mRoomName->setObjectName(u"mRoomName"_s);
mRoomName->setTextInteractionFlags(Qt::TextBrowserInteraction);
- roomNameLayout->addWidget(mRoomName);
+ // Render the name as rich text (bold, via setRoomName) so it shares the topic label's text
+ // metrics: both then carry the same rich-text document margin, so a plain top alignment
+ // lines their first lines up exactly — on a single row and when the topic wraps below.
+ mRoomName->setTextFormat(Qt::RichText);
+ mRoomName->setAlignment(Qt::AlignLeft | Qt::AlignTop);
+ roomNameLayout->addWidget(mRoomName, 0, Qt::AlignTop);
mRoomName->setVisible(false);
+ // Centre the first text line against the button band without centring the complete topic.
+ const int nameTopMargin = qMax(0, (mFavoriteButton->sizeHint().height() - mRoomName->fontMetrics().height()) / 2);
+ roomNameLayout->setContentsMargins(0, nameTopMargin, 0, 0);
mTeamName->setObjectName(u"mTeamName"_s);
- roomNameLayout->addWidget(mTeamName);
+ roomNameLayout->addWidget(mTeamName, 0, Qt::AlignTop);
mTeamName->setVisible(false);
- roomNameLayout->addStretch(1);
+ // The topic shares the room-name row rather than taking a line of its own, reclaiming the
+ // empty space that used to sit to the right of the name. A short topic stays on that line;
+ // a long topic, an announcement, or an expanded topic wraps below as before. Reusing
+ // RoomHeaderLabel keeps its rich-text links, width squeezing and "Show More" expansion.
mRoomHeaderLabel->setObjectName(u"mRoomHeaderLabel"_s);
- infoLayout->addWidget(mRoomHeaderLabel);
+ // Anchor the topic text to the top of its cell (QLabel centres vertically by default): the
+ // cell can be a touch taller than the name, and centring would drop the topic below the
+ // top-aligned name. Top-anchoring both keeps their first lines level.
+ mRoomHeaderLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
+ roomNameLayout->addWidget(mRoomHeaderLabel, 1);
auto buttonLayout = new QHBoxLayout;
buttonLayout->setObjectName(u"buttonLayout"_s);
buttonLayout->setContentsMargins({});
buttonLayout->setSpacing(0);
headerLayout->addLayout(buttonLayout);
+ headerLayout->setAlignment(buttonLayout, Qt::AlignTop);
mAIActionButton->setAutoRaise(true);
mAIActionButton->setObjectName(u"mAIActionButton"_s);
mAIActionButton->setToolTip(i18nc("@info:tooltip", "AI Actions"));
mAIActionButton->setPopupMode(QToolButton::InstantPopup);
mAIActionButton->setIcon(QIcon::fromTheme(u"irc-operator"_s));
- buttonLayout->addWidget(mAIActionButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mAIActionButton, 0, Qt::AlignVCenter);
mAIActionButton->setMenu(new QMenu(mAIActionButton));
// Disable by default
mAIActionButton->hide();
@@ -149,7 +172,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
mCallButton->setAccessibleName(i18n("Call"));
#endif
- buttonLayout->addWidget(mCallButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mCallButton, 0, Qt::AlignVCenter);
connect(mCallButton, &QToolButton::clicked, this, &RoomHeaderWidget::callRequested);
mChannelInfoButton->setAutoRaise(true);
@@ -160,7 +183,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
mChannelInfoButton->setAccessibleName(i18n("Channel's Information"));
#endif
- buttonLayout->addWidget(mChannelInfoButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mChannelInfoButton, 0, Qt::AlignVCenter);
connect(mChannelInfoButton, &QToolButton::clicked, this, &RoomHeaderWidget::channelInfoRequested);
mTeamChannelsButton->setAutoRaise(true);
@@ -171,7 +194,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
mTeamChannelsButton->setAccessibleName(i18n("Team Channels"));
#endif
- buttonLayout->addWidget(mTeamChannelsButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mTeamChannelsButton, 0, Qt::AlignVCenter);
connect(mTeamChannelsButton, &QToolButton::clicked, this, &RoomHeaderWidget::teamChannelsRequested);
mListOfUsersButton->setAutoRaise(true);
@@ -183,7 +206,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
mListOfUsersButton->setAccessibleName(i18n("Show List of Users"));
#endif
- buttonLayout->addWidget(mListOfUsersButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mListOfUsersButton, 0, Qt::AlignVCenter);
connect(mListOfUsersButton, &QToolButton::clicked, this, &RoomHeaderWidget::listOfUsersChanged);
mSearchMessageButton->setAutoRaise(true);
@@ -195,7 +218,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
#ifndef QT_NO_ACCESSIBILITY
mSearchMessageButton->setAccessibleName(i18n("Search Messages…"));
#endif
- buttonLayout->addWidget(mSearchMessageButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mSearchMessageButton, 0, Qt::AlignVCenter);
connect(mSearchMessageButton, &QToolButton::clicked, this, &RoomHeaderWidget::searchMessageRequested);
for (PluginTool *plugin : std::as_const(plugins)) {
if (plugin->enabled()) {
@@ -226,7 +249,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
} else {
connect(mPluginButton, &QToolButton::clicked, interface, &PluginToolInterface::activateRequested);
}
- buttonLayout->addWidget(mPluginButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mPluginButton, 0, Qt::AlignVCenter);
}
}
}
@@ -235,7 +258,7 @@ RoomHeaderWidget::RoomHeaderWidget(QWidget *parent)
mChannelActionButton->setObjectName(u"mChannelAction"_s);
mChannelActionButton->setPopupMode(QToolButton::InstantPopup);
mChannelActionButton->setIcon(QIcon::fromTheme(u"irc-operator"_s));
- buttonLayout->addWidget(mChannelActionButton, 0, Qt::AlignTop);
+ buttonLayout->addWidget(mChannelActionButton, 0, Qt::AlignVCenter);
mChannelActionPopupMenu = new ChannelActionPopupMenu(mChannelActionButton);
mChannelActionPopupMenu->setObjectName(u"mChannelActionPopupMenu"_s);
@@ -271,13 +294,16 @@ void RoomHeaderWidget::setTeamRoomInfo(const Room::TeamRoomInfo &info)
void RoomHeaderWidget::setRoomName(const QString &name)
{
- mRoomName->setText(name);
+ mRoomNamePlain = name;
+ // Bold via markup (the label is rich text so it matches the topic's metrics); keep the
+ // plain name for roomName().
+ mRoomName->setText(name.isEmpty() ? QString() : u"<b>%1</b>"_s.arg(name.toHtmlEscaped()));
mRoomName->setVisible(!name.isEmpty());
}
QString RoomHeaderWidget::roomName() const
{
- return mRoomName->text();
+ return mRoomNamePlain;
}
void RoomHeaderWidget::setRoomAnnouncement(const QString &name)
diff --git a/src/widgets/room/roomheaderwidget.h b/src/widgets/room/roomheaderwidget.h
index 2f0dfa5b46..d813faf112 100644
--- a/src/widgets/room/roomheaderwidget.h
+++ b/src/widgets/room/roomheaderwidget.h
@@ -104,4 +104,5 @@ private:
ActionButtonsGenerator *const mActionButtonsGenerator;
QPointer<RocketChatAccount> mCurrentRocketChatAccount;
QPointer<Room> mRoom;
+ QString mRoomNamePlain;
};
diff --git a/src/widgets/room/roomwidget.cpp b/src/widgets/room/roomwidget.cpp
index e0ff12efef..1cbd12e5b9 100644
--- a/src/widgets/room/roomwidget.cpp
+++ b/src/widgets/room/roomwidget.cpp
@@ -100,12 +100,13 @@ RoomWidget::RoomWidget(QWidget *parent)
auto mainLayout = new QVBoxLayout(this);
mainLayout->setObjectName(u"mainLayout"_s);
mainLayout->setContentsMargins({});
+ mainLayout->setSpacing(0);
mRoomHeaderWidget->setObjectName(u"mRoomHeaderWidget"_s);
mainLayout->addWidget(mRoomHeaderWidget);
auto roomWidget = new QWidget(this);
- mainLayout->addWidget(roomWidget);
+ mainLayout->addWidget(roomWidget, 1);
mRoomWidgetLayout = new QVBoxLayout(roomWidget);
mRoomWidgetLayout->setObjectName(u"roomWidgetLayout"_s);
mRoomWidgetLayout->setContentsMargins({});