Files
mail/ui/components/email-composer.slint

941 lines
43 KiB
Plaintext

import { AccountMailPreferences } from "account-mail-preferences.slint";
import { SettingsSelectRow } from "settings-components.slint";
import { ScrollView, VerticalBox } from "std-widgets.slint";
import { AccountRow, ComposeAttachment, ComposeContact, ComposeEditorTile, EmailTemplateRow, SpellingLanguage } from "../models.slint";
import { ComposerFromRow, ComposerHeader, ComposerRecipients, ComposerSubject } from "composer-fields.slint";
import { ComposerAttachments, ComposerFooter, ComposerNotice, ComposerToolbar, DiscardConfirmation, LinkEditor } from "composer-actions.slint";
import { EmailTemplates } from "email-templates.slint";
import { TypeScale } from "../typography.slint";
import { IconButton } from "controls.slint";
// cosmic-text owns visible layout, selection, hit-testing, and caret geometry.
// The transparent TextInput only preserves Slint's native IME and clipboard
// integration; it never supplies coordinates for anything the user can see.
export component EmailComposer inherits Rectangle {
in property <bool> compact: false;
// In its own window: no frame of its own, and the window's title bar
// replaces the heading and its close button.
in property <bool> windowed: false;
in property <bool> attachment-picker-supported: true;
in property <bool> open: false;
in property <string> mode: "new";
in-out property <string> to;
in-out property <string> cc;
in-out property <string> bcc;
in-out property <string> subject;
in-out property <string> body;
in property <bool> sending: false;
in property <[AccountRow]> accounts: [];
in-out property <int> selected-account-id: -1;
in property <[ComposeContact]> contact-suggestions: [];
in property <[ComposeAttachment]> attachments: [];
in property <[EmailTemplateRow]> templates: [];
in property <string> attachment-summary: "";
in-out property <string> from-label: @tr("No connected account");
in property <string> notice: "";
in property <bool> notice-is-error: false;
in property <int> selection-start: 0;
in property <int> selection-end: 0;
in property <[ComposeEditorTile]> editor-tiles: [];
in property <float> editor-content-height: 18;
in property <float> editor-caret-x: 0;
in property <float> editor-caret-y: 0;
in property <float> editor-caret-height: 18;
in property <bool> bold-active: false;
in property <bool> italic-active: false;
in property <bool> underline-active: false;
in property <bool> strike-active: false;
in property <bool> code-active: false;
in property <string> active-link-url: "";
in property <bool> can-pop-out: false;
// "reply" or "forward" when the original goes below the editor.
in property <string> quote: "";
in property <string> quote-left-out: "";
in property <string> quote-text: "";
in-out property <bool> quote-expanded: false;
in property <[ComposeEditorTile]> quote-tiles: [];
in property <float> quote-height: 0;
in property <bool> spelling-available: false;
in property <bool> spelling-enabled: false;
in property <[SpellingLanguage]> spelling-languages: [];
in property <bool> spelling-languages-automatic: false;
in property <string> spelling-word: "";
in property <[string]> spelling-suggestions: [];
in property <int> editor-menu-serial: 0;
// The menu is for a tapped word: only spelling, below the word's line.
in property <bool> editor-menu-touch: false;
in property <length> editor-menu-word-bottom: 0px;
in property <color> background-color;
in property <color> raised-color;
in property <color> control-color;
in property <color> control-hover-color;
in property <color> frame-border-color;
in property <color> subtle-border-color;
in property <color> title-color;
in property <color> body-color;
in property <color> secondary-color;
in property <color> muted-color;
in property <color> accent-color;
in property <color> accent-background;
out property <length> editor-width: max(1px, body-scroll.visible-width - 36px);
out property <length> editor-viewport-height: max(1px, body-scroll.visible-height - 28px);
out property <length> editor-scroll-y: max(0px, -body-scroll.content-y);
out property <string> editor-preedit-text: body-input.preedit-text;
// Where the original starts below the editor, for the Rust layout.
out property <length> quote-top: 14px + root.editor-area-height + 48px;
// A message carrying the original, or able to put it back, keeps a few
// lines for the reply above it rather than the whole viewport.
private property <length> editor-area-height: root.quote == "" && root.quote-left-out == ""
? max(root.editor-viewport-height, root.editor-content-height * 1px)
: max(96px, root.editor-content-height * 1px + 8px);
callback close;
callback pop-out;
callback back-requested;
callback submit(bool);
callback pick-attachments;
callback remove-attachment(int);
callback format-requested(string, string, int, int);
callback link-requested(string, string, int, int);
callback body-edited(string, int, int, string);
callback selection-changed(string, int, int);
callback history-requested(string);
callback editor-pointer-event(float, float, string, bool, bool, bool, bool);
callback editor-key-event(string, bool, bool, bool, bool) -> bool;
callback editor-layout-changed;
callback search-contacts(string, int);
callback select-contact(int, string) -> string;
callback refresh-templates;
callback use-template(int) -> bool;
callback save-template(int, string, string, string, string) -> bool;
callback delete-template(int) -> bool;
callback edit-quote();
callback remove-quote();
callback restore-quote();
callback editor-menu-requested(float, float);
callback editor-caret-menu-requested();
callback editor-tapped(float, float);
callback spelling-correct(int);
callback spelling-ignore();
callback spelling-learn();
callback spelling-toggle();
callback spelling-toggle-language(int);
private property <bool> confirm-discard: false;
private property <bool> link-editor-open: false;
private property <bool> templates-open: false;
private property <string> link-url: "";
private property <int> link-selection-start: 0;
private property <int> link-selection-end: 0;
private property <bool> caret-visible: true;
private property <string> pending-key-text: "";
private property <bool> ime-edit-pending: false;
private property <length> input-caret-x: 0px;
private property <length> input-caret-y: 0px;
// Where the editor menu opens, and the state its spelling switch shows: a menu toggles
// its own copy, which would otherwise stop following the setting.
private property <Point> editor-menu-position;
private property <bool> spelling-switch-shown;
// Where a finger went down on the editor, and whether it has stayed there, so lifting
// it is a tap.
private property <Point> touch-down-position;
private property <bool> touch-still;
// The application answers a request for the editor menu once the corrections for the
// word under the pointer are ready. A tapped word's menu opens below its line, so the
// word stays in sight, as phones show corrections.
changed editor-menu-serial => {
root.spelling-switch-shown = root.spelling-enabled;
if (root.editor-menu-touch) {
root.editor-menu-position = {
x: root.editor-menu-position.x,
y: root.editor-menu-word-bottom,
};
}
editor-menu.show(root.editor-menu-position);
// A menu takes the focus, which puts a phone's keyboard away. Taps choose from it
// without focus, so the message keeps it and the keyboard stays, as phones keep it.
if (root.editor-menu-touch) {
body-input.focus();
}
}
private property <bool> has-content: root.to != "" || root.cc != "" || root.bcc != ""
|| root.subject != "" || root.body != "" || root.attachments.length > 0 || root.quote != "";
private property <bool> can-send: !root.sending && root.selected-account-id > 0 && root.to != "" && (root.body != "" || root.quote != "") && AccountMailPreferences.composer-preferences-ready;
function apply-format(kind: string) {
if (kind == "link") {
if (body-input.anchor-position-byte-offset == body-input.cursor-position_byte_offset) {
return;
}
root.link-selection-start = body-input.anchor-position-byte-offset;
root.link-selection-end = body-input.cursor-position_byte_offset;
root.link-url = root.active-link-url;
root.link-editor-open = true;
return;
}
root.format-requested(
kind,
root.body,
body-input.anchor-position-byte-offset,
body-input.cursor-position_byte-offset,
);
body-input.set-selection-offsets(root.selection-start, root.selection-end);
body-input.focus();
}
function request-close() {
if (root.sending) {
return;
}
if (root.has-content) {
root.confirm-discard = true;
} else {
root.close();
}
}
back-requested => { root.request-close(); }
function reveal-editor-caret() {
if (root.editor-caret-y * 1px + body-scroll.content-y < 8px) {
body-scroll.content-y = min(
0px,
max(
body-scroll.visible-height - body-input.height,
8px - root.editor-caret-y * 1px,
),
);
} else if ((root.editor-caret-y + root.editor-caret-height) * 1px + body-scroll.content-y > body-scroll.visible-height - 8px) {
body-scroll.content-y = min(
0px,
max(
body-scroll.visible-height - body-input.height,
body-scroll.visible-height
- (root.editor-caret-y + root.editor-caret-height) * 1px - 8px,
),
);
}
}
changed open => {
if (!root.open) {
root.confirm-discard = false;
root.templates-open = false;
} else {
// The typing proxy starts where the visible caret is.
body-input.set-selection-offsets(root.selection-start, root.selection-end);
root.editor-layout-changed();
// The recipients' row takes focus only while it is empty.
if root.to != "" {
body-input.focus();
}
}
}
changed editor-width => { root.editor-layout-changed(); }
changed editor-viewport-height => { root.editor-layout-changed(); }
// `restart` alone only re-arms a timer that has run before, so without
// `start` nothing drew the text scrolled into view.
changed editor-scroll-y => {
editor-scroll-layout-timer.start();
editor-scroll-layout-timer.restart();
}
changed quote-expanded => { root.editor-layout-changed(); }
changed quote-top => { root.editor-layout-changed(); }
changed quote => { root.editor-layout-changed(); }
changed editor-preedit-text => {
if (root.editor-preedit-text != "") {
root.ime-edit-pending = true;
root.pending-key-text = "";
}
root.editor-layout-changed();
// Re-submit the unchanged selection so Slint refreshes the native
// candidate rectangle after cosmic-text advances the preedit caret.
body-input.set-selection-offsets(
body-input.anchor-position-byte-offset,
body-input.cursor-position_byte-offset,
);
}
changed body-color => { root.editor-layout-changed(); }
changed accent-color => { root.editor-layout-changed(); }
changed editor-caret-y => { root.reveal-editor-caret(); }
changed editor-caret-height => { root.reveal-editor-caret(); }
editor-scroll-layout-timer := Timer {
interval: 1ms;
running: false;
triggered => {
self.stop();
root.editor-layout-changed();
}
}
caret-timer := Timer {
interval: 520ms;
running: root.open && body-input.has-focus;
triggered => { root.caret-visible = !root.caret-visible; }
}
private property <bool> framed: !root.compact && !root.windowed;
background: root.background-color;
border-radius: root.framed ? 12px : 0px;
border-width: root.framed ? 1px : 0px;
border-color: root.frame-border-color;
drop-shadow-blur: root.framed ? 24px : 0px;
drop-shadow-offset-y: root.framed ? 6px : 0px;
drop-shadow-color: root.framed ? #00000035 : transparent;
clip: true;
VerticalBox {
spacing: 0px;
if !root.windowed : ComposerHeader {
compact: root.compact;
mode: root.mode;
can-pop-out: root.can-pop-out && !root.sending;
pop-out => { root.pop-out(); }
control-hover-color: root.control-hover-color;
muted-color: root.muted-color;
raised-color: root.raised-color;
secondary-color: root.secondary-color;
subtle-border-color: root.subtle-border-color;
title-color: root.title-color;
request-close => { root.request-close(); }
}
ComposerFromRow {
accounts: root.accounts;
selected-account-id <=> root.selected-account-id;
from-label <=> root.from-label;
background-color: root.background-color;
frame-border-color: root.subtle-border-color;
hover-color: root.control-hover-color;
muted-color: root.muted-color;
secondary-color: root.secondary-color;
selected-background: root.accent-background;
title-color: root.title-color;
changed(account-id) => {
AccountMailPreferences.composer-sender-email = "";
root.search-contacts(root.to, account-id);
AccountMailPreferences.composer-account-changed(account-id);
}
}
SettingsSelectRow {
compact: root.compact;
title: @tr("From");
description: @tr("Verified sender identity");
choices: AccountMailPreferences.composer-sender-names;
selected-index: AccountMailPreferences.composer-sender-index;
title-color: root.title-color; description-color: root.secondary-color;
selected(index, value) => {
if AccountMailPreferences.composer-preferences-ready {
AccountMailPreferences.composer-sender(index);
}
}
}
SettingsSelectRow {
compact: root.compact;
title: @tr("Signature");
description: AccountMailPreferences.composer-security;
choices: AccountMailPreferences.composer-signature-names;
selected-index: AccountMailPreferences.composer-signature-index;
title-color: root.title-color; description-color: root.secondary-color;
selected(index, value) => { if AccountMailPreferences.composer-preferences-ready { AccountMailPreferences.composer-signature(index); } }
}
recipients := ComposerRecipients {
open: root.open;
to <=> root.to;
cc <=> root.cc;
bcc <=> root.bcc;
contacts: root.contact-suggestions;
accent-background: root.accent-background;
accent-color: root.accent-color;
background-color: root.background-color;
body-color: root.body-color;
control-hover-color: root.control-hover-color;
muted-color: root.muted-color;
raised-color: root.raised-color;
subtle-border-color: root.subtle-border-color;
title-color: root.title-color;
search-contacts(value) => {
root.search-contacts(value, root.selected-account-id);
}
contact-selected(index) => {
root.to = root.select-contact(index, root.to);
}
}
Rectangle {
height: 1px;
background: root.subtle-border-color;
}
ComposerSubject {
subject <=> root.subject;
accent-color: root.accent-color;
background-color: root.background-color;
muted-color: root.muted-color;
title-color: root.title-color;
}
ComposerToolbar {
compact: root.compact;
accent-background: root.accent-background;
accent-color: root.accent-color;
control-hover-color: root.control-hover-color;
muted-color: root.muted-color;
raised-color: root.raised-color;
secondary-color: root.secondary-color;
subtle-border-color: root.subtle-border-color;
bold-active: root.bold-active;
italic-active: root.italic-active;
underline-active: root.underline-active;
strike-active: root.strike-active;
code-active: root.code-active;
link-active: root.active-link-url != "";
can-link: root.selection-start != root.selection-end;
apply-format(kind) => { root.apply-format(kind); }
open-templates => { root.templates-open = true; }
undo => {
root.history-requested("undo");
body-input.set-selection-offsets(root.selection-start, root.selection-end);
body-input.focus();
}
redo => {
root.history-requested("redo");
body-input.set-selection-offsets(root.selection-start, root.selection-end);
body-input.focus();
}
}
editor-surface := Rectangle {
vertical-stretch: 1;
min-height: 110px;
background: root.background-color;
clip: true;
body-scroll := ScrollView {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
content-width: self.visible-width;
content-height: root.quote == "" && root.quote-left-out == ""
? max(self.visible-height, root.editor-content-height * 1px + 32px)
: max(self.visible-height, root.quote-top + (root.quote-expanded ? root.quote-height * 1px + 16px : 0px));
horizontal-scrollbar-policy: always-off;
mouse-drag-pan-enabled: root.compact;
Text {
x: 18px;
y: 16px;
width: body-scroll.visible-width - 36px;
visible: root.body == "" && root.editor-preedit-text == "";
text: @tr("Write your message…");
color: root.muted-color;
font-size: TypeScale.body-large;
wrap: word-wrap;
}
for tile in root.editor-tiles : Image {
x: 18px;
y: 14px + tile.y * 1px;
width: root.editor-width;
height: tile.height * 1px;
source: tile.image;
image-fit: fill;
image-rendering: smooth;
// The transparent TextInput below exposes the editable
// body; these raster tiles are its visual presentation.
accessible-role: none;
}
body-input := TextInput {
// Keep Slint's native IME candidate rectangle aligned with
// the caret from cosmic-text, despite the proxy's plain layout.
x: 18px + root.editor-caret-x * 1px - root.input-caret-x;
y: 14px + root.editor-caret-y * 1px - root.input-caret-y;
width: root.editor-width;
height: root.editor-area-height;
text <=> root.body;
color: transparent;
selection-background-color: transparent;
selection-foreground-color: transparent;
font-size: TypeScale.body-large;
text-cursor-width: 0px;
single-line: false;
enabled: !root.sending;
wrap: word-wrap;
page-height: body-scroll.visible-height;
edited => {
root.body = self.text;
root.body-edited(
self.text,
self.anchor-position-byte-offset,
self.cursor-position_byte_offset,
root.ime-edit-pending ? "" : root.pending-key-text,
);
root.pending-key-text = "";
root.ime-edit-pending = false;
self.set-selection-offsets(root.selection-start, root.selection-end);
}
key-pressed(event) => {
root.pending-key-text = "";
// The menu key and Shift+F10 open the editor menu at the caret.
if (event.text == Key.Menu || (event.modifiers.shift && event.text == Key.F10)) {
root.editor-menu-position = {
x: root.editor-caret-x * 1px,
y: (root.editor-caret-y + root.editor-caret-height) * 1px,
};
root.editor-caret-menu-requested();
return accept;
}
if ((event.modifiers.control || event.modifiers.meta) && event.text.to-lowercase() == "b") {
root.apply-format("bold");
return accept;
}
if ((event.modifiers.control || event.modifiers.meta) && event.text.to-lowercase() == "i") {
root.apply-format("italic");
return accept;
}
if ((event.modifiers.control || event.modifiers.meta) && event.text.to-lowercase() == "u") {
root.apply-format("underline");
return accept;
}
if ((event.modifiers.control || event.modifiers.meta) && event.modifiers.shift && event.text.to-lowercase() == "x") {
root.apply-format("strike");
return accept;
}
if ((event.modifiers.control || event.modifiers.meta) && event.text.to-lowercase() == "z") {
root.history-requested(event.modifiers.shift ? "redo" : "undo");
self.set-selection-offsets(root.selection-start, root.selection-end);
return accept;
}
if ((event.modifiers.control || event.modifiers.meta) && event.text == Key.Return && root.can-send) {
root.submit(true);
return accept;
}
if (root.editor-key-event(
event.text,
event.modifiers.control,
event.modifiers.shift,
event.modifiers.alt,
event.modifiers.meta,
)) {
self.set-selection-offsets(root.selection-start, root.selection-end);
return accept;
}
if (!event.modifiers.control && !event.modifiers.alt && !event.modifiers.meta
&& root.editor-preedit-text == "" && !root.ime-edit-pending) {
root.pending-key-text = event.text;
}
reject
}
cursor-position-changed(position) => {
root.input-caret-x = position.x;
root.input-caret-y = position.y;
root.caret-visible = true;
// Only a focused proxy moves the caret for the user.
// Unfocused it follows the document, and a new window's
// would report the start as where the user is typing.
if (self.has-focus) {
root.selection-changed(
self.text,
self.anchor-position-byte-offset,
self.cursor-position_byte_offset,
);
}
root.reveal-editor-caret();
}
}
// Corrections for a misspelled word, editing, and spelling languages. The
// editor's pointer area sits inside it and asks for it on a right press, so
// presses reach the editor first, as on Android, where Slint holds every
// press on a menu's own area to time a long press.
editor-menu := ContextMenuArea {
x: 18px;
y: 14px;
width: root.editor-width;
height: root.editor-area-height;
Menu {
for suggestion[index] in root.spelling-suggestions : MenuItem {
title: suggestion;
activated => {
root.spelling-correct(index);
body-input.set-selection-offsets(root.selection-start, root.selection-end);
}
}
if root.spelling-word != "" && root.spelling-suggestions.length == 0 : MenuItem {
title: @tr("No suggestions");
enabled: false;
}
if root.spelling-word != "" : MenuItem {
title: @tr("Add to dictionary");
activated => { root.spelling-learn(); }
}
if root.spelling-word != "" : MenuItem {
title: @tr("Ignore");
activated => { root.spelling-ignore(); }
}
// A tapped word's menu holds only spelling, as phones offer.
if root.spelling-word != "" && !root.editor-menu-touch : MenuSeparator {}
if !root.editor-menu-touch : MenuItem {
title: @tr("Cut");
enabled: !root.sending && root.selection-start != root.selection-end;
activated => { body-input.cut(); }
}
if !root.editor-menu-touch : MenuItem {
title: @tr("Copy");
enabled: root.selection-start != root.selection-end;
activated => { body-input.copy(); }
}
if !root.editor-menu-touch : MenuItem {
title: @tr("Paste");
enabled: !root.sending;
activated => { body-input.paste(); }
}
if !root.editor-menu-touch : MenuItem {
title: @tr("Select all");
enabled: root.body != "";
activated => { body-input.select-all(); }
}
if root.spelling-available : MenuSeparator {}
if root.spelling-available : Menu {
title: @tr("Spelling");
MenuItem {
title: @tr("Check spelling");
checkable: true;
checked <=> root.spelling-switch-shown;
activated => { root.spelling-toggle(); }
}
if !root.spelling-languages-automatic : MenuSeparator {}
for language[index] in root.spelling-languages : MenuItem {
title: language.name;
checkable: true;
checked: language.checked;
enabled: root.spelling-enabled;
activated => { root.spelling-toggle-language(index); }
}
if !root.spelling-languages-automatic && root.spelling-languages.length == 0 : MenuItem {
title: @tr("No dictionaries installed");
enabled: false;
}
}
}
editor-hit := TouchArea {
width: parent.width;
height: parent.height;
mouse-cursor: text;
pointer-event(event) => {
if (event.button == PointerEventButton.right && event.kind == PointerEventKind.down) {
body-input.focus();
root.editor-menu-position = { x: self.mouse-x, y: self.mouse-y };
root.editor-menu-requested(self.mouse-x / 1px, self.mouse-y / 1px);
}
if (event.button == PointerEventButton.left || event.kind == PointerEventKind.move || event.kind == PointerEventKind.cancel) {
if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
body-input.focus();
}
root.editor-pointer-event(
editor-hit.mouse-x / 1px,
editor-hit.mouse-y / 1px,
event.kind == PointerEventKind.down ? "down" : event.kind == PointerEventKind.move ? "move" : event.kind == PointerEventKind.up ? "up" : "cancel",
event.modifiers.control,
event.modifiers.shift,
event.modifiers.alt,
event.modifiers.meta,
);
body-input.set-selection-offsets(root.selection-start, root.selection-end);
}
// A finger lifted where it went down: a tap, which may be on a
// misspelled word. A drag, or a scroll that takes the touch, isn't.
if (event.touch-finger-id != 0) {
if (event.kind == PointerEventKind.down) {
root.touch-down-position = { x: self.mouse-x, y: self.mouse-y };
root.touch-still = true;
} else if (event.kind == PointerEventKind.move) {
if (abs(self.mouse-x - root.touch-down-position.x) > 8px || abs(self.mouse-y - root.touch-down-position.y) > 8px) {
root.touch-still = false;
}
} else if (event.kind == PointerEventKind.up && root.touch-still) {
root.touch-still = false;
root.editor-menu-position = { x: self.mouse-x, y: self.mouse-y };
root.editor-tapped(self.mouse-x / 1px, self.mouse-y / 1px);
} else {
root.touch-still = false;
}
}
}
}
}
if root.quote != "" : Rectangle {
x: 18px;
y: root.quote-top - 40px;
width: root.editor-width;
height: 32px;
quote-toggle := TouchArea {
width: parent.width - 80px;
mouse-cursor: pointer;
clicked => { root.quote-expanded = !root.quote-expanded; }
}
Image {
x: 0px;
y: (parent.height - 16px) / 2;
width: 16px;
height: 16px;
source: root.quote-expanded
? @image-url("../icons/lucide/outline/chevron-down.svg")
: @image-url("../icons/lucide/outline/chevron-right.svg");
colorize: root.secondary-color;
}
Text {
x: 22px;
width: parent.width - 102px;
height: parent.height;
text: root.quote == "forward" ? @tr("Forwarded message") : @tr("Quoted text");
color: quote-toggle.has-hover ? root.body-color : root.secondary-color;
font-size: TypeScale.body;
vertical-alignment: center;
overflow: elide;
accessible-role: button;
accessible-label: root.quote-expanded ? @tr("Hide the quoted message") : @tr("Show the quoted message");
accessible-action-default => { root.quote-expanded = !root.quote-expanded; }
}
IconButton {
x: parent.width - 72px;
width: 32px;
height: 32px;
icon: @image-url("../icons/lucide/outline/pencil.svg");
icon-color: root.secondary-color;
hover-background: root.control-hover-color;
tooltip-text: @tr("Edit the quoted message as text");
clicked => { root.edit-quote(); }
}
IconButton {
x: parent.width - 36px;
width: 32px;
height: 32px;
icon: @image-url("../icons/lucide/outline/x.svg");
icon-color: root.secondary-color;
hover-background: root.control-hover-color;
tooltip-text: root.quote == "forward" ? @tr("Remove the forwarded message") : @tr("Remove the quoted message");
clicked => { root.remove-quote(); }
}
}
// A left-out original can come back until the message closes.
if root.quote == "" && root.quote-left-out != "" : Rectangle {
x: 18px;
y: root.quote-top - 40px;
width: root.editor-width;
height: 32px;
restore-touch := TouchArea {
width: parent.width;
mouse-cursor: pointer;
clicked => { root.restore-quote(); }
}
Image {
x: 0px;
y: (parent.height - 16px) / 2;
width: 16px;
height: 16px;
source: @image-url("../icons/lucide/outline/undo.svg");
colorize: restore-touch.has-hover ? root.body-color : root.secondary-color;
}
Text {
x: 22px;
width: parent.width - 22px;
height: parent.height;
text: root.quote-left-out == "forward" ? @tr("Add the forwarded message back") : @tr("Add the quoted text back");
color: restore-touch.has-hover ? root.body-color : root.secondary-color;
font-size: TypeScale.body;
vertical-alignment: center;
overflow: elide;
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { root.restore-quote(); }
}
}
for tile in root.quote-tiles : Image {
x: 18px;
y: root.quote-top + tile.y * 1px;
width: root.editor-width;
height: tile.height * 1px;
source: tile.image;
image-fit: fill;
image-rendering: smooth;
visible: root.quote-expanded;
accessible-role: none;
}
// The drawn original reads out as its text, as the reader's
// message does.
if root.quote != "" && root.quote-expanded : Rectangle {
x: 18px;
y: root.quote-top;
width: root.editor-width;
height: root.quote-height * 1px;
accessible-role: text;
accessible-label: root.quote-text;
}
Rectangle {
x: 18px + root.editor-caret-x * 1px;
y: 14px + root.editor-caret-y * 1px;
width: 1px;
height: root.editor-caret-height * 1px;
visible: body-input.has-focus
&& body-input.anchor-position-byte-offset == body-input.cursor-position_byte_offset
&& root.caret-visible;
background: root.body-color;
}
}
}
ComposerAttachments {
compact: root.compact;
attachments: root.attachments;
accent-color: root.accent-color;
background-color: root.background-color;
body-color: root.body-color;
control-hover-color: root.control-hover-color;
muted-color: root.muted-color;
raised-color: root.raised-color;
subtle-border-color: root.subtle-border-color;
remove-attachment(index) => { root.remove-attachment(index); }
}
ComposerNotice {
notice: root.notice;
notice-is-error: root.notice-is-error;
accent-background: root.accent-background;
accent-color: root.accent-color;
}
ComposerFooter {
compact: root.compact;
attachment-picker-supported: root.attachment-picker-supported;
attachment-summary: root.attachment-summary;
can-send: root.can-send;
has-content: root.has-content;
sending: root.sending;
control-hover-color: root.control-hover-color;
muted-color: root.muted-color;
raised-color: root.raised-color;
secondary-color: root.secondary-color;
subtle-border-color: root.subtle-border-color;
submit(send-now) => { root.submit(send-now); }
pick-attachments => { root.pick-attachments(); }
request-close => { root.request-close(); }
}
}
DiscardConfirmation {
compact: root.compact;
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
open: root.confirm-discard;
background-color: root.background-color;
frame-border-color: root.frame-border-color;
secondary-color: root.secondary-color;
title-color: root.title-color;
keep => { root.confirm-discard = false; }
discard => {
root.confirm-discard = false;
root.close();
}
}
LinkEditor {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
compact: root.compact;
open: root.link-editor-open;
url <=> root.link-url;
has-existing-link: root.active-link-url != "";
background-color: root.background-color;
body-color: root.body-color;
frame-border-color: root.frame-border-color;
muted-color: root.muted-color;
title-color: root.title-color;
accent-color: root.accent-color;
cancel => {
root.link-editor-open = false;
body-input.set-selection-offsets(root.link-selection-start, root.link-selection-end);
body-input.focus();
}
apply(url) => {
root.link-editor-open = false;
root.link-requested(url, root.body, root.link-selection-start, root.link-selection-end);
body-input.set-selection-offsets(root.selection-start, root.selection-end);
body-input.focus();
}
remove => {
root.link-editor-open = false;
root.link-requested("", root.body, root.link-selection-start, root.link-selection-end);
body-input.set-selection-offsets(root.selection-start, root.selection-end);
body-input.focus();
}
}
EmailTemplates {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
open <=> root.templates-open;
compact: root.compact;
templates: root.templates;
draft-subject: root.subject;
draft-body: root.body;
notice: root.notice;
notice-is-error: root.notice-is-error;
surface-color: root.background-color;
raised-color: root.raised-color;
ui-border-color: root.frame-border-color;
hover-color: root.control-hover-color;
title-color: root.title-color;
body-color: root.body-color;
muted-color: root.muted-color;
accent-color: root.accent-color;
refresh => { root.refresh-templates(); }
use-template(id) => root.use-template(id);
save-template(id, name, shortcut, subject, body) => root.save-template(id, name, shortcut, subject, body);
delete-template(id) => root.delete-template(id);
}
// Freeze the exact draft snapshot being submitted. If delivery fails the
// overlay disappears and the same fields become editable again.
if root.sending : TouchArea {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
mouse-cursor: wait;
}
}