feat: Knopf für das Seitenverhältnis-Schloss der Auswahl (Issue #29)
Im Tablet-Betrieb gibt es keine Umschalttaste. Beim Auswahlwerkzeug steht unter den Stiftfarben jetzt ein Schloss: 🔒 skaliert gleichmäßig, 🔓 frei. Vorgabe ist gesperrt, sobald ein Bild ausgewählt ist, sonst frei. Das Umschalten gilt für die aktuelle Auswahl; eine neue Auswahl beginnt wieder mit der Vorgabe. Shift sperrt am Rechner zusätzlich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -286,6 +286,8 @@ pub const EN: &[(&str, &str)] = &[
|
||||
("Seite {number}", "Page {number}"),
|
||||
("Seiten ({count})", "Pages ({count})"),
|
||||
("Seiten durchgehend scrollen", "Scroll pages continuously"),
|
||||
("Seitenverhältnis frei – tippen zum Sperren", "Aspect ratio free – tap to lock"),
|
||||
("Seitenverhältnis gesperrt – tippen zum Freigeben", "Aspect ratio locked – tap to unlock"),
|
||||
("Serialisierungsfehler: {e}", "Serialization error: {e}"),
|
||||
("Server-Einstellungen gespeichert", "Server settings saved"),
|
||||
("Server:", "Server:"),
|
||||
|
||||
+10
-2
@@ -651,8 +651,8 @@ fn selection_press(
|
||||
_,
|
||||
) => {
|
||||
let initial_strokes = page.get_strokes(&selection_state.selected_ids);
|
||||
// Bilder nicht verzerren (Issue #29); Shift hält das Verhältnis auch bei Strichen
|
||||
let keep_aspect = shift || initial_strokes.iter().any(|s| s.is_image());
|
||||
// Schloss der Auswahl (Bilder: an, Issue #29); Shift sperrt zusätzlich
|
||||
let keep_aspect = shift || selection_state.keeps_aspect(page);
|
||||
DragMode::Scale {
|
||||
fixed_corner_page,
|
||||
start_pos_page,
|
||||
@@ -3101,6 +3101,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 3. Toolbar & Widgets rendern
|
||||
updater.update();
|
||||
ui_state.page_view = Some(page_view(&canvas, window.scale_factor() as f32));
|
||||
ui_state.selection_aspect_lock = (!selection_state.is_empty())
|
||||
.then(|| selection_state.keeps_aspect(notebook.current_page()));
|
||||
ui_state.restorable_page = last_deleted_page
|
||||
.as_ref()
|
||||
.filter(|(id, _, _)| *id == manifest.id)
|
||||
@@ -3201,6 +3203,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::ToggleAspectLock => {
|
||||
if !selection_state.is_empty() {
|
||||
selection_state.toggle_aspect_lock(notebook.current_page());
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
UiAction::RotateSelected => {
|
||||
if !selection_state.is_empty()
|
||||
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
||||
|
||||
+65
-1
@@ -36,7 +36,7 @@ pub enum DragMode {
|
||||
start_pos_page: Vec2,
|
||||
initial_box_size: Vec2,
|
||||
initial_strokes: Vec<Stroke>,
|
||||
/// Seitenverhältnis beibehalten (Bilder in der Auswahl oder Shift gedrückt)
|
||||
/// Seitenverhältnis beibehalten (Schloss der Auswahl oder Shift gedrückt)
|
||||
keep_aspect: bool,
|
||||
},
|
||||
}
|
||||
@@ -89,6 +89,9 @@ pub struct SelectionState {
|
||||
pub selected_ids: Vec<u64>,
|
||||
pub drag_mode: DragMode,
|
||||
pub clipboard: Vec<Stroke>,
|
||||
/// Von Hand umgeschaltetes Seitenverhältnis-Schloss und die Auswahl (IDs), für die es
|
||||
/// gilt. Ändert sich die Auswahl, gilt wieder die Vorgabe (siehe [`Self::keeps_aspect`]).
|
||||
aspect_lock: Option<(Vec<u64>, bool)>,
|
||||
}
|
||||
|
||||
impl SelectionState {
|
||||
@@ -97,9 +100,28 @@ impl SelectionState {
|
||||
selected_ids: Vec::new(),
|
||||
drag_mode: DragMode::None,
|
||||
clipboard: Vec::new(),
|
||||
aspect_lock: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ob Skalieren an den Eckgriffen das Seitenverhältnis der Auswahl beibehält. Vorgabe:
|
||||
/// ja, sobald ein Bild ausgewählt ist (Issue #29); per Knopf für diese Auswahl umschaltbar.
|
||||
pub fn keeps_aspect(&self, page: &Page) -> bool {
|
||||
match &self.aspect_lock {
|
||||
Some((ids, locked)) if *ids == self.selected_ids => *locked,
|
||||
_ => page
|
||||
.get_strokes(&self.selected_ids)
|
||||
.iter()
|
||||
.any(|s| s.is_image()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Schaltet das Seitenverhältnis-Schloss der aktuellen Auswahl um.
|
||||
pub fn toggle_aspect_lock(&mut self, page: &Page) {
|
||||
let locked = !self.keeps_aspect(page);
|
||||
self.aspect_lock = Some((self.selected_ids.clone(), locked));
|
||||
}
|
||||
|
||||
/// Prüft, ob aktuell Objekte ausgewählt sind.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selected_ids.is_empty()
|
||||
@@ -577,6 +599,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aspect_lock_defaults_to_images_and_toggles_per_selection() {
|
||||
let mut page = Page::new(1, PageBackground::Blank);
|
||||
let corners = [
|
||||
Vec2::new(0.0, 0.0),
|
||||
Vec2::new(100.0, 0.0),
|
||||
Vec2::new(100.0, 50.0),
|
||||
Vec2::new(0.0, 50.0),
|
||||
];
|
||||
page.add_stroke(Stroke::new_image(1, "a.png".into(), corners));
|
||||
page.add_stroke(make_test_stroke(2, 200.0, 200.0, [0.1, 0.1, 0.1, 1.0]));
|
||||
let mut sel = SelectionState::new();
|
||||
|
||||
// Vorgabe: Bilder gesperrt, Striche frei
|
||||
sel.select_single(1);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.select_single(2);
|
||||
assert!(!sel.keeps_aspect(&page));
|
||||
|
||||
// Umschalten gilt für diese Auswahl
|
||||
sel.toggle_aspect_lock(&page);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.select_single(1);
|
||||
sel.toggle_aspect_lock(&page);
|
||||
assert!(!sel.keeps_aspect(&page));
|
||||
sel.toggle_aspect_lock(&page);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.toggle_aspect_lock(&page);
|
||||
|
||||
// Neue Auswahl: wieder die Vorgabe
|
||||
sel.select_multiple(vec![1, 2]);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.select_single(1);
|
||||
assert!(
|
||||
!sel.keeps_aspect(&page),
|
||||
"gleiche Auswahl behält das Umschalten"
|
||||
);
|
||||
sel.clear();
|
||||
sel.select_single(1);
|
||||
assert!(!sel.keeps_aspect(&page));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recolor_ignores_images() {
|
||||
let mut page = Page::new(1, PageBackground::Blank);
|
||||
|
||||
@@ -255,6 +255,8 @@ pub enum UiAction {
|
||||
/// Zwischenablage an dieser Stelle der aktuellen Seite einfügen (Kontextmenü)
|
||||
PasteAt(glam::Vec2),
|
||||
RotateSelected,
|
||||
/// Seitenverhältnis beim Skalieren der Auswahl sperren bzw. freigeben
|
||||
ToggleAspectLock,
|
||||
// Dokumenten- & Notizbuch-Verwaltung
|
||||
OpenNotebook(String),
|
||||
/// Neues Notizbuch im Ordner `folder` anlegen und öffnen
|
||||
@@ -410,6 +412,8 @@ pub struct UiState {
|
||||
pub confirm_delete_page: bool,
|
||||
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
||||
pub restorable_page: Option<usize>,
|
||||
/// Seitenverhältnis-Schloss der Auswahl (`None`: nichts ausgewählt)
|
||||
pub selection_aspect_lock: Option<bool>,
|
||||
/// Fehler beim Flushen vor dem Beenden
|
||||
pub shutdown_error: Option<crate::autosave::AutosaveError>,
|
||||
/// Fensterseite der Werkzeugleiste
|
||||
@@ -508,6 +512,7 @@ impl Default for UiState {
|
||||
pressed_stylus_button: None,
|
||||
confirm_delete_page: false,
|
||||
restorable_page: None,
|
||||
selection_aspect_lock: None,
|
||||
shutdown_error: None,
|
||||
sidebar_side: SidebarSide::Left,
|
||||
language: None,
|
||||
@@ -3045,6 +3050,30 @@ pub fn render_ui(
|
||||
used += btn + spacing;
|
||||
}
|
||||
}
|
||||
// Auswahl: unter den 4 Stiftfarben ist Platz für das Schloss des
|
||||
// Seitenverhältnisses beim Skalieren (Bilder: gesperrt, Issue #29)
|
||||
if state.active_tool == ActiveTool::Select {
|
||||
let locked = state.selection_aspect_lock;
|
||||
let (icon, tooltip) = if locked == Some(true) {
|
||||
("🔒", t!("Seitenverhältnis gesperrt – tippen zum Freigeben"))
|
||||
} else {
|
||||
("🔓", t!("Seitenverhältnis frei – tippen zum Sperren"))
|
||||
};
|
||||
if sidebar_button(
|
||||
ui,
|
||||
swatch,
|
||||
SidebarIcon::Glyph(icon),
|
||||
locked == Some(true),
|
||||
locked.is_some(),
|
||||
None,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::ToggleAspectLock);
|
||||
}
|
||||
used += swatch + spacing;
|
||||
}
|
||||
let reserved =
|
||||
UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing);
|
||||
if reserved > used {
|
||||
@@ -5189,6 +5218,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aspect_lock_button_only_with_selection() {
|
||||
// Den Knopf in der Leiste des Auswahlwerkzeugs suchen: ohne Auswahl gesperrt
|
||||
let notebook = Notebook::new();
|
||||
let toggles = |lock: Option<bool>| {
|
||||
let mut probe = UiState::new();
|
||||
probe.active_tool = ActiveTool::Select;
|
||||
render_frames(&mut probe, ¬ebook, 10);
|
||||
let bar = probe.toolbar_rect.unwrap();
|
||||
let mut found = 0;
|
||||
let mut y = bar.min.y + 4.0;
|
||||
while y < bar.max.y {
|
||||
let mut state = UiState::new();
|
||||
state.active_tool = ActiveTool::Select;
|
||||
state.selection_aspect_lock = lock;
|
||||
let ctx = render_frames(&mut state, ¬ebook, 10);
|
||||
let actions = click_at(&ctx, &mut state, ¬ebook, egui::pos2(bar.center().x, y));
|
||||
if actions.contains(&UiAction::ToggleAspectLock) {
|
||||
found += 1;
|
||||
}
|
||||
y += 4.0;
|
||||
}
|
||||
found
|
||||
};
|
||||
assert_eq!(toggles(None), 0);
|
||||
assert!(toggles(Some(true)) > 0);
|
||||
assert!(toggles(Some(false)) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sidebar_fits_window_height_for_every_tool() {
|
||||
let notebook = Notebook::new();
|
||||
|
||||
Reference in New Issue
Block a user