Files
OmaPhoto/src/UI/ProjectTabs.cpp
T

338 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "UI/ProjectTabs.h"
#include "IO/ImageFileDrop.h"
#include <QApplication>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QEvent>
#include <QFontMetricsF>
#include <QHBoxLayout>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QStyleOptionToolButton>
#include <QStylePainter>
namespace {
QFont tabFont(bool active)
{
QFont font;
font.setPixelSize(12);
font.setWeight(active ? QFont::DemiBold : QFont::Medium);
return font;
}
// Swift's leading label: text from the left, 8 kept after.
class TabTitle : public QToolButton {
public:
using QToolButton::QToolButton;
protected:
void paintEvent(QPaintEvent *) override
{
QStylePainter painter(this);
QStyleOptionToolButton option;
initStyleOption(&option);
option.text.clear();
painter.drawComplexControl(QStyle::CC_ToolButton, option);
painter.setPen(palette().color(isEnabled() ? QPalette::Active : QPalette::Disabled, QPalette::ButtonText));
painter.drawText(rect().adjusted(0, 0, -8, 0), Qt::AlignLeft | Qt::AlignVCenter, text());
}
};
// Swift's projectTabLabelWidth: the title's own width, 35 to 155.
int labelWidth(const QString &title, bool active, bool modified)
{
const double titleWidth = QFontMetricsF(tabFont(active)).horizontalAdvance(title);
return int(std::min(155.0, std::max(35.0, std::ceil(titleWidth) + (modified ? 10 : 0))));
}
}
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"));
m_select->setAutoRaise(true);
m_select->setFocusPolicy(Qt::NoFocus);
m_close->setObjectName(QStringLiteral("closeTab"));
m_close->setAutoRaise(true);
m_close->setFocusPolicy(Qt::NoFocus);
m_close->setText(QStringLiteral("×"));
m_close->setFixedSize(16, 28);
auto *row = new QHBoxLayout(this);
row->setContentsMargins(11, 0, 5, 0);
row->setSpacing(0);
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.
connect(&this->tab->session, &EditorSession::changed, this, &ProjectTabButton::synchronize);
synchronize();
}
void ProjectTabButton::synchronize()
{
const bool active = m_workspace.selectedID() == tab->id;
// The unsaved dot leads; fixed widths move later tabs only.
const bool modified = tab->session.isModified();
const int label = labelWidth(tab->title(), active, modified);
const QString dot = modified ? QStringLiteral("● ") : QString();
const QFontMetrics metrics(tabFont(active));
m_select->setText(dot + metrics.elidedText(tab->title(), Qt::ElideRight, label - (modified ? 10 : 0)));
m_select->setFont(tabFont(active));
// Trailing 8, or 11 closed up; a dot outgrows 10.
const bool closable = m_workspace.canClose(*tab);
m_select->setFixedWidth(label + (closable ? 8 : 11) + (modified ? metrics.horizontalAdvance(dot) - 10 : 0));
layout()->setContentsMargins(11, 0, closable ? 5 : 0, 0);
m_close->setVisible(closable);
m_select->setToolTip(tab->title());
m_select->setEnabled(m_workspace.canSwitch() || active);
m_close->setToolTip(QStringLiteral("Close %1").arg(tab->title()));
m_close->setAccessibleName(QStringLiteral("Close %1").arg(tab->title()));
m_close->setEnabled(m_workspace.canSwitch());
// The capsule repaints with the buttons it holds.
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
{
const bool layer = data.hasFormat(ProjectWorkspace::layerType);
if ((layer && QApplication::keyboardModifiers().testFlag(Qt::AltModifier)) || !(layer || ImageFileDrop::holdsImages(data)))
return false;
return m_workspace.canSwitch() && m_workspace.canReceiveDrag(data, tab->id);
}
void ProjectTabButton::setTargeted(bool targeted)
{
m_targeted = targeted;
m_select->setToolTip(targeted ? QStringLiteral("Add to %1").arg(tab->title()) : tab->title());
QWidget::update();
}
void ProjectTabButton::dragEnterEvent(QDragEnterEvent *event)
{
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
setTargeted(true);
}
void ProjectTabButton::dragMoveEvent(QDragMoveEvent *event)
{
if (!acceptsDrop(*event->mimeData())) {
event->ignore();
return;
}
event->setDropAction(Qt::CopyAction);
event->accept();
}
void ProjectTabButton::dragLeaveEvent(QDragLeaveEvent *)
{
setTargeted(false);
}
void ProjectTabButton::dropEvent(QDropEvent *event)
{
setTargeted(false);
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
m_workspace.receiveProviders(*event->mimeData(), tab->id);
}
bool ProjectTabButton::isActive() const
{
return property("active").toBool();
}
void ProjectTabButton::paintEvent(QPaintEvent *)
{
// A capsule: brighter under the front tab.
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const bool active = property("active").toBool();
QColor fill = palette().color(QPalette::WindowText), edge = fill;
fill.setAlphaF(active ? 0.12 : 0.035);
edge.setAlphaF(active ? 0.22 : 0.08);
// A drop target takes the accent, filled and rimmed.
if (m_targeted) {
fill = palette().color(QPalette::Highlight);
fill.setAlphaF(0.3);
edge = palette().color(QPalette::Highlight);
}
painter.setPen(QPen(edge, m_targeted ? 2 : 1));
painter.setBrush(fill);
const double inset = m_targeted ? 1 : 0.5;
painter.drawRoundedRect(QRectF(inset, inset, width() - 2 * inset, height() - 2 * inset), 14, 14);
}
NewCanvasButton::NewCanvasButton(ProjectWorkspace &workspace, QWidget *parent) : QToolButton(parent), m_workspace(workspace)
{
setObjectName(QStringLiteral("newCanvasButton"));
setAcceptDrops(true);
}
// Any tab's layer or files, for a fresh tab.
bool NewCanvasButton::acceptsDrop(const QMimeData &data) const
{
const bool layer = data.hasFormat(ProjectWorkspace::layerType);
if ((layer && QApplication::keyboardModifiers().testFlag(Qt::AltModifier)) || !(layer || ImageFileDrop::holdsImages(data)))
return false;
return m_workspace.canSwitch() && m_workspace.canReceiveDrag(data, std::nullopt);
}
void NewCanvasButton::setTargeted(bool targeted)
{
m_targeted = targeted;
setToolTip(targeted ? QStringLiteral("Open in a new project tab") : defaultAction()->toolTip());
update();
}
void NewCanvasButton::paintEvent(QPaintEvent *event)
{
QToolButton::paintEvent(event);
if (!m_targeted)
return;
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(QPen(palette().color(QPalette::Highlight), 2));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(QRectF(rect()).adjusted(1, 1, -1, -1), 6, 6);
}
void NewCanvasButton::dragEnterEvent(QDragEnterEvent *event)
{
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
setTargeted(true);
}
void NewCanvasButton::dragMoveEvent(QDragMoveEvent *event)
{
if (!acceptsDrop(*event->mimeData())) {
event->ignore();
return;
}
event->setDropAction(Qt::CopyAction);
event->accept();
}
void NewCanvasButton::dragLeaveEvent(QDragLeaveEvent *)
{
setTargeted(false);
}
void NewCanvasButton::dropEvent(QDropEvent *event)
{
setTargeted(false);
if (!acceptsDrop(*event->mimeData()))
return;
event->setDropAction(Qt::CopyAction);
event->accept();
m_workspace.receiveProviders(*event->mimeData(), std::nullopt);
}
OverflowTabsPill::OverflowTabsPill(ProjectWorkspace &workspace, QWidget *parent) : QToolButton(parent), m_workspace(workspace)
{
setObjectName(QStringLiteral("projectTabsOverflow"));
setFocusPolicy(Qt::NoFocus);
QFont font;
font.setPixelSize(12);
font.setWeight(QFont::Medium);
setFont(font);
connect(this, &QToolButton::pressed, this, &OverflowTabsPill::showMenu);
}
double OverflowTabsPill::pillWidth(int hiddenCount)
{
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 OverflowTabsPill::setHiddenIDs(std::vector<QUuid> hiddenIDs)
{
m_hiddenIDs = std::move(hiddenIDs);
const QString label = projectTabOverflowLabel(int(m_hiddenIDs.size()));
setText(label);
setToolTip(label);
setAccessibleName(label);
update();
}
// Below the pill: hidden tabs in order, unsaved marked.
void OverflowTabsPill::showMenu()
{
setDown(false);
if (!m_workspace.canSwitch())
return;
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);
}