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}"),
|
("Seite {number}", "Page {number}"),
|
||||||
("Seiten ({count})", "Pages ({count})"),
|
("Seiten ({count})", "Pages ({count})"),
|
||||||
("Seiten durchgehend scrollen", "Scroll pages continuously"),
|
("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}"),
|
("Serialisierungsfehler: {e}", "Serialization error: {e}"),
|
||||||
("Server-Einstellungen gespeichert", "Server settings saved"),
|
("Server-Einstellungen gespeichert", "Server settings saved"),
|
||||||
("Server:", "Server:"),
|
("Server:", "Server:"),
|
||||||
|
|||||||
+10
-2
@@ -651,8 +651,8 @@ fn selection_press(
|
|||||||
_,
|
_,
|
||||||
) => {
|
) => {
|
||||||
let initial_strokes = page.get_strokes(&selection_state.selected_ids);
|
let initial_strokes = page.get_strokes(&selection_state.selected_ids);
|
||||||
// Bilder nicht verzerren (Issue #29); Shift hält das Verhältnis auch bei Strichen
|
// Schloss der Auswahl (Bilder: an, Issue #29); Shift sperrt zusätzlich
|
||||||
let keep_aspect = shift || initial_strokes.iter().any(|s| s.is_image());
|
let keep_aspect = shift || selection_state.keeps_aspect(page);
|
||||||
DragMode::Scale {
|
DragMode::Scale {
|
||||||
fixed_corner_page,
|
fixed_corner_page,
|
||||||
start_pos_page,
|
start_pos_page,
|
||||||
@@ -3101,6 +3101,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// 3. Toolbar & Widgets rendern
|
// 3. Toolbar & Widgets rendern
|
||||||
updater.update();
|
updater.update();
|
||||||
ui_state.page_view = Some(page_view(&canvas, window.scale_factor() as f32));
|
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
|
ui_state.restorable_page = last_deleted_page
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|(id, _, _)| *id == manifest.id)
|
.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());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
UiAction::ToggleAspectLock => {
|
||||||
|
if !selection_state.is_empty() {
|
||||||
|
selection_state.toggle_aspect_lock(notebook.current_page());
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
UiAction::RotateSelected => {
|
UiAction::RotateSelected => {
|
||||||
if !selection_state.is_empty()
|
if !selection_state.is_empty()
|
||||||
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
||||||
|
|||||||
+65
-1
@@ -36,7 +36,7 @@ pub enum DragMode {
|
|||||||
start_pos_page: Vec2,
|
start_pos_page: Vec2,
|
||||||
initial_box_size: Vec2,
|
initial_box_size: Vec2,
|
||||||
initial_strokes: Vec<Stroke>,
|
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,
|
keep_aspect: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -89,6 +89,9 @@ pub struct SelectionState {
|
|||||||
pub selected_ids: Vec<u64>,
|
pub selected_ids: Vec<u64>,
|
||||||
pub drag_mode: DragMode,
|
pub drag_mode: DragMode,
|
||||||
pub clipboard: Vec<Stroke>,
|
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 {
|
impl SelectionState {
|
||||||
@@ -97,9 +100,28 @@ impl SelectionState {
|
|||||||
selected_ids: Vec::new(),
|
selected_ids: Vec::new(),
|
||||||
drag_mode: DragMode::None,
|
drag_mode: DragMode::None,
|
||||||
clipboard: Vec::new(),
|
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.
|
/// Prüft, ob aktuell Objekte ausgewählt sind.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.selected_ids.is_empty()
|
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]
|
#[test]
|
||||||
fn test_recolor_ignores_images() {
|
fn test_recolor_ignores_images() {
|
||||||
let mut page = Page::new(1, PageBackground::Blank);
|
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ü)
|
/// Zwischenablage an dieser Stelle der aktuellen Seite einfügen (Kontextmenü)
|
||||||
PasteAt(glam::Vec2),
|
PasteAt(glam::Vec2),
|
||||||
RotateSelected,
|
RotateSelected,
|
||||||
|
/// Seitenverhältnis beim Skalieren der Auswahl sperren bzw. freigeben
|
||||||
|
ToggleAspectLock,
|
||||||
// Dokumenten- & Notizbuch-Verwaltung
|
// Dokumenten- & Notizbuch-Verwaltung
|
||||||
OpenNotebook(String),
|
OpenNotebook(String),
|
||||||
/// Neues Notizbuch im Ordner `folder` anlegen und öffnen
|
/// Neues Notizbuch im Ordner `folder` anlegen und öffnen
|
||||||
@@ -410,6 +412,8 @@ pub struct UiState {
|
|||||||
pub confirm_delete_page: bool,
|
pub confirm_delete_page: bool,
|
||||||
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
||||||
pub restorable_page: Option<usize>,
|
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
|
/// Fehler beim Flushen vor dem Beenden
|
||||||
pub shutdown_error: Option<crate::autosave::AutosaveError>,
|
pub shutdown_error: Option<crate::autosave::AutosaveError>,
|
||||||
/// Fensterseite der Werkzeugleiste
|
/// Fensterseite der Werkzeugleiste
|
||||||
@@ -508,6 +512,7 @@ impl Default for UiState {
|
|||||||
pressed_stylus_button: None,
|
pressed_stylus_button: None,
|
||||||
confirm_delete_page: false,
|
confirm_delete_page: false,
|
||||||
restorable_page: None,
|
restorable_page: None,
|
||||||
|
selection_aspect_lock: None,
|
||||||
shutdown_error: None,
|
shutdown_error: None,
|
||||||
sidebar_side: SidebarSide::Left,
|
sidebar_side: SidebarSide::Left,
|
||||||
language: None,
|
language: None,
|
||||||
@@ -3045,6 +3050,30 @@ pub fn render_ui(
|
|||||||
used += btn + spacing;
|
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 =
|
let reserved =
|
||||||
UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing);
|
UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing);
|
||||||
if reserved > used {
|
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]
|
#[test]
|
||||||
fn test_sidebar_fits_window_height_for_every_tool() {
|
fn test_sidebar_fits_window_height_for_every_tool() {
|
||||||
let notebook = Notebook::new();
|
let notebook = Notebook::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user