Feature history on the left side, with contiguous folders (#449)
The feature history can sit in a panel on the left, with folders that hold one unbroken run of features, drag and drop as one undo step, and a resizable divider. Timeline folders save under format 17.
This commit is contained in:
1 parent
be55a49b04
commit
a8b3b21fd0
17 files changed
+2336
-37
No files matched your search
@@ -55,6 +55,10 @@ pub struct TreeState {
|
||||
pub picked_components: Vec<u64>,
|
||||
/// Canvases picked in the tree.
|
||||
pub picked_canvases: Vec<u64>,
|
||||
/// Folders picked in the feature history (features use the engine selection).
|
||||
pub picked_timeline_groups: Vec<u64>,
|
||||
/// Last feature-history item clicked, for Shift ranges (`f:<id>` or `g:<id>`).
|
||||
pub timeline_anchor: Option<String>,
|
||||
/// The Edit Canvas / Calibrate panel.
|
||||
pub canvas_panel: Option<CanvasPanel>,
|
||||
/// What waits for the Capture Position question: a command id, or [`SAVE`].
|
||||
@@ -496,6 +500,15 @@ struct Actions {
|
||||
}
|
||||
|
||||
pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
browser_panel(app, ui, false);
|
||||
}
|
||||
|
||||
/// The left-sidebar layout: feature history first, parts and scene browser below it.
|
||||
pub fn browser_below_timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
browser_panel(app, ui, true);
|
||||
}
|
||||
|
||||
fn browser_panel(app: &mut SolveApp, ui: &mut egui::Ui, timeline_above: bool) {
|
||||
let t = Tokens::get();
|
||||
app.tree.order.clear();
|
||||
DRAWN.with(|d| d.borrow_mut().clear());
|
||||
@@ -506,16 +519,66 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
.resizable(false)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(6, 6)))
|
||||
.show(ui, |ui| {
|
||||
ui.label(RichText::new("BROWSER").size(11.0).strong().color(t.text_dim));
|
||||
ui.add_space(4.0);
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.scroll_source(egui::scroll_area::ScrollSource { scroll_bar: true, mouse_wheel: true, ..egui::scroll_area::ScrollSource::NONE })
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
component_rows(app, ui, 0, 0, &mut acts);
|
||||
if timeline_above {
|
||||
let available = ui.available_height();
|
||||
let browser_collapsed = app.preferences.timeline_left_browser_collapsed;
|
||||
let max_timeline_height = (available - if browser_collapsed { 30.0 } else { 130.0 }).max(150.0);
|
||||
let remembered = app.preferences.timeline_left_height;
|
||||
let timeline_height =
|
||||
if remembered.is_finite() { remembered.clamp(150.0, max_timeline_height) } else { 360.0_f32.clamp(150.0, max_timeline_height) };
|
||||
ui.allocate_ui_with_layout(vec2(ui.available_width(), timeline_height), egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||
crate::timeline_left::left_timeline(app, ui)
|
||||
});
|
||||
if !browser_collapsed {
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), 8.0), egui::Sense::hover());
|
||||
let response = ui
|
||||
.interact(rect, egui::Id::new("left_history_divider"), egui::Sense::drag())
|
||||
.on_hover_cursor(egui::CursorIcon::ResizeVertical);
|
||||
if response.dragged() {
|
||||
let delta = ui.input(|input| input.pointer.delta().y);
|
||||
if delta.is_finite() {
|
||||
app.preferences.timeline_left_height = (timeline_height + delta).clamp(150.0, max_timeline_height);
|
||||
}
|
||||
}
|
||||
ui.painter().hline(rect.x_range(), rect.center().y, egui::Stroke::new(1.0, t.border));
|
||||
crate::scenario::publish_handle("left-history-divider", rect.center());
|
||||
}
|
||||
let arrow = if browser_collapsed { "▶" } else { "▼" };
|
||||
let response =
|
||||
ui.add(egui::Button::new(RichText::new(format!("{arrow} BROWSER")).size(11.0).strong().color(t.text_dim)).frame(false));
|
||||
crate::scenario::publish_handle("left-browser-toggle", response.rect.center());
|
||||
if response.clicked() {
|
||||
app.preferences.timeline_left_browser_collapsed = !browser_collapsed;
|
||||
}
|
||||
if !browser_collapsed {
|
||||
ui.add_space(4.0);
|
||||
browser_rows(app, ui, &mut acts);
|
||||
}
|
||||
} else {
|
||||
browser_contents(app, ui, &mut acts);
|
||||
}
|
||||
});
|
||||
finish_browser(app, ui, acts);
|
||||
}
|
||||
|
||||
fn browser_contents(app: &mut SolveApp, ui: &mut egui::Ui, acts: &mut Actions) {
|
||||
let t = Tokens::get();
|
||||
ui.label(RichText::new("BROWSER").size(11.0).strong().color(t.text_dim));
|
||||
ui.add_space(4.0);
|
||||
browser_rows(app, ui, acts);
|
||||
}
|
||||
|
||||
fn browser_rows(app: &mut SolveApp, ui: &mut egui::Ui, acts: &mut Actions) {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.scroll_source(egui::scroll_area::ScrollSource { scroll_bar: true, mouse_wheel: true, ..egui::scroll_area::ScrollSource::NONE })
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
component_rows(app, ui, 0, 0, acts);
|
||||
});
|
||||
}
|
||||
|
||||
fn finish_browser(app: &mut SolveApp, ui: &mut egui::Ui, acts: Actions) {
|
||||
app.viewport.hover_bodies = acts.hover_bodies;
|
||||
if let Some(d) = acts.drop {
|
||||
apply_drop(app, d);
|
||||
|
||||
@@ -174,6 +174,7 @@ pub enum RenameWhat {
|
||||
Feature(u64),
|
||||
Component(u64),
|
||||
Group(u64),
|
||||
TimelineGroup(u64),
|
||||
Canvas(u64),
|
||||
View(String),
|
||||
Joint(u64),
|
||||
@@ -184,7 +185,7 @@ pub struct Rename {
|
||||
pub what: RenameWhat,
|
||||
pub text: String,
|
||||
pub at: Pos2,
|
||||
focused: bool,
|
||||
pub(crate) focused: bool,
|
||||
}
|
||||
|
||||
/// Radial slots, clockwise from the top.
|
||||
@@ -1136,6 +1137,7 @@ pub fn start_rename(app: &mut SolveApp, what: RenameWhat, at: Pos2) {
|
||||
RenameWhat::Component(0) => doc.name.clone(),
|
||||
RenameWhat::Component(c) => doc.components.iter().find(|x| x.id == *c).map(|x| x.name.clone()).unwrap_or_default(),
|
||||
RenameWhat::Group(g) => crate::browser::group_name(app, *g).unwrap_or_default(),
|
||||
RenameWhat::TimelineGroup(g) => doc.timeline_groups.iter().find(|group| group.id == *g).map(|group| group.name.clone()).unwrap_or_default(),
|
||||
RenameWhat::Canvas(c) => doc.canvases.iter().find(|x| x.id == *c).map(|x| x.name.clone()).unwrap_or_default(),
|
||||
RenameWhat::View(v) => v.clone(),
|
||||
RenameWhat::Joint(j) => doc.assembly.joints.iter().find(|x| x.id == *j).map(|x| x.name.clone()).unwrap_or_default(),
|
||||
@@ -1143,7 +1145,7 @@ pub fn start_rename(app: &mut SolveApp, what: RenameWhat, at: Pos2) {
|
||||
app.menu.rename = Some(Rename { what, text, at, focused: false });
|
||||
}
|
||||
|
||||
fn commit_rename(app: &mut SolveApp, r: &Rename) {
|
||||
pub(crate) fn commit_rename(app: &mut SolveApp, r: &Rename) {
|
||||
let name = r.text.trim().to_string();
|
||||
if name.is_empty() {
|
||||
return;
|
||||
@@ -1161,6 +1163,7 @@ fn commit_rename(app: &mut SolveApp, r: &Rename) {
|
||||
RenameWhat::Feature(f) => drop(app.run("timeline.rename", json!({ "feature": f, "name": name }))),
|
||||
RenameWhat::Component(c) => drop(app.run("component.rename", json!({ "component": c, "name": name }))),
|
||||
RenameWhat::Group(g) => crate::browser::group_action(app, "ui.renameGroup", &json!({ "group": g, "name": name })),
|
||||
RenameWhat::TimelineGroup(g) => drop(app.run("timeline.rename_group", json!({ "group": g, "name": name }))),
|
||||
RenameWhat::Canvas(c) => drop(app.run("canvas.edit", json!({ "canvas": c, "name": name }))),
|
||||
RenameWhat::View(v) => drop(app.run("view.rename", json!({ "view": v, "name": name }))),
|
||||
RenameWhat::Joint(j) => drop(app.run("joint.edit", json!({ "joint": j, "name": name }))),
|
||||
@@ -1566,6 +1569,11 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
|
||||
}
|
||||
|
||||
fn show_rename(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// Feature-history folders own their editor inside the row. Keep the shared rename state so
|
||||
// automation and keyboard handling still use the same path, but do not draw the floating box.
|
||||
if app.preferences.timeline_left && matches!(app.menu.rename.as_ref().map(|rename| &rename.what), Some(RenameWhat::TimelineGroup(_))) {
|
||||
return;
|
||||
}
|
||||
let Some(mut r) = app.menu.rename.take() else { return };
|
||||
let t = Tokens::get();
|
||||
let mut done: Option<bool> = None;
|
||||
|
||||
@@ -227,7 +227,7 @@ pub enum Kind {
|
||||
},
|
||||
/// Delete a feature that others depend on.
|
||||
ConfirmDelete {
|
||||
feature: u64,
|
||||
features: Vec<u64>,
|
||||
with: Vec<String>,
|
||||
fail: Vec<String>,
|
||||
},
|
||||
@@ -617,7 +617,11 @@ impl Dialog {
|
||||
}
|
||||
|
||||
pub fn confirm_delete(feature: u64, with: Vec<String>, fail: Vec<String>) -> Dialog {
|
||||
Dialog::new(Kind::ConfirmDelete { feature, with, fail }, vec![])
|
||||
Dialog::confirm_delete_many(vec![feature], with, fail)
|
||||
}
|
||||
|
||||
pub fn confirm_delete_many(features: Vec<u64>, with: Vec<String>, fail: Vec<String>) -> Dialog {
|
||||
Dialog::new(Kind::ConfirmDelete { features, with, fail }, vec![])
|
||||
}
|
||||
|
||||
pub fn edit_param(s: &Session, name: &str) -> Dialog {
|
||||
@@ -3212,7 +3216,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
}
|
||||
Kind::EditParam { name, expr } => ("parameters.change", json!({"name": name, "expression": expr})),
|
||||
Kind::Rename { feature, name } => ("timeline.rename", json!({"feature": feature, "name": name})),
|
||||
Kind::ConfirmDelete { feature, .. } => ("timeline.delete", json!({ "features": [feature.to_string()] })),
|
||||
Kind::ConfirmDelete { features, .. } => ("timeline.delete", json!({ "features": features })),
|
||||
Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } => return Ok(Vec::new()),
|
||||
Kind::Assembly(k) => return crate::dialogs_assembly::commands(app, k, &d.inputs),
|
||||
Kind::Sheet(k) => return crate::dialogs_sheet::commands(app, k, &d.inputs, &d.extra),
|
||||
|
||||
@@ -1049,6 +1049,50 @@ pub fn timeline_joints(app: &mut SolveApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
x
|
||||
}
|
||||
|
||||
/// Named joint rows for the vertical feature-history layout.
|
||||
pub fn timeline_joints_vertical(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get();
|
||||
let joints: Vec<(u64, String, JointKind, bool)> =
|
||||
app.session.doc.assembly.joints.iter().map(|joint| (joint.id, joint.name.clone(), joint.kind, joint.suppressed)).collect();
|
||||
if joints.is_empty() {
|
||||
return;
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("JOINTS").size(10.0).strong().color(t.text_dim));
|
||||
let mut open = None;
|
||||
let mut run = None;
|
||||
for (id, name, kind, suppressed) in joints {
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(ui.available_width(), 30.0), egui::Sense::click());
|
||||
if response.hovered() {
|
||||
ui.painter().rect_filled(rect, 3.0, t.hover);
|
||||
}
|
||||
let icon = egui::Rect::from_min_size(egui::pos2(rect.left() + 4.0, rect.top() + 4.0), vec2(22.0, 22.0));
|
||||
crate::icons::paint(ui.painter(), icon, "joint", if suppressed { t.border } else { t.icon }, t.icon_fill, t.accent);
|
||||
if suppressed {
|
||||
ui.painter().line_segment([icon.left_bottom(), icon.right_top()], Stroke::new(1.5, t.text_dim));
|
||||
}
|
||||
ui.painter().text(
|
||||
egui::pos2(icon.right() + 7.0, rect.center().y),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
&name,
|
||||
egui::FontId::proportional(12.0),
|
||||
if suppressed { t.text_dim } else { t.text },
|
||||
);
|
||||
let response = response.on_hover_text(format!("{name} ({kind:?} joint)\nDouble-click to edit · right-click for more"));
|
||||
if response.double_clicked() {
|
||||
open = edit_joint(app, id);
|
||||
}
|
||||
response.context_menu(|ui| joint_menu(app, ui, (id, kind, suppressed), &mut open, &mut run));
|
||||
}
|
||||
if let Some((command, params)) = run {
|
||||
let _ = app.run(command, params);
|
||||
}
|
||||
if let Some(dialog) = open {
|
||||
app.tool = None;
|
||||
app.dialog = Some(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Joints folder at the end of the Browser: a row per joint (double-click edits, the menu
|
||||
/// edits, drives, suppresses or deletes).
|
||||
pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! commands ([`SolveApp::run`]). Nothing here owns design data. The layout follows the familiar
|
||||
//! parametric-CAD desktop: application bar, a toolbar of workspace tabs and panels, the browser
|
||||
//! on the left, the 3D viewport with an axis view and navigation bar, the command dialog on the
|
||||
//! right of the viewport, the command palette and the timeline at the bottom.
|
||||
//! right of the viewport, the command palette and a bottom or left-sidebar feature timeline.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
@@ -117,6 +117,7 @@ pub mod sketch_tools;
|
||||
pub mod stand_up;
|
||||
pub mod theme;
|
||||
pub mod timeline;
|
||||
pub mod timeline_left;
|
||||
pub mod titlebar;
|
||||
pub mod toolbar;
|
||||
pub mod toolbar_custom;
|
||||
@@ -1077,12 +1078,19 @@ impl SolveApp {
|
||||
}
|
||||
}
|
||||
toolbar::toolbar(self, ui);
|
||||
if self.ui.show_timeline {
|
||||
let left_timeline = self.ui.show_timeline && self.preferences.timeline_left;
|
||||
if self.ui.show_timeline && !left_timeline {
|
||||
timeline::timeline(self, ui);
|
||||
}
|
||||
palette::palette_bar(self, ui);
|
||||
if self.ui.show_browser {
|
||||
browser::browser(self, ui);
|
||||
if left_timeline {
|
||||
browser::browser_below_timeline(self, ui);
|
||||
} else {
|
||||
browser::browser(self, ui);
|
||||
}
|
||||
} else if left_timeline {
|
||||
timeline_left::left_panel(self, ui);
|
||||
}
|
||||
preview::update(self, ui.ctx());
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Preferences, one dialog in the spirit of Fusion's, opened from the gear at the top right of the
|
||||
//! window, File › Preferences or Ctrl+, (Cmd+, on macOS): General (theme, UI scale, units of new
|
||||
//! designs, autosave, display precision, the agent cursor), Navigation (mouse presets, zoom
|
||||
//! designs, autosave, display precision, timeline layout, the agent cursor), Navigation (mouse presets, zoom
|
||||
//! direction, zoom to the cursor, orbit centre, Look At animation), Keyboard Shortcuts (search,
|
||||
//! rebind, conflicts; `keymap`), Design (default modelling orientation, Y up or Z up), Graphics
|
||||
//! (visual style, grid, shadow, perspective, anti-aliasing), Sketch and Selection. Changes apply
|
||||
@@ -223,6 +223,13 @@ pub struct Prefs {
|
||||
pub language: String,
|
||||
/// Interface scale (1 = 100 %).
|
||||
pub ui_scale: f32,
|
||||
/// Put the feature history above the parts browser in the left sidebar instead of along the
|
||||
/// bottom of the viewport.
|
||||
pub timeline_left: bool,
|
||||
/// Height of the feature-history section when it shares the left sidebar with the browser.
|
||||
pub timeline_left_height: f32,
|
||||
/// Whether the browser below the left feature history is collapsed.
|
||||
pub timeline_left_browser_collapsed: bool,
|
||||
/// The wheel zooms toward the point under the cursor (else toward the view's centre).
|
||||
pub zoom_to_cursor: bool,
|
||||
/// Spread discrete wheel input across frames (off: apply each event immediately).
|
||||
@@ -285,6 +292,9 @@ impl Default for Prefs {
|
||||
theme: "dark".into(),
|
||||
language: "en".into(),
|
||||
ui_scale: 1.0,
|
||||
timeline_left: false,
|
||||
timeline_left_height: 360.0,
|
||||
timeline_left_browser_collapsed: false,
|
||||
zoom_to_cursor: true,
|
||||
smooth_wheel_zoom: true,
|
||||
trackpad_pan: true,
|
||||
@@ -376,6 +386,9 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
p.theme = d.theme;
|
||||
p.language = d.language;
|
||||
p.ui_scale = d.ui_scale;
|
||||
p.timeline_left = d.timeline_left;
|
||||
p.timeline_left_height = d.timeline_left_height;
|
||||
p.timeline_left_browser_collapsed = d.timeline_left_browser_collapsed;
|
||||
p.default_units = d.default_units;
|
||||
p.length_decimals = d.length_decimals;
|
||||
p.angle_decimals = d.angle_decimals;
|
||||
@@ -847,6 +860,13 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
);
|
||||
crate::scenario::publish_rect("prefs:scale", scale.rect);
|
||||
ui.end_row();
|
||||
ui.label("Feature history");
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut app.preferences.timeline_left, false, "Bottom timeline");
|
||||
ui.radio_value(&mut app.preferences.timeline_left, true, "Left sidebar");
|
||||
});
|
||||
ui.end_row();
|
||||
note(ui, "The left layout shows the rollback bar above the parts and scene browser.");
|
||||
ui.label(app.tr("ui.units_of_new_designs"));
|
||||
egui::ComboBox::from_id_salt("pref_units").selected_text(app.preferences.default_units.clone()).show_ui(ui, |ui| {
|
||||
for u in UNITS {
|
||||
@@ -1405,8 +1425,11 @@ mod tests {
|
||||
assert_eq!(app.tr("ui.extrude"), "Extrude");
|
||||
|
||||
// Switch language to Japanese
|
||||
set(&mut app, &json!({"language": "ja"})).unwrap();
|
||||
set(&mut app, &json!({"language": "ja", "timeline_left": true})).unwrap();
|
||||
app.preferences.timeline_left_height = 275.0;
|
||||
app.preferences.timeline_left_browser_collapsed = true;
|
||||
assert_eq!(app.preferences.language, "ja");
|
||||
assert!(app.preferences.timeline_left);
|
||||
assert_eq!(app.tr("ui.preferences"), "環境設定");
|
||||
assert_eq!(app.tr("ui.extrude"), "押し出し");
|
||||
|
||||
@@ -1414,6 +1437,9 @@ mod tests {
|
||||
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
other.load_prefs(&app.prefs());
|
||||
assert_eq!(other.preferences.language, "ja");
|
||||
assert!(other.preferences.timeline_left);
|
||||
assert_eq!(other.preferences.timeline_left_height, 275.0);
|
||||
assert!(other.preferences.timeline_left_browser_collapsed);
|
||||
assert_eq!(other.tr("ui.preferences"), "環境設定");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use solidcraft_engine::doc::FeatureKind;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{SolveApp, icons};
|
||||
|
||||
fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
pub(crate) fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
match k {
|
||||
FeatureKind::Sketch { .. } => "sketch",
|
||||
FeatureKind::Extrude { .. } => "extrude",
|
||||
@@ -37,8 +37,7 @@ fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a drag in the timeline is moving.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Drag {
|
||||
Marker,
|
||||
Feature(u64),
|
||||
@@ -57,16 +56,24 @@ fn slot_at(xs: &[f32], end: f32, x: f32) -> usize {
|
||||
}
|
||||
|
||||
/// Ask before deleting a feature others depend on; delete right away otherwise.
|
||||
fn delete_feature(app: &mut SolveApp, id: u64) {
|
||||
let deps = app.session.execute("timeline.dependents", &json!({ "feature": id })).unwrap_or_default();
|
||||
let list = |k: &str| -> Vec<String> {
|
||||
deps.get(k).and_then(|v| v.as_array()).into_iter().flatten().filter_map(|x| x.as_str().map(String::from)).collect()
|
||||
};
|
||||
let (with, fail) = (list("deleted_with_it"), list("would_fail"));
|
||||
pub(crate) fn delete_feature(app: &mut SolveApp, id: u64) {
|
||||
delete_features(app, vec![id]);
|
||||
}
|
||||
|
||||
pub(crate) fn delete_features(app: &mut SolveApp, ids: Vec<u64>) {
|
||||
let mut with = std::collections::BTreeSet::new();
|
||||
let mut fail = std::collections::BTreeSet::new();
|
||||
for id in &ids {
|
||||
let deps = app.session.execute("timeline.dependents", &json!({ "feature": id })).unwrap_or_default();
|
||||
for (key, output) in [("deleted_with_it", &mut with), ("would_fail", &mut fail)] {
|
||||
output
|
||||
.extend(deps.get(key).and_then(|value| value.as_array()).into_iter().flatten().filter_map(|value| value.as_str().map(String::from)));
|
||||
}
|
||||
}
|
||||
if with.is_empty() && fail.is_empty() {
|
||||
let _ = app.run("timeline.delete", json!({ "features": [id.to_string()] }));
|
||||
let _ = app.run("timeline.delete", json!({ "features": ids }));
|
||||
} else {
|
||||
app.dialog = Some(crate::dialogs::Dialog::confirm_delete(id, with, fail));
|
||||
app.dialog = Some(crate::dialogs::Dialog::confirm_delete_many(ids, with.into_iter().collect(), fail.into_iter().collect()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,12 +288,12 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// The scroll range follows the drawn content (the buttons don't take layout space themselves).
|
||||
ui.allocate_exact_size(vec2((content_end - origin).max(0.0), r.height()), Sense::hover());
|
||||
// Drag feedback, and the drop.
|
||||
if let (Some(dg), Some(pp)) = (drag, pointer) {
|
||||
if let (Some(dg), Some(pp)) = (drag.as_ref(), pointer) {
|
||||
let slot = slot_at(&xs, end, pp.x);
|
||||
let sx = xs.get(slot).copied().unwrap_or(end) - 4.0;
|
||||
p.line_segment(
|
||||
[pos2(sx, r.top() + 3.0), pos2(sx, r.bottom() - 3.0)],
|
||||
Stroke::new(2.0, if dg == Drag::Marker { t.accent } else { t.sketch_accent }),
|
||||
Stroke::new(2.0, if matches!(dg, Drag::Marker) { t.accent } else { t.sketch_accent }),
|
||||
);
|
||||
}
|
||||
let released = ui.input(|i| i.pointer.any_released());
|
||||
@@ -312,7 +319,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
ui.data_mut(|d| match drag {
|
||||
Some(dg) => {
|
||||
d.insert_temp(drag_id, dg);
|
||||
d.insert_temp(drag_id, dg.clone());
|
||||
}
|
||||
None => d.remove::<Drag>(drag_id),
|
||||
});
|
||||
@@ -334,7 +341,7 @@ fn find_in_browser(app: &mut SolveApp, id: u64) {
|
||||
}
|
||||
|
||||
/// Transport buttons drawn as shapes (the UI font has no media glyphs).
|
||||
fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
|
||||
pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
|
||||
let tri = |dir: f32, dx: f32| {
|
||||
let pts = vec![pos2(c.x + dx - 4.0 * dir, c.y - 5.0), pos2(c.x + dx + 4.0 * dir, c.y), pos2(c.x + dx - 4.0 * dir, c.y + 5.0)];
|
||||
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1235,7 +1235,11 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
crate::context_menu::rename_selection(app, at);
|
||||
}
|
||||
Key::F6 => app.animate_view("fit"),
|
||||
Key::Delete | Key::Backspace => crate::viewport::delete_selection(app),
|
||||
Key::Delete | Key::Backspace => {
|
||||
if !crate::timeline_left::delete_selected_folders(app) {
|
||||
crate::viewport::delete_selection(app);
|
||||
}
|
||||
}
|
||||
_ => crate::keymap::press(app, &crate::keymap::key_name(k, mods)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user