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.
- data/History.txt +7 -0
- data/README.rdoc +69 -0
- data/Rakefile +42 -0
- data/bin/hexwrench +14 -0
- data/hexwrench.gemspec +38 -0
- data/lib/hexwrench/build_xrc.sh +1 -0
- data/lib/hexwrench/data_inspector.rb +138 -0
- data/lib/hexwrench/edit_frame.rb +501 -0
- data/lib/hexwrench/edit_window.rb +910 -0
- data/lib/hexwrench/gui.rb +62 -0
- data/lib/hexwrench/stringsgrid.rb +72 -0
- data/lib/hexwrench/stringslist.rb +105 -0
- data/lib/hexwrench/stringsvlist.rb +123 -0
- data/lib/hexwrench/ui/gui.xrc +93 -0
- data/lib/hexwrench.rb +14 -0
- data/samples/ascii_heat_map.rb +13 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- metadata +105 -0
@@ -0,0 +1,910 @@
|
|
1
|
+
|
2
|
+
module Hexwrench
|
3
|
+
|
4
|
+
# The CursorMoveEvent event is fired when a user moves the cursor around the
|
5
|
+
# hex editor window. It is used mostly by the parent window to trigger
|
6
|
+
# behaviours in various other UI elements when this happens.
|
7
|
+
class CursorMoveEvent < Wx::CommandEvent
|
8
|
+
EVT_CURSOR_MOVED = Wx::EvtHandler.register_class(self, nil, 'evt_cursor_moved', 1)
|
9
|
+
def initialize(editor)
|
10
|
+
super(EVT_CURSOR_MOVED)
|
11
|
+
self.client_data = {:editor => editor}
|
12
|
+
self.id = editor.get_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def editor ; client_data[:editor] ; end
|
16
|
+
end
|
17
|
+
|
18
|
+
# The DataChangeEvent event is fired when a user makes any change to data
|
19
|
+
# in the editor window. It is used mostly by the parent window to trigger
|
20
|
+
# behaviours in various other UI elements when this happens.
|
21
|
+
class DataChangeEvent < Wx::CommandEvent
|
22
|
+
EVT_DATA_CHANGED = Wx::EvtHandler.register_class(self, nil, 'evt_data_changed', 1)
|
23
|
+
def initialize(editor)
|
24
|
+
super(EVT_DATA_CHANGED)
|
25
|
+
self.client_data = {:editor => editor}
|
26
|
+
self.id = editor.get_id
|
27
|
+
end
|
28
|
+
|
29
|
+
def editor ; client_data[:editor] ; end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# The EditWindow is the actual hex editor widget. This is a pure ruby
|
34
|
+
# implementation using WxRuby's VScrolledWindow and direct painting for
|
35
|
+
# the hexdump and all editor actions within it.
|
36
|
+
class EditWindow < Wx::VScrolledWindow
|
37
|
+
attr_reader :font, :cursor, :data, :selection
|
38
|
+
attr_accessor :post_paint_proc
|
39
|
+
|
40
|
+
HEX_CHARS=['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']
|
41
|
+
|
42
|
+
DEFAULT_FONT = Wx::Font.new(10, Wx::MODERN, Wx::NORMAL, Wx::NORMAL)
|
43
|
+
STYLE = Wx::VSCROLL |Wx::ALWAYS_SHOW_SB
|
44
|
+
|
45
|
+
AREAS = [ :hex, :ascii ]
|
46
|
+
HEX_AREA = 0
|
47
|
+
ASCII_AREA = 1
|
48
|
+
|
49
|
+
def initialize(parent, data, opt={})
|
50
|
+
super parent,
|
51
|
+
:id => (opt[:id] || -1),
|
52
|
+
:pos => (opt[:pos] || Wx::DEFAULT_POSITION),
|
53
|
+
:size => (opt[:size] || Wx::DEFAULT_SIZE),
|
54
|
+
:style => (opt[:style] || 0) | STYLE,
|
55
|
+
:name => (opt[:name] || '')
|
56
|
+
|
57
|
+
@data = (data || '')
|
58
|
+
|
59
|
+
Struct.new "Cursor",
|
60
|
+
:pos, # current index into the data
|
61
|
+
:area, # primary data_area the cursor is in
|
62
|
+
:ins_mode # Insert/Overwrite mode boolean
|
63
|
+
|
64
|
+
@cursor = Struct::Cursor.new()
|
65
|
+
@cursor.pos = 0
|
66
|
+
@cursor.area = HEX_AREA
|
67
|
+
@other_area = ASCII_AREA
|
68
|
+
@cursor.ins_mode = true
|
69
|
+
|
70
|
+
@selection = nil
|
71
|
+
|
72
|
+
@post_paint_proc = opt[:post_paint_proc]
|
73
|
+
|
74
|
+
init_font( (opt[:font] || DEFAULT_FONT) )
|
75
|
+
|
76
|
+
## set color palette:
|
77
|
+
|
78
|
+
# color behind address and in dead-space to right
|
79
|
+
@bg_color = Wx::WHITE
|
80
|
+
set_background_colour(@bg_color)
|
81
|
+
|
82
|
+
# hexdump text and bounds colors
|
83
|
+
@dump_color=Wx::Colour.new(Wx::BLACK)
|
84
|
+
@addr_color=Wx::Colour.new(128,128,128)
|
85
|
+
@area_bounds_pen = Wx::Pen.new("GREY", 2, Wx::SOLID)
|
86
|
+
@word_bounds_pen = Wx::Pen.new("LIGHT GREY", 1, Wx::SOLID)
|
87
|
+
|
88
|
+
# alternating row background colors
|
89
|
+
@alt_row_bgs = [
|
90
|
+
Wx::Brush.new( Wx::Colour.new("WHITE") ),
|
91
|
+
Wx::Brush.new( Wx::Colour.new(237, 247, 254 ) )
|
92
|
+
]
|
93
|
+
|
94
|
+
# colors for selection in primary and 'other' data displays
|
95
|
+
@select_bgs = [
|
96
|
+
Wx::Brush.new( Wx::Colour.new(181,213,255) ),
|
97
|
+
Wx::Brush.new( Wx::Colour.new(212,212,212) )
|
98
|
+
]
|
99
|
+
|
100
|
+
# colors for the cursor
|
101
|
+
@cursor_text1 = Wx::Colour.new(Wx::BLACK)
|
102
|
+
@cursor_pen1 = Wx::Colour.new(220, 158, 50)
|
103
|
+
@cursor_bg1 = Wx::Colour.new(220, 158, 50)
|
104
|
+
@cursor_text2 = @dump_color
|
105
|
+
@cursor_pen2 = Wx::Colour.new(Wx::BLACK)
|
106
|
+
@cursor_bg2 = Wx::TRANSPARENT_BRUSH
|
107
|
+
|
108
|
+
## initialize event handlers
|
109
|
+
evt_window_create :on_create
|
110
|
+
evt_size :on_size
|
111
|
+
evt_paint {|evt| paint_buffered {|dc| on_paint(dc)} }
|
112
|
+
|
113
|
+
evt_idle :on_idle
|
114
|
+
|
115
|
+
evt_char :on_char
|
116
|
+
evt_left_down :on_left_button_down
|
117
|
+
evt_motion :on_mouse_motion
|
118
|
+
evt_left_up :on_left_button_up
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
def ins_mode; @cursor.ins_mode ; end
|
123
|
+
def ins_mode=(v) ; @cursor.ins_mode = v ; end
|
124
|
+
|
125
|
+
# returns the current cursor position
|
126
|
+
def cur_pos
|
127
|
+
@cursor.pos
|
128
|
+
end
|
129
|
+
|
130
|
+
# set the cursor position to 'idx'
|
131
|
+
def cur_pos=(idx)
|
132
|
+
@cursor_moved=true
|
133
|
+
@cursor.pos=idx
|
134
|
+
end
|
135
|
+
|
136
|
+
# Triggered when the window is first created. Establishes dimensions
|
137
|
+
# (by calling update_dimensions) along with various associated instance
|
138
|
+
# variables.
|
139
|
+
def on_create(evt=nil)
|
140
|
+
update_dimensions()
|
141
|
+
@started = true
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# Triggered whenever the window size changes. Updates dimensions and
|
146
|
+
# keeps the scroll bar in place.
|
147
|
+
def on_size(evt=nil)
|
148
|
+
update_dimensions()
|
149
|
+
@started = true
|
150
|
+
scroll_to_idx((self.cur_pos || 0))
|
151
|
+
refresh
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
# This method is required by the Wx::VScrolledWindow super-class which
|
156
|
+
# calls it to determine the size of scroll units for the scrollbar at
|
157
|
+
# a given line. We just return our @row_height for all lines (which
|
158
|
+
# is calculated from do_row_count() ).
|
159
|
+
# Returns -1 if for some reason the row height has not been calculated.
|
160
|
+
def on_get_line_height(x); (@row_height || -1); end
|
161
|
+
|
162
|
+
|
163
|
+
# This method initializes a new font for the hexdump and determines
|
164
|
+
# text sizes. If it is called after the editor has been initialized,
|
165
|
+
# it will also update scrollbar information with the new dimensions.
|
166
|
+
#
|
167
|
+
# This method is always called during initialization and is passed the
|
168
|
+
# default font or whatever is provided when calling new().
|
169
|
+
def init_font(font)
|
170
|
+
@font=font
|
171
|
+
dc = Wx::WindowDC.new(self)
|
172
|
+
dc.set_font(font)
|
173
|
+
@asc_width, asc_h = dc.get_text_extent("@")[0,2]
|
174
|
+
@asc_width+2 # compact, but not too much so
|
175
|
+
@hex_width, hex_h = dc.get_text_extent("@@")[0,2]
|
176
|
+
@txt_height = (hex_h > asc_h)? hex_h : asc_h
|
177
|
+
@addr_width = dc.get_text_extent(@data.size.to_s(16).rjust(4,'@'))[0]
|
178
|
+
@row_height = @txt_height
|
179
|
+
|
180
|
+
update_dimensions() if @started
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# This method must be called on initialization, on size events, and
|
185
|
+
# whenever the size of data contents has changed.
|
186
|
+
def update_dimensions
|
187
|
+
spacer=@hex_width # 3-char space between areas
|
188
|
+
ui_width = @addr_width + (spacer*4) # addr sp sp hex sp ascii sp
|
189
|
+
|
190
|
+
@columns = (client_size.width - ui_width) / (@hex_width + @asc_width*2)
|
191
|
+
@columns = 1 if @columns < 1
|
192
|
+
@rows = (@data.size / @columns)+1
|
193
|
+
|
194
|
+
# calculate hex/ascii area boundaries
|
195
|
+
@hex0 = @addr_width + spacer*2
|
196
|
+
@hexN = @hex0 + (@columns * (@hex_width+@asc_width))
|
197
|
+
@asc0 = @hexN + spacer
|
198
|
+
@ascN = @asc0 + (@columns * @asc_width)
|
199
|
+
@row_width = @ascN - @addr_width - @hex_width
|
200
|
+
|
201
|
+
# update scroll-bar info
|
202
|
+
old_pos=first_visible_line
|
203
|
+
set_line_count(@rows)
|
204
|
+
scroll_to_line(old_pos)
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
# An idle event handler for Wx::IdleEvent. Keeps track of data changes and
|
209
|
+
# cursor movements and produces the appropriate events for them.
|
210
|
+
# See also: CursorMoveEvent and DataChangeEvent
|
211
|
+
def on_idle(evt)
|
212
|
+
if @cursor_moved
|
213
|
+
@cursor_moved=false
|
214
|
+
event_handler.process_event( CursorMoveEvent.new(self) )
|
215
|
+
end
|
216
|
+
|
217
|
+
if @data_changed
|
218
|
+
@data_changed=false
|
219
|
+
event_handler.process_event( DataChangeEvent.new(self) )
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
# Use this method to set a new internal data value from outside the
|
225
|
+
# class. Doing so should keep all the furniture aranged correctly.
|
226
|
+
def set_data(data)
|
227
|
+
@data_changed=true
|
228
|
+
data ||= ""
|
229
|
+
self.cur_pos=@last_pos=0
|
230
|
+
clear_selection
|
231
|
+
@data = data
|
232
|
+
update_dimensions()
|
233
|
+
refresh()
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
# This method does the heavy lifting in drawing the hex editor dump
|
238
|
+
# window. Takes a 'dc' device context parameter
|
239
|
+
def on_paint(dc)
|
240
|
+
return unless @started
|
241
|
+
dc.set_font(@font)
|
242
|
+
first_row = row = get_first_visible_line
|
243
|
+
last_row = get_last_visible_line+1
|
244
|
+
y = 0
|
245
|
+
hX = @hex0
|
246
|
+
aX = @asc0
|
247
|
+
idx = (row.zero?)? 0 : @columns * row
|
248
|
+
|
249
|
+
hex_w = @hex_width + @asc_width
|
250
|
+
h_off = @hex_width / 2
|
251
|
+
|
252
|
+
# draw blank background
|
253
|
+
dc.set_pen(Wx::TRANSPARENT_PEN)
|
254
|
+
dc.set_brush(Wx::Brush.new(@bg_color))
|
255
|
+
dc.draw_rectangle(0, 0, client_size.width, client_size.height)
|
256
|
+
|
257
|
+
paint_row(dc, y, idx, row)
|
258
|
+
|
259
|
+
while(c=@data[idx]) and row <= last_row
|
260
|
+
if(hX >= @hexN)
|
261
|
+
hX = @hex0
|
262
|
+
aX = @asc0
|
263
|
+
y += @txt_height
|
264
|
+
row +=1
|
265
|
+
paint_row(dc, y, idx, row)
|
266
|
+
end
|
267
|
+
|
268
|
+
# call byte colorization block if we have one
|
269
|
+
text_color =
|
270
|
+
if( @post_paint_proc and
|
271
|
+
bret=@post_paint_proc.call(self,dc,idx,c,hX+h_off,aX,y) )
|
272
|
+
bret
|
273
|
+
else
|
274
|
+
@dump_color
|
275
|
+
end
|
276
|
+
|
277
|
+
# selection stuff goes here
|
278
|
+
if @selection and @selection.include?(idx)
|
279
|
+
sbrushes = [
|
280
|
+
@select_bgs[ @cursor.area ],
|
281
|
+
@select_bgs[ (@cursor.area+1) % AREAS.size ]
|
282
|
+
]
|
283
|
+
colorize_byte_bg(sbrushes, dc, hX+h_off, aX, y)
|
284
|
+
end
|
285
|
+
|
286
|
+
dc.set_text_foreground(text_color)
|
287
|
+
dc.draw_text("#{disp_hex_byte(c)}", hX+h_off, y)
|
288
|
+
dc.draw_text("#{disp_ascii_byte(c)}", aX, y)
|
289
|
+
|
290
|
+
hX += hex_w
|
291
|
+
aX += @asc_width
|
292
|
+
idx += 1
|
293
|
+
end
|
294
|
+
|
295
|
+
paint_boundaries(dc)
|
296
|
+
paint_cursor(dc)
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
# This method is called from the on_paint method to draw a row for each
|
301
|
+
# hexdump row in the display
|
302
|
+
def paint_row(dc, y, addr, row_num)
|
303
|
+
dc.set_pen(Wx::TRANSPARENT_PEN)
|
304
|
+
dc.set_text_foreground(@addr_color)
|
305
|
+
addr_str = addr.to_s(16).rjust(2,"0")
|
306
|
+
w = dc.get_text_extent(addr_str)[0]
|
307
|
+
dc.draw_text(addr_str, (@hex0 - w - @asc_width), y)
|
308
|
+
if row_num
|
309
|
+
dc.set_brush(@alt_row_bgs[ row_num % @alt_row_bgs.size ])
|
310
|
+
dc.draw_rectangle(@hex0, y, @row_width, @txt_height)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
|
315
|
+
# This method is called from the on_paint method to draw bounding lines
|
316
|
+
# on 4-byte word boundaries and between the address/hex/ascii columns
|
317
|
+
def paint_boundaries(dc)
|
318
|
+
height = @rows * @txt_height
|
319
|
+
|
320
|
+
# draw area boundaries
|
321
|
+
dc.set_pen(@area_bounds_pen)
|
322
|
+
dc.draw_line(x2=(@hex0)-2, 0, x2, height)
|
323
|
+
dc.draw_line(x2=(@asc0-@asc_width), 0, x2, height)
|
324
|
+
dc.draw_line(x2=(@hex0+@row_width), 0, x2, height)
|
325
|
+
|
326
|
+
hex_w = @hex_width + @asc_width
|
327
|
+
h_off = @hex_width /2
|
328
|
+
l_off = @asc_width /2
|
329
|
+
|
330
|
+
# draw WORD boundary indicator lines in the hex area
|
331
|
+
divW = (hex_w << 2)
|
332
|
+
divX = @hex0 + divW - 1
|
333
|
+
dc.set_pen( @word_bounds_pen )
|
334
|
+
while divX < @hexN-h_off
|
335
|
+
dc.draw_line(x2=(divX+l_off), 0, x2, height)
|
336
|
+
divX += divW
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
# A helper method for colorizing post_paint_proc blocks. this colorizes
|
342
|
+
# the hex and ascii background for a given byte position with the same
|
343
|
+
# color. brush must be a Wx::Brush object.
|
344
|
+
def colorize_byte_bg(brush, dc, hX, aX, y)
|
345
|
+
if brush.kind_of? Array
|
346
|
+
hbrush, abrush = brush[0..1]
|
347
|
+
else
|
348
|
+
hbrush = abrush = brush
|
349
|
+
end
|
350
|
+
|
351
|
+
h_off = @hex_width /4
|
352
|
+
|
353
|
+
dc.set_pen(Wx::TRANSPARENT_PEN)
|
354
|
+
|
355
|
+
dc.set_brush(hbrush)
|
356
|
+
dc.draw_rectangle(hX-h_off, y, @hex_width+h_off+h_off, @txt_height)
|
357
|
+
dc.set_brush(abrush)
|
358
|
+
dc.draw_rectangle(aX, y, @asc_width, @txt_height)
|
359
|
+
|
360
|
+
return nil
|
361
|
+
end
|
362
|
+
|
363
|
+
# Called from the on_paint method to draw the editor cursor
|
364
|
+
def paint_cursor(dc)
|
365
|
+
return unless self.cur_pos and @selection.nil?
|
366
|
+
|
367
|
+
pos = self.cur_pos
|
368
|
+
|
369
|
+
if pos == 0
|
370
|
+
row = col = 0
|
371
|
+
else
|
372
|
+
row = pos / @columns
|
373
|
+
col = pos % @columns
|
374
|
+
end
|
375
|
+
|
376
|
+
return unless (first_visible_line..last_visible_line+1).include? row
|
377
|
+
row -= first_visible_line
|
378
|
+
|
379
|
+
w_hex = @hex_width+2
|
380
|
+
w_asc = @asc_width+2
|
381
|
+
|
382
|
+
h_pen, h_brush, h_txt, a_pen, a_brush, a_txt =
|
383
|
+
case @cursor.area
|
384
|
+
when HEX_AREA : [ @cursor_pen1, @cursor_bg1, @cursor_text1,
|
385
|
+
@cursor_pen2, @cursor_bg2, @cursor_text2 ]
|
386
|
+
when ASCII_AREA : [ @cursor_pen2, @cursor_bg2, @cursor_text2,
|
387
|
+
@cursor_pen1, @cursor_bg1, @cursor_text1 ]
|
388
|
+
else
|
389
|
+
[ @cursor_pen2, @cursor_bg2, @cursor_txt2,
|
390
|
+
@cursor_pen2, @cursor_bg2, @cursor_txt2 ]
|
391
|
+
end
|
392
|
+
|
393
|
+
h_off = (@hex_width /2)
|
394
|
+
|
395
|
+
y = row * @txt_height
|
396
|
+
hX = (col * (@hex_width+@asc_width)) + @hex0
|
397
|
+
aX = (col * @asc_width) + @asc0
|
398
|
+
|
399
|
+
dc.set_text_foreground(h_txt)
|
400
|
+
dc.set_pen(Wx::Pen.new(h_pen))
|
401
|
+
dc.set_brush(Wx::Brush.new(h_brush))
|
402
|
+
dc.draw_rectangle(hX+h_off-1, y, w_hex-1, @txt_height)
|
403
|
+
|
404
|
+
dc.set_text_foreground(a_txt)
|
405
|
+
dc.set_pen(Wx::Pen.new(a_pen))
|
406
|
+
dc.set_brush(Wx::Brush.new(a_brush))
|
407
|
+
dc.draw_rectangle(aX-1, y, w_asc-1, @txt_height)
|
408
|
+
|
409
|
+
return unless c=@data[pos]
|
410
|
+
|
411
|
+
dc.draw_text("#{disp_hex_byte(c)}", hX+h_off, y )
|
412
|
+
dc.draw_text("#{disp_ascii_byte(c)}", aX, y )
|
413
|
+
end
|
414
|
+
|
415
|
+
|
416
|
+
# Returns a two-character hex byte representation, always a gives
|
417
|
+
# a leading nibble.
|
418
|
+
def disp_hex_byte(ch)
|
419
|
+
HEX_CHARS[(ch >> 4)] + HEX_CHARS[(ch & 0x0f)]
|
420
|
+
end
|
421
|
+
|
422
|
+
|
423
|
+
# Returns a single ascii character given its numeric value.
|
424
|
+
# If the character is non-printible, this method returns '.'
|
425
|
+
def disp_ascii_byte(ch)
|
426
|
+
(0x20..0x7e).include?(ch) ? ch.chr : '.'
|
427
|
+
end
|
428
|
+
|
429
|
+
|
430
|
+
# moves the scroll-bar so that it includes the row for the
|
431
|
+
# specified data index
|
432
|
+
def scroll_to_idx(idx)
|
433
|
+
row = idx / @columns
|
434
|
+
d_row = get_first_visible_line
|
435
|
+
max_row = get_last_visible_line
|
436
|
+
|
437
|
+
if (d_row..max_row-1).include?(row)
|
438
|
+
return
|
439
|
+
elsif row==max_row
|
440
|
+
scroll_to_line(d_row+1)
|
441
|
+
else
|
442
|
+
scroll_to_line(row)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
|
447
|
+
def select_range(rng)
|
448
|
+
if rng.first >= 0 and rng.last <= @data.size
|
449
|
+
clear_selection()
|
450
|
+
self.cur_pos = @last_pos = rng.first
|
451
|
+
@selection = rng
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
|
456
|
+
# Used internally to expand selections from mouse dragging or
|
457
|
+
# shift+arrow cursor movement.
|
458
|
+
def expand_selection(idx)
|
459
|
+
@selection =
|
460
|
+
if @last_pos
|
461
|
+
if idx < @last_pos
|
462
|
+
(idx..@last_pos)
|
463
|
+
else
|
464
|
+
(@last_pos..idx)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
|
470
|
+
# Clear's the text/data selection if one has been made
|
471
|
+
def clear_selection()
|
472
|
+
@last_pos = nil
|
473
|
+
@selection = nil
|
474
|
+
end
|
475
|
+
|
476
|
+
|
477
|
+
# This method implements cursor movement
|
478
|
+
def move_to_idx(idx, adj=nil, expand_sel=false)
|
479
|
+
@hexbyte_started=false
|
480
|
+
adj ||= 0
|
481
|
+
newidx = idx + adj
|
482
|
+
if @selection and not expand_sel
|
483
|
+
if adj < 0
|
484
|
+
newidx = @selection.first + adj
|
485
|
+
pos = (newidx < 0)? 0 : newidx
|
486
|
+
else
|
487
|
+
newidx = @selection.last + adj
|
488
|
+
pos = (newidx > @data.size)? @data.size : newidx
|
489
|
+
end
|
490
|
+
clear_selection()
|
491
|
+
@last_pos = self.cur_pos = pos
|
492
|
+
scroll_to_idx(pos)
|
493
|
+
refresh
|
494
|
+
return pos
|
495
|
+
elsif (0..@data.size).include? newidx
|
496
|
+
@last_pos ||= self.cur_pos
|
497
|
+
self.cur_pos = newidx
|
498
|
+
if expand_sel
|
499
|
+
expand_selection(newidx)
|
500
|
+
else
|
501
|
+
@last_pos = nil
|
502
|
+
end
|
503
|
+
scroll_to_idx(newidx)
|
504
|
+
refresh
|
505
|
+
return newidx
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
|
510
|
+
def move_cursor_right(expand_sel=false)
|
511
|
+
move_to_idx(idx = self.cur_pos, 1, expand_sel)
|
512
|
+
end
|
513
|
+
|
514
|
+
def move_cursor_left(expand_sel=false)
|
515
|
+
move_to_idx(idx = self.cur_pos, -1, expand_sel)
|
516
|
+
end
|
517
|
+
|
518
|
+
def move_cursor_down(expand_sel=false)
|
519
|
+
move_to_idx(self.cur_pos, @columns, expand_sel)
|
520
|
+
end
|
521
|
+
|
522
|
+
def move_cursor_up(expand_sel=false)
|
523
|
+
move_to_idx(self.cur_pos, -@columns, expand_sel)
|
524
|
+
end
|
525
|
+
|
526
|
+
|
527
|
+
# Sets a value at the given index, or if a selection is active,
|
528
|
+
# overwrites the selection area with the value. For non-selection
|
529
|
+
# edits, the insert-mode flag is checked to determine whether to
|
530
|
+
# overwrite or insert at the index.
|
531
|
+
#
|
532
|
+
# Parameters:
|
533
|
+
# idx : The index to the data where the value is set.
|
534
|
+
# (The idx parameter is ignored in selection overwrites.)
|
535
|
+
# val : The value to set at @data[idx]
|
536
|
+
# Value can be zero-length in which-case the area or index
|
537
|
+
# is deleted.
|
538
|
+
# force_overwrite : Causes the @data[idx] to be overwritten regardless
|
539
|
+
# of the insert-mode flag.
|
540
|
+
#
|
541
|
+
# Returns: the index where the change was made.
|
542
|
+
#
|
543
|
+
def gui_set_value(idx, val, force_overwrite=false)
|
544
|
+
sel=@selection
|
545
|
+
clear_selection()
|
546
|
+
ret=nil
|
547
|
+
if not sel.nil?
|
548
|
+
@data[sel] = val
|
549
|
+
@data_changed=true
|
550
|
+
ret=sel.first
|
551
|
+
elsif idx and (0..@data.size).include? idx
|
552
|
+
if ins_mode and not val.empty? and not force_overwrite
|
553
|
+
@data[idx, 0] = val
|
554
|
+
else
|
555
|
+
vsize = (val.size>0)? val.size : 1
|
556
|
+
@data[idx, vsize] = val
|
557
|
+
end
|
558
|
+
@data_changed=true
|
559
|
+
ret=idx
|
560
|
+
end
|
561
|
+
update_dimensions
|
562
|
+
refresh
|
563
|
+
return ret
|
564
|
+
end
|
565
|
+
|
566
|
+
|
567
|
+
# Takes a key code parameter and looks it up against constants
|
568
|
+
# to return a 'name' if one is found.
|
569
|
+
def resolve_key_code(code)
|
570
|
+
name=nil
|
571
|
+
Wx.constants.grep(/^K_/).each do |kconst|
|
572
|
+
if Wx.const_get(kconst) == code
|
573
|
+
return kconst.sub(/^K_/, '').downcase
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
|
579
|
+
# Keyboard event handler.
|
580
|
+
#
|
581
|
+
# Key-presses with ASCII values are deferred to evt_char for correct
|
582
|
+
# translation via the key-press event. However, character-specific handlers
|
583
|
+
# using key modifiers (alt, shift, cmd) are honored and override the
|
584
|
+
# evt_char handler.
|
585
|
+
#
|
586
|
+
# The resolver looks up key names and calls matching char-specific
|
587
|
+
# handlers by name if they are defined.
|
588
|
+
#
|
589
|
+
# See http://wxruby.rubyforge.org/doc/keycode.html#keycodes for a
|
590
|
+
# list of key names.
|
591
|
+
#
|
592
|
+
# This method is not designed for calling directly.
|
593
|
+
def on_char(evt)
|
594
|
+
ch = evt.get_key_code
|
595
|
+
mflag = evt.modifiers
|
596
|
+
|
597
|
+
case ch
|
598
|
+
when Wx::K_RIGHT : move_cursor_right(evt.shift_down)
|
599
|
+
when Wx::K_LEFT : move_cursor_left(evt.shift_down)
|
600
|
+
when Wx::K_DOWN : move_cursor_down(evt.shift_down)
|
601
|
+
when Wx::K_UP : move_cursor_up(evt.shift_down)
|
602
|
+
when Wx::K_BACK : on_key_back(evt)
|
603
|
+
when Wx::K_DELETE : on_key_delete(evt)
|
604
|
+
when Wx::K_TAB : on_key_tab(evt)
|
605
|
+
when (mflag == Wx::MOD_CMD and ?a) # select all
|
606
|
+
do_select_all
|
607
|
+
when (mflag == Wx::MOD_CMD and ?c) # copy
|
608
|
+
do_clipboard_copy
|
609
|
+
when (mflag == Wx::MOD_CMD and ?x) # cut
|
610
|
+
do_clipboard_cut
|
611
|
+
when (mflag == Wx::MOD_CMD and ?v) # paste
|
612
|
+
do_clipboard_paste
|
613
|
+
when ((mflag == Wx::MOD_NONE or mflag == Wx::MOD_SHIFT) and 0x20..0x7e)
|
614
|
+
if @cursor.area
|
615
|
+
# redirect regular typing to on_char_AREANAME
|
616
|
+
return self.send("on_char_#{AREAS[@cursor.area]}", evt)
|
617
|
+
end
|
618
|
+
else # everything else is for dynamically handling key combo handlers
|
619
|
+
m = []
|
620
|
+
m << 'alt' if (mflag & Wx::MOD_ALT) != 0
|
621
|
+
m << 'cmd' if (mflag & Wx::MOD_CMD) != 0
|
622
|
+
m << 'shift' if (mflag & Wx::MOD_SHIFT) != 0
|
623
|
+
mods = (m.empty?)? "" : "_" + m.join('_')
|
624
|
+
|
625
|
+
ch = evt.get_key_code
|
626
|
+
hex = ch.to_s(16).rjust(2,'0')
|
627
|
+
meth=nil
|
628
|
+
|
629
|
+
if (n=resolve_key_code(ch)) and respond_to?("on_key#{mods}_#{n}")
|
630
|
+
meth="on_key#{mods}_#{n}"
|
631
|
+
elsif respond_to?("on_key#{mods}_0x#{hex}")
|
632
|
+
meth="on_key#{mods}_#{hex}"
|
633
|
+
end
|
634
|
+
|
635
|
+
if meth and ret=self.send(meth, evt)
|
636
|
+
return ret
|
637
|
+
else
|
638
|
+
evt.skip()
|
639
|
+
end
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
# Handles a Backspace keypress
|
644
|
+
def on_key_back(evt)
|
645
|
+
@hexbyte_started=false
|
646
|
+
if not @selection.nil?
|
647
|
+
idx=gui_set_value(nil, '')
|
648
|
+
move_to_idx(idx)
|
649
|
+
elsif (didx=self.cur_pos-1) >= 0
|
650
|
+
gui_set_value(didx, '')
|
651
|
+
move_cursor_left()
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
|
656
|
+
# Handles a DEL keypress
|
657
|
+
def on_key_delete(evt)
|
658
|
+
@hexbyte_started=false
|
659
|
+
if not @selection.nil?
|
660
|
+
idx=gui_set_value(nil, '')
|
661
|
+
move_to_idx(idx)
|
662
|
+
elsif @data[self.cur_pos]
|
663
|
+
gui_set_value(self.cur_pos, '')
|
664
|
+
refresh
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
|
669
|
+
# switches the cursor between hex and ascii area
|
670
|
+
def switch_areas
|
671
|
+
o = @cursor.area
|
672
|
+
@cursor.area = @other_area
|
673
|
+
@other_area = o
|
674
|
+
end
|
675
|
+
|
676
|
+
def set_area_ascii
|
677
|
+
@cursor.area = ASCII_AREA
|
678
|
+
@other_area = HEX_AREA
|
679
|
+
end
|
680
|
+
|
681
|
+
def set_area_hex
|
682
|
+
@cursor.area = HEX_AREA
|
683
|
+
@other_area = ASCII_AREA
|
684
|
+
end
|
685
|
+
|
686
|
+
# Handles editor entry actions made in the ascii section.
|
687
|
+
def on_char_ascii(evt)
|
688
|
+
@hexbyte_started=false
|
689
|
+
ch = evt.get_key_code
|
690
|
+
pos = self.cur_pos
|
691
|
+
|
692
|
+
return if (pos > @data.size)
|
693
|
+
|
694
|
+
if (idx = gui_set_value(pos, ch.chr)) != pos
|
695
|
+
move_to_idx(idx, 1)
|
696
|
+
else
|
697
|
+
move_cursor_right()
|
698
|
+
end
|
699
|
+
@selection=nil
|
700
|
+
end
|
701
|
+
|
702
|
+
|
703
|
+
# Handles editor entry actions made in the hex section.
|
704
|
+
def on_char_hex(evt)
|
705
|
+
ch = evt.get_key_code
|
706
|
+
pos = self.cur_pos
|
707
|
+
|
708
|
+
if self.respond_to?(:on_key_space)
|
709
|
+
return self.send(:on_key_space)
|
710
|
+
elsif (binv=HEX_CHARS.index(ch.chr.downcase)).nil?
|
711
|
+
return
|
712
|
+
elsif @selection and @selection.to_a.size > 1
|
713
|
+
self.cur_pos = pos = gui_set_value(nil, '')
|
714
|
+
end
|
715
|
+
|
716
|
+
if (@hexbyte_started)
|
717
|
+
orig = (@data[pos] || 0)
|
718
|
+
binv = ((orig << 4) + binv) & 0xFF
|
719
|
+
@hexbyte_started=false
|
720
|
+
overwrite=true
|
721
|
+
else
|
722
|
+
@hexbyte_started = true
|
723
|
+
overwrite=false
|
724
|
+
end
|
725
|
+
|
726
|
+
if (idx=gui_set_value(pos, binv.chr, overwrite)) != pos
|
727
|
+
move_to_idx(idx, 1)
|
728
|
+
elsif not @hexbyte_started
|
729
|
+
move_cursor_right()
|
730
|
+
else
|
731
|
+
refresh
|
732
|
+
end
|
733
|
+
end
|
734
|
+
|
735
|
+
|
736
|
+
# Returns data index for [x, y] inside the specified data area
|
737
|
+
def coords_to_idx(x, y, area)
|
738
|
+
if area == HEX_AREA
|
739
|
+
left = @hex0
|
740
|
+
right = @hexN
|
741
|
+
col_w = @hex_width + @asc_width
|
742
|
+
elsif area == ASCII_AREA
|
743
|
+
left = @asc0
|
744
|
+
right = @ascN
|
745
|
+
col_w = @asc_width
|
746
|
+
else
|
747
|
+
return nil
|
748
|
+
end
|
749
|
+
|
750
|
+
xcol = if x < left
|
751
|
+
0
|
752
|
+
elsif x > right
|
753
|
+
@columns-1
|
754
|
+
else
|
755
|
+
((x-left) / col_w)
|
756
|
+
end
|
757
|
+
|
758
|
+
if (row=hit_test(x,y)) != -1
|
759
|
+
return( (hit_test(x,y) * @columns) + xcol )
|
760
|
+
else
|
761
|
+
return @data.size
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
|
766
|
+
# returns the area (display column) for an x window coordinate
|
767
|
+
def area_for_x(x)
|
768
|
+
if (@hex0..@hexN-(@asc_width>>1)).include?(x) then HEX_AREA
|
769
|
+
elsif (@asc0..@ascN).include?(x) then ASCII_AREA
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
|
774
|
+
# Handles a left mouse button click
|
775
|
+
def on_left_button_down(evt)
|
776
|
+
if evt.left_is_down()
|
777
|
+
@hexbyte_started = false
|
778
|
+
set_focus()
|
779
|
+
x=evt.get_x ; y=evt.get_y
|
780
|
+
if ( @dragging or evt.shift_down )
|
781
|
+
if ( idx=coords_to_idx(x,y, @cursor.area) )
|
782
|
+
expand_selection(idx)
|
783
|
+
refresh
|
784
|
+
end
|
785
|
+
elsif area=area_for_x(x)
|
786
|
+
switch_areas() if area != @cursor.area and not AREAS[area].nil?
|
787
|
+
if idx=coords_to_idx(x,y, area)
|
788
|
+
clear_selection()
|
789
|
+
@cursor.area = area
|
790
|
+
@last_pos = self.cur_pos = (idx <= @data.size)? idx : @data.size
|
791
|
+
end
|
792
|
+
refresh
|
793
|
+
end
|
794
|
+
else
|
795
|
+
evt.skip()
|
796
|
+
return
|
797
|
+
end
|
798
|
+
end
|
799
|
+
|
800
|
+
# Handles a left mouse button release
|
801
|
+
def on_left_button_up(evt)
|
802
|
+
if !evt.left_is_down()
|
803
|
+
@dragging = false
|
804
|
+
x=evt.get_x ; y=evt.get_y
|
805
|
+
if @selection.nil? and
|
806
|
+
idx=coords_to_idx(x,y, @cursor.area) and
|
807
|
+
@data[idx]
|
808
|
+
self.cur_pos = idx
|
809
|
+
end
|
810
|
+
else
|
811
|
+
evt.skip()
|
812
|
+
return
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
# Handles mouse motion - skips if left mouse button is not down
|
817
|
+
def on_mouse_motion(evt)
|
818
|
+
if evt.left_is_down()
|
819
|
+
@dragging = true
|
820
|
+
x=evt.get_x ; y=evt.get_y
|
821
|
+
idx=coords_to_idx(x,y, @cursor.area)
|
822
|
+
if idx
|
823
|
+
idx = @data.size unless @data[idx]
|
824
|
+
@last_pos ||= self.cur_pos
|
825
|
+
self.cur_pos = idx
|
826
|
+
expand_selection(idx)
|
827
|
+
refresh
|
828
|
+
end
|
829
|
+
else
|
830
|
+
evt.skip()
|
831
|
+
return
|
832
|
+
end
|
833
|
+
end
|
834
|
+
|
835
|
+
# Handles a 'tab' keypress. Switches between hex/ascii areas
|
836
|
+
def on_key_tab(evt)
|
837
|
+
switch_areas
|
838
|
+
refresh
|
839
|
+
end
|
840
|
+
|
841
|
+
# Selects all data loaded in the hex editor. like doing select all in
|
842
|
+
# a text editor.
|
843
|
+
def do_select_all
|
844
|
+
return nil unless @data.size > 0
|
845
|
+
select_range(0..@data.size-1)
|
846
|
+
refresh
|
847
|
+
end
|
848
|
+
|
849
|
+
# Clipboard notes: Wx::Clipboard is not quite stable yet. Specifically
|
850
|
+
# the copy/pasting of raw binary data with non-ascii bytes is buggy and
|
851
|
+
# inconsistent between platforms.
|
852
|
+
#
|
853
|
+
# This is a clipboard format that will work reliably *within* the app
|
854
|
+
# but does not to/from other apps.
|
855
|
+
#
|
856
|
+
# XXX We can't use Wx::DF_TEXT or other externally compatible formats
|
857
|
+
# until (hopefully) problems are addressed in a future wxruby version.
|
858
|
+
class RawDataObject < Wx::DataObject
|
859
|
+
RAW_FORMAT = Wx::DataFormat.new("raw.format")
|
860
|
+
attr_accessor :raw_data
|
861
|
+
def initialize(data = nil)
|
862
|
+
super()
|
863
|
+
@raw_data = data
|
864
|
+
end
|
865
|
+
|
866
|
+
def get_all_formats(dir); [RAW_FORMAT]; end
|
867
|
+
def set_data(format, buf); @raw_data = buf.dup; end
|
868
|
+
def get_data_here(format); @raw_data; end
|
869
|
+
end
|
870
|
+
|
871
|
+
# Implements the clipboard 'copy' operation
|
872
|
+
def do_clipboard_copy
|
873
|
+
return nil unless sel=@selection and dat=@data[sel]
|
874
|
+
# XXX i feel dirty
|
875
|
+
if Wx::PLATFORM == "WXMAC"
|
876
|
+
IO.popen("pbcopy", "w") {|io| io.write dat}
|
877
|
+
stat = $?.success?
|
878
|
+
else
|
879
|
+
dobj = RawDataObject.new(dat)
|
880
|
+
stat = Wx::Clipboard.open {|clip| clip.place dobj}
|
881
|
+
end
|
882
|
+
return stat
|
883
|
+
end
|
884
|
+
|
885
|
+
# Implements the clipboard 'cut' operation (calls do_clipboard_copy under
|
886
|
+
# the hood -- then deletes the selection if it was successful)
|
887
|
+
def do_clipboard_cut
|
888
|
+
if do_clipboard_copy()
|
889
|
+
pos = @selection.first
|
890
|
+
self.gui_set_value(nil, '')
|
891
|
+
self.cur_pos = pos
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
# Implements the clipboard 'paste' operation
|
896
|
+
def do_clipboard_paste
|
897
|
+
dat = if Wx::PLATFORM == "WXMAC"
|
898
|
+
# XXX i feel dirty
|
899
|
+
`pbpaste`
|
900
|
+
else
|
901
|
+
dobj=RawDataObject.new
|
902
|
+
Wx::Clipboard.open {|clip| clip.fetch dobj}
|
903
|
+
dobj.raw_data
|
904
|
+
end
|
905
|
+
|
906
|
+
self.gui_set_value(self.cur_pos, dat) if dat and dat.size > 0
|
907
|
+
end
|
908
|
+
end
|
909
|
+
end
|
910
|
+
|