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: >k::Window) -> FindDialogRef
+ pub fn new(parent: >k::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: >k::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: >k::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: >k::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);
+ },
+ }
}
}