emonti-hexwrench 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Hexwrench
4
+ begin
5
+ require 'wxirb'
6
+ HAVE_WXIRB=true
7
+ rescue LoadError
8
+ HAVE_WXIRB=false
9
+ end
10
+
11
+ # Our main application window super-classes EditorFrameBase, which is
12
+ # pulled in from XRC via gui.rb
13
+ class EditorFrame < EditorFrameBase
14
+ attr_accessor :filename
15
+ attr_reader :editor, :config, :util_search, :util_jump
16
+
17
+ def initialize(parent, opts={})
18
+ super(parent)
19
+ set_title "Hexwrench"
20
+
21
+ # XXX how we gonna do config? hmm.
22
+ @config ||= {}
23
+
24
+ buf = opts.delete(:data)
25
+ sizer = Wx::BoxSizer.new(Wx::HORIZONTAL)
26
+ @editor = EditWindow.new(self, buf)
27
+ if f=opts.delete(:filename)
28
+ do_open_file(f)
29
+ end
30
+
31
+ sizer.add(@editor, 1, Wx::EXPAND|Wx::ALL, 2)
32
+ self.sizer = sizer
33
+
34
+ # Set up the 'jump' and 'search' toolbar utilities
35
+ @util_search.extend(UtilTextCtrl)
36
+ @util_jump.extend(UtilTextCtrl)
37
+
38
+ @util_search.init
39
+ @util_jump.init
40
+
41
+ evt_text_enter @util_search, :on_search_util
42
+ evt_text_enter @util_jump, :on_jump_util
43
+
44
+ @editor.instance_eval do
45
+ # redirect the hex editor's 'ins_mode accessors to our own
46
+ def ins_mode; parent.ins_mode ; end
47
+ def ins_mode=(val); parent.ins_mode=(val) ; end
48
+ end
49
+
50
+ update_status_bar()
51
+ init_menu_bar()
52
+
53
+ # Custom event handlers from the EditorWindow
54
+ evt_cursor_moved @editor, :on_cursor_move
55
+ evt_data_changed @editor, :on_data_change
56
+
57
+ evt_close :on_close
58
+
59
+ @editor.set_focus
60
+ end
61
+
62
+
63
+ # Arranges all the event handlers, hot-keys, help text, and various
64
+ # other things related to menu items
65
+ def init_menu_bar
66
+ # File menu
67
+ evt_menu @mitem_open, :on_menu_open
68
+ evt_menu @mitem_new, :on_menu_new
69
+ evt_menu(@mitem_save) {|evt| do_save() }
70
+ evt_menu(@mitem_quit) {|evt| do_quit() }
71
+
72
+ # Edit menu
73
+ evt_menu @mitem_copy, :on_menu_copy
74
+ evt_menu @mitem_cut, :on_menu_cut
75
+ evt_menu @mitem_paste, :on_menu_paste
76
+ evt_menu @mitem_select_all, :on_menu_select_all
77
+ evt_menu @mitem_select_range, :on_menu_stub
78
+ evt_menu @mitem_adv_search, :on_menu_stub
79
+
80
+ # Tools menu
81
+ evt_menu @mitem_data_inspector, :on_menu_data_inspector
82
+ evt_menu @mitem_strings, :on_menu_strings
83
+
84
+ mb = self.get_menu_bar
85
+
86
+ # Create hot-key "accelerators" for menu items
87
+ # Note: Clipboard hot-keys are handled in child controls
88
+ shortcuts=[
89
+ [Wx::MOD_CMD, ?o, @mitem_open],
90
+ [Wx::MOD_CMD, ?n, @mitem_new],
91
+ [Wx::MOD_CMD, ?s, @mitem_save],
92
+ [Wx::MOD_CMD, ?q, @mitem_quit],
93
+ [Wx::MOD_CMD, ?r, @mitem_select_range],
94
+ [Wx::MOD_CMD, ?f, @mitem_adv_search],
95
+ [Wx::MOD_CMD, ?i, @mitem_data_inspector],
96
+ [Wx::MOD_CMD|Wx::MOD_SHIFT, ?s, @mitem_strings],
97
+ ]
98
+
99
+ # add the WXIRB console option to the tools menu if it's available
100
+ if HAVE_WXIRB
101
+ tools = mb.get_menu(mb.find_menu("Tools"))
102
+ cons_item = Wx::MenuItem.new(
103
+ tools,
104
+ Wx::ID_ANY,
105
+ "WxIRB Console",
106
+ "Toggle Console. Key: shift+cmd+C"
107
+ )
108
+ tools.append_separator
109
+ tools.append_item(cons_item)
110
+ @mitem_console = cons_item.get_id
111
+ evt_menu @mitem_console, :on_menu_console
112
+ shortcuts << [Wx::MOD_CMD|Wx::MOD_SHIFT, ?c, @mitem_console]
113
+ end
114
+
115
+ self.accelerator_table = Wx::AcceleratorTable[*shortcuts]
116
+
117
+ # Set usage help so it will appear in status bar on mouse-over.
118
+ # We need to look menu items up since they are given to us as ID
119
+ # values by the XRC stub
120
+ mb.find_item(@mitem_open).help="Open a file in editor. Key: cmd+o"
121
+ mb.find_item(@mitem_new).help="Start a new buffer. Key: cmd+n"
122
+ mb.find_item(@mitem_save).help="Save to a file. Key: cmd+s"
123
+ mb.find_item(@mitem_quit).help="Quit program. Key: cmd+q"
124
+ mb.find_item(@mitem_copy).help="Copy to clipboard. Key: cmd+c"
125
+ mb.find_item(@mitem_cut).help="Cut to clipboard. Key: cmd+x"
126
+ mb.find_item(@mitem_paste).help="Paste from clipboard. Key: cmd+v"
127
+ mb.find_item(@mitem_select_all).help="Select entire buffer. Key: cmd+a"
128
+ mb.find_item(@mitem_select_range).help="Select a range. Key: cmd+r"
129
+ mb.find_item(@mitem_adv_search).help="Adv. search/replace. Key: cmd+f"
130
+ mb.find_item(@mitem_data_inspector).help="Toggle Inspector. Key: cmd+i"
131
+ mb.find_item(@mitem_strings).help="Toggle Strings. Key: shift+cmd+S"
132
+
133
+ return mb
134
+ end
135
+
136
+ # Called when the user clicks on File -> Quit, closes the window, or
137
+ # uses the CMD+q hotkey
138
+ def do_quit
139
+ self.close
140
+ end
141
+
142
+ # Called internally to update the status bar information
143
+ def update_status_bar
144
+ set_status_text("Offset: #{@editor.cur_pos}/#{@editor.data.size}", 0)
145
+
146
+ sel_txt = if sel=@editor.selection
147
+ "#{sel.last-sel.first+1} bytes (#{sel})"
148
+ else
149
+ "nil"
150
+ end
151
+
152
+ set_status_text("Selection: #{sel_txt}", 1)
153
+ end
154
+
155
+ # returns true/false depending on whether the "Ins:" toolbar checkbox is
156
+ # checked
157
+ def ins_mode ; @util_ins_chk.value ; end
158
+
159
+ # Changes the "Ins:" toolbar checkbox to true/false (checked/unchecked)
160
+ def ins_mode=(val); @util_ins_chk.value=value ; end
161
+
162
+ # Converts a string from hex to binary - used in the Hex Search feature
163
+ # from the tool-bar
164
+ def unhexify(val)
165
+ if (val =~ /^[a-f0-9 ]+$/i)
166
+ val.strip.gsub(/([a-f0-9]{1,2}) */i) { $1.hex.chr }
167
+ end
168
+ end
169
+
170
+ # Handles Wx::CloseEvent. Confirms the user want's to close when
171
+ # unsaved changes exist
172
+ def on_close(evt)
173
+ if @buffer_changed and not confirm_discard_changes?
174
+ evt.can_veto=true
175
+ evt.veto(true)
176
+ else
177
+ evt.skip(true)
178
+ end
179
+ end
180
+
181
+ # Called when a user presses enter in the "Search" tool-bar textbox
182
+ def on_search_util(evt)
183
+ val = @util_search.value
184
+ kstr = @util_search_kind.string_selection
185
+ do_search( val, EditWindow::AREAS[ {"Hex" => 0, "ASCII" => 1}[kstr] ] )
186
+ end
187
+
188
+ # Implements data search for the "Search" tool-bar item
189
+ def do_search(val, kind)
190
+ pos = @editor.cur_pos+1
191
+ if (
192
+ ( (kind == :ascii) or (kind == :hex and val=unhexify(val)) ) and
193
+ ( dat = @editor.data[pos..-1]) and
194
+ ( idx = @editor.data[pos..-1].index(val) )
195
+ )
196
+
197
+ idx+=pos
198
+ @editor.select_range(idx..idx+val.size-1)
199
+ @editor.scroll_to_idx(idx)
200
+ @editor.send("set_area_#{kind.to_s}")
201
+ @editor.refresh
202
+ else
203
+ @util_search.do_error
204
+ end
205
+ @editor.set_focus
206
+ end
207
+
208
+
209
+ # Called when a user presses enter in the "Jump to" tool-bar textbox
210
+ def on_jump_util(evt)
211
+ val = @util_jump.value
212
+ if((m=/^(?:0?x([A-Fa-f0-9]+)|(\d+))$/.match(val)) and
213
+ (idx = (m[1])? m[1].hex : m[2].to_i) and
214
+ (@editor.data.size > idx))
215
+ @editor.clear_selection()
216
+ @editor.set_area_hex()
217
+ @editor.move_to_idx(idx)
218
+ @editor.refresh
219
+ else
220
+ @util_jump.do_error
221
+ end
222
+ @editor.set_focus
223
+ end
224
+
225
+
226
+ # Called from event handlers to clear all utility textbox errors
227
+ def clear_util_errors
228
+ @util_search.clear_error
229
+ @util_jump.clear_error
230
+ end
231
+
232
+
233
+ # Set's the internal filename and window title info.
234
+ def set_filename(name)
235
+ @filename = name
236
+ if name
237
+ set_title "Hexwrench - "+
238
+ "#{File.basename(name)} "+
239
+ "(#{File.dirname(File.expand_path(name))})"
240
+ else
241
+ set_title "Hexwrench"
242
+ end
243
+ end
244
+
245
+ # Stub indicating inactive menu items with a message dialog popup
246
+ # note: XXX this is mostly to remind me to add these features =)
247
+ def on_menu_stub(evt)
248
+ Wx::MessageDialog.new(self, :caption => "Coming soon",
249
+ :message => "Sorry. This feature not yet implemented.").show_modal
250
+ end
251
+
252
+ # Used to pop-up a confirmation dialog when the user is about to discard
253
+ # changes in the editor.
254
+ def confirm_discard_changes?
255
+ ret = Wx::MessageDialog.new(
256
+ self,
257
+ :style => Wx::YES_NO|Wx::NO_DEFAULT,
258
+ :caption => "Discard Changes?",
259
+ :message => "Un-saved changes will be lost. Proceed anyway?"
260
+ ).show_modal
261
+
262
+ if ret == Wx::ID_YES
263
+ @buffer_changed=nil
264
+ return true
265
+ else
266
+ return false
267
+ end
268
+ end
269
+
270
+ # Handles the File -> Open menu item.
271
+ def on_menu_open(evt)
272
+ return nil if @buffer_changed and not confirm_discard_changes?
273
+
274
+ open_dlg = Wx::FileDialog.new( self,
275
+ :style => Wx::FD_OPEN|Wx::FD_FILE_MUST_EXIST)
276
+
277
+ if open_dlg.show_modal == Wx::ID_OK
278
+ do_open_file(open_dlg.path)
279
+ end
280
+ end
281
+
282
+ # Implements opening new files - pops up a error dialog if a file
283
+ # error is encountered when reading the file.
284
+ def do_open_file(filename)
285
+ dat=nil
286
+ begin
287
+ dat = File.read(filename)
288
+ rescue => e
289
+ Wx::MessageDialog.new(self,
290
+ :caption => "Error Opening File",
291
+ :message => "#{e.class} - #{e.to_s}"
292
+ ).show_modal
293
+ end
294
+ if dat
295
+ set_filename(filename)
296
+ @editor.set_data(dat) if dat
297
+ @editor.move_to_idx(0)
298
+ @new_buffer=true
299
+ end
300
+ end
301
+
302
+ # Implements the File -> New menu item. Replaces the current editor
303
+ # buffer with an empty string.
304
+ def on_menu_new(evt)
305
+ return nil if @buffer_changed and not confirm_discard_changes?
306
+ @new_buffer=true
307
+ set_filename(nil)
308
+ @editor.set_data nil
309
+ end
310
+
311
+ # Handles the user clicking on the Edit -> Select All menu item.
312
+ # This method just calls the EditWindow.do_select_all() method
313
+ def on_menu_select_all(evt)
314
+ @editor.do_select_all
315
+ end
316
+
317
+ # Implements the File -> Save menu item.
318
+ # This method will call on_save_as if the user has not specified a
319
+ # file yet
320
+ def do_save(filename = nil)
321
+ filename ||= @filename
322
+ if filename
323
+ begin
324
+ File.open(filename, "w") {|f| f.write @editor.data }
325
+ @filename = filename
326
+ @new_buffer=true
327
+ @buffer_changed=false
328
+ rescue => e
329
+ Wx::MessageDialog.new(self,
330
+ :caption => "Error Saving File",
331
+ :message => "#{e.class} - #{e.to_s}"
332
+ ).show_modal
333
+ end
334
+ else
335
+ do_save_as()
336
+ end
337
+ end
338
+
339
+ # Implements the 'Save As' feature - presenting the user with a file
340
+ # save dialog.
341
+ def do_save_as()
342
+ save_dlg = Wx::FileDialog.new(self,
343
+ :style => Wx::FD_SAVE|Wx::FD_OVERWRITE_PROMPT)
344
+ if save_dlg.show_modal == Wx::ID_OK
345
+ set_filename(save_dlg.path)
346
+ do_save(save_dlg.path)
347
+ end
348
+ end
349
+
350
+ # Handles the user clicking on the Edit -> Copy menu item.
351
+ # This method just calls the EditWindow.do_clipboard_copy() method
352
+ def on_menu_copy(evt)
353
+ @editor.do_clipboard_copy()
354
+ end
355
+
356
+ # Handles the user clicking on the Edit -> Cut menu item.
357
+ # This method just calls the EditWindow.do_clipboard_cut() method
358
+ def on_menu_cut(evt)
359
+ @editor.do_clipboard_cut()
360
+ end
361
+
362
+ # Handles the user clicking on the Edit -> Paste menu item.
363
+ # This method just calls the EditWindow.do_clipboard_paste() method
364
+ def on_menu_paste(evt)
365
+ @editor.do_clipboard_paste()
366
+ end
367
+
368
+ # Toggles a strings listing pop-up when the user selects the
369
+ # 'Tools -> Strings' menu item
370
+ def on_menu_strings(evt)
371
+ if @strings
372
+ @strings.destroy()
373
+ @strings = nil
374
+ else
375
+ @strings = StringsFrame.new(self, @editor, @config[:strings_opts])
376
+ @strings.accelerator_table = self.accelerator_table # clone hotkeys
377
+ @strings.evt_window_destroy {|evt| @strings=nil;evt.skip() }
378
+ @strings.show
379
+ end
380
+ end
381
+
382
+ # Toggles the data inspector window on and off when user
383
+ # selects the "Tools -> Data Inspector" menu item
384
+ def on_menu_data_inspector(evt)
385
+ if @d_inspector
386
+ @d_inspector.destroy()
387
+ @d_inspector = nil
388
+ else
389
+ @d_inspector = DataInspector.new(self, :editor => @editor)
390
+ @d_inspector.accelerator_table = self.accelerator_table # clone hotkeys
391
+ @d_inspector.evt_window_destroy { |evt| @d_inspector=nil; evt.skip() }
392
+ @d_inspector.do_inspectors
393
+ @d_inspector.show
394
+ end
395
+ end
396
+
397
+ # Event handler for the Tools -> Console menu item.
398
+ # This method only ever fires if wxirb is available. (based on HAVE_WXIRB)
399
+ def on_menu_console(evt)
400
+ return nil unless HAVE_WXIRB
401
+ if $wxirb
402
+ $wxirb.destroy()
403
+ $wxirb = nil
404
+ else
405
+ $wxirb = WxIRB::BaseFrame.new(self, :binding => binding)
406
+ $wxirb.accelerator_table = self.accelerator_table # clone hotkeys
407
+ $wxirb.evt_window_destroy { |evt| $wxirb=nil; evt.skip() }
408
+ $wxirb.show
409
+ end
410
+ end
411
+
412
+ # Event handler for cursor movement to update various UI elements and
413
+ # active tool windows.
414
+ def on_cursor_move(evt)
415
+ clear_util_errors
416
+ update_status_bar()
417
+ @d_inspector.do_inspectors if @d_inspector
418
+ evt.skip()
419
+ end
420
+
421
+ # Event handler for data changes to update various UI elements and active
422
+ # tool windows.
423
+ def on_data_change(evt)
424
+ if @new_buffer
425
+ @new_buffer=false
426
+ else
427
+ @buffer_changed=true
428
+ end
429
+ clear_util_errors
430
+ update_status_bar()
431
+ @d_inspector.do_inspectors if @d_inspector
432
+ @strings.notify_data_change if @strings
433
+ end
434
+ end
435
+
436
+
437
+ # This module is used to extend a regular Wx::TextCtrl text box to display
438
+ # greyed out text when idle which describes the control's purpose.
439
+ # As soon as focus is set on the control, the text disappears.
440
+ module UtilTextCtrl
441
+
442
+ # This init method is called after creation since we don't
443
+ # override 'new' in the XRC derived window element. Takes a hash of
444
+ # :name => value options. The only option is currently :default_text,
445
+ # which is the text to display when the control is idle. If :default_text
446
+ # is not specified, the control will use the initial value in the text box
447
+ # as default text.
448
+ def init(opts={})
449
+ @dflt_text = (opts[:default_text] || get_value.dup)
450
+ evt_set_focus :on_set_focus
451
+ evt_kill_focus :on_kill_focus
452
+ do_default_text
453
+ end
454
+
455
+ # Returns text to its greyed-out default display
456
+ def do_default_text
457
+ @error = false
458
+ clear()
459
+ set_default_style(Wx::TextAttr.new( Wx::LIGHT_GREY) )
460
+ set_value(@dflt_text)
461
+ set_default_style(Wx::TextAttr.new( Wx::BLACK) )
462
+ end
463
+
464
+ # Indicates an error in the text box by turning current text red.
465
+ def do_error
466
+ @error = true
467
+ val = self.value
468
+ clear()
469
+ set_default_style(Wx::TextAttr.new( Wx::RED))
470
+ set_value(val)
471
+ set_default_style(Wx::TextAttr.new( Wx::BLACK))
472
+ set_insertion_point_end
473
+ end
474
+
475
+ # Clears error text set by do_error and returns the textbox to its
476
+ # default text display.
477
+ def clear_error
478
+ if @error
479
+ @error = false
480
+ do_default_text
481
+ end
482
+ end
483
+
484
+ # Event handler for when focus is set to this text control. Clears
485
+ # default text in preparation for user input.
486
+ def on_set_focus(evt)
487
+ @error = false
488
+ clear()
489
+ set_default_style(Wx::TextAttr.new( Wx::BLACK))
490
+ evt.skip()
491
+ end
492
+
493
+ # Event handler for when focus is lost to this text control. Returns
494
+ # the text to its default display unless an error in input was flagged.
495
+ def on_kill_focus(evt)
496
+ do_default_text if not @error
497
+ evt.skip()
498
+ end
499
+ end
500
+ end
501
+