Project tabs drag to reorder; tabs that don't fit gather in a menu (13.14)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 16:22:27 -04:00
1 parent 0616f9b006
commit 3f2b99f045
15 files changed
+957 -138

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
#include "UI/ProjectTabLayout.h"
#include <algorithm>
double ProjectTabOverflow::contentWidth() const
{
// A pill always has a tab after it.
return visible.empty() ? 0 : visible.back().x + visible.back().width;
}
QString projectTabOverflowLabel(int hiddenCount)
{
return hiddenCount == 1 ? QStringLiteral("1 more tab") : QStringLiteral("%1 more tabs").arg(hiddenCount);
}
ProjectTabOverflow projectTabOverflow(const std::vector<QUuid> &order, const std::map<QUuid, double> &widths, QUuid selectedID,
double availableWidth, const std::function<double(int)> &pillWidth)
{
if (order.empty())
return {};
const auto span = [&](auto first, auto last) {
double total = 0;
for (auto id = first; id != last; ++id)
total += widths.at(*id);
// Never asked of no tabs.
return total + projectTabSpacing * double(std::distance(first, last) - 1);
};
const auto place = [&](const std::vector<QUuid> &ids, double x) {
std::vector<ProjectTabSlot> placed;
for (const QUuid &id : ids) {
placed.push_back({id, x, widths.at(id)});
x += widths.at(id) + projectTabSpacing;
}
return placed;
};
// Not yet measured, or everything fits: no pill.
if (availableWidth <= 0 || span(order.begin(), order.end()) <= availableWidth)
return {place(order, 0), {}, std::nullopt};
qsizetype shown = qsizetype(order.size());
while (shown > 1) {
const qsizetype hidden = qsizetype(order.size()) - shown;
if (pillWidth(int(hidden)) + projectTabSpacing + span(order.end() - shown, order.end()) <= availableWidth)
break;
shown -= 1;
}
std::vector<QUuid> visibleIDs(order.end() - shown, order.end()), hiddenIDs(order.begin(), order.end() - shown);
if (const auto selected = std::ranges::find(hiddenIDs, selectedID); selected != hiddenIDs.end()) {
hiddenIDs.erase(selected);
hiddenIDs.push_back(visibleIDs[0]);
visibleIDs[0] = selectedID;
// A wider selected tab: the tabs after it make room.
while (visibleIDs.size() > 1 && pillWidth(int(hiddenIDs.size())) + projectTabSpacing + span(visibleIDs.begin(), visibleIDs.end()) > availableWidth) {
hiddenIDs.push_back(visibleIDs[1]);
visibleIDs.erase(visibleIDs.begin() + 1);
}
}
const double pill = pillWidth(int(hiddenIDs.size()));
return {place(visibleIDs, pill + projectTabSpacing), hiddenIDs, ProjectTabPillSlot{0, pill}};
}
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <QString>
#include <QUuid>
#include <functional>
#include <map>
#include <optional>
#include <vector>
// Between pills, and after the overflow pill.
inline constexpr double projectTabSpacing = 6;
// A tab's place in the strip.
struct ProjectTabSlot {
QUuid id;
double x;
double width;
bool operator==(const ProjectTabSlot &) const = default;
};
// The overflow pill's place: it is no tab.
struct ProjectTabPillSlot {
double x;
double width;
bool operator==(const ProjectTabPillSlot &) const = default;
};
// What the strip draws: tabs that fit, the rest.
struct ProjectTabOverflow {
std::vector<ProjectTabSlot> visible;
std::vector<QUuid> hiddenIDs;
std::optional<ProjectTabPillSlot> pill;
double contentWidth() const;
bool operator==(const ProjectTabOverflow &) const = default;
};
// "N more tabs", singular for one.
QString projectTabOverflowLabel(int hiddenCount);
// Drops tabs from the front until the rest fit.
ProjectTabOverflow projectTabOverflow(const std::vector<QUuid> &order, const std::map<QUuid, double> &widths, QUuid selectedID,
double availableWidth, const std::function<double(int)> &pillWidth);
+213
View File
@@ -0,0 +1,213 @@
#include "UI/ProjectTabs.h"
#include <QPropertyAnimation>
#include <algorithm>
#include <cmath>
ProjectTabStrip::ProjectTabStrip(ProjectWorkspace &workspace, QWidget *parent)
: QWidget(parent), m_workspace(workspace), m_pill(new OverflowTabsPill(workspace, this))
{
setObjectName(QStringLiteral("projectTabs"));
setAccessibleName(QStringLiteral("Project tabs"));
setFixedHeight(34);
// The toolbar's free width; the toolbar shows through.
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
connect(&m_workspace, &ProjectWorkspace::changed, this, &ProjectTabStrip::synchronize);
synchronize();
}
// Every tab in a row, as if unmeasured.
QSize ProjectTabStrip::sizeHint() const
{
std::vector<QUuid> order;
for (const std::shared_ptr<ProjectTab> &tab : m_workspace.tabs())
order.push_back(tab->id);
return {int(std::ceil(projectTabOverflow(order, widths(), m_workspace.selectedID(), 0, &OverflowTabsPill::pillWidth).contentWidth())), 34};
}
QSize ProjectTabStrip::minimumSizeHint() const
{
return {0, 34};
}
QList<ProjectTabButton *> ProjectTabStrip::buttons() const
{
return QList<ProjectTabButton *>(m_buttons.begin(), m_buttons.end());
}
void ProjectTabStrip::synchronize()
{
// Buttons follow the tabs: kept where the tab stays.
std::vector<ProjectTabButton *> kept;
for (const std::shared_ptr<ProjectTab> &tab : m_workspace.tabs()) {
const auto found = std::ranges::find(m_buttons, tab, &ProjectTabButton::tab);
ProjectTabButton *button = nullptr;
if (found == m_buttons.end()) {
const QUuid id = tab->id;
button = new ProjectTabButton(m_workspace, tab, [this, id](TabDragPhase phase, double translation) { handleReorder(id, phase, translation); }, this);
// A title or dot changes the widths.
connect(&tab->session, &EditorSession::changed, button, [this] { layOut(); });
} else {
button = *found;
m_buttons.erase(found);
}
button->synchronize();
kept.push_back(button);
}
for (ProjectTabButton *gone : m_buttons)
delete gone;
m_buttons = std::move(kept);
// A drag whose tab closed under it is over.
if (m_reorder && !m_workspace.tab(m_reorder->id))
m_reorder.reset();
layOut();
}
std::map<QUuid, double> ProjectTabStrip::widths() const
{
std::map<QUuid, double> result;
for (const ProjectTabButton *button : m_buttons)
result.insert({button->tab->id, button->sizeHint().width()});
return result;
}
ProjectTabOverflow ProjectTabStrip::overflow() const
{
std::vector<QUuid> order;
for (const std::shared_ptr<ProjectTab> &tab : m_workspace.tabs())
order.push_back(tab->id);
return projectTabOverflow(order, widths(), m_workspace.selectedID(), width(), &OverflowTabsPill::pillWidth);
}
void ProjectTabStrip::layOut(bool animated)
{
const ProjectTabOverflow layout = overflow();
m_pill->setVisible(layout.pill.has_value());
if (layout.pill) {
m_pill->setHiddenIDs(layout.hiddenIDs);
m_pill->setGeometry(int(layout.pill->x), 3, int(layout.pill->width), 28);
}
for (ProjectTabButton *button : m_buttons) {
const auto slot = std::ranges::find(layout.visible, button->tab->id, &ProjectTabSlot::id);
button->setVisible(slot != layout.visible.end());
if (slot == layout.visible.end())
continue;
button->resize(int(slot->width), 28);
const QPoint target(int(std::round(renderX(*slot, layout.contentWidth()))), 3);
// The dragged tab follows the pointer; the others ease aside.
auto *slide = button->findChild<QPropertyAnimation *>(QString(), Qt::FindDirectChildrenOnly);
const bool eases = animated && !(m_reorder && m_reorder->id == button->tab->id);
if (slide && (!eases || slide->endValue() != QVariant(target)))
delete std::exchange(slide, nullptr);
if (!eases)
button->move(target);
else if (!slide && button->pos() != target) {
slide = new QPropertyAnimation(button, "pos", button);
slide->setDuration(150);
slide->setEasingCurve(QEasingCurve::OutQuad);
slide->setEndValue(target);
slide->start(QAbstractAnimation::DeleteWhenStopped);
}
}
updateGeometry();
}
double ProjectTabStrip::renderX(const ProjectTabSlot &slot, double contentWidth) const
{
if (!m_reorder)
return slot.x;
if (slot.id == m_reorder->id)
return std::min(std::max(m_reorder->originX + m_reorder->translation, m_reorder->startX), std::max(m_reorder->startX, contentWidth - slot.width));
const auto index = std::ranges::find(m_reorder->others, slot.id);
if (index == m_reorder->others.end())
return slot.x;
const double x = m_reorder->compactedX.at(slot.id);
return index - m_reorder->others.begin() >= m_reorder->targetIndex ? x + m_reorder->widths.at(m_reorder->id) + projectTabSpacing : x;
}
// The gap nearest the dragged tab: where it lands.
int ProjectTabStrip::Reorder::nearestSlot() const
{
const double x = originX + translation;
std::vector<double> gaps;
for (const QUuid &other : others)
gaps.push_back(compactedX.at(other));
gaps.push_back(others.empty() ? startX : compactedX.at(others.back()) + widths.at(others.back()) + projectTabSpacing);
return int(std::ranges::min_element(gaps, {}, [x](double slot) { return std::abs(slot - x); }) - gaps.begin());
}
void ProjectTabStrip::handleReorder(QUuid id, TabDragPhase phase, double translation)
{
if (phase == TabDragPhase::changed) {
// Another tab's drag lost its release, as Swift's timer finds.
if (m_reorder && m_reorder->id != id)
m_reorder.reset();
if (!m_reorder) {
if (!m_workspace.canSwitch() || std::abs(translation) < 3)
return;
// Dragging a tab selects it, as in Safari.
m_workspace.select(id);
m_reorder = makeReorder(id);
// Above the tabs it crosses.
for (ProjectTabButton *button : m_buttons) {
if (button->tab->id == id)
button->raise();
}
}
m_reorder->translation = translation;
m_reorder->targetIndex = m_reorder->nearestSlot();
layOut(true);
return;
}
// Busy by the drop: the tabs go back.
if (m_reorder && m_reorder->id == id && m_workspace.canSwitch())
commitReorder(*m_reorder);
m_reorder.reset();
layOut(true);
}
ProjectTabStrip::Reorder ProjectTabStrip::makeReorder(QUuid id) const
{
const ProjectTabOverflow layout = overflow();
Reorder state{.id = id, .others = {}, .widths = widths(), .compactedX = {}, .startX = layout.visible.empty() ? 0 : layout.visible.front().x,
.originX = 0};
double x = state.startX;
state.originX = state.startX;
for (const ProjectTabSlot &slot : layout.visible) {
if (slot.id == id) {
state.originX = slot.x;
continue;
}
state.others.push_back(slot.id);
state.compactedX.insert({slot.id, x});
x += state.widths.at(slot.id) + projectTabSpacing;
}
return state;
}
// Only the tabs visible as the drag began ever move.
void ProjectTabStrip::commitReorder(const Reorder &state)
{
std::vector<QUuid> order;
for (const std::shared_ptr<ProjectTab> &tab : m_workspace.tabs())
order.push_back(tab->id);
// A tab closed mid-drag has no place.
const auto position = [&order](QUuid id) -> std::optional<qsizetype> {
const auto found = std::ranges::find(order, id);
return found == order.end() ? std::nullopt : std::optional(found - order.begin());
};
const qsizetype from = position(state.id).value();
qsizetype target = from;
if (const auto neighbor = state.targetIndex < int(state.others.size()) ? position(state.others[size_t(state.targetIndex)]) : std::nullopt)
target = *neighbor;
else if (const auto last = state.others.empty() ? std::nullopt : position(state.others.back()))
target = *last + 1;
if (from < target)
target -= 1;
m_workspace.moveTab(state.id, int(target));
}
void ProjectTabStrip::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
layOut();
}
+87 -71
View File
@@ -6,11 +6,12 @@
#include <QEvent>
#include <QFontMetricsF>
#include <QHBoxLayout>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QScrollBar>
#include <QPainterPath>
#include <QStyleOptionToolButton>
#include <QStylePainter>
#include <QTimer>
namespace {
QFont tabFont(bool active)
@@ -47,8 +48,10 @@ int labelWidth(const QString &title, bool active, bool modified)
}
}
ProjectTabButton::ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<ProjectTab> tab, QWidget *parent)
: QWidget(parent), tab(std::move(tab)), m_workspace(workspace), m_select(new TabTitle(this)), m_close(new QToolButton(this))
ProjectTabButton::ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<ProjectTab> tab, std::function<void(TabDragPhase, double)> onReorder,
QWidget *parent)
: QWidget(parent), tab(std::move(tab)), m_workspace(workspace), m_onReorder(std::move(onReorder)), m_select(new TabTitle(this)),
m_close(new QToolButton(this))
{
// 28 high: the strip's 34 less its margins.
m_select->setObjectName(QStringLiteral("selectTab"));
@@ -65,6 +68,7 @@ ProjectTabButton::ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<
row->addWidget(m_select);
row->addWidget(m_close);
setAcceptDrops(true);
m_select->installEventFilter(this);
connect(m_select, &QToolButton::clicked, this, [this] { m_workspace.select(this->tab->id); });
connect(m_close, &QToolButton::clicked, this, [this] { m_workspace.close(this->tab->id); });
// The strip syncs every button on the workspace's news.
@@ -93,6 +97,26 @@ void ProjectTabButton::synchronize()
setProperty("active", active);
}
// A press moved three points reorders; measured in the window.
bool ProjectTabButton::eventFilter(QObject *watched, QEvent *event)
{
const auto *mouse = static_cast<QMouseEvent *>(event);
if (event->type() == QEvent::MouseButtonPress && mouse->button() == Qt::LeftButton) {
m_press = mouse->globalPosition();
m_dragging = false;
} else if (event->type() == QEvent::MouseMove && m_press && mouse->buttons().testFlag(Qt::LeftButton)) {
m_dragging = m_dragging || QLineF(*m_press, mouse->globalPosition()).length() >= 3;
if (m_dragging)
m_onReorder(TabDragPhase::changed, mouse->globalPosition().x() - m_press->x());
} else if (event->type() == QEvent::MouseButtonRelease && mouse->button() == Qt::LeftButton && m_press) {
const double translation = mouse->globalPosition().x() - m_press->x();
m_press.reset();
if (std::exchange(m_dragging, false))
m_onReorder(TabDragPhase::ended, translation);
}
return QWidget::eventFilter(watched, event);
}
// Another tab's layer, unless Alt duplicates it, or files.
bool ProjectTabButton::acceptsDrop(const QMimeData &data) const
{
@@ -237,82 +261,74 @@ void NewCanvasButton::dropEvent(QDropEvent *event)
m_workspace.receiveProviders(*event->mimeData(), std::nullopt);
}
ProjectTabStrip::ProjectTabStrip(ProjectWorkspace &workspace, QWidget *parent)
: QScrollArea(parent), m_workspace(workspace), m_row(new QWidget(this))
OverflowTabsPill::OverflowTabsPill(ProjectWorkspace &workspace, QWidget *parent) : QToolButton(parent), m_workspace(workspace)
{
setObjectName(QStringLiteral("projectTabs"));
setAccessibleName(QStringLiteral("Project tabs"));
setFixedHeight(34);
setFrameShape(QFrame::NoFrame);
// The toolbar shows through; a stylesheet would freeze the palette.
setAutoFillBackground(false);
viewport()->setAutoFillBackground(false);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
auto *row = new QHBoxLayout(m_row);
row->setContentsMargins(0, 3, 0, 3);
row->setSpacing(6);
row->addStretch(1);
setWidget(m_row);
// setWidget turns the row's fill on; the toolbar shows through.
m_row->setAutoFillBackground(false);
m_row->installEventFilter(this);
connect(&m_workspace, &ProjectWorkspace::changed, this, &ProjectTabStrip::synchronize);
synchronize();
setObjectName(QStringLiteral("projectTabsOverflow"));
setFocusPolicy(Qt::NoFocus);
QFont font;
font.setPixelSize(12);
font.setWeight(QFont::Medium);
setFont(font);
connect(this, &QToolButton::pressed, this, &OverflowTabsPill::showMenu);
}
QList<ProjectTabButton *> ProjectTabStrip::buttons() const
double OverflowTabsPill::pillWidth(int hiddenCount)
{
QList<ProjectTabButton *> result;
for (int index = 0; index < m_row->layout()->count(); ++index) {
if (auto *button = qobject_cast<ProjectTabButton *>(m_row->layout()->itemAt(index)->widget()))
result << button;
}
return result;
QFont font;
font.setPixelSize(12);
font.setWeight(QFont::Medium);
// Leading, gap before the chevron, chevron, trailing.
return std::ceil(QFontMetricsF(font).horizontalAdvance(projectTabOverflowLabel(hiddenCount))) + 11 + 4 + 10 + 11;
}
void ProjectTabStrip::synchronize()
void OverflowTabsPill::setHiddenIDs(std::vector<QUuid> hiddenIDs)
{
// Buttons follow the tabs: kept where the tab stays.
auto *row = static_cast<QHBoxLayout *>(m_row->layout());
QList<ProjectTabButton *> shown = buttons();
int position = 0;
for (const std::shared_ptr<ProjectTab> &tab : m_workspace.tabs()) {
const auto kept = std::find_if(shown.begin(), shown.end(), [&](ProjectTabButton *button) { return button->tab == tab; });
ProjectTabButton *button = kept == shown.end() ? new ProjectTabButton(m_workspace, tab, m_row) : *kept;
if (kept != shown.end())
shown.erase(kept);
row->removeWidget(button);
row->insertWidget(position, button);
button->show();
button->synchronize();
position += 1;
}
for (ProjectTabButton *gone : shown)
delete gone;
// The row is as wide as its tabs, never squeezed.
m_row->resize(m_row->sizeHint());
showFront();
m_hiddenIDs = std::move(hiddenIDs);
const QString label = projectTabOverflowLabel(int(m_hiddenIDs.size()));
setText(label);
setToolTip(label);
setAccessibleName(label);
update();
}
bool ProjectTabStrip::eventFilter(QObject *watched, QEvent *event)
// Below the pill: hidden tabs in order, unsaved marked.
void OverflowTabsPill::showMenu()
{
// A title changed size: the row takes its new width.
if (event->type() == QEvent::LayoutRequest)
m_row->resize(m_row->sizeHint());
return QScrollArea::eventFilter(watched, event);
}
// Only a new front tab scrolls into view.
void ProjectTabStrip::showFront()
{
if (std::exchange(m_shownFront, m_workspace.selectedID()) == m_workspace.selectedID())
setDown(false);
if (!m_workspace.canSwitch())
return;
// Posted, as Swift's: the row's layout request runs first.
QTimer::singleShot(0, this, [this] {
for (ProjectTabButton *button : buttons()) {
if (button->tab->id == m_workspace.selectedID())
ensureWidgetVisible(button, 0, 0);
}
});
auto *menu = new QMenu(this);
menu->setObjectName(QStringLiteral("projectTabsOverflowMenu"));
menu->setAttribute(Qt::WA_DeleteOnClose);
for (const QUuid &id : m_hiddenIDs) {
const std::shared_ptr<ProjectTab> tab = m_workspace.tab(id);
const QString title = (tab->session.isModified() ? QStringLiteral("• ") : QString()) + tab->title();
connect(menu->addAction(title), &QAction::triggered, this, [this, id] { m_workspace.select(id); });
}
menu->popup(mapToGlobal(QPoint(0, height() + 4)));
}
void OverflowTabsPill::paintEvent(QPaintEvent *)
{
// An inactive tab's capsule; the label dims while busy.
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QColor fill = palette().color(QPalette::WindowText), edge = fill;
fill.setAlphaF(0.035);
edge.setAlphaF(0.08);
painter.setPen(QPen(edge, 1));
painter.setBrush(fill);
painter.drawRoundedRect(QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5), 14, 14);
painter.setOpacity(m_workspace.canSwitch() ? 1 : 0.5);
painter.setPen(palette().color(QPalette::ButtonText));
painter.drawText(QRectF(11, 0, width() - 36, height()), Qt::AlignLeft | Qt::AlignVCenter, text());
// Swift's chevron.down, nine points, medium.
QPainterPath chevron;
const double left = width() - 21, middle = height() / 2.0;
chevron.moveTo(left + 1, middle - 2);
chevron.lineTo(left + 5, middle + 2);
chevron.lineTo(left + 9, middle - 2);
painter.setPen(QPen(palette().color(QPalette::ButtonText), 1.4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
painter.setBrush(Qt::NoBrush);
painter.drawPath(chevron);
}
+62 -9
View File
@@ -1,14 +1,18 @@
#pragma once
#include "Document/ProjectWorkspace.h"
#include <QScrollArea>
#include "UI/ProjectTabLayout.h"
#include <QToolButton>
#include <QWidget>
// Swift's TabDragPhase: a reorder drag goes on, or ends.
enum class TabDragPhase { changed, ended };
// One tab: its title, the unsaved dot, its close button.
class ProjectTabButton : public QWidget {
Q_OBJECT
public:
ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<ProjectTab> tab, QWidget *parent = nullptr);
ProjectTabButton(ProjectWorkspace &workspace, std::shared_ptr<ProjectTab> tab, std::function<void(TabDragPhase, double)> onReorder,
QWidget *parent = nullptr);
const std::shared_ptr<ProjectTab> tab;
// What the session and the workspace hold, shown.
@@ -16,6 +20,7 @@ public:
bool isActive() const;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
@@ -27,9 +32,13 @@ private:
void setTargeted(bool targeted);
ProjectWorkspace &m_workspace;
const std::function<void(TabDragPhase, double)> m_onReorder;
QToolButton *const m_select;
QToolButton *const m_close;
bool m_targeted = false;
// Swift's DragGesture: the press, in the window's space.
std::optional<QPointF> m_press;
bool m_dragging = false;
};
// Swift's NewProjectDropTarget: the New canvas button takes drops.
@@ -53,23 +62,67 @@ private:
bool m_targeted = false;
};
// The row of tabs, one a project, scrolling when long.
class ProjectTabStrip : public QScrollArea {
// Swift's OverflowTabsPill: the tabs that do not fit, a menu.
class OverflowTabsPill : public QToolButton {
Q_OBJECT
public:
explicit OverflowTabsPill(ProjectWorkspace &workspace, QWidget *parent = nullptr);
// Swift's projectTabOverflowPillWidth.
static double pillWidth(int hiddenCount);
void setHiddenIDs(std::vector<QUuid> hiddenIDs);
protected:
void paintEvent(QPaintEvent *event) override;
private:
void showMenu();
ProjectWorkspace &m_workspace;
std::vector<QUuid> m_hiddenIDs;
};
// The row of tabs; the rest gather in a menu.
class ProjectTabStrip : public QWidget {
Q_OBJECT
public:
explicit ProjectTabStrip(ProjectWorkspace &workspace, QWidget *parent = nullptr);
QList<ProjectTabButton *> buttons() const;
OverflowTabsPill *pill() const { return m_pill; }
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
// Swift's TabReorderState, taken once as the drag starts.
struct Reorder {
QUuid id;
std::vector<QUuid> others;
std::map<QUuid, double> widths;
std::map<QUuid, double> compactedX;
double startX;
double originX;
double translation = 0;
int targetIndex = 0;
int nearestSlot() const;
};
void synchronize();
void showFront();
std::map<QUuid, double> widths() const;
ProjectTabOverflow overflow() const;
// Places the pill and the tabs; others slide when `animated`.
void layOut(bool animated = false);
double renderX(const ProjectTabSlot &slot, double contentWidth) const;
void handleReorder(QUuid id, TabDragPhase phase, double translation);
Reorder makeReorder(QUuid id) const;
void commitReorder(const Reorder &state);
ProjectWorkspace &m_workspace;
QWidget *const m_row;
// Swift's lastSelectedID: only a new front tab scrolls.
std::optional<QUuid> m_shownFront;
OverflowTabsPill *const m_pill;
std::vector<ProjectTabButton *> m_buttons;
std::optional<Reorder> m_reorder;
};
+1 -4
View File
@@ -26,11 +26,8 @@ ProjectWorkspaceView::ProjectWorkspaceView(ProjectWorkspace &workspace, QWidget
auto *newButton = new NewCanvasButton(m_workspace, m_toolbar);
newButton->setDefaultAction(m_newCanvas);
m_toolbar->addWidget(newButton);
m_toolbar->addWidget(m_tabs);
// The strip takes the toolbar's free width; zooms stay right.
auto *spacer = new QWidget(m_toolbar);
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_toolbar->addWidget(spacer);
m_toolbar->addWidget(m_tabs);
m_fit = action(m_toolbar, QStringLiteral("Fit"), QStringLiteral("Fit canvas in window (Ctrl+0)"), QStringLiteral("fitCanvas"));
m_actualPixels = action(m_toolbar, QStringLiteral("100%"), QStringLiteral("Actual pixels (Ctrl+1)"), QStringLiteral("actualPixels"));
m_zoomIn = action(m_toolbar, QStringLiteral("+"), QStringLiteral("Zoom in (Ctrl++)"), QStringLiteral("zoomIn"));