//! Page organizing: insert, delete, move, turn, undo (PDF-02). use std::path::PathBuf; use ely_gpui_component::primitives::Severity; use ely_office::{Closing as _, Tabs as _}; use gpui::{App, Context, Div, InteractiveElement as _, KeyBinding, Menu, MenuItem, actions}; use crate::view::PdfView; use crate::viewer::Viewer; actions!( pdf, [ Undo, Redo, InsertBlankPage, InsertPagesFromFile, DeletePages, RotatePagesLeft, RotatePagesRight, MovePagesUp, MovePagesDown, MovePagesFirst, MovePagesLast, CropPages, ExtractPages, SplitDocument ] ); pub fn bind(cx: &mut App) { let at = Some(ely_ui::commands::CONTEXT); cx.bind_keys([ KeyBinding::new("secondary-z", Undo, at), KeyBinding::new("secondary-shift-z", Redo, at), KeyBinding::new("backspace", DeletePages, Some(crate::side::PAGES)), KeyBinding::new("delete", DeletePages, Some(crate::side::PAGES)), ]); } /// The Pages menu. pub fn menu(cx: &App) -> Menu { let label = |key: &str| Viewer::text(cx, key, &[]); Menu::new(label("menu.pages")).items([ MenuItem::action(label("pdf.pages.blank"), InsertBlankPage), MenuItem::action(label("pdf.pages.file"), InsertPagesFromFile), MenuItem::action(label("pdf.pages.delete"), DeletePages), MenuItem::separator(), MenuItem::action(label("pdf.pages.rotate_left"), RotatePagesLeft), MenuItem::action(label("pdf.pages.rotate_right"), RotatePagesRight), MenuItem::action(label("pdf.pages.crop"), CropPages), MenuItem::separator(), MenuItem::action(label("pdf.pages.up"), MovePagesUp), MenuItem::action(label("pdf.pages.down"), MovePagesDown), MenuItem::action(label("pdf.pages.first"), MovePagesFirst), MenuItem::action(label("pdf.pages.last"), MovePagesLast), MenuItem::separator(), MenuItem::action(label("pdf.pages.extract"), ExtractPages), MenuItem::action(label("pdf.pages.split"), SplitDocument), ]) } /// The Edit menu's undo and redo. pub fn history_items(cx: &App) -> [MenuItem; 2] { let label = |key: &str| Viewer::text(cx, key, &[]); [MenuItem::action(label("pdf.undo"), Undo), MenuItem::action(label("pdf.redo"), Redo)] } impl PdfView { /// The pages an organizing step acts on: picked, else current. pub fn chosen_pages(&self) -> Vec { match self.picked.is_empty() { true => vec![self.current], false => self.picked.iter().copied().collect(), } } /// Moves the chosen pages before page `to`; they stay picked. pub fn move_to(&mut self, to: usize, cx: &mut Context) { let pages = self.chosen_pages(); let before = pages.iter().filter(|page| **page < to).count(); let landed = (to - before..to - before + pages.len()).collect(); self.queue_edit("moving pages", Some(landed), true, move |worker, doc| worker.move_pages(doc, &pages, to), cx); } } impl Viewer { /// Runs `step` on the active view; with none, says so. fn on_pages(&mut self, cx: &mut Context, step: impl FnOnce(&mut PdfView, &mut Context)) { match self.active_view() { Some(view) => view.update(cx, step), None => self.no_document(cx), } } pub(crate) fn delete(&mut self, cx: &mut Context) { let Some(view) = self.active_view() else { return self.no_document(cx); }; let (pages, count) = (view.read(cx).chosen_pages(), view.read(cx).sizes.len()); if pages.len() == count { let text = Self::text(cx, "pdf.pages.keep_one", &[]); return self.toast(text, Severity::Warning, cx); } view.update(cx, |view, cx| { view.picked.clear(); view.edit_pages("deleting pages", move |worker, doc| worker.delete_pages(doc, &pages), cx); }); } fn turn_pages(&mut self, by: i32, cx: &mut Context) { self.on_pages(cx, |view, cx| { let pages = view.chosen_pages(); view.edit_pages("turning pages", move |worker, doc| worker.rotate_pages(doc, &pages, by), cx); }); } /// Moves the chosen pages by `by`, or to an end. fn shift(&mut self, by: isize, cx: &mut Context) { self.on_pages(cx, |view, cx| { let (pages, count) = (view.chosen_pages(), view.sizes.len() as isize); let to = match by { isize::MIN => 0, isize::MAX => count, -1 => pages[0] as isize - 1, _ => *pages.last().expect("one page at least") as isize + 2, }; if (0..=count).contains(&to) { view.move_to(to as usize, cx); } }); } fn blank(&mut self, cx: &mut Context) { self.on_pages(cx, |view, cx| { let at = view.chosen_pages().last().copied().expect("one page at least"); let size = view.sizes[at]; view.edit_pages("inserting a page", move |worker, doc| worker.insert_blank(doc, at + 1, size), cx); }); } /// Asks for a PDF; its pages follow the chosen. fn insert_file(&mut self, cx: &mut Context) { if self.active_view().is_none() { return self.no_document(cx); } self.pick(false, cx, |viewer, path: PathBuf, cx| { viewer.on_pages(cx, |view, cx| { let at = view.chosen_pages().last().copied().expect("one page at least") + 1; view.edit_pages( "inserting pages", move |worker, doc| worker.insert_file(doc, at, &path).map(|(_, mark)| mark), cx, ); }) }); } } /// Wires the organizing actions onto the root. pub fn on(root: Div, cx: &mut Context) -> Div { root.on_action(cx.listener(|v, _: &Undo, _, cx| v.on_pages(cx, |view, cx| view.undo(false, cx)))) .on_action(cx.listener(|v, _: &Redo, _, cx| v.on_pages(cx, |view, cx| view.undo(true, cx)))) .on_action(cx.listener(|v, _: &DeletePages, _, cx| v.delete(cx))) .on_action(cx.listener(|v, _: &RotatePagesLeft, _, cx| v.turn_pages(-90, cx))) .on_action(cx.listener(|v, _: &RotatePagesRight, _, cx| v.turn_pages(90, cx))) .on_action(cx.listener(|v, _: &MovePagesUp, _, cx| v.shift(-1, cx))) .on_action(cx.listener(|v, _: &MovePagesDown, _, cx| v.shift(1, cx))) .on_action(cx.listener(|v, _: &MovePagesFirst, _, cx| v.shift(isize::MIN, cx))) .on_action(cx.listener(|v, _: &MovePagesLast, _, cx| v.shift(isize::MAX, cx))) .on_action(cx.listener(|v, _: &InsertBlankPage, _, cx| v.blank(cx))) .on_action(cx.listener(|v, _: &InsertPagesFromFile, _, cx| v.insert_file(cx))) .on_action(cx.listener(|v, _: &CropPages, window, cx| v.ask_crop(window, cx))) .on_action(cx.listener(|v, _: &ExtractPages, _, cx| v.extract(cx))) .on_action(cx.listener(|v, _: &SplitDocument, window, cx| v.ask_split(window, cx))) }