Colour only the selected letters of text being edited; project format version 10 (12.14d1)
This commit is contained in:
1 parent
2fa8d9921d
commit
27a4fe8428
32 files changed
+1398
-82
No files matched your search
@@ -27,6 +27,8 @@ public:
|
||||
void previewEffectColor();
|
||||
// The open text draft previews the picker's working colour.
|
||||
void previewTextColor();
|
||||
// Paints the open text's selected letters, or all of it.
|
||||
void setDraftTextColor(const PaletteColor &color);
|
||||
// Swift writes the picker's hsb in place; here, the session.
|
||||
void setColorPickerHSB(const PickerHSB &hsb);
|
||||
void sampleIntoColorPicker(QPointF point);
|
||||
@@ -34,6 +36,7 @@ public:
|
||||
std::optional<PaletteColor> sampleCompositeColor(QPointF point) const;
|
||||
|
||||
private:
|
||||
void paintText(const PaletteColor &color);
|
||||
// Puts back the open text's colours from the picker's opening.
|
||||
void restoreDraftTextColors(const LayerTextStyle &original);
|
||||
void setGradientMapColor(const PaletteColor &color, bool highlights);
|
||||
void setVignetteColor(const PaletteColor &color);
|
||||
@@ -156,13 +156,8 @@ void EditorSession::setPaletteColor(const PaletteColor &color, bool background)
|
||||
} else {
|
||||
setForegroundColor(color);
|
||||
// Open text follows the swatch; drafts live under Type.
|
||||
if (m_textDraft) {
|
||||
changeTextStyle([&color](LayerTextStyle &style) {
|
||||
style.red = color.red;
|
||||
style.green = color.green;
|
||||
style.blue = color.blue;
|
||||
});
|
||||
}
|
||||
if (m_textDraft)
|
||||
setDraftTextColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +193,7 @@ void EditorSession::openColorPicker(bool background)
|
||||
ColorPickerState picker({ColorPickerTarget::Kind::palette, background}, paletteColor(background));
|
||||
// Open text follows the foreground: it previews, Cancel restores.
|
||||
if (!background && m_tool == NavigationTool::type && m_textDraft)
|
||||
picker.editedText = ColorPickerState::EditedText{m_textDraft->id, {m_textDraft->style.red, m_textDraft->style.green, m_textDraft->style.blue}};
|
||||
picker.editedText = ColorPickerState::EditedText{m_textDraft->id, m_textDraft->style};
|
||||
m_colorPicker = picker;
|
||||
notify();
|
||||
}
|
||||
@@ -207,7 +202,9 @@ PaletteColor EditorSession::typeColor() const
|
||||
{
|
||||
if (!m_textDraft)
|
||||
return foregroundColor();
|
||||
return {m_textDraft->style.red, m_textDraft->style.green, m_textDraft->style.blue};
|
||||
// The first selected letter, else the one before the caret.
|
||||
const TextSpan selection = m_textDraft->selection;
|
||||
return m_textDraft->style.color(selection.length > 0 ? selection.location : std::max<qint64>(0, selection.location - 1));
|
||||
}
|
||||
|
||||
void EditorSession::openTextColorPicker()
|
||||
@@ -215,7 +212,10 @@ void EditorSession::openTextColorPicker()
|
||||
if (!canEditPalette() || m_colorPicker || m_tool != NavigationTool::type)
|
||||
return;
|
||||
const std::optional<QUuid> draftID = m_textDraft ? std::optional(m_textDraft->id) : std::nullopt;
|
||||
m_colorPicker = ColorPickerState({ColorPickerTarget::Kind::text, false, draftID}, typeColor());
|
||||
ColorPickerState picker({ColorPickerTarget::Kind::text, false, draftID}, typeColor());
|
||||
if (m_textDraft)
|
||||
picker.editedText = ColorPickerState::EditedText{m_textDraft->id, m_textDraft->style};
|
||||
m_colorPicker = picker;
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -231,12 +231,15 @@ void EditorSession::closeColorPicker(bool commit)
|
||||
setPaletteColor(color, picker.target.background);
|
||||
} else if (!commit && picker.target.kind == ColorPickerTarget::Kind::palette && picker.editedText && m_tool == NavigationTool::type
|
||||
&& draftID == picker.editedText->draftID) {
|
||||
paintText(picker.editedText->color);
|
||||
restoreDraftTextColors(picker.editedText->style);
|
||||
} else if (picker.target.kind == ColorPickerTarget::Kind::text && m_tool == NavigationTool::type && draftID == picker.target.draftID) {
|
||||
// The text previewed the working colour; Cancel puts it back.
|
||||
const PaletteColor chosen = commit ? color : picker.original;
|
||||
if (draftID) {
|
||||
paintText(chosen);
|
||||
if (commit)
|
||||
setDraftTextColor(chosen);
|
||||
else if (picker.editedText)
|
||||
restoreDraftTextColors(picker.editedText->style);
|
||||
} else if (commit) {
|
||||
m_textDefaults.red = chosen.red;
|
||||
m_textDefaults.green = chosen.green;
|
||||
@@ -295,16 +298,25 @@ void EditorSession::previewTextColor()
|
||||
? std::optional(m_colorPicker->editedText->draftID)
|
||||
: std::nullopt;
|
||||
if (draftID && m_tool == NavigationTool::type && m_textDraft && m_textDraft->id == *draftID)
|
||||
paintText(m_colorPicker->color());
|
||||
setDraftTextColor(m_colorPicker->color());
|
||||
}
|
||||
|
||||
// The open draft's colour.
|
||||
void EditorSession::paintText(const PaletteColor &color)
|
||||
void EditorSession::setDraftTextColor(const PaletteColor &color)
|
||||
{
|
||||
changeTextStyle([&color](LayerTextStyle &style) {
|
||||
style.red = color.red;
|
||||
style.green = color.green;
|
||||
style.blue = color.blue;
|
||||
if (!m_textDraft)
|
||||
return;
|
||||
const TextSpan selection = m_textDraft->selection;
|
||||
changeTextStyle([&](LayerTextStyle &style) { style.setColor(color, selection); });
|
||||
}
|
||||
|
||||
void EditorSession::restoreDraftTextColors(const LayerTextStyle &original)
|
||||
{
|
||||
changeTextStyle([&original](LayerTextStyle &style) {
|
||||
style.red = original.red;
|
||||
style.green = original.green;
|
||||
style.blue = original.blue;
|
||||
// Retyped since, the runs cannot come back letter for letter.
|
||||
style.colorRuns = original.content == style.content ? original.colorRuns : std::nullopt;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "Document/TypeTool.h"
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <QUuid>
|
||||
@@ -57,10 +58,10 @@ struct ColorPickerState {
|
||||
PickerHSB hsb;
|
||||
// Swift's object identity: a new picker opens its panel anew.
|
||||
QUuid id = QUuid::createUuid();
|
||||
// The foreground picker over open text: that text, its colour.
|
||||
// The picker over open text: that text, its colours then.
|
||||
struct EditedText {
|
||||
QUuid draftID;
|
||||
PaletteColor color;
|
||||
LayerTextStyle style;
|
||||
};
|
||||
std::optional<EditedText> editedText;
|
||||
PaletteColor color() const { return hsb.rgb().quantized(); }
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include "Rendering/TextLayout.h"
|
||||
#include <QPainter>
|
||||
#include <QTextBoundaryFinder>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
QString rawValue(TextAlignment alignment)
|
||||
@@ -44,7 +46,94 @@ bool LayerTextStyle::isValid() const
|
||||
// Ranges refuse NaN and infinities: Swift's isFinite adds nothing.
|
||||
const auto within = [](double value, double low, double high) { return value >= low && value <= high; };
|
||||
return content.size() <= 100'000 && boxIsValid() && within(fontSize, 1, 2000) && within(red, 0, 1) && within(green, 0, 1)
|
||||
&& within(blue, 0, 1) && within(tracking, -100, 1000) && within(leading, 0, 5000);
|
||||
&& within(blue, 0, 1) && within(tracking, -100, 1000) && within(leading, 0, 5000) && colorRunsAreValid();
|
||||
}
|
||||
|
||||
bool LayerTextStyle::colorRunsAreValid() const
|
||||
{
|
||||
if (!colorRuns)
|
||||
return true;
|
||||
qint64 end = 0;
|
||||
for (const LayerTextColorRun &run : *colorRuns) {
|
||||
// Each after the last, not empty, its end within Int.
|
||||
if (run.location < end || run.length <= 0 || run.location > std::numeric_limits<qint64>::max() - run.length)
|
||||
return false;
|
||||
for (const double channel : {run.red, run.green, run.blue}) {
|
||||
if (!(channel >= 0 && channel <= 1))
|
||||
return false;
|
||||
}
|
||||
end = run.location + run.length;
|
||||
}
|
||||
return !colorRuns->empty() && end <= content.size();
|
||||
}
|
||||
|
||||
PaletteColor LayerTextStyle::color(qint64 index) const
|
||||
{
|
||||
for (const LayerTextColorRun &run : colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
if (run.location <= index && index < run.location + run.length)
|
||||
return {run.red, run.green, run.blue};
|
||||
}
|
||||
return {red, green, blue};
|
||||
}
|
||||
|
||||
void LayerTextStyle::setColor(const PaletteColor &color, TextSpan span)
|
||||
{
|
||||
const qint64 count = content.size();
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
if (start == end || (start == 0 && end == count)) {
|
||||
red = color.red;
|
||||
green = color.green;
|
||||
blue = color.blue;
|
||||
colorRuns = std::nullopt;
|
||||
return;
|
||||
}
|
||||
std::vector<PaletteColor> colors = unitColors();
|
||||
std::fill(colors.begin() + start, colors.begin() + end, color);
|
||||
setUnitColors(colors);
|
||||
}
|
||||
|
||||
void LayerTextStyle::replaceCharacters(TextSpan span, qint64 length)
|
||||
{
|
||||
if (!colorRuns)
|
||||
return;
|
||||
std::vector<PaletteColor> colors = unitColors();
|
||||
const qint64 count = qint64(colors.size());
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
// New letters take the colour before them, as typing does.
|
||||
const PaletteColor inherited = start > 0 ? colors[size_t(start - 1)] : end > start ? colors[size_t(start)] : !colors.empty() ? colors.front() : PaletteColor{red, green, blue};
|
||||
colors.erase(colors.begin() + start, colors.begin() + end);
|
||||
colors.insert(colors.begin() + start, size_t(std::max<qint64>(0, length)), inherited);
|
||||
setUnitColors(colors);
|
||||
}
|
||||
|
||||
std::vector<PaletteColor> LayerTextStyle::unitColors() const
|
||||
{
|
||||
std::vector<PaletteColor> colors(size_t(content.size()), PaletteColor{red, green, blue});
|
||||
for (const LayerTextColorRun &run : colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
const qint64 from = std::max<qint64>(0, run.location), to = std::min<qint64>(qint64(colors.size()), run.location + run.length);
|
||||
for (qint64 index = from; index < to; ++index)
|
||||
colors[size_t(index)] = {run.red, run.green, run.blue};
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
void LayerTextStyle::setUnitColors(const std::vector<PaletteColor> &colors)
|
||||
{
|
||||
const PaletteColor base{red, green, blue};
|
||||
std::vector<LayerTextColorRun> runs;
|
||||
for (size_t index = 0; index < colors.size(); ++index) {
|
||||
const PaletteColor &color = colors[index];
|
||||
if (color == base)
|
||||
continue;
|
||||
if (!runs.empty() && size_t(runs.back().location + runs.back().length) == index
|
||||
&& PaletteColor{runs.back().red, runs.back().green, runs.back().blue} == color)
|
||||
runs.back().length += 1;
|
||||
else
|
||||
runs.push_back({qint64(index), 1, color.red, color.green, color.blue});
|
||||
}
|
||||
colorRuns = runs.empty() ? std::nullopt : std::optional(runs);
|
||||
}
|
||||
|
||||
std::optional<LayerText> LayerText::loaded(const std::optional<LayerTextStyle> &style, const std::optional<ImportedImage> &image)
|
||||
@@ -102,7 +191,7 @@ void EditorSession::beginText(QPointF point, bool newLayer)
|
||||
const QPointF origin = target ? target->origin() : point - QPointF(padding, padding + TextLines(style, QSizeF(100'000, 100'000)).baseline(0));
|
||||
setTextDraft(TextDraft{.documentID = m_document->id, .layerID = target ? std::optional(target->id) : std::nullopt,
|
||||
.origin = origin,
|
||||
.transform = target ? std::optional(target->transform) : std::nullopt, .style = style});
|
||||
.transform = target ? std::optional(target->transform) : std::nullopt, .style = style, .selection = TextSpan()});
|
||||
}
|
||||
|
||||
void EditorSession::editActiveText()
|
||||
@@ -112,7 +201,7 @@ void EditorSession::editActiveText()
|
||||
return;
|
||||
m_tool = NavigationTool::type;
|
||||
setTextDraft(TextDraft{.documentID = m_document->id, .layerID = layer->id, .origin = layer->origin(), .transform = layer->transform,
|
||||
.style = layer->liveText()->style});
|
||||
.style = layer->liveText()->style, .selection = TextSpan()});
|
||||
}
|
||||
|
||||
bool EditorSession::applyText(TextDraft draft)
|
||||
@@ -166,6 +255,8 @@ bool EditorSession::applyText(TextDraft draft)
|
||||
addPixelLayer(image, draft.origin, layerName(draft.style.content), QStringLiteral("New Text Layer"), false, std::nullopt, draft.style);
|
||||
}
|
||||
m_textDefaults = draft.style;
|
||||
// The next text starts in one colour.
|
||||
m_textDefaults.colorRuns = std::nullopt;
|
||||
requestCanvasFocus();
|
||||
return succeed();
|
||||
} catch (const ProjectError &error) {
|
||||
@@ -221,11 +312,9 @@ bool EditorSession::recolorText(QUuid id, const PaletteColor &color)
|
||||
return false;
|
||||
const ImageLayer layer = m_document->layers[size_t(index)];
|
||||
LayerTextStyle style = layer.liveText()->style;
|
||||
if (style.red == color.red && style.green == color.green && style.blue == color.blue)
|
||||
if (style.red == color.red && style.green == color.green && style.blue == color.blue && !style.colorRuns)
|
||||
return true;
|
||||
style.red = color.red;
|
||||
style.green = color.green;
|
||||
style.blue = color.blue;
|
||||
style.setColor(color, TextSpan());
|
||||
QImage image, thumbnail;
|
||||
try {
|
||||
image = textImage(style);
|
||||
|
||||
@@ -6,12 +6,32 @@
|
||||
#include <QUuid>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
enum class TextAlignment { left, center, right };
|
||||
inline constexpr std::array allTextAlignments{TextAlignment::left, TextAlignment::center, TextAlignment::right};
|
||||
QString rawValue(TextAlignment alignment);
|
||||
std::optional<TextAlignment> textAlignment(const QString &text);
|
||||
|
||||
struct PaletteColor;
|
||||
|
||||
// Swift's NSRange: UTF-16 units into a text's content.
|
||||
struct TextSpan {
|
||||
qint64 location = 0;
|
||||
qint64 length = 0;
|
||||
friend bool operator==(const TextSpan &, const TextSpan &) = default;
|
||||
};
|
||||
|
||||
// Letters in a colour of their own, as Swift's LayerTextColorRun.
|
||||
struct LayerTextColorRun {
|
||||
qint64 location = 0;
|
||||
qint64 length = 0;
|
||||
double red = 0;
|
||||
double green = 0;
|
||||
double blue = 0;
|
||||
friend bool operator==(const LayerTextColorRun &, const LayerTextColorRun &) = default;
|
||||
};
|
||||
|
||||
// What a text layer says and how, in layer pixels.
|
||||
struct LayerTextStyle {
|
||||
QString content = QStringLiteral("Text");
|
||||
@@ -27,13 +47,26 @@ struct LayerTextStyle {
|
||||
double leading = 0;
|
||||
// A paragraph's fixed box; none makes point text.
|
||||
std::optional<QSizeF> boxSize = std::nullopt;
|
||||
// Sorted, apart, within the content; none is one colour.
|
||||
std::optional<std::vector<LayerTextColorRun>> colorRuns = std::nullopt;
|
||||
// The gap between the text and its box, either kind.
|
||||
static constexpr double padding = 12;
|
||||
double autoLeading() const { return fontSize * 1.2; }
|
||||
double lineHeight() const { return leading > 0 ? leading : autoLeading(); }
|
||||
bool boxIsValid() const;
|
||||
bool isValid() const;
|
||||
// The colour of the UTF-16 unit at `index`.
|
||||
PaletteColor color(qint64 index) const;
|
||||
// Paints `span`; empty or whole, it recolours all the text.
|
||||
void setColor(const PaletteColor &color, TextSpan span);
|
||||
// Moves the colours as `span` becomes `length` units.
|
||||
void replaceCharacters(TextSpan span, qint64 length);
|
||||
friend bool operator==(const LayerTextStyle &, const LayerTextStyle &) = default;
|
||||
|
||||
private:
|
||||
bool colorRunsAreValid() const;
|
||||
std::vector<PaletteColor> unitColors() const;
|
||||
void setUnitColors(const std::vector<PaletteColor> &colors);
|
||||
};
|
||||
|
||||
// A text layer: its style and the image it drew.
|
||||
@@ -52,5 +85,7 @@ struct TextDraft {
|
||||
QPointF origin;
|
||||
std::optional<LayerTransform> transform = std::nullopt;
|
||||
LayerTextStyle style;
|
||||
// What the editor selects; colour applies to it alone.
|
||||
TextSpan selection;
|
||||
friend bool operator==(const TextDraft &, const TextDraft &) = default;
|
||||
};
|
||||
@@ -102,7 +102,25 @@ QSizeF size(const QJsonValue &value)
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
// Synthesized Codable skips defaults: all but the box required.
|
||||
// A run's every key is required, as synthesized Codable asks.
|
||||
LayerTextColorRun colorRun(const QJsonValue &value)
|
||||
{
|
||||
const QJsonObject object = value.toObject();
|
||||
return {integer(object.value("location")), integer(object.value("length")), number(object.value("red")), number(object.value("green")),
|
||||
number(object.value("blue"))};
|
||||
}
|
||||
|
||||
std::vector<LayerTextColorRun> colorRuns(const QJsonValue &value)
|
||||
{
|
||||
if (!value.isArray())
|
||||
refuse();
|
||||
std::vector<LayerTextColorRun> runs;
|
||||
for (const QJsonValue &each : value.toArray())
|
||||
runs.push_back(colorRun(each));
|
||||
return runs;
|
||||
}
|
||||
|
||||
// Synthesized Codable: all required but the box and runs.
|
||||
LayerTextStyle textStyle(const QJsonValue &value)
|
||||
{
|
||||
const QJsonObject object = value.toObject();
|
||||
@@ -112,7 +130,7 @@ LayerTextStyle textStyle(const QJsonValue &value)
|
||||
return {.content = string(object.value("content")), .fontName = string(object.value("fontName")), .fontSize = number(object.value("fontSize")),
|
||||
.red = number(object.value("red")), .green = number(object.value("green")), .blue = number(object.value("blue")),
|
||||
.alignment = *alignment, .tracking = number(object.value("tracking")), .leading = number(object.value("leading")),
|
||||
.boxSize = optional(object, "boxSize", size)};
|
||||
.boxSize = optional(object, "boxSize", size), .colorRuns = optional(object, "colorRuns", colorRuns)};
|
||||
}
|
||||
|
||||
CanvasGuide guide(const QJsonValue &value)
|
||||
@@ -176,6 +194,12 @@ QJsonObject encoded(const LayerTextStyle &text)
|
||||
{"blue", text.blue}, {"alignment", rawValue(text.alignment)}, {"tracking", text.tracking}, {"leading", text.leading}};
|
||||
if (text.boxSize)
|
||||
object.insert("boxSize", QJsonArray{text.boxSize->width(), text.boxSize->height()});
|
||||
if (text.colorRuns) {
|
||||
QJsonArray runs;
|
||||
for (const LayerTextColorRun &run : *text.colorRuns)
|
||||
runs.append(QJsonObject{{"location", run.location}, {"length", run.length}, {"red", run.red}, {"green", run.green}, {"blue", run.blue}});
|
||||
object.insert("colorRuns", runs);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,8 @@ void validate(const ProjectManifest &manifest)
|
||||
if (manifest.width < 1 || manifest.width > DocumentLimits::maxSide || manifest.height < 1 || manifest.height > DocumentLimits::maxSide || manifest.layers.size() > 10'000)
|
||||
throw ProjectError(ProjectError::Kind::tooLarge);
|
||||
for (const ProjectLayerRecord &layer : manifest.layers) {
|
||||
// Text needs a valid style and pixels to show it.
|
||||
if (layer.text && (!layer.text->isValid() || !layer.imageFile))
|
||||
// Text needs a valid style, pixels; runs from 10.
|
||||
if (layer.text && (!layer.text->isValid() || !layer.imageFile || (layer.text->colorRuns && manifest.version < 10)))
|
||||
throw ProjectError(ProjectError::Kind::invalid);
|
||||
// Version 7's adjustments hold settings, never pixels or children.
|
||||
if (layer.adjustment && (manifest.version < 7 || layer.isGroup == true || layer.imageFile || !layer.adjustment->isValid()))
|
||||
|
||||
@@ -51,7 +51,7 @@ QString uuidString(const QUuid &id);
|
||||
|
||||
struct ProjectManifest {
|
||||
// Swift's current and supported: what saves write, what loads accept.
|
||||
static constexpr qint64 current = 9;
|
||||
static constexpr qint64 current = 10;
|
||||
static constexpr std::pair<qint64, qint64> supported{1, current};
|
||||
static bool supports(qint64 version) { return version >= supported.first && version <= supported.second; }
|
||||
QString format = QStringLiteral("com.compositor.project");
|
||||
|
||||
@@ -171,27 +171,57 @@ void InlineTextEditor::replace(TextRange range, const QString &text, std::option
|
||||
// Swift's shouldChangeTextIn: 100,000 UTF-16 units at most.
|
||||
if (content.size() - (range.end - range.start) + text.size() > 100'000)
|
||||
return;
|
||||
std::vector<Step> steps;
|
||||
// A preedit left behind stays as text: history gains it.
|
||||
if (m_composing)
|
||||
steps.push_back({{m_composing->marked.start, m_composing->marked.start}, m_composing->marked.end - m_composing->marked.start, false, true});
|
||||
if (!edit || edit != m_lastEdit)
|
||||
m_undo.push_back(m_composing ? m_composing->before : Snapshot{content, m_anchor, m_position});
|
||||
m_redo.clear();
|
||||
m_lastEdit = edit;
|
||||
m_composing.reset();
|
||||
m_anchor = m_position = range.start + int(text.size());
|
||||
publish(QString(content).replace(range.start, range.end - range.start, text));
|
||||
steps.push_back({range, int(text.size())});
|
||||
publish(QString(content).replace(range.start, range.end - range.start, text), steps);
|
||||
}
|
||||
|
||||
// Swift's textDidChange: the draft takes the text view's string.
|
||||
void InlineTextEditor::publish(const QString &content)
|
||||
void InlineTextEditor::publish(const QString &content, const std::vector<Step> &steps)
|
||||
{
|
||||
// An edit ends a drag or resize, like a press.
|
||||
release();
|
||||
TextDraft draft = m_session.textDraft().value();
|
||||
// The text's length as each step leaves it.
|
||||
QString shaped = draft.style.content;
|
||||
for (const Step &step : steps) {
|
||||
// Each replacement moves the colours, as shouldChangeTextIn does.
|
||||
if (step.runs) {
|
||||
draft.style.content = shaped;
|
||||
draft.style.replaceCharacters({step.replaced.start, step.replaced.end - step.replaced.start}, step.length);
|
||||
shaped.replace(step.replaced.start, step.replaced.end - step.replaced.start, QString(step.length, u' '));
|
||||
}
|
||||
if (step.history && !m_undo.empty())
|
||||
m_undo.back().since.push_back(step);
|
||||
}
|
||||
draft.style.content = content;
|
||||
const TextRange range = selection();
|
||||
draft.selection = {range.start, range.end - range.start};
|
||||
m_goalX.reset();
|
||||
showCaret();
|
||||
m_session.setTextDraft(draft);
|
||||
}
|
||||
|
||||
void InlineTextEditor::noteSelection()
|
||||
{
|
||||
const TextRange range = selection();
|
||||
const TextSpan span{range.start, range.end - range.start};
|
||||
if (!m_session.textDraft() || m_session.textDraft()->id != m_draftID || m_session.textDraft()->selection == span)
|
||||
return;
|
||||
TextDraft draft = m_session.textDraft().value();
|
||||
draft.selection = span;
|
||||
m_session.setTextDraft(draft);
|
||||
}
|
||||
|
||||
void InlineTextEditor::moveTo(int position, bool extend, bool keepGoal)
|
||||
{
|
||||
m_position = position;
|
||||
@@ -202,20 +232,34 @@ void InlineTextEditor::moveTo(int position, bool extend, bool keepGoal)
|
||||
m_lastEdit.reset();
|
||||
showCaret();
|
||||
updateInputMethod();
|
||||
noteSelection();
|
||||
}
|
||||
|
||||
void InlineTextEditor::restore(std::vector<Snapshot> &from, std::vector<Snapshot> &to)
|
||||
{
|
||||
if (from.empty())
|
||||
return;
|
||||
to.push_back(m_composing ? m_composing->before : Snapshot{style().content, m_anchor, m_position});
|
||||
const Snapshot snapshot = from.back();
|
||||
from.pop_back();
|
||||
std::vector<Step> steps;
|
||||
// A preedit goes first; it was never history.
|
||||
if (m_composing)
|
||||
steps.push_back({m_composing->marked, 0, true, false});
|
||||
// NSTextView undoes each replacement: the inverses, newest first.
|
||||
std::vector<Step> back;
|
||||
for (auto step = snapshot.since.rbegin(); step != snapshot.since.rend(); ++step)
|
||||
back.push_back({{step->replaced.start, step->replaced.start + step->length}, step->replaced.end - step->replaced.start, true, false});
|
||||
steps.insert(steps.end(), back.begin(), back.end());
|
||||
Snapshot current = m_composing ? m_composing->before : Snapshot{style().content, m_anchor, m_position};
|
||||
for (Step &step : back)
|
||||
step.history = true;
|
||||
current.since = back;
|
||||
to.push_back(current);
|
||||
m_lastEdit.reset();
|
||||
m_composing.reset();
|
||||
m_anchor = snapshot.anchor;
|
||||
m_position = snapshot.position;
|
||||
publish(snapshot.content);
|
||||
publish(snapshot.content, steps);
|
||||
}
|
||||
|
||||
void InlineTextEditor::undo()
|
||||
|
||||
@@ -38,7 +38,7 @@ void InlineTextEditor::selectAll()
|
||||
if (m_composing) {
|
||||
m_lastEdit.reset();
|
||||
const QString text = plain().content;
|
||||
settle(text, 0, int(text.size()));
|
||||
settle(text, 0, int(text.size()), {{m_composing->marked, 0, true, false}});
|
||||
return;
|
||||
}
|
||||
m_anchor = 0;
|
||||
@@ -65,11 +65,15 @@ void InlineTextEditor::change(const QString &text)
|
||||
m_redo.clear();
|
||||
m_lastEdit.reset();
|
||||
const int cursor = range.start + int(text.size());
|
||||
settle(QString(around.content).replace(range.start, range.end - range.start, text), cursor, cursor);
|
||||
std::vector<Step> steps;
|
||||
if (m_composing)
|
||||
steps.push_back({m_composing->marked, 0, true, false});
|
||||
steps.push_back({range, int(text.size())});
|
||||
settle(QString(around.content).replace(range.start, range.end - range.start, text), cursor, cursor, steps);
|
||||
}
|
||||
|
||||
// The preedit returns to the cursor, as Qt shows it.
|
||||
void InlineTextEditor::settle(QString text, int anchor, int cursor)
|
||||
void InlineTextEditor::settle(QString text, int anchor, int cursor, std::vector<Step> steps)
|
||||
{
|
||||
if (m_composing) {
|
||||
const TextRange marked = m_composing->marked;
|
||||
@@ -79,11 +83,12 @@ void InlineTextEditor::settle(QString text, int anchor, int cursor)
|
||||
m_composing->anchor = anchor;
|
||||
m_composing->marked = {cursor, cursor + int(preedit.size())};
|
||||
text.insert(cursor, preedit);
|
||||
steps.push_back({{cursor, cursor}, int(preedit.size()), true, false});
|
||||
anchor = cursor = cursor + caret;
|
||||
}
|
||||
m_anchor = anchor;
|
||||
m_position = cursor;
|
||||
publish(text);
|
||||
publish(text, steps);
|
||||
}
|
||||
|
||||
// Marked text sits in the content, as in NSTextView's storage.
|
||||
@@ -95,10 +100,14 @@ void InlineTextEditor::inputMethod(const QInputMethodEvent &event)
|
||||
// Qt's steps, on the text without the preedit.
|
||||
QString text = style().content, shown;
|
||||
int cursor = m_position, anchor = m_anchor;
|
||||
const std::optional<TextRange> oldMarked = m_composing ? std::optional(m_composing->marked) : std::nullopt;
|
||||
// The preedit's going moves colours, never history.
|
||||
std::vector<Step> steps;
|
||||
if (m_composing) {
|
||||
const TextRange marked = m_composing->marked;
|
||||
shown = text.mid(marked.start, marked.end - marked.start);
|
||||
text.remove(marked.start, marked.end - marked.start);
|
||||
steps.push_back({marked, 0, true, false});
|
||||
cursor = marked.start;
|
||||
anchor = m_composing->anchor;
|
||||
}
|
||||
@@ -110,6 +119,7 @@ void InlineTextEditor::inputMethod(const QInputMethodEvent &event)
|
||||
const bool takes = input && !selected.isEmpty();
|
||||
if (takes) {
|
||||
text.remove(selected.start, selected.end - selected.start);
|
||||
steps.push_back({selected, 0});
|
||||
cursor = selected.start;
|
||||
}
|
||||
// Where Qt leaves the cursor when nothing commits.
|
||||
@@ -130,6 +140,11 @@ void InlineTextEditor::inputMethod(const QInputMethodEvent &event)
|
||||
m_lastEdit = Edit::typing;
|
||||
}
|
||||
text.replace(start, end - start, commit);
|
||||
// Over the selection taken, one replacement, as NSTextView's.
|
||||
if (takes && start == selected.start && end == start)
|
||||
steps.back().length = int(commit.size());
|
||||
else if (end > start || !commit.isEmpty())
|
||||
steps.push_back({{start, end}, int(commit.size())});
|
||||
cursor = commit.isEmpty() ? std::clamp(after, 0, int(text.size())) : start + int(commit.size());
|
||||
if (input)
|
||||
anchor = cursor;
|
||||
@@ -144,14 +159,25 @@ void InlineTextEditor::inputMethod(const QInputMethodEvent &event)
|
||||
m_anchor = anchor;
|
||||
m_position = cursor;
|
||||
} else {
|
||||
compose(preedit, attributes, lands ? Snapshot{text, anchor, cursor} : before, anchor, text, cursor);
|
||||
compose(preedit, attributes, lands ? Snapshot{text, anchor, cursor} : before, anchor, text, steps, cursor);
|
||||
// A lone preedit over the selection replaces it whole.
|
||||
if (takes && commit.isEmpty() && start == end && cursor == selected.start) {
|
||||
steps[steps.size() - 2].runs = false;
|
||||
steps.back() = {selected, int(preedit.size()), true, false};
|
||||
}
|
||||
}
|
||||
publish(text);
|
||||
// Swift replaces marked text whole: its colour carries on.
|
||||
if (oldMarked && !takes && start == end && start == oldMarked->start && (preedit.isEmpty() || cursor == start + int(commit.size()))) {
|
||||
steps.front().length = int(commit.size() + preedit.size());
|
||||
for (size_t index = 1; index < steps.size(); ++index)
|
||||
steps[index].runs = false;
|
||||
}
|
||||
publish(text, steps);
|
||||
}
|
||||
|
||||
// The preedit goes in at the cursor, with Qt's attributes.
|
||||
void InlineTextEditor::compose(const QString &preedit, const QList<QInputMethodEvent::Attribute> &attributes, const Snapshot &before, int anchor,
|
||||
QString &text, int cursor)
|
||||
QString &text, std::vector<Step> &steps, int cursor)
|
||||
{
|
||||
const int length = int(preedit.size());
|
||||
Composing composing{.marked = {cursor, cursor + length}, .anchor = anchor, .before = before};
|
||||
@@ -172,6 +198,7 @@ void InlineTextEditor::compose(const QString &preedit, const QList<QInputMethodE
|
||||
}
|
||||
}
|
||||
text.insert(cursor, preedit);
|
||||
steps.push_back({{cursor, cursor}, length, true, false});
|
||||
m_anchor = m_position = cursor + caret;
|
||||
m_composing = composing;
|
||||
}
|
||||
|
||||
@@ -68,8 +68,11 @@ void InlineTextEditor::synchronize(const TextDraft &draft)
|
||||
}
|
||||
m_shownStyle = style;
|
||||
const double padding = LayerTextStyle::padding;
|
||||
// Clear glyphs: the runs' colours belong to the layer beneath.
|
||||
LayerTextStyle clear = style;
|
||||
clear.colorRuns = std::nullopt;
|
||||
m_lines = std::make_unique<TextLines>(
|
||||
style, QSizeF(std::max(1.0, m_logicalSize.width() - 2 * padding), std::max(1.0, m_logicalSize.height() - 2 * padding)));
|
||||
clear, QSizeF(std::max(1.0, m_logicalSize.width() - 2 * padding), std::max(1.0, m_logicalSize.height() - 2 * padding)));
|
||||
updateInputMethod();
|
||||
if (!fresh)
|
||||
return;
|
||||
@@ -168,8 +171,11 @@ void InlineTextEditor::draw(QPainter &painter) const
|
||||
m_lines->draw(painter, QPointF(padding, padding), shownSelection(), formats, highlight);
|
||||
// The insertion point shows where nothing is selected.
|
||||
const bool hidden = m_composing && m_composing->caretHidden;
|
||||
// The caret takes the colour of the letter before it.
|
||||
const PaletteColor before = style().color(m_position > 0 ? m_position - 1 : 0);
|
||||
const QColor caretInk = QColor::fromRgbF(float(before.red), float(before.green), float(before.blue));
|
||||
if (const std::optional<QRectF> caret = m_lines->caret(m_position); caret && caretShown() && selection().isEmpty() && !hidden)
|
||||
painter.fillRect(caret->translated(padding, padding), m_composing ? m_composing->caretColour.value_or(ink) : ink);
|
||||
painter.fillRect(caret->translated(padding, padding), m_composing ? m_composing->caretColour.value_or(caretInk) : caretInk);
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,20 @@ public:
|
||||
bool caretShown() const { return m_focused && m_caretShown; }
|
||||
|
||||
private:
|
||||
// One replacement: what it took, and the new text's length.
|
||||
struct Step {
|
||||
TextRange replaced;
|
||||
int length;
|
||||
// Moves the colour runs; joins the undo entry's history.
|
||||
bool runs = true;
|
||||
bool history = true;
|
||||
};
|
||||
struct Snapshot {
|
||||
QString content;
|
||||
int anchor;
|
||||
int position;
|
||||
// Plain steps from this text to the next state.
|
||||
std::vector<Step> since = {};
|
||||
};
|
||||
// An input method's preedit, which Qt keeps out of history.
|
||||
struct Composing {
|
||||
@@ -101,8 +111,11 @@ private:
|
||||
static TextRange spanning(int anchor, int position);
|
||||
TextRange selection() const;
|
||||
void replace(TextRange range, const QString &text, std::optional<Edit> edit);
|
||||
void publish(const QString &content);
|
||||
// Swift's shouldChangeTextIn: each step moves the colours.
|
||||
void publish(const QString &content, const std::vector<Step> &steps);
|
||||
void moveTo(int position, bool extend, bool keepGoal = false);
|
||||
// Swift's textViewDidChangeSelection: the draft learns the selection.
|
||||
void noteSelection();
|
||||
void restore(std::vector<Snapshot> &from, std::vector<Snapshot> &to);
|
||||
void changeSpacing(int key, double step);
|
||||
bool move(const QKeyEvent &event);
|
||||
@@ -115,11 +128,11 @@ private:
|
||||
void beginResize(int handle, QPointF point);
|
||||
void resizeTo(QPointF point);
|
||||
void compose(const QString &preedit, const QList<QInputMethodEvent::Attribute> &attributes, const Snapshot &before, int anchor, QString &text,
|
||||
int cursor);
|
||||
std::vector<Step> &steps, int cursor);
|
||||
TextRange shownSelection() const;
|
||||
Snapshot plain() const;
|
||||
void change(const QString &text);
|
||||
void settle(QString text, int anchor, int cursor);
|
||||
void settle(QString text, int anchor, int cursor, std::vector<Step> steps);
|
||||
bool erase(const QKeyEvent &event);
|
||||
int nextCharacter(int position) const;
|
||||
int previousCharacter(int position) const;
|
||||
|
||||
@@ -93,6 +93,17 @@ TextLines::TextLines(const LayerTextStyle &style, QSizeF container)
|
||||
const QString paragraph = style.content.mid(range.start, range.end - range.start);
|
||||
auto layout = std::make_unique<QTextLayout>(paragraph, font);
|
||||
layout->setTextOption(option);
|
||||
// Run letters draw once in their colour, as CoreText's.
|
||||
QList<QTextLayout::FormatRange> runs;
|
||||
for (const LayerTextColorRun &run : style.colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
const qint64 start = std::max<qint64>(run.location, range.start), end = std::min<qint64>(run.location + run.length, range.end);
|
||||
if (start >= end)
|
||||
continue;
|
||||
QTextCharFormat format;
|
||||
format.setForeground(QColor::fromRgbF(float(run.red), float(run.green), float(run.blue)));
|
||||
runs << QTextLayout::FormatRange{int(start - range.start), int(end - start), format};
|
||||
}
|
||||
layout->setFormats(runs);
|
||||
layout->beginLayout();
|
||||
const auto laid = [&] {
|
||||
const int count = layout->lineCount();
|
||||
|
||||
Reference in new issue
Block a user