Files
OmaPhoto/src/Document/EditorSession.cpp
T

384 lines
14 KiB
C++

#include "Document/EditorSession.h"
#include "Document/DocumentLimits.h"
#include "Document/Crop.h"
#include "Logging.h"
#include <cmath>
EditorSession::EditorSession(QObject *parent) : QObject(parent), m_busyTimer(this)
{
// A coarse timer may fire a twentieth early.
m_busyTimer.setTimerType(Qt::PreciseTimer);
m_busyTimer.setSingleShot(true);
m_busyTimer.setInterval(busyIndicatorDelay);
connect(&m_busyTimer, &QTimer::timeout, this, [this] {
m_showsBusy = true;
notify();
});
connect(&m_decoder, &QFutureWatcher<Decoded>::finished, this, &EditorSession::finishDecode);
connect(&m_photoshopReader, &QFutureWatcher<PhotoshopRead>::finished, this, &EditorSession::finishPhotoshopRead);
connect(&m_baker, &QFutureWatcher<Baked>::finished, this, &EditorSession::finishBake);
connect(&m_inverter, &QFutureWatcher<Inverted>::finished, this, &EditorSession::finishInvert);
connect(&m_wand, &QFutureWatcher<Wanded>::finished, this, &EditorSession::finishWand);
connect(&m_subject, &QFutureWatcher<Subjected>::finished, this, &EditorSession::finishSubject);
connect(&m_filterPreview, &QFutureWatcher<Filtered>::finished, this, &EditorSession::finishFilterPreview);
connect(&m_filterCommit, &QFutureWatcher<FilterMade>::finished, this, &EditorSession::finishFilterCommit);
connect(&m_cropCommit, &QFutureWatcher<Cropped>::finished, this, &EditorSession::finishCropCommit);
connect(&m_levelsHistogram, &QFutureWatcher<std::optional<LevelsHistogram>>::finished, this, &EditorSession::finishLevelsHistogram);
connect(&m_levelsPreview, &QFutureWatcher<std::optional<QImage>>::finished, this, &EditorSession::finishLevelsPreview);
connect(&m_levelsCommit, &QFutureWatcher<Leveled>::finished, this, &EditorSession::finishLevelsCommit);
connect(&m_hueSaturationTask, &QFutureWatcher<Adjusted>::finished, this, &EditorSession::finishHueSaturationPreview);
connect(&m_hueSaturationCommit, &QFutureWatcher<Adjusted>::finished, this, &EditorSession::finishHueSaturationCommit);
connect(&m_rasterCommit, &QFutureWatcher<RasterMade>::finished, this, &EditorSession::finishRasterCommit);
}
void EditorSession::setActiveLayerID(std::optional<QUuid> id)
{
// Another layer: its pixels are the target again.
if (id != m_activeLayerID)
m_isMaskSelected = false;
m_activeLayerID = id;
m_selectedLayerIDs = id ? QSet<QUuid>{*id} : QSet<QUuid>();
notify();
}
std::optional<ImageLayer> EditorSession::activeLayer() const
{
if (!m_document || !m_activeLayerID)
return std::nullopt;
const auto found = std::find_if(m_document->layers.begin(), m_document->layers.end(),
[&](const ImageLayer &layer) { return layer.id == *m_activeLayerID; });
return found == m_document->layers.end() ? std::nullopt : std::optional(*found);
}
void EditorSession::setIsProjectBusy(bool busy)
{
m_isProjectBusy = busy;
// Freed waiters each look again when their turn comes.
if (!busy) {
for (const std::function<void()> &waiter : std::exchange(m_projectWaiters, {}))
QMetaObject::invokeMethod(this, [this, waiter] { waitForProjectAccess(waiter); }, Qt::QueuedConnection);
}
resumeFileRequests();
updateBusyIndicator();
notify();
}
void EditorSession::setShowsNewDocument(bool shows)
{
m_showsNewDocument = shows;
resumeFileRequests();
notify();
}
void EditorSession::setShowsImporter(bool shows)
{
m_showsImporter = shows;
resumeFileRequests();
notify();
}
void EditorSession::setIsImporting(bool importing)
{
m_isImporting = importing;
resumeFileRequests();
notify();
}
void EditorSession::setImportError(std::optional<QString> error)
{
m_importError = std::move(error);
resumeFileRequests();
notify();
}
void EditorSession::setBrushError(std::optional<QString> error)
{
m_brushError = std::move(error);
notify();
}
void EditorSession::setRenamingLayerID(std::optional<QUuid> id)
{
m_renamingLayerID = id;
resumeFileRequests();
notify();
}
void EditorSession::setAdjustmentEditingID(std::optional<QUuid> id)
{
m_adjustmentEditingID = id;
resumeFileRequests();
notify();
}
void EditorSession::selectLayer(std::optional<QUuid> id)
{
dropEffectSelection();
if (id != m_activeLayerID && !finishText())
return;
if (m_brushStroke || m_warpStroke || m_levels)
return;
if (id != m_activeLayerID) {
commitTransform();
resolveGradient();
}
setActiveLayerID(id);
}
void EditorSession::selectTool(NavigationTool value)
{
if (m_tool != value && !finishText())
return;
if (m_isProjectBusy || m_brushStroke || m_warpStroke || m_levels)
return;
if (m_tool != value) {
commitTransform();
cancelCrop();
resolveGradient();
cancelLasso();
cancelShape();
}
// Each family's tip parks on leaving; sessions begin in 0.
const int from = tipFamily(m_tool), to = tipFamily(value);
if (from != to) {
const ParkedTip parked = m_parkedBrushTips.at(to);
m_parkedBrushTips[from] = ParkedTip{m_brushSettings.diameter, m_brushSettings.hardness, m_brushSettings.opacity};
m_brushSettings.diameter = parked.diameter;
m_brushSettings.hardness = parked.hardness;
m_brushSettings.opacity = parked.opacity;
}
m_tool = value;
// The Crop tool opens freely: the selection, else the canvas.
if (value == NavigationTool::crop && !m_cropRect && m_document) {
m_cropRatioChoice = QStringLiteral("Free");
const QRectF canvas(QPointF(0, 0), m_document->size());
m_cropRect = canvas;
if (m_document->selection && !m_document->selection->isEmpty()) {
// CoreGraphics' `integral`: whole pixels, rounded outwards.
const QRectF box = m_document->selection->path.boundingRect();
const QRectF whole = QRectF(QPointF(std::floor(box.left()), std::floor(box.top())), QPointF(std::ceil(box.right()), std::ceil(box.bottom())))
.intersected(canvas);
if (CropGeometry::valid(whole))
m_cropRect = whole;
}
}
notify();
}
int EditorSession::tipFamily(NavigationTool tool)
{
return tool == NavigationTool::cloneStamp ? 1 : tool == NavigationTool::blur ? 2 : 0;
}
// Tab: the tool's next mode; others arrive with their tools.
void EditorSession::cycleToolMode()
{
if (m_isProjectBusy || m_brushStroke || m_warpStroke)
return;
if (m_tool == NavigationTool::marquee)
toggleMarqueeKind();
else if (m_tool == NavigationTool::wand)
setWandMode(allWandModes[(size_t(m_wandMode) + 1) % allWandModes.size()]);
else if (m_tool == NavigationTool::lasso)
toggleLassoKind();
else if (m_tool == NavigationTool::shape)
toggleShapeKind();
else if (m_tool == NavigationTool::brush)
setBrushMode(m_brushMode == BrushToolMode::paint ? BrushToolMode::erase : BrushToolMode::paint);
else if (m_tool == NavigationTool::gradient)
setGradientSettings(GradientSettings{allGradientShapes[(size_t(m_gradientSettings.shape) + 1) % allGradientShapes.size()], m_gradientSettings.style,
m_gradientSettings.reversed, m_gradientSettings.opacity});
else if (m_tool == NavigationTool::blur)
setBlurMode(allBlurToolModes[(size_t(m_blurMode) + 1) % allBlurToolModes.size()]);
else if (m_tool == NavigationTool::spotHealing)
setSpotHealingMode(allSpotHealingModes[(size_t(m_spotHealingMode) + 1) % allSpotHealingModes.size()]);
else if (m_tool == NavigationTool::cloneStamp)
setCloneSettings(CloneSettings{m_cloneSettings.aligned, !m_cloneSettings.sampleAllLayers});
}
bool EditorSession::canUseHistory() const
{
return !m_selectionAmountOperation && !m_colorRange && !m_textDraft && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_warpStroke
&& !m_showsNewDocument && !m_showsImporter && !m_renamingLayerID && !m_importError && !m_transformEdit && !m_levels;
}
void EditorSession::undo()
{
// As Photoshop, the first Undo discards a pending gradient.
if (m_gradientEdit) {
cancelGradient();
return;
}
if (!canUndo())
return;
if (const std::optional<DocumentHistory::Snapshot> snapshot = history.undo())
restore(*snapshot);
}
void EditorSession::redo()
{
if (!canRedo())
return;
if (const std::optional<DocumentHistory::Snapshot> snapshot = history.redo())
restore(*snapshot);
}
void EditorSession::restore(const DocumentHistory::Snapshot &snapshot)
{
cancelCrop();
cancelGradient();
const auto canvas = [](const std::optional<CanvasDocument> &document) { return document ? std::optional(document->id) : std::nullopt; };
const bool changedCanvas = canvas(m_document) != canvas(snapshot.document);
// The mask stays the target while layer and mask remain.
const bool keepsMaskTarget = m_isMaskSelected && m_activeLayerID == snapshot.activeLayerID;
m_document = snapshot.document;
setActiveLayerID(snapshot.activeLayerID);
const std::optional<ImageLayer> active = activeLayer();
m_isMaskSelected = keepsMaskTarget && active && active->mask;
if (changedCanvas && m_document)
viewport.fit(m_document->size());
qCInfo(lcApp) << "restored a history snapshot of" << (m_document ? int(m_document->layers.size()) : 0) << "layers";
notify();
}
void EditorSession::beginEdit(const QString &name)
{
history.begin(name, m_document, m_activeLayerID);
// An open transaction closes undo and redo: observers must know.
notify();
}
void EditorSession::endEdit()
{
history.end(m_document, m_activeLayerID);
notify();
}
bool EditorSession::canEditLayers() const
{
return !m_selectionAmountOperation && !m_colorRange && !m_textDraft && m_document && !m_brushStroke && !m_warpStroke && !m_isProjectBusy && !m_isImporting
&& !m_showsNewDocument && !m_showsImporter
&& !m_renamingLayerID && !m_transformEdit && !m_cropRect && !m_gradientEdit && !m_filterEdit && !m_pixelMove && !m_levels
&& !m_hueSaturation && !m_adjustmentEditingID;
}
void EditorSession::insert(const ImportedImage &asset, std::optional<QPointF> centeredAt)
{
beginEdit(QStringLiteral("Import Image"));
if (!m_document) {
m_document = CanvasDocument(asset.size().width(), asset.size().height());
viewport.fit(m_document->size());
}
const QPointF center = centeredAt.value_or(QPointF(m_document->width / 2.0, m_document->height / 2.0));
ImageLayer layer(asset, QPointF(std::floor(center.x() - asset.size().width() / 2.0), std::floor(center.y() - asset.size().height() / 2.0)));
// Beside the active layer, or inside the active folder.
const std::optional<ImageLayer> active = activeLayer();
layer.parentID = active && active->isGroup ? m_activeLayerID : active ? active->parentID : std::nullopt;
if (layer.parentID)
m_collapsedGroupIDs.remove(*layer.parentID);
m_document->layers.push_back(layer);
setActiveLayerID(layer.id);
endEdit();
}
void EditorSession::createDocument(int width, int height, bool emptyLayer)
{
if (m_isProjectBusy || m_isImporting || width < 1 || width > DocumentLimits::maxSide || height < 1 || height > DocumentLimits::maxSide)
return;
commitTransform();
beginEdit(QStringLiteral("New Canvas"));
CanvasDocument document(width, height);
// File > New starts with a selected blank "Layer 1".
std::optional<QUuid> selected;
if (emptyLayer) {
document.layers.push_back(ImageLayer(QStringLiteral("Layer 1"), document.size()));
selected = document.layers.back().id;
}
m_document = document;
setActiveLayerID(selected);
m_renamingLayerID = std::nullopt;
viewport.fit(m_document->size());
m_showsNewDocument = false;
// A closed sheet or an ended rename frees file requests.
resumeFileRequests();
qCInfo(lcApp) << "new canvas" << width << "x" << height;
endEdit();
}
void EditorSession::fit()
{
if (!m_document)
return;
const CanvasViewport before = viewport;
viewport.fit(m_document->size());
if (!(viewport == before))
notify();
}
void EditorSession::zoom(double value, std::optional<QPointF> anchor)
{
if (!m_document)
return;
// The viewport refuses a zoom that is no number.
const CanvasViewport before = viewport;
viewport.setZoom(value, anchor.value_or(viewport.center()), m_document->size());
if (!(viewport == before))
notify();
}
void EditorSession::zoomKeyboard(int step)
{
if (!m_document)
return;
const double target = viewport.keyboardZoomTarget(step);
if (target == viewport.zoom())
return;
viewport.setZoom(target, viewport.center(), m_document->size());
notify();
}
void EditorSession::setShowsPixelGrid(bool shows)
{
m_showsPixelGrid = shows;
ToolDefaults::set(shows, QStringLiteral("pixelGrid"));
notify();
}
void EditorSession::setSnappingEnabled(bool enabled)
{
m_snappingEnabled = enabled;
if (!enabled)
snapGuides = {};
notify();
}
void EditorSession::setCropRect(std::optional<QRectF> rect)
{
if (std::exchange(m_cropRect, rect) != rect)
notify();
}
void EditorSession::setCropRatioChoice(const QString &choice)
{
if (std::exchange(m_cropRatioChoice, choice) != choice)
notify();
}
void EditorSession::setCropError(std::optional<QString> error)
{
if (std::exchange(m_cropError, error) != error)
notify();
}
void EditorSession::setShowsSampleRing(bool shows)
{
m_showsSampleRing = shows;
notify();
}
void EditorSession::requestCanvasFocus()
{
++m_canvasFocusRequest;
notify();
}