mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Pictures: Picture Styles gallery and Picture Border
Picture Styles was a menu of six names, each running two or three separate commands that left the previous style's effects behind. New picture.style applies one of PowerPoint's 24 picture styles (frames, shadows, soft edges, reflections, bevels, rounded, snipped and oval shapes, a rotated frame) as one step that replaces the whole previous look; picture.styles lists them and "none" clears the style. Picture Format shows them as a thumbnail gallery, and a Picture Border split button sets outline colour, weight and dashes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@ pub mod insert;
|
||||
pub mod inspect;
|
||||
pub mod media;
|
||||
pub mod merge;
|
||||
pub mod picture_style;
|
||||
pub mod review;
|
||||
pub mod selection;
|
||||
pub mod shape;
|
||||
@@ -130,6 +131,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(slide::specs());
|
||||
v.extend(insert::specs());
|
||||
v.extend(shape::specs());
|
||||
v.extend(picture_style::specs());
|
||||
v.extend(arrange::specs());
|
||||
v.extend(selection::specs());
|
||||
v.extend(merge::specs());
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Picture Format ▸ Picture Styles: PowerPoint's quick styles for pictures, each a frame (shape,
|
||||
//! outline) and effects applied as one step, replacing the previous style.
|
||||
|
||||
use deckcraft_color::{ColorTransform, Rgba};
|
||||
use deckcraft_model::style::{Effects, Reflection, Shadow};
|
||||
use deckcraft_model::{ColorRef, Geom, Line, Shape};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"picture.style",
|
||||
"Picture Styles",
|
||||
["Picture Format", "Picture Styles"],
|
||||
None,
|
||||
"{style: simpleFrameWhite|dropShadowRectangle|softEdgeOval|… | none (see picture.styles), ids?}",
|
||||
has_selection,
|
||||
style
|
||||
),
|
||||
cmd!(query "picture.styles", "Picture Style Gallery", [], None, "{} → [{id, label}]", always, |_s, _p| Ok(json!(
|
||||
STYLES.iter().map(|(id, label)| json!({"id": id, "label": label})).collect::<Vec<_>>()
|
||||
))),
|
||||
]
|
||||
}
|
||||
|
||||
/// (id, label), in PowerPoint's gallery order.
|
||||
pub const STYLES: [(&str, &str); 24] = [
|
||||
("simpleFrameWhite", "Simple Frame, White"),
|
||||
("beveledMatteWhite", "Beveled Matte, White"),
|
||||
("metalFrame", "Metal Frame"),
|
||||
("dropShadowRectangle", "Drop Shadow Rectangle"),
|
||||
("roundedDiagonalCornerWhite", "Rounded Diagonal Corner, White"),
|
||||
("snipDiagonalCornerWhite", "Snip Diagonal Corner, White"),
|
||||
("moderateFrameWhite", "Moderate Frame, White"),
|
||||
("rotatedWhite", "Rotated, White"),
|
||||
("perspectiveShadowWhite", "Perspective Shadow, White"),
|
||||
("softEdgeRectangle", "Soft Edge Rectangle"),
|
||||
("doubleFrameBlack", "Double Frame, Black"),
|
||||
("thickMatteBlack", "Thick Matte, Black"),
|
||||
("simpleFrameBlack", "Simple Frame, Black"),
|
||||
("beveledOvalBlack", "Beveled Oval, Black"),
|
||||
("compoundFrameBlack", "Compound Frame, Black"),
|
||||
("centerShadowRectangle", "Center Shadow Rectangle"),
|
||||
("softEdgeOval", "Soft Edge Oval"),
|
||||
("reflectedRoundedRectangle", "Reflected Rounded Rectangle"),
|
||||
("reflectedBevelWhite", "Reflected Bevel, White"),
|
||||
("metalRoundedRectangle", "Metal Rounded Rectangle"),
|
||||
("moderateFrameBlack", "Moderate Frame, Black"),
|
||||
("bevelRectangle", "Bevel Rectangle"),
|
||||
("bevelPerspective", "Bevel Perspective"),
|
||||
("metalOval", "Metal Oval"),
|
||||
];
|
||||
|
||||
/// The look of a style: geometry, outline (colour, width) and effects.
|
||||
pub struct Look {
|
||||
pub preset: &'static str,
|
||||
pub frame: Option<(Rgba, f64)>,
|
||||
pub shadow: Option<&'static str>,
|
||||
pub soft_edge: Option<f64>,
|
||||
pub reflection: bool,
|
||||
pub bevel: Option<&'static str>,
|
||||
pub rot: Option<f64>,
|
||||
}
|
||||
|
||||
pub fn look(id: &str) -> Option<Look> {
|
||||
let white = Rgba::rgb(0xFF, 0xFF, 0xFF);
|
||||
let black = Rgba::rgb(0, 0, 0);
|
||||
let silver = Rgba::rgb(0xC0, 0xC0, 0xC0);
|
||||
let l = |preset, frame, shadow| Look { preset, frame, shadow, soft_edge: None, reflection: false, bevel: None, rot: None };
|
||||
Some(match id {
|
||||
"simpleFrameWhite" => l("rect", Some((white, 7.0)), Some("soft")),
|
||||
"beveledMatteWhite" => Look { bevel: Some("circle"), ..l("rect", Some((white, 6.0)), Some("soft")) },
|
||||
"metalFrame" => Look { bevel: Some("angle"), ..l("rect", Some((silver, 7.0)), Some("soft")) },
|
||||
"dropShadowRectangle" => l("rect", None, Some("drop")),
|
||||
"roundedDiagonalCornerWhite" => l("round2DiagRect", Some((white, 6.0)), Some("soft")),
|
||||
"snipDiagonalCornerWhite" => l("snip2DiagRect", Some((white, 6.0)), Some("soft")),
|
||||
"moderateFrameWhite" => l("rect", Some((white, 4.5)), Some("soft")),
|
||||
"rotatedWhite" => Look { rot: Some(356.0), ..l("rect", Some((white, 6.0)), Some("soft")) },
|
||||
"perspectiveShadowWhite" => l("rect", Some((white, 4.0)), Some("perspective")),
|
||||
"softEdgeRectangle" => Look { soft_edge: Some(10.0), ..l("rect", None, None) },
|
||||
"doubleFrameBlack" => l("rect", Some((black, 6.0)), None),
|
||||
"thickMatteBlack" => l("rect", Some((black, 10.0)), Some("soft")),
|
||||
"simpleFrameBlack" => l("rect", Some((black, 5.0)), None),
|
||||
"beveledOvalBlack" => Look { bevel: Some("circle"), ..l("ellipse", Some((black, 4.0)), Some("soft")) },
|
||||
"compoundFrameBlack" => l("rect", Some((black, 4.5)), None),
|
||||
"centerShadowRectangle" => l("rect", None, Some("center")),
|
||||
"softEdgeOval" => Look { soft_edge: Some(12.0), ..l("ellipse", None, None) },
|
||||
"reflectedRoundedRectangle" => Look { reflection: true, ..l("roundRect", None, None) },
|
||||
"reflectedBevelWhite" => Look { reflection: true, bevel: Some("circle"), ..l("rect", Some((white, 4.0)), None) },
|
||||
"metalRoundedRectangle" => Look { bevel: Some("angle"), ..l("roundRect", Some((silver, 6.0)), Some("soft")) },
|
||||
"moderateFrameBlack" => l("rect", Some((black, 4.5)), Some("soft")),
|
||||
"bevelRectangle" => Look { bevel: Some("angle"), ..l("rect", None, Some("soft")) },
|
||||
"bevelPerspective" => Look { bevel: Some("relaxedInset"), ..l("rect", None, Some("perspective")) },
|
||||
"metalOval" => Look { bevel: Some("angle"), ..l("ellipse", Some((silver, 6.0)), Some("soft")) },
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn shadow(kind: &str) -> Shadow {
|
||||
let base = Shadow {
|
||||
color: ColorRef::rgb(Rgba::BLACK).with(ColorTransform::Alpha(40000)),
|
||||
blur: 4.0,
|
||||
dist: 3.0,
|
||||
dir: 45.0,
|
||||
inner: false,
|
||||
sx: 1.0,
|
||||
sy: 1.0,
|
||||
kx: 0.0,
|
||||
ky: 0.0,
|
||||
align: "tl".into(),
|
||||
rotate_with_shape: false,
|
||||
};
|
||||
match kind {
|
||||
"drop" => Shadow { blur: 12.0, dist: 6.0, dir: 90.0, ..base },
|
||||
"center" => Shadow { blur: 16.0, dist: 0.0, ..base },
|
||||
"perspective" => Shadow { sy: 0.23, kx: -20.0, dir: 90.0, dist: 0.0, blur: 6.0, align: "bl".into(), ..base },
|
||||
_ => base,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply style `id` (or clear the style with `none`) to `sh`.
|
||||
pub fn apply(sh: &mut Shape, id: &str) -> bool {
|
||||
if id == "none" {
|
||||
sh.geom = Geom::preset("rect");
|
||||
sh.line = None;
|
||||
sh.effects = None;
|
||||
return true;
|
||||
}
|
||||
let Some(lk) = look(id) else { return false };
|
||||
sh.geom = Geom::preset(lk.preset);
|
||||
sh.line = Some(lk.frame.map_or_else(Line::none, |(c, w)| Line::solid(ColorRef::rgb(c), w)));
|
||||
let e = Effects {
|
||||
outer_shadow: lk.shadow.map(shadow),
|
||||
soft_edge: lk.soft_edge,
|
||||
reflection: lk.reflection.then_some(Reflection { blur: 0.5, start_alpha: 0.5, end_alpha: 0.0, end_pos: 0.35, dist: 0.0 }),
|
||||
bevel: lk.bevel.map(str::to_string),
|
||||
..Default::default()
|
||||
};
|
||||
sh.effects = (!e.is_empty()).then_some(e);
|
||||
if let (Some(r), Some(x)) = (lk.rot, sh.xfrm.as_mut()) {
|
||||
x.rot = r;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = str_param(p, "style").ok_or_else(|| bad("picture.style", "missing `style`"))?.to_string();
|
||||
if id != "none" && look(&id).is_none() {
|
||||
return Err(bad("picture.style", format!("unknown style `{id}`; see picture.styles")));
|
||||
}
|
||||
edit_shapes(s, p, "picture.style", |sh| {
|
||||
apply(sh, &id);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -835,3 +835,51 @@ fn slide_ranges_parse_like_powerpoints_print_box() {
|
||||
let opts = cmd::file::pdf_options(&json!({"slides": [0, 7, 1]}), 2).unwrap();
|
||||
assert_eq!(opts.slides, Some(vec![0, 1]), "indices past the end are dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picture_styles_replace_each_other_in_one_step() {
|
||||
const PNG: &[u8] = &[
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0xf8, 0xcf,
|
||||
0xc0, 0xf0, 0x1f, 0x00, 0x05, 0x00, 0x01, 0xff, 0x89, 0x99, 0x3d, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
|
||||
0x82,
|
||||
];
|
||||
let mut s = session();
|
||||
let data = cmd::base64_encode(PNG);
|
||||
let id = s.execute("insert.picture", &json!({"data": data, "name": "a.png", "rect": [100.0, 100.0, 200.0, 150.0]})).unwrap()["id"].clone();
|
||||
let pic = |s: &Session| s.doc().unwrap().shapes().iter().find(|sh| json!(sh.id) == id).unwrap().clone();
|
||||
assert_eq!(s.execute("picture.styles", &json!({})).unwrap().as_array().unwrap().len(), 24);
|
||||
|
||||
s.execute("picture.style", &json!({"ids": [id], "style": "reflectedRoundedRectangle"})).unwrap();
|
||||
let p = pic(&s);
|
||||
assert_eq!(p.geom.preset_name(), Some("roundRect"));
|
||||
assert!(p.effects.as_ref().unwrap().reflection.is_some());
|
||||
|
||||
// The next style replaces the whole look: no leftover reflection.
|
||||
s.execute("picture.style", &json!({"ids": [id], "style": "simpleFrameWhite"})).unwrap();
|
||||
let p = pic(&s);
|
||||
assert_eq!(p.geom.preset_name(), Some("rect"));
|
||||
let e = p.effects.as_ref().unwrap();
|
||||
assert!(e.reflection.is_none() && e.outer_shadow.is_some());
|
||||
assert_eq!(p.line.as_ref().and_then(|l| l.width), Some(7.0));
|
||||
|
||||
s.execute("picture.style", &json!({"ids": [id], "style": "softEdgeOval"})).unwrap();
|
||||
let p = pic(&s);
|
||||
assert_eq!(p.geom.preset_name(), Some("ellipse"));
|
||||
assert_eq!(p.effects.as_ref().unwrap().soft_edge, Some(12.0));
|
||||
assert!(p.line.as_ref().is_some_and(|l| l.fill == Some(deckcraft_model::Fill::None)));
|
||||
|
||||
// Survives PPTX.
|
||||
let doc = s.doc().unwrap().doc.as_ref().clone();
|
||||
let back = deckcraft_pptx::import(&deckcraft_pptx::export(&doc).unwrap()).unwrap();
|
||||
let bp = back.slides.iter().flat_map(|sl| &sl.shapes).find(|sh| matches!(sh.kind, deckcraft_model::ShapeKind::Picture { .. })).unwrap();
|
||||
assert_eq!(bp.geom.preset_name(), Some("ellipse"));
|
||||
assert_eq!(bp.effects.as_ref().and_then(|e| e.soft_edge), Some(12.0));
|
||||
|
||||
// One undo step per style; none clears; unknown is refused.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(pic(&s).geom.preset_name(), Some("rect"));
|
||||
s.execute("picture.style", &json!({"ids": [id], "style": "none"})).unwrap();
|
||||
assert!(pic(&s).effects.is_none() && pic(&s).line.is_none());
|
||||
assert!(s.execute("picture.style", &json!({"ids": [id], "style": "sparkly"})).is_err());
|
||||
}
|
||||
|
||||
@@ -2090,29 +2090,59 @@ fn picture_format(app: &mut SlideApp, ui: &mut Ui) {
|
||||
});
|
||||
});
|
||||
group(ui, |ui| {
|
||||
let r = big_button(ui, Icon::QuickStyles, "Picture\nStyles", true);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
if ui.button("Simple Frame, White").clicked() {
|
||||
run(app, "shape.line", json!({"color": "#FFFFFF", "width": 6}));
|
||||
run(app, "shape.effects", json!({"shadow": "outer"}));
|
||||
}
|
||||
if ui.button("Drop Shadow Rectangle").clicked() {
|
||||
run(app, "shape.effects", json!({"shadow": "bottom"}));
|
||||
}
|
||||
if ui.button("Rounded Diagonal Corner").clicked() {
|
||||
run(app, "shape.change", json!({"preset": "round2DiagRect"}));
|
||||
}
|
||||
if ui.button("Soft Edge Rectangle").clicked() {
|
||||
run(app, "shape.effects", json!({"softEdges": 10}));
|
||||
}
|
||||
if ui.button("Reflected Rounded Rectangle").clicked() {
|
||||
run(app, "shape.change", json!({"preset": "roundRect"}));
|
||||
run(app, "shape.effects", json!({"reflection": "half"}));
|
||||
}
|
||||
if ui.button("Oval").clicked() {
|
||||
run(app, "shape.change", json!({"preset": "ellipse"}));
|
||||
use deckcraft_engine::cmd::picture_style::{STYLES, look};
|
||||
let names: Vec<String> = STYLES.iter().map(|(_, l)| l.to_string()).collect();
|
||||
if let Some(i) = widgets::gallery(
|
||||
ui,
|
||||
STYLES.len(),
|
||||
vec2(56.0, 44.0),
|
||||
None,
|
||||
|p, r, i| paint_picture_style(p, r, STYLES.get(i).and_then(|(id, _)| look(id))),
|
||||
|i| names.get(i).cloned().unwrap_or_default(),
|
||||
) && let Some((id, _)) = STYLES.get(i)
|
||||
{
|
||||
run(app, "picture.style", json!({"style": id}));
|
||||
}
|
||||
});
|
||||
group(ui, |ui| {
|
||||
let sc = scheme(app);
|
||||
let (m, a) = color_split(ui, Icon::QuickStyles, "Picture Border", Color32::BLACK, true);
|
||||
if m.clicked() {
|
||||
run(app, "shape.line", json!({"color": "#000000", "width": 1.0}));
|
||||
}
|
||||
widgets::color_popup(&a).show(|ui| {
|
||||
if let Some(c) = widgets::color_grid(ui, &sc, Some("No Outline")) {
|
||||
match c {
|
||||
Some(c) => run(app, "shape.line", json!({"color": cref_param(&c)})),
|
||||
None => run(app, "shape.line", json!({"none": true})),
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("Weight", |ui| {
|
||||
for w in [0.25, 0.5, 0.75, 1.0, 1.5, 2.25, 3.0, 4.5, 6.0] {
|
||||
if ui.button(format!("{w} pt")).clicked() {
|
||||
run(app, "shape.line", json!({"width": w}));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button("Dashes", |ui| {
|
||||
for (l, d) in [
|
||||
("Solid", "solid"),
|
||||
("Round Dot", "sysDot"),
|
||||
("Square Dot", "sysDash"),
|
||||
("Dash", "dash"),
|
||||
("Dash Dot", "dashDot"),
|
||||
("Long Dash", "lgDash"),
|
||||
] {
|
||||
if ui.button(l).clicked() {
|
||||
run(app, "shape.line", json!({"dash": d}));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if ui.small_button("Reset Style").on_hover_text("Remove the picture style").clicked() {
|
||||
run(app, "picture.style", json!({"style": "none"}));
|
||||
}
|
||||
big(app, ui, Icon::AltText, "Alt\nText", "app.dialog", json!({"id": "altText"}));
|
||||
});
|
||||
arrange_group(app, ui);
|
||||
@@ -2226,6 +2256,46 @@ fn table_design(app: &mut SlideApp, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// A Picture Styles gallery tile: a landscape "picture" in the style's shape and frame.
|
||||
fn paint_picture_style(p: &egui::Painter, r: Rect, lk: Option<deckcraft_engine::cmd::picture_style::Look>) {
|
||||
let Some(lk) = lk else { return };
|
||||
let r = r.shrink(7.0);
|
||||
if lk.shadow.is_some() {
|
||||
p.rect_filled(r.translate(vec2(2.0, 2.0)), CornerRadius::same(2), Color32::from_black_alpha(60));
|
||||
}
|
||||
let sky = Color32::from_rgb(0x7F, 0xB2, 0xE5);
|
||||
let frame = lk.frame.map(|(c, w)| Stroke::new((w as f32 / 2.5).clamp(1.0, 4.0), theme::to_color32(c)));
|
||||
let edge = Stroke::new(0.5, Color32::from_gray(150));
|
||||
match lk.preset {
|
||||
"ellipse" => {
|
||||
p.add(egui::Shape::ellipse_filled(r.center(), r.size() / 2.0, sky));
|
||||
p.add(egui::Shape::ellipse_stroke(r.center(), r.size() / 2.0, frame.unwrap_or(edge)));
|
||||
}
|
||||
preset => {
|
||||
let round = if preset == "rect" { 0 } else { 6 };
|
||||
p.rect_filled(r, CornerRadius::same(round), sky);
|
||||
p.rect_stroke(r, CornerRadius::same(round), frame.unwrap_or(edge), egui::StrokeKind::Middle);
|
||||
}
|
||||
}
|
||||
// Hills, so the tile reads as a picture.
|
||||
let base = r.bottom() - r.height() * 0.15;
|
||||
p.line_segment(
|
||||
[pos2(r.left() + r.width() * 0.2, base), pos2(r.center().x, r.top() + r.height() * 0.45)],
|
||||
Stroke::new(1.5, Color32::from_rgb(0x3A, 0x7D, 0x44)),
|
||||
);
|
||||
p.line_segment(
|
||||
[pos2(r.center().x, r.top() + r.height() * 0.45), pos2(r.right() - r.width() * 0.2, base)],
|
||||
Stroke::new(1.5, Color32::from_rgb(0x3A, 0x7D, 0x44)),
|
||||
);
|
||||
if lk.reflection {
|
||||
p.rect_filled(
|
||||
Rect::from_min_max(pos2(r.left(), r.bottom() + 1.0), pos2(r.right(), r.bottom() + 5.0)),
|
||||
CornerRadius::ZERO,
|
||||
sky.gamma_multiply(0.3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint_table_style(p: &egui::Painter, r: Rect, id: &str, sc: &deckcraft_color::ColorScheme) {
|
||||
let slot = match id.rsplit('-').next().unwrap_or("accent1") {
|
||||
"accent2" => deckcraft_color::SchemeSlot::Accent2,
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
> **Generated:** 2026-10-10 (summary by `cargo xtask parity`; rows re-scored 2026-10-10 against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
The summary between `<!-- SUMMARY -->
|
||||
**Weighted breadth parity: 79%** over 191 features.
|
||||
**Weighted breadth parity: 80%** over 191 features.
|
||||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 72 | 61 | 11 | 0 | 92% |
|
||||
| P1 | 65 | 38 | 21 | 6 | 75% |
|
||||
| P1 | 65 | 39 | 20 | 6 | 75% |
|
||||
| P2 | 54 | 13 | 15 | 26 | 38% |
|
||||
|
||||
| Area | Parity |
|
||||
@@ -25,7 +25,7 @@ The summary between `<!-- SUMMARY -->
|
||||
| Tables | 89% |
|
||||
| Charts | 65% |
|
||||
| SmartArt | 33% |
|
||||
| Pictures | 48% |
|
||||
| Pictures | 52% |
|
||||
| Media | 67% |
|
||||
| Transitions | 92% |
|
||||
| Animations | 79% |
|
||||
@@ -184,7 +184,7 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou
|
||||
| Pictures | Corrections, colour, transparency | P1 | P |
|
||||
| Pictures | Artistic effects | P2 | M |
|
||||
| Pictures | Remove background | P2 | M |
|
||||
| Pictures | Picture styles and borders | P1 | P |
|
||||
| Pictures | Picture styles and borders | P1 | D |
|
||||
| Pictures | Change / reset picture | P1 | D |
|
||||
| Pictures | Compress pictures | P2 | M |
|
||||
| Pictures | Paste image from clipboard (fails on Windows and Linux, #53) | P0 | P |
|
||||
|
||||
Reference in New Issue
Block a user