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,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
+