emonti-hexwrench 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+