~zanneth/hirogen

0668f95b684762125537bfe7491ed623d558c907 — Charles Magahern 2 years ago 1ca5d66
Implement Find and Replace
5 files changed, 296 insertions(+), 63 deletions(-)

M res/find-dialog.ui
M res/main.ui
M src/document.rs
M src/document_window.rs
M src/find_dialog.rs
M res/find-dialog.ui => res/find-dialog.ui +2 -2
@@ 46,8 46,8 @@
                <property name="can_focus">True</property>
                <property name="receives_default">True</property>
                <property name="use_stock">True</property>
                <accelerator key="Return" signal="activate"/>
                <accelerator key="KP_Enter" signal="activate"/>
                <accelerator key="Return" signal="activate"/>
              </object>
              <packing>
                <property name="expand">True</property>


@@ 149,7 149,7 @@
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label_xalign">0</property>
                <property name="shadow_type">none</property>
                <property name="shadow_type">in</property>
                <child>
                  <object class="GtkAlignment">
                    <property name="visible">True</property>

M res/main.ui => res/main.ui +17 -0
@@ 246,6 246,23 @@
                        <accelerator key="g" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
                      </object>
                    </child>
                    <child>
                      <object class="GtkSeparatorMenuItem">
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-find-and-replace</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="action_name">win.replace</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                        <accelerator key="f" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
                      </object>
                    </child>
                  </object>
                </child>
              </object>

M src/document.rs => src/document.rs +22 -14
@@ 15,7 15,6 @@ use crate::gdk_additions::{Direction, RGBAExt};
use crate::guard_unwrap;
use crate::find_dialog::{
    FindDirection,
    FindDirective,
    FindNeedle,
};



@@ 451,38 450,47 @@ impl Document
        }
    }
    
    pub fn find(&self, directive: &FindDirective) -> Option<DocumentLocation>
    pub fn off_from_cur_selection(&self, direction: &FindDirection) -> usize
    {
        let data = match &self.data {
            Some(d) => d,
            None    => return None,
        };
        
        let mut off: isize = match directive.direction {
        match direction {
            FindDirection::FromBeginning => 0,
            FindDirection::FromEnd       => (data.len() - 1) as isize,
            FindDirection::FromEnd       => (self.data_len() - 1),
            
            FindDirection::Backward => {
                match &self.selection {
                    Some(selection) => selection.byte_range.start as isize - 1,
                    Some(selection) => selection.byte_range.start - 1,
                    None            => 0,
                }
            },
            
            FindDirection::Forward => {
                match &self.selection {
                    Some(selection) => selection.byte_range.end as isize,
                    Some(selection) => selection.byte_range.end,
                    None            => 0,
                }
            }
        }
    }
    
    pub fn find(
        &self,
        needle:     &FindNeedle,
        direction:  &FindDirection,
        start:      usize
    ) -> Option<DocumentLocation>
    {
        let data = match &self.data {
            Some(d) => d,
            None    => return None,
        };
        
        let inc: isize = match directive.direction {
        let mut off = start as isize;
        let inc: isize = match direction {
            FindDirection::FromBeginning | FindDirection::Forward => 1,
            FindDirection::FromEnd | FindDirection::Backward      => -1
        };
        
        let needle_bytes = match &directive.needle {
        let needle_bytes = match needle {
            FindNeedle::Text(s)  => s.as_bytes(),
            FindNeedle::Value(v) => v.as_slice(),
        };


@@ 502,7 510,7 @@ impl Document
            if bytes_checked == needle_len {
                return Some(DocumentLocation {
                    byte_offset:    off as usize,
                    region:         match directive.needle {
                    region:         match needle {
                        FindNeedle::Value(_) => DocumentRegion::HexText,
                        FindNeedle::Text(_)  => DocumentRegion::ASCIIText,
                    }

M src/document_window.rs => src/document_window.rs +60 -4
@@ 229,6 229,7 @@ impl DocumentWindow
        self.connect_simple_action("find",       Self::handle_find_action);
        self.connect_simple_action("find-next",  Self::handle_find_next_action);
        self.connect_simple_action("find-prev",  Self::handle_find_prev_action);
        self.connect_simple_action("replace",    Self::handle_replace_action);
        self.connect_simple_action("go-to",      Self::handle_goto_action);
        self.connect_simple_action("about",      Self::handle_about_action);
        


@@ 512,12 513,12 @@ impl DocumentWindow
        self.reload_ui();
    }
    
    fn handle_find_action(self: &DocumentWindowRef)
    fn handle_find_or_replace_action(self: &DocumentWindowRef, dialog_type: &FindDialogType)
    {
        let dialog = FindDialog::new(&self.window.clone().upcast::<gtk::Window>());
        let dialog = FindDialog::new(&self.window.clone().upcast::<gtk::Window>(), dialog_type);
        match dialog.run() {
            Some(directive) => {
                let found_result = self.handle_find(&directive);
                let found_result = self.handle_find_or_replace(&directive);
                if !found_result {
                    self.show_dialog_msg(gtk::MessageType::Info, "No results.");
                }


@@ 527,6 528,11 @@ impl DocumentWindow
        };
    }
    
    fn handle_find_action(self: &DocumentWindowRef)
    {
        self.handle_find_or_replace_action(&FindDialogType::Find);
    }
    
    fn handle_find_next_action(self: &DocumentWindowRef)
    {
        self.handle_repeated_find(&FindDirection::Forward);


@@ 537,6 543,11 @@ impl DocumentWindow
        self.handle_repeated_find(&FindDirection::Backward);
    }
    
    fn handle_replace_action(self: &DocumentWindowRef)
    {
        self.handle_find_or_replace_action(&FindDialogType::Replace);
    }
    
    fn handle_goto_action(self: &DocumentWindowRef)
    {
        let goto_dialog = GoToDialog::new(&self.window.clone().upcast::<gtk::Window>());


@@ 980,7 991,9 @@ impl DocumentWindow
    {
        let found_result_and_updated_sel = {
            let mut document = self.document.borrow_mut();
            match document.find(directive) {
            let start = document.off_from_cur_selection(&directive.direction);
            
            match document.find(&directive.needle, &directive.direction, start) {
                Some(loc) => {
                    let begin = loc.byte_offset;
                    document.selection = Some(DocumentSelection {


@@ 1012,6 1025,49 @@ impl DocumentWindow
        found_result_and_updated_sel
    }
    
    fn handle_replace(self: &DocumentWindowRef, directive: &FindDirective) -> bool
    {
        let replace = match &directive.replace {
            Some(r) => r,
            None    => return false,
        };
        
        let found_and_replaced_result = {
            let mut replaced_at_least_once = false;
            let mut document = self.document.borrow_mut();
            let mut start = document.off_from_cur_selection(&directive.direction);
            
            while let Some(result) = document.find(&directive.needle, &directive.direction, start) {
                let range = result.byte_offset .. result.byte_offset + directive.needle.len();
                document.replace_bytes(range, replace.needle.as_bytes());

                replaced_at_least_once = true;
                if replace.action == ReplaceAction::Replace {
                    // we only wanted to replace once.
                    break;
                }
                
                start += directive.needle.len();
            }
            
            replaced_at_least_once
        };
        
        if found_and_replaced_result {
            self.reload_ui();
        }
        
        found_and_replaced_result
    }
    
    fn handle_find_or_replace(self: &DocumentWindowRef, directive: &FindDirective) -> bool
    {
        match directive.replace {
            Some(_) => self.handle_replace(directive),
            None    => self.handle_find(directive),
        }
    }
    
    fn handle_repeated_find(self: &DocumentWindowRef, direction: &FindDirection)
    {
        use FindDirection::*;

M src/find_dialog.rs => src/find_dialog.rs +195 -43
@@ 7,19 7,38 @@ use std::result::Result;

use crate::gtk_additions::*;

struct FindDetails
{
    find_button:    gtk::Button,
    find_entry:     gtk::Entry,
}

struct ReplaceDetails
{
    replace_button:     gtk::Button,
    replace_all_button: gtk::Button,
    
    find_entry:         gtk::Entry,
    replace_entry:      gtk::Entry,
}

enum FindDialogDetails
{
    Find(FindDetails),
    Replace(ReplaceDetails),
}

pub struct FindDialog
{
    dialog:                     gtk::Dialog,
    
    entry:                      gtk::Entry,
    
    hex_value_radio:            gtk::RadioButton,
    text_string_radio:          gtk::RadioButton,
    direction_all_radio:        gtk::RadioButton,
    direction_backward_radio:   gtk::RadioButton,
    direction_forward_radio:    gtk::RadioButton,
    
    find_button:                gtk::Button,
    details:                    FindDialogDetails,
}

type FindDialogRef = Rc<FindDialog>;


@@ 40,55 59,94 @@ pub enum FindDirection
    Forward,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ReplaceAction
{
    Replace,
    ReplaceAll,
}

#[derive(Clone, Debug)]
pub struct Replace
{
    pub needle: FindNeedle,
    pub action: ReplaceAction,
}

#[derive(Clone, Debug)]
pub struct FindDirective
{
    pub needle:     FindNeedle,
    pub direction:  FindDirection,
    pub replace:    Option<Replace>,
}

#[derive(Copy, Clone, Debug)]
pub enum FindDialogType
{
    Find,
    Replace,
}

impl FindDialog
{
    pub fn new(parent: &gtk::Window) -> FindDialogRef
    pub fn new(parent: &gtk::Window, dialog_type: &FindDialogType) -> FindDialogRef
    {
        let builder = gtk::Builder::from_string(include_str!("../res/find-dialog.ui"));
        let dialog: gtk::Dialog = builder.get_object("find-dialog").unwrap();
        let builder = match dialog_type {
            FindDialogType::Find    => gtk::Builder::from_string(include_str!("../res/find-dialog.ui")),
            FindDialogType::Replace => gtk::Builder::from_string(include_str!("../res/replace-dialog.ui"))
        };
        
        let dialog: gtk::Dialog = match dialog_type {
            FindDialogType::Find    => builder.get_object("find-dialog").unwrap(),
            FindDialogType::Replace => builder.get_object("replace-dialog").unwrap()
        };
        dialog.set_transient_for(Some(parent));
        dialog.set_title("Find");
        
        let entry: gtk::Entry = builder.get_object("find-entry").unwrap();
        
        let hex_value_radio: gtk::RadioButton = builder.get_object("hex-value-radio").unwrap();
        let text_string_radio: gtk::RadioButton = builder.get_object("text-string-radio").unwrap();
        let direction_all_radio: gtk::RadioButton = builder.get_object("direction-all-radio").unwrap();
        let direction_backward_radio: gtk::RadioButton = builder.get_object("direction-backward-radio").unwrap();
        let direction_forward_radio: gtk::RadioButton = builder.get_object("direction-forward-radio").unwrap();
        
        let find_button: gtk::Button = builder.get_object("find-button").unwrap();
        let details = match dialog_type {
            FindDialogType::Find => FindDialogDetails::Find(FindDetails {
                find_button:    builder.get_object("find-button").unwrap(),
                find_entry:     builder.get_object("find-entry").unwrap(),
            }),
            
            FindDialogType::Replace => FindDialogDetails::Replace(ReplaceDetails {
                replace_button:     builder.get_object("replace-button").unwrap(),
                replace_all_button: builder.get_object("replace-all-button").unwrap(),
                find_entry:         builder.get_object("find-entry").unwrap(),
                replace_entry:      builder.get_object("replace-entry").unwrap(),
            }),
        };
        
        Rc::new(Self {
            dialog,
            
            entry,
            
            hex_value_radio,
            text_string_radio,
            
            direction_all_radio,
            direction_backward_radio,
            direction_forward_radio,
            
            find_button,
            details,
        })
    }
    
    pub fn run(self: &FindDialogRef) -> Option<FindDirective>
    {
        self.update_button_state();

        let _signal_handlers = self.setup_signal_handlers();
        
        match self.dialog.run() {
            ResponseType::Ok => {
                match self.entered_directive() {
        let response = self.dialog.run();

        match response {
            ResponseType::Ok | ResponseType::Apply => {
                match self.entered_directive(&response) {
                    Ok(directive) => Some(directive),
                    Err(_)        => None
                }


@@ 100,22 158,34 @@ impl FindDialog
    
    // -- Internal -------------------------------------------------------------
    
    fn create_changed_handler(self: &FindDialogRef, entry: &gtk::Entry) -> ScopedSignalHandler
    {
        let weak_self = Rc::downgrade(self);
        ScopedSignalHandler::new(
            entry,
            entry.connect_changed(move |_| {
                if let Some(find_dialog) = weak_self.upgrade() {
                    find_dialog.update_button_state();
                }
            })
        )
    }
    
    fn setup_signal_handlers(self: &FindDialogRef) -> Vec<ScopedSignalHandler>
    {
        let mut signal_handlers = vec!();
        
        // connect text field changed signal
        let weak_self = Rc::downgrade(self);
        signal_handlers.push(
            ScopedSignalHandler::new(
                &self.entry,
                self.entry.connect_changed(move |_| {
                    if let Some(find_dialog) = weak_self.upgrade() {
                        find_dialog.update_button_state();
                    }
                })
            )
        );
        // connect text field changed signal(s)
        match &self.details {
            FindDialogDetails::Find(details) => {
                signal_handlers.push(self.create_changed_handler(&details.find_entry));
            },

            FindDialogDetails::Replace(details) => {
                signal_handlers.push(self.create_changed_handler(&details.find_entry));
                signal_handlers.push(self.create_changed_handler(&details.replace_entry));
            },
        }
        
        // connect data type radio toggled signal
        for group_member in self.text_string_radio.get_group() {


@@ 140,14 210,26 @@ impl FindDialog
        self.update_button_state();
    }
    
    fn entered_text(self: &FindDialogRef) -> String
    fn entered_find_text(self: &FindDialogRef) -> String
    {
        let entry = match &self.details {
            FindDialogDetails::Find(details)    => &details.find_entry,
            FindDialogDetails::Replace(details) => &details.find_entry,
        };
        entry.get_text().to_string()
    }
    
    fn entered_replace_text(self: &FindDialogRef) -> String
    {
        self.entry.get_text().to_string()
        match &self.details {
            FindDialogDetails::Replace(details) => details.replace_entry.get_text().to_string(),
            _                                   => unreachable!()
        }
    }
    
    fn entered_hex_data(self: &FindDialogRef) -> Result<Vec<u8>, ParseIntError>
    fn entered_hex_data(entry: &gtk::Entry) -> Result<Vec<u8>, ParseIntError>
    {
        self.entry
        entry
            .get_text()
            .chars()
            .filter(|c| *c != ' ')


@@ 157,6 239,22 @@ impl FindDialog
            .collect()
    }
    
    fn entered_find_hex_data(self: &FindDialogRef) -> Result<Vec<u8>, ParseIntError>
    {
        match &self.details {
            FindDialogDetails::Find(details)    => Self::entered_hex_data(&details.find_entry),
            FindDialogDetails::Replace(details) => Self::entered_hex_data(&details.find_entry),
        }
    }
    
    fn entered_replace_hex_data(self: &FindDialogRef) -> Result<Vec<u8>, ParseIntError>
    {
        match &self.details {
            FindDialogDetails::Replace(details) => Self::entered_hex_data(&details.replace_entry),
            _                                   => unreachable!()
        }
    }
    
    fn selected_direction(self: &FindDialogRef) -> FindDirection
    {
        *[


@@ 171,34 269,88 @@ impl FindDialog
            .unwrap()
    }
    
    fn entered_needle(self: &FindDialogRef) -> Result<FindNeedle, ParseIntError>
    fn entered_find_needle(self: &FindDialogRef) -> Result<FindNeedle, ParseIntError>
    {
        if self.text_string_radio.get_active() {
            Ok(FindNeedle::Text(self.entered_text()))
            Ok(FindNeedle::Text(self.entered_find_text()))
        } else {
            assert!(self.hex_value_radio.get_active());
            let hex_data = self.entered_hex_data()?;
            let hex_data = self.entered_find_hex_data()?;
            Ok(FindNeedle::Value(hex_data))
        }
    }
    
    fn entered_directive(self: &FindDialogRef) -> Result<FindDirective, ParseIntError>
    fn entered_replace_needle(self: &FindDialogRef) -> Result<Option<FindNeedle>, ParseIntError>
    {
        match &self.details {
            FindDialogDetails::Replace(_) => {
                if self.text_string_radio.get_active() {
                    Ok(Some(FindNeedle::Text(self.entered_replace_text())))
                } else {
                    assert!(self.hex_value_radio.get_active());
                    let hex_data = self.entered_replace_hex_data()?;
                    Ok(Some(FindNeedle::Value(hex_data)))
                }
            },

            _ => Ok(None)
        }
    }
    
    fn entered_directive(self: &FindDialogRef, response_type: &gtk::ResponseType) -> Result<FindDirective, ParseIntError>
    {
        let needle = self.entered_needle()?;
        let find_needle = self.entered_find_needle()?;
        let replace_needle = self.entered_replace_needle()?;
        let replace = match replace_needle {
            Some(needle) => Some(Replace {
                needle,
                action: match response_type {
                    // "Ok" = Replace, "Apply" = Replace All, according to find-dialog.ui
                    ResponseType::Ok    => ReplaceAction::Replace,
                    ResponseType::Apply => ReplaceAction::ReplaceAll,
                    _                   => unreachable!()
                }
            }),

            None => None
        };
        
        Ok(FindDirective {
            needle,
            direction: self.selected_direction()
            needle:     find_needle,
            direction:  self.selected_direction(),
            replace:    replace,
        })
    }
    
    fn update_button_state(self: &FindDialogRef)
    {
        let input_valid =
            self.text_string_radio.get_active() || (
            (
                self.text_string_radio.get_active() &&
                self.entered_find_text().len() > 0
            ) || (
                self.hex_value_radio.get_active() &&
                self.entered_hex_data().is_ok()
                self.entered_find_hex_data().is_ok() &&
                self.entered_find_hex_data().unwrap().len() > 0
            );
        self.find_button.set_sensitive(input_valid);
        
        match &self.details {
            FindDialogDetails::Find(details) => {
                details.find_button.set_sensitive(input_valid);
            },

            FindDialogDetails::Replace(details) => {
                let replace_input_valid =
                    input_valid && (
                        self.text_string_radio.get_active() || (
                            self.hex_value_radio.get_active() &&
                            self.entered_replace_hex_data().is_ok()
                        )
                    );
                details.replace_button.set_sensitive(replace_input_valid);
                details.replace_all_button.set_sensitive(replace_input_valid);
            },
        }
    }
}