Implemented new undo/redo system. - icy_draw - icy_draw is the successor to mystic draw. fork / mirror
 (HTM) git clone https://git.drkhsh.at/icy_draw.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 060f8efb33dab6ae225328fa545a7ecbc63fb42b
 (DIR) parent 06798e3045807e9ec78245d1fa8ac82c018f8e2d
 (HTM) Author: Mike Krüger <mkrueger@posteo.de>
       Date:   Sun,  3 Sep 2023 09:04:11 +0200
       
       Implemented new undo/redo system.
       
       Diffstat:
         D doc/ICEDFormat.md                   |      95 ------------------------------
         M i18n/en/icy_draw.ftl                |       2 ++
         M src/model/tools/font_imp.rs         |       2 +-
         M src/ui/document.rs                  |       3 ++-
         M src/ui/editor/ansi/mod.rs           |      35 +++++++++++++++++++++++++++----
         M src/ui/editor/bitfont/mod.rs        |      28 +++++++++++++++++++++++++++-
         M src/ui/editor/charfont/mod.rs       |      30 +++++++++++++++++++++++++++++-
         M src/ui/main_window.rs               |       2 +-
         M src/ui/messages.rs                  |      32 +++++--------------------------
         M src/ui/top_bar.rs                   |      84 ++++++++++++++++++++++++-------
       
       10 files changed, 163 insertions(+), 150 deletions(-)
       ---
 (DIR) diff --git a/doc/ICEDFormat.md b/doc/ICEDFormat.md
       @@ -1,95 +0,0 @@
       -# Icy Draw file format
       -
       -I dump the Mystic Draw format completely. Fortunately there are 0 files in this format. 
       -I learned too much about modern ANSIs to restart.
       -Let's look.
       -
       -## Goals
       -
       -- Every supported format should be represented. Including tundra.
       -- Be compatible to Sauce/XBin models as much as possible.
       -- Try to be extensible
       -
       -
       -## Format
       -[ID]       4      'iced'
       -[EOF]      1      EOF Char, usually 0x1A hex
       -[Checksum] 4      BE_U32 CRC32 checksum for [HEADER] and [BLOCKS]
       -[HEADER]   11     HEADER
       -[BLOCKS]   *      BLOCKS
       -<EOF>
       -
       -### Header
       -```
       -Field      Bytes  Meaning
       -[VER]      2      BE_U16 u8 Major:u8 Minor - [00:00] atm
       -[Type]     1      0 - ANSI, 1 - PETSCII, 2 - ATASCII, 3 - VIEWDATA
       -[Width]    4      BE_U32
       -[Height]   4      BE_U32
       -
       -```
       -
       -### Blocks
       -Until EOF or End Block - read blocks.
       -
       -#### End Block
       -Marks EOF
       -```
       -Field      Bytes  Meaning
       -[1]        1      End of blocks
       -```
       -
       -#### SAUCE block (only 1 is valid)
       -```
       -Field      Bytes  Meaning
       -[1]        1      ID == 1
       -[Title]   35      CP 437 Chars - filled with b' ' SAUCE string
       -[Author]  20      CP 437 Chars - filled with b' ' SAUCE string
       -[Group]   20      CP 437 Chars - filled with b' ' SAUCE string
       -[NUM]      1      number of comments (max 255 - 0 is wasted)
       -[1]..[n]   n*64   Comment line CP 437 0 Terminated - 64 chars max
       -```
       -
       -#### Palette block (only 1 is valid)
       -```
       -Field      Bytes  Meaning
       -[2]        1      ID == 2
       -[NUM]      4      BE_I32 number of colors (atm only 0xFFFF colors are supported - but it may change)
       -                  In future (maybe): -1 means no numbers and RGB values are directly stored in the Layer    
       -[1]..[n]   n*4    U8 r,g,b,a values from 0..255
       -```
       -
       -#### Bitfont Font Block
       -Font is a bit more flexible than in the other formats - however due to sauce the 'font name' is always an option.
       -So there is no real limit here. Extended fonts just get splitted into 2 256 font blocks.
       -
       -```
       -Field      Bytes  Meaning
       -[4]        1      ID == 3
       -[Slot]     4      BE_U32 Font slot used
       -[NameLen]  4      BE_U32 Length of Name
       -[Name]     *      U8 - UTF8 encoded chars
       -[Length]   4      BE_U32 Data Length
       -[Data]     *      Font data as PSF
       -```
       -
       -#### Layer
       -Layers have the option to save some space by using a lower attribute and char length than theoretically need by buffer type.
       -
       -```
       -Field      Bytes  Meaning
       -[5]        1      ID == 5
       -[Title_Len]4      BE_U32 length of the utf8 title
       -[Title]    *      U8 - UTF8 encoded chars - Note: May only be 16 chars depending on language.
       -[Flags]    4      BE_U32
       -                  Bit 1   : is_visible
       -                  Bit 2   : edit_locked
       -                  Bit 3   : position_locked
       -                  Bit 4   : is_transparent
       -[X]        4      BE_I32
       -[Y]        4      BE_I32
       -[Width]    4      BE_U32
       -[Height]   4      BE_U32
       -[DataLen]  8      BE_U64 Length of Data
       -[Data]     *      Ansi encoded data
       -```
 (DIR) diff --git a/i18n/en/icy_draw.ftl b/i18n/en/icy_draw.ftl
       @@ -14,6 +14,8 @@ menu-edit-font-outline=Font Outline…
        menu-edit=Edit
        menu-undo=Undo
        menu-redo=Redo
       +menu-undo-op=Undo: { $op }
       +menu-redo-op=Redo: { $op }
        
        menu-copy=Copy
        menu-paste=Paste
 (DIR) diff --git a/src/model/tools/font_imp.rs b/src/model/tools/font_imp.rs
       @@ -338,7 +338,7 @@ impl Tool for FontTool {
                        editor.begin_atomic_undo();
                        let attr = editor.buffer_view.lock().get_caret().get_attribute();
                        let opt_size: Option<Size> = font.render(
       -                    &mut editor.buffer_view.lock().get_buffer_mut(),
       +                    editor.buffer_view.lock().get_buffer_mut(),
                            0,
                            c_pos.as_uposition(),
                            attr,
 (DIR) diff --git a/src/ui/document.rs b/src/ui/document.rs
       @@ -1,8 +1,9 @@
        use eframe::{egui, epaint::Vec2};
       +use icy_engine::editor::UndoState;
        
        use crate::{model::Tool, AnsiEditor, TerminalResult};
        
       -pub trait Document {
       +pub trait Document: UndoState {
            fn get_title(&self) -> String;
            fn is_dirty(&self) -> bool;
        
 (DIR) diff --git a/src/ui/editor/ansi/mod.rs b/src/ui/editor/ansi/mod.rs
       @@ -13,8 +13,8 @@ use eframe::{
        };
        use i18n_embed_fl::fl;
        use icy_engine::{
       -    ansi, AttributedChar, Buffer, BufferParser, Layer, Line, Position, Rectangle, SaveOptions,
       -    Size, TextAttribute, UPosition,
       +    editor::UndoState, AttributedChar, Buffer, Layer, Line, Position, Rectangle, SaveOptions, Size,
       +    TextAttribute, UPosition,
        };
        pub mod undo_stack;
        use undo_stack::{AtomicUndo, UndoOperation, UndoSetChar, UndoSwapChar};
       @@ -58,6 +58,32 @@ pub struct AnsiEditor {
            //pub attr_changed: std::boxed::Box<dyn Fn(TextAttribute)>
        }
        
       +impl UndoState for AnsiEditor {
       +    fn undo_description(&self) -> Option<String> {
       +        self.buffer_view.lock().get_edit_state().undo_description()
       +    }
       +
       +    fn can_undo(&self) -> bool {
       +        self.buffer_view.lock().get_edit_state().can_undo()
       +    }
       +
       +    fn undo(&mut self) {
       +        self.buffer_view.lock().get_edit_state_mut().undo()
       +    }
       +
       +    fn redo_description(&self) -> Option<String> {
       +        self.buffer_view.lock().get_edit_state().redo_description()
       +    }
       +
       +    fn can_redo(&self) -> bool {
       +        self.buffer_view.lock().get_edit_state().can_redo()
       +    }
       +
       +    fn redo(&mut self) {
       +        self.buffer_view.lock().get_edit_state_mut().redo()
       +    }
       +}
       +
        impl Document for AnsiEditor {
            fn get_title(&self) -> String {
                if let Some(file_name) = &self.buffer_view.lock().get_buffer().file_name {
       @@ -121,6 +147,7 @@ impl Document for AnsiEditor {
            fn get_ansi_editor_mut(&mut self) -> Option<&mut AnsiEditor> {
                Some(self)
            }
       +
            fn get_ansi_editor(&self) -> Option<&AnsiEditor> {
                Some(self)
            }
       @@ -382,14 +409,14 @@ impl AnsiEditor {
        
            pub fn undo(&mut self) {
                if let Some(op) = self.undo_stack.pop() {
       -            op.undo(&mut self.buffer_view.lock().get_buffer_mut());
       +            op.undo(self.buffer_view.lock().get_buffer_mut());
                    self.redo_stack.push(op);
                }
            }
        
            pub fn redo(&mut self) {
                if let Some(op) = self.redo_stack.pop() {
       -            op.redo(&mut self.buffer_view.lock().get_buffer_mut());
       +            op.redo(self.buffer_view.lock().get_buffer_mut());
                    self.undo_stack.push(op);
                }
            }
 (DIR) diff --git a/src/ui/editor/bitfont/mod.rs b/src/ui/editor/bitfont/mod.rs
       @@ -6,7 +6,7 @@ use eframe::{
            epaint::{Color32, FontFamily, FontId, Pos2, Rect, Rounding, Vec2},
        };
        use i18n_embed_fl::fl;
       -use icy_engine::BitFont;
       +use icy_engine::{editor::UndoState, BitFont};
        
        use crate::{model::Tool, AnsiEditor, Document, DocumentOptions, TerminalResult};
        
       @@ -345,6 +345,32 @@ impl BitFontEditor {
            }
        }
        
       +impl UndoState for BitFontEditor {
       +    fn undo_description(&self) -> Option<String> {
       +        todo!()
       +    }
       +
       +    fn can_undo(&self) -> bool {
       +        false
       +    }
       +
       +    fn undo(&mut self) {
       +        todo!()
       +    }
       +
       +    fn redo_description(&self) -> Option<String> {
       +        todo!()
       +    }
       +
       +    fn can_redo(&self) -> bool {
       +        false
       +    }
       +
       +    fn redo(&mut self) {
       +        todo!()
       +    }
       +}
       +
        impl Document for BitFontEditor {
            fn get_title(&self) -> String {
                self.font.name.to_string()
 (DIR) diff --git a/src/ui/editor/charfont/mod.rs b/src/ui/editor/charfont/mod.rs
       @@ -1,7 +1,9 @@
        use std::{fs, path::PathBuf, sync::Arc};
        
        use eframe::egui::{self, RichText};
       -use icy_engine::{BitFont, Buffer, Layer, Size, TextAttribute, TheDrawFont, UPosition};
       +use icy_engine::{
       +    editor::UndoState, BitFont, Buffer, Layer, Size, TextAttribute, TheDrawFont, UPosition,
       +};
        
        use crate::{
            model::Tool, AnsiEditor, BitFontEditor, Document, DocumentOptions, DrawGlyphStyle,
       @@ -18,6 +20,32 @@ pub struct CharFontEditor {
            fonts: Vec<TheDrawFont>,
        }
        
       +impl UndoState for CharFontEditor {
       +    fn undo_description(&self) -> Option<String> {
       +        todo!()
       +    }
       +
       +    fn can_undo(&self) -> bool {
       +        false
       +    }
       +
       +    fn undo(&mut self) {
       +        todo!()
       +    }
       +
       +    fn redo_description(&self) -> Option<String> {
       +        todo!()
       +    }
       +
       +    fn can_redo(&self) -> bool {
       +        false
       +    }
       +
       +    fn redo(&mut self) {
       +        todo!()
       +    }
       +}
       +
        impl Document for CharFontEditor {
            fn get_title(&self) -> String {
                if let Some(file_name) = &self.file_name {
 (DIR) diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs
       @@ -198,7 +198,7 @@ impl MainWindow {
        
                if let Some(ext) = path.extension() {
                    let ext = ext.to_str().unwrap().to_ascii_lowercase();
       -            if "psf" == ext || "f16" == ext || "f14" == ext || "f8" == ext {
       +            if "psf" == ext || "f16" == ext || "f14" == ext || "f8" == ext || "fon" == ext {
                        if let Ok(data) = fs::read(path) {
                            let file_name = path.file_name();
                            if file_name.is_none() {
 (DIR) diff --git a/src/ui/messages.rs b/src/ui/messages.rs
       @@ -112,27 +112,13 @@ impl MainWindow {
                        self.open_dialog(SelectOutlineDialog::default());
                    }
                    Message::Undo => {
       -                if let Some(editor) = self
       -                    .get_active_document()
       -                    .unwrap()
       -                    .lock()
       -                    .unwrap()
       -                    .get_ansi_editor_mut()
       -                {
       -                    editor.undo();
       -                    editor.buffer_view.lock().redraw_view();
       +                if let Some(editor) = self.get_active_document() {
       +                    editor.lock().unwrap().undo();
                        }
                    }
                    Message::Redo => {
       -                if let Some(editor) = self
       -                    .get_active_document()
       -                    .unwrap()
       -                    .lock()
       -                    .unwrap()
       -                    .get_ansi_editor_mut()
       -                {
       -                    editor.redo();
       -                    editor.buffer_view.lock().redraw_view();
       +                if let Some(editor) = self.get_active_document() {
       +                    editor.lock().unwrap().redo();
                        }
                    }
        
       @@ -241,15 +227,7 @@ impl MainWindow {
                            .get_ansi_editor_mut()
                        {
                            let mut lock = editor.buffer_view.lock();
       -                    let buf = lock.get_buffer_mut();
       -                    let size = buf.get_buffer_size();
       -                    let mut new_layer = icy_engine::Layer::new("New Layer", size);
       -                    new_layer.has_alpha_channel = true;
       -                    if buf.layers.is_empty() {
       -                        new_layer.has_alpha_channel = false;
       -                    }
       -
       -                    buf.layers.insert(0, new_layer);
       +                    lock.get_edit_state_mut().create_new_layer();
                        }
                    }
                    Message::MoveLayerUp(cur_layer) => {
 (DIR) diff --git a/src/ui/top_bar.rs b/src/ui/top_bar.rs
       @@ -122,27 +122,58 @@ impl MainWindow {
                    });
        
                    ui.menu_button(fl!(crate::LANGUAGE_LOADER, "menu-edit"), |ui| {
       -                let button = button_with_shortcut(
       -                    ui,
       -                    has_buffer,
       -                    fl!(crate::LANGUAGE_LOADER, "menu-undo"),
       -                    "Ctrl+Z",
       -                );
       -                if button.clicked() {
       -                    result = Some(Message::Undo);
       -                    ui.close_menu();
       -                }
       +                if let Some(doc) = self.get_active_document() {
       +                    if doc.lock().unwrap().can_undo() {
       +                        let button = button_with_shortcut(
       +                            ui,
       +                            has_buffer,
       +                            fl!(
       +                                crate::LANGUAGE_LOADER,
       +                                "menu-undo-op",
       +                                op = doc.lock().unwrap().undo_description().unwrap()
       +                            ),
       +                            "Ctrl+Z",
       +                        );
       +                        if button.clicked() {
       +                            result = Some(Message::Undo);
       +                            ui.close_menu();
       +                        }
       +                    } else {
       +                        button_with_shortcut(
       +                            ui,
       +                            false,
       +                            fl!(crate::LANGUAGE_LOADER, "menu-undo"),
       +                            "Ctrl+Z",
       +                        );
       +                    }
        
       -                let button = button_with_shortcut(
       -                    ui,
       -                    has_buffer,
       -                    fl!(crate::LANGUAGE_LOADER, "menu-redo"),
       -                    "Ctrl+Shift+Z",
       -                );
       -                if button.clicked() {
       -                    result = Some(Message::Redo);
       -                    ui.close_menu();
       +                    if doc.lock().unwrap().can_redo() {
       +                        let button = button_with_shortcut(
       +                            ui,
       +                            has_buffer,
       +                            fl!(
       +                                crate::LANGUAGE_LOADER,
       +                                "menu-redo-op",
       +                                op = doc.lock().unwrap().redo_description().unwrap()
       +                            ),
       +                            "Ctrl+Shift+Z",
       +                        );
       +                        if button.clicked() {
       +                            result = Some(Message::Redo);
       +                            ui.close_menu();
       +                        }
       +                    } else {
       +                        button_with_shortcut(
       +                            ui,
       +                            false,
       +                            fl!(crate::LANGUAGE_LOADER, "menu-redo"),
       +                            "Ctrl+Shift+Z",
       +                        );
       +                    }
       +                } else {
       +                    add_default_undo_redo(ui);
                        }
       +
                        ui.separator();
                        if ui
                            .add_enabled(
       @@ -404,6 +435,21 @@ impl MainWindow {
            }
        }
        
       +fn add_default_undo_redo(ui: &mut Ui) {
       +    button_with_shortcut(
       +        ui,
       +        false,
       +        fl!(crate::LANGUAGE_LOADER, "menu-undo"),
       +        "Ctrl+Z",
       +    );
       +    button_with_shortcut(
       +        ui,
       +        false,
       +        fl!(crate::LANGUAGE_LOADER, "menu-redo"),
       +        "Ctrl+Shift+Z",
       +    );
       +}
       +
        pub fn medium_toggle_button(
            ui: &mut egui::Ui,
            icon: &RetainedImage,