ruby-gtkhex 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/COPYING +674 -0
  2. data/lib/gtkhex.rb +1341 -0
  3. metadata +79 -0
@@ -0,0 +1,1341 @@
1
+ =begin
2
+
3
+ = File
4
+ gtkhex.rb
5
+
6
+ = Info
7
+ This file is part of PDF Walker, a graphical PDF file browser
8
+ Copyright (C) 2010 Guillaume Delugré <guillaume@security-labs.org>
9
+ All right reserved.
10
+
11
+ PDF Walker is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
15
+
16
+ PDF Walker is distributed in the hope that it will be useful,
17
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ GNU General Public License for more details.
20
+
21
+ You should have received a copy of the GNU General Public License
22
+ along with PDF Walker. If not, see <http://www.gnu.org/licenses/>.
23
+
24
+ This work has been derived from the GHex project. Thanks to them.
25
+ Original implementation: Jaka Mocnik <jaka@gnu.org>
26
+ =end
27
+
28
+ require 'gtk2'
29
+
30
+ module Gtk
31
+
32
+ class HexEditor < Fixed
33
+ module View
34
+ HEX = 1
35
+ ASCII = 2
36
+ end
37
+
38
+ module Group
39
+ BYTE = 1
40
+ WORD = 2
41
+ LONG = 4
42
+ end
43
+
44
+ class Highlight
45
+ attr_accessor :start, :end
46
+ attr_accessor :start_line, :end_line
47
+ attr_accessor :style
48
+ attr_accessor :min_select
49
+ attr_accessor :valid
50
+ end
51
+
52
+ class AutoHighlight
53
+ attr_accessor :search_view
54
+ attr_accessor :search_string
55
+ attr_accessor :search_len
56
+
57
+ attr_accessor :color
58
+
59
+ attr_accessor :view_min
60
+ attr_accessor :view_max
61
+
62
+ attr_accessor :highlights
63
+ end
64
+
65
+ DEFAULT_FONT = "Monospace 12"
66
+ DEFAULT_CPL = 32
67
+ DEFAULT_LINES = 16
68
+ DISPLAY_BORDER = 4
69
+ SCROLL_TIMEOUT = 100
70
+
71
+ type_register
72
+
73
+ @@primary = Clipboard.get(Gdk::Selection::PRIMARY)
74
+ @@clipboard = Clipboard.get(Gdk::Selection::CLIPBOARD)
75
+
76
+ def initialize(data = '')
77
+ super()
78
+
79
+ @data = data
80
+ if RUBY_VERSION >= '1.9'
81
+ @data.force_encoding('binary')
82
+ end
83
+
84
+ @scroll_timeout = -1
85
+ @disp_buffer = ""
86
+ @starting_offset = 0
87
+
88
+ @xdisp_width = @adisp_width = 200
89
+ @xdisp_gc = @adisp_gc = nil
90
+ @active_view = View::HEX
91
+ @group_type = Group::BYTE
92
+ @lines = @vis_lines = @top_line = @cpl = 0
93
+ @cursor_pos = 0
94
+ @lower_nibble = false
95
+ @cursor_shown = false
96
+ @button = 0
97
+ @insert = false
98
+ @selecting = false
99
+
100
+ @selection = Highlight.new
101
+ @selection.start = @selection.end = 0
102
+ @selection.style = nil
103
+ @selection.min_select = 1
104
+ @selection.valid = false
105
+
106
+ @highlights = [ @selection ]
107
+
108
+ @auto_highlight = nil
109
+
110
+ @disp_font_metrics = load_font DEFAULT_FONT
111
+ @font_desc = Pango::FontDescription.new DEFAULT_FONT
112
+
113
+ @char_width = get_max_char_width(@disp_font_metrics)
114
+ @char_height = Pango.pixels(@disp_font_metrics.ascent) + Pango.pixels(@disp_font_metrics.descent) + 2
115
+
116
+ self.can_focus = true
117
+ self.events = Gdk::Event::KEY_PRESS_MASK
118
+ self.border_width = DISPLAY_BORDER
119
+
120
+ mouse_handler = lambda do |widget, event|
121
+ if event.event_type == Gdk::Event::BUTTON_RELEASE and event.button == 1
122
+ if @scroll_timeout
123
+ GLib::Source.remove @scroll_timeout
124
+ @scroll_timeout = nil
125
+ @scroll_dir = 0
126
+ end
127
+
128
+ @selecting = false
129
+ Gtk.grab_remove(widget)
130
+ @button = 0
131
+ elsif event.event_type == Gdk::Event::BUTTON_PRESS and event.button == 1
132
+ self.grab_focus unless self.has_focus?
133
+
134
+ Gtk.grab_add(widget)
135
+ @button = event.button
136
+
137
+ focus_view = (widget == @xdisp) ? View::HEX : View::ASCII
138
+
139
+ if @active_view == focus_view
140
+ if @active_view == View::HEX
141
+ hex_to_pointer(event.x, event.y)
142
+ else
143
+ ascii_to_pointer(event.x, event.y)
144
+ end
145
+
146
+ unless @selecting
147
+ @selecting = true
148
+ set_selection(@cursor_pos, @cursor_pos)
149
+ end
150
+ else
151
+ hide_cursor
152
+ @active_view = focus_view
153
+ show_cursor
154
+ end
155
+ elsif event.event_type == Gdk::Event::BUTTON_PRESS and event.button == 2
156
+ # TODO
157
+ else
158
+ @button = 0
159
+ end
160
+ end
161
+
162
+ @xdisp = DrawingArea.new
163
+ @xdisp.modify_font @font_desc
164
+ @xlayout = @xdisp.create_pango_layout('')
165
+ @xdisp.events =
166
+ Gdk::Event::EXPOSURE_MASK |
167
+ Gdk::Event::BUTTON_PRESS_MASK |
168
+ Gdk::Event::BUTTON_RELEASE_MASK |
169
+ Gdk::Event::BUTTON_MOTION_MASK |
170
+ Gdk::Event::SCROLL_MASK
171
+
172
+ @xdisp.signal_connect 'realize' do
173
+ @xdisp_gc = Gdk::GC.new(@xdisp.window)
174
+ @xdisp_gc.set_exposures(true)
175
+ end
176
+
177
+ @xdisp.signal_connect 'expose_event' do |xdisp, event|
178
+ imin = (event.area.y / @char_height).to_i
179
+ imax = ((event.area.y + event.area.height) / @char_height).to_i
180
+ imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0
181
+
182
+ imax = [ imax, @vis_lines ].min
183
+
184
+ render_hex_lines(imin, imax)
185
+ end
186
+
187
+ @xdisp.signal_connect 'scroll_event' do |xdisp, event|
188
+ @scrollbar.event(event)
189
+ end
190
+
191
+ @xdisp.signal_connect 'button_press_event' do |xdisp, event|
192
+ mouse_handler[xdisp, event]
193
+ end
194
+
195
+ @xdisp.signal_connect 'button_release_event' do |xdisp, event|
196
+ mouse_handler[xdisp, event]
197
+ end
198
+
199
+ @xdisp.signal_connect 'motion_notify_event' do |xdisp, event|
200
+ w, x, y, m = xdisp.window.pointer
201
+
202
+ if y < 0
203
+ @scroll_dir = -1
204
+ elsif y >= xdisp.allocation.height
205
+ @scroll_dir = 1
206
+ else
207
+ @scroll_dir = 0
208
+ end
209
+
210
+ if @scroll_dir != 0
211
+ if @scroll_timeout == nil
212
+ @scroll_timeout = GLib::Timeout.add(SCROLL_TIMEOUT) {
213
+ if @scroll_dir < 0
214
+ set_cursor([ 0, @cursor_pos - @cpl ].max)
215
+ elsif @scroll_dir > 0
216
+ set_cursor([ @data.size - 1, @cursor_pos + @cpl ].min)
217
+ end
218
+
219
+ true
220
+ }
221
+ next
222
+ end
223
+ else
224
+ if @scroll_timeout != nil
225
+ GLib::Source.remove @scroll_timeout
226
+ @scroll_timeout = nil
227
+ end
228
+ end
229
+
230
+ next if event.window != xdisp.window
231
+
232
+ hex_to_pointer(x,y) if @active_view == View::HEX and @button == 1
233
+ end
234
+
235
+ put @xdisp, 0, 0
236
+ @xdisp.show
237
+
238
+ @adisp = DrawingArea.new
239
+ @adisp.modify_font @font_desc
240
+ @alayout = @adisp.create_pango_layout('')
241
+ @adisp.events =
242
+ Gdk::Event::EXPOSURE_MASK |
243
+ Gdk::Event::BUTTON_PRESS_MASK |
244
+ Gdk::Event::BUTTON_RELEASE_MASK |
245
+ Gdk::Event::BUTTON_MOTION_MASK |
246
+ Gdk::Event::SCROLL_MASK
247
+
248
+
249
+ @adisp.signal_connect 'realize' do
250
+ @adisp_gc = Gdk::GC.new(@adisp.window)
251
+ @adisp_gc.set_exposures(true)
252
+ end
253
+
254
+ @adisp.signal_connect 'expose_event' do |adisp, event|
255
+ imin = (event.area.y / @char_height).to_i
256
+ imax = ((event.area.y + event.area.height) / @char_height).to_i
257
+ imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0
258
+
259
+ imax = [ imax, @vis_lines ].min
260
+ render_ascii_lines(imin, imax)
261
+ end
262
+
263
+ @adisp.signal_connect 'scroll_event' do |adisp, event|
264
+ @scrollbar.event(event)
265
+ end
266
+
267
+ @adisp.signal_connect 'button_press_event' do |adisp, event|
268
+ mouse_handler[adisp, event]
269
+ end
270
+
271
+ @adisp.signal_connect 'button_release_event' do |adisp, event|
272
+ mouse_handler[adisp, event]
273
+ end
274
+
275
+ @adisp.signal_connect 'motion_notify_event' do |adisp, event|
276
+ w, x, y, m = adisp.window.pointer
277
+
278
+ if y < 0
279
+ @scroll_dir = -1
280
+ elsif y >= adisp.allocation.height
281
+ @scroll_dir = 1
282
+ else
283
+ @scroll_dir = 0
284
+ end
285
+
286
+ if @scroll_dir != 0
287
+ if @scroll_timeout == nil
288
+ @scroll_timeout = GLib::Timeout.add(SCROLL_TIMEOUT) {
289
+ if @scroll_dir < 0
290
+ set_cursor([ 0, @cursor_pos - @cpl ].max)
291
+ elsif @scroll_dir > 0
292
+ set_cursor([ @data.size - 1, @cursor_pos + @cpl ].min)
293
+ end
294
+
295
+ true
296
+ }
297
+ next
298
+ end
299
+ else
300
+ if @scroll_timeout != nil
301
+ GLib::Source.remove @scroll_timeout
302
+ @scroll_timeout = nil
303
+ end
304
+ end
305
+
306
+ next if event.window != adisp.window
307
+
308
+ ascii_to_pointer(x,y) if @active_view == View::ASCII and @button == 1
309
+ end
310
+
311
+ put @adisp, 0, 0
312
+ @adisp.show
313
+
314
+ @adj = Gtk::Adjustment.new(0, 0, 0, 0, 0, 0)
315
+ @scrollbar = VScrollbar.new(@adj)
316
+ @adj.signal_connect 'value_changed' do |adj|
317
+ unless @xdisp_gc.nil? or @adisp_gc.nil? or not @xdisp.drawable? or not @adisp.drawable?
318
+
319
+ source_min = (adj.value.to_i - @top_line) * @char_height
320
+ source_max = source_min + @xdisp.allocation.height
321
+ dest_min = 0
322
+ dest_max = @xdisp.allocation.height
323
+
324
+ rect = Gdk::Rectangle.new(0, 0, 0, 0)
325
+ @top_line = adj.value.to_i
326
+ if source_min < 0
327
+ rect.y = 0
328
+ rect.height = -source_min
329
+ rect_height = [ rect.height, @xdisp.allocation.height ].min
330
+ source_min = 0
331
+ dest_min = rect.height
332
+ else
333
+ rect.y = 2 * @xdisp.allocation.height - source_max
334
+ rect.y = 0 if rect.y < 0
335
+ rect.height = @xdisp.allocation.height - rect.y
336
+ source_max = @xdisp.allocation.height
337
+ dest_max = rect.y
338
+ end
339
+
340
+ if source_min != source_max
341
+ @xdisp.window.draw_drawable(
342
+ @xdisp_gc,
343
+ @xdisp.window,
344
+ 0, source_min,
345
+ 0, dest_min,
346
+ @xdisp.allocation.width,
347
+ source_max - source_min
348
+ )
349
+ @adisp.window.draw_drawable(
350
+ @adisp_gc,
351
+ @adisp.window,
352
+ 0, source_min,
353
+ 0, dest_min,
354
+ @adisp.allocation.width,
355
+ source_max - source_min
356
+ )
357
+
358
+ if @offsets
359
+ if @offsets_gc.nil?
360
+ @offsets_gc = Gdk::GC.new(@offsets.window)
361
+ @offsets_gc.set_exposures(true)
362
+ end
363
+
364
+ @offsets.window.draw_drawable(
365
+ @offsets_gc,
366
+ @offsets.window,
367
+ 0, source_min,
368
+ 0, dest_min,
369
+ @offsets.allocation.width,
370
+ source_max - source_min
371
+ )
372
+ end
373
+
374
+ # TODO update_all_auto_highlights(true, true)
375
+ invalidate_all_highlights
376
+
377
+ rect.width = @xdisp.allocation.width
378
+ @xdisp.window.invalidate(rect, false)
379
+ rect.width = @adisp.allocation.width
380
+ @adisp.window.invalidate(rect, false)
381
+
382
+ if @offsets
383
+ rect.width = @offsets.allocation.width
384
+ @offsets.window.invalidate(rect, false)
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ put @scrollbar, 0, 0
391
+ @scrollbar.show
392
+ end
393
+
394
+ def set_selection(s, e)
395
+ e = [ e, @data.size ].min
396
+
397
+ @@primary.clear if @selection.start != @selection.end
398
+
399
+ os, oe = [ @selection.start, @selection.end ].sort
400
+
401
+ @selection.start = [ 0, s ].max
402
+ @selection.start = [ @selection.start, @data.size ].min
403
+ @selection.end = [ e, @data.size ].min
404
+
405
+ invalidate_highlight(@selection)
406
+
407
+ ns, ne = [ @selection.start, @selection.end ].sort
408
+
409
+ if ns != os and ne != oe
410
+ bytes_changed([ns, os].min, [ne, oe].max)
411
+ elsif ne != oe
412
+ bytes_changed(*[ne, oe].sort)
413
+ elsif ns != os
414
+ bytes_changed(*[ns, os].sort)
415
+ end
416
+
417
+ if @selection.start != @selection.end
418
+ if @active_view == View::HEX
419
+ brk_len = 2 * @cpl + @cpl / @group_type
420
+ format_xblock(s,e)
421
+ (@disp_buffer.size / brk_len + 1).times do |i| @disp_buffer.insert(i * (brk_len + 1), $/) end
422
+ else
423
+ brk_len = @cpl
424
+ format_ablock(s,e)
425
+ end
426
+
427
+ @@primary.set_text(@disp_buffer)
428
+ end
429
+ end
430
+
431
+ def get_selection
432
+ [ @selection.start, @selection.end ].sort
433
+ end
434
+
435
+ def clear_selection
436
+ set_selection(0, 0)
437
+ end
438
+
439
+ def cursor
440
+ @cursor_pos
441
+ end
442
+
443
+ def set_cursor(index)
444
+ return if index < 0 or index > @data.size
445
+
446
+ old_pos = @cursor_pos
447
+ index -= 1 if @insert and index == @data.size
448
+ index = [ 0, index ].max
449
+
450
+ hide_cursor
451
+
452
+ @cursor_pos = index
453
+ return if @cpl == 0
454
+
455
+ y = index / @cpl
456
+ if y >= @top_line + @vis_lines
457
+ @adj.value = [ y - @vis_lines + 1, @lines - @vis_lines ].min
458
+ @adj.value = [ 0, @adj.value ].max
459
+ @adj.signal_emit 'value_changed'
460
+ elsif y < @top_line
461
+ @adj.value = y
462
+ @adj.signal_emit 'value_changed'
463
+ end
464
+
465
+ @lower_nibble = false if index == @data.size
466
+
467
+ if @selecting
468
+ set_selection(@selection.start, @cursor_pos)
469
+ bytes_changed(*[@cursor_pos, old_pos].sort)
470
+ else# @selection.start != @selection.end
471
+ s, e = [@selection.start, @selection.end].sort
472
+ @selection.end = @selection.start = @cursor_pos
473
+ bytes_changed(s, e)
474
+ end
475
+
476
+ self.signal_emit 'cursor_moved'
477
+
478
+ bytes_changed(old_pos, old_pos)
479
+ show_cursor
480
+ end
481
+
482
+ def set_cursor_xy(x, y)
483
+ pos = y.to_i * @cpl + x.to_i
484
+ return if y < 0 or y >= @lines or x < 0 or x >= @cpl or pos > @data.size
485
+
486
+ set_cursor(pos)
487
+ end
488
+
489
+ def set_cursor_on_lower_nibble(bool)
490
+ if @selecting
491
+ bytes_changed(@cursor_pos, @cursor_pos)
492
+ @lower_nibble = bool
493
+ elsif @selection.start != @selection.end
494
+ s, e = [ @selection.start, @selection.end ].sort
495
+
496
+ @selection.start = @selection.end = 0
497
+ bytes_changed(s, e)
498
+ @lower_nibble = bool
499
+ else
500
+ hide_cursor
501
+ @lower_nibble = bool
502
+ show_cursor
503
+ end
504
+ end
505
+
506
+ def set_group_type(type)
507
+ hide_cursor
508
+ @group_type = type
509
+ recalc_displays(self.allocation.width, self.allocation.height)
510
+ self.queue_resize
511
+ show_cursor
512
+ end
513
+
514
+ def show_offsets(bool)
515
+ return unless @show_offsets ^ bool
516
+
517
+ @show_offsets = bool
518
+ if bool
519
+ show_offsets_widget
520
+ else
521
+ hide_offsets_widget
522
+ end
523
+ end
524
+
525
+ def set_font(fontname)
526
+ @font_desc = Pango::FontDescription.new(fontname)
527
+ @disp_font_metrics = load_font(fontname)
528
+
529
+ @xdisp.modify_font(@font_desc) if @xdisp
530
+ @adisp.modify_font(@font_desc) if @adisp
531
+ @offsets.modify_font(@font_desc) if @offsets
532
+
533
+ @char_width = get_max_char_width(@disp_font_metrics)
534
+ @char_height = Pango.pixels(@disp_font_metrics.ascent) + Pango.pixels(@disp_font_metrics.descent) + 2
535
+ recalc_displays(self.allocation.width, self.allocation.height)
536
+
537
+ redraw_widget
538
+ end
539
+
540
+ def set_data(data)
541
+ prev_data_size = @data.size
542
+ @data = data.dup
543
+
544
+ recalc_displays(self.allocation.width, self.allocation.height)
545
+
546
+ set_cursor 0
547
+ bytes_changed(0, [ prev_data_size, @data.size ].max)
548
+ redraw_widget
549
+ end
550
+
551
+ def validate_highlight(hl)
552
+ unless hl.valid
553
+ hl.start_line = [ hl.start, hl.end ].min / @cpl - @top_line
554
+ hl.end_line = [ hl.start, hl.end ].max / @cpl - @top_line
555
+ hl.valid = true
556
+ end
557
+ end
558
+
559
+ def invalidate_highlight(hl)
560
+ hl.valid = false
561
+ end
562
+
563
+ def invalidate_all_highlights
564
+ @highlights.each do |hl| invalidate_highlight(hl) end
565
+ end
566
+
567
+ private
568
+
569
+ signal_new(
570
+ 'data_changed',
571
+ GLib::Signal::RUN_FIRST,
572
+ nil,
573
+ nil,
574
+ String
575
+ )
576
+
577
+ signal_new(
578
+ 'cursor_moved',
579
+ GLib::Signal::RUN_FIRST,
580
+ nil,
581
+ nil
582
+ )
583
+
584
+ def signal_do_cursor_moved
585
+ end
586
+
587
+ def signal_do_data_changed(data)
588
+ # TODO
589
+ end
590
+
591
+ def signal_do_realize
592
+ super
593
+
594
+ self.window.set_back_pixmap(nil, true)
595
+ end
596
+
597
+ def signal_do_size_allocate(alloc)
598
+ hide_cursor
599
+
600
+ recalc_displays(alloc.width, alloc.height)
601
+
602
+ self.set_allocation(alloc.x, alloc.y, alloc.width, alloc.height)
603
+ self.window.move_resize(
604
+ alloc.x, alloc.y,
605
+ alloc.width, alloc.height
606
+ ) if self.realized?
607
+
608
+ bw = self.border_width
609
+ xt = widget_get_xt
610
+ yt = widget_get_yt
611
+
612
+ my_alloc = Gtk::Allocation.new(0, 0, 0, 0)
613
+ my_alloc.x = bw + xt
614
+ my_alloc.y = bw + yt
615
+ my_alloc.height = [ alloc.height - 2*bw - 2*yt, 1 ].max
616
+ if @show_offsets
617
+ my_alloc.width = 8 * @char_width
618
+ @offsets.size_allocate(my_alloc)
619
+ @offsets.queue_draw
620
+ my_alloc.x += 2*xt + my_alloc.width
621
+ end
622
+
623
+ my_alloc.width = @xdisp_width
624
+ @xdisp.size_allocate(my_alloc)
625
+
626
+ my_alloc.x = alloc.width - bw - @scrollbar.requisition[0]
627
+ my_alloc.y = bw
628
+ my_alloc.width = @scrollbar.requisition[0]
629
+ my_alloc.height = [ alloc.height - 2*bw, 1 ].max
630
+ @scrollbar.size_allocate(my_alloc)
631
+
632
+ my_alloc.x -= @adisp_width + xt
633
+ my_alloc.y = bw + yt
634
+ my_alloc.width = @adisp_width
635
+ my_alloc.height = [ alloc.height - 2*bw - 2*yt, 1 ].max
636
+ @adisp.size_allocate(my_alloc)
637
+
638
+ show_cursor
639
+ end
640
+
641
+ def signal_do_size_request(req)
642
+ sb_width, sb_height = @scrollbar.size_request
643
+ bw = self.border_width
644
+ xt, yt = widget_get_xt, widget_get_yt
645
+
646
+ width = 4*xt + 2*bw + sb_width +
647
+ @char_width*(DEFAULT_CPL + (DEFAULT_CPL-1)/@group_type)
648
+
649
+ width += 2*xt + 8*@char_width if @show_offsets
650
+
651
+ height = DEFAULT_LINES * @char_height + 2*yt + 2*bw
652
+
653
+ req[0] = width
654
+ req[1] = height
655
+ end
656
+
657
+ def signal_do_expose_event(event)
658
+ draw_shadow(event.area)
659
+ super(event)
660
+
661
+ true
662
+ end
663
+
664
+ def signal_do_key_press_event(event)
665
+ old_cp = @cursor_pos
666
+
667
+ hide_cursor
668
+ @selecting = (event.state & Gdk::Window::SHIFT_MASK) != 0
669
+ ret = true
670
+
671
+ case event.keyval
672
+ when Gdk::Keyval::GDK_KP_Tab, Gdk::Keyval::GDK_Tab
673
+ @active_view = (@active_view == View::HEX) ? View::ASCII : View::HEX
674
+
675
+ when Gdk::Keyval::GDK_Up
676
+ set_cursor(@cursor_pos - @cpl)
677
+
678
+ when Gdk::Keyval::GDK_Down
679
+ set_cursor(@cursor_pos + @cpl)
680
+
681
+ when Gdk::Keyval::GDK_Page_Up
682
+ set_cursor([0, @cursor_pos - @vis_lines * @cpl].max)
683
+
684
+ when Gdk::Keyval::GDK_Page_Down
685
+ set_cursor([@cursor_pos + @vis_lines * @cpl, @data.size].min)
686
+
687
+ when Gdk::Keyval::GDK_Left
688
+ if @active_view == View::HEX
689
+ if @selecting
690
+ set_cursor(@cursor_pos - 1)
691
+ else
692
+ @lower_nibble ^= 1
693
+ set_cursor(@cursor_pos - 1) if @lower_nibble
694
+ end
695
+ else
696
+ set_cursor(@cursor_pos - 1)
697
+ end
698
+
699
+ when Gdk::Keyval::GDK_Right
700
+ if @active_view == View::HEX
701
+ if @selecting
702
+ set_cursor(@cursor_pos + 1)
703
+ else
704
+ @lower_nibble ^= 1
705
+ set_cursor(@cursor_pos + 1) unless @lower_nibble
706
+ end
707
+ else
708
+ set_cursor(@cursor_pos + 1)
709
+ end
710
+
711
+ when Gdk::Keyval::GDK_c, Gdk::Keyval::GDK_C
712
+ if event.state & Gdk::Window::CONTROL_MASK != 0
713
+ s,e = @selection.start, @selection.end + 1
714
+ if @active_view == View::HEX
715
+ brk_len = 2 * @cpl + @cpl / @group_type
716
+ format_xblock(s,e)
717
+ (@disp_buffer.size / brk_len + 1).times do |i| @disp_buffer.insert(i * (brk_len + 1), $/) end
718
+ else
719
+ brk_len = @cpl
720
+ format_ablock(s,e)
721
+ end
722
+
723
+ @@clipboard.set_text(@disp_buffer)
724
+ end
725
+ else
726
+ ret = false
727
+ end
728
+
729
+ show_cursor
730
+
731
+ ret
732
+ end
733
+
734
+ def hex_to_pointer(mx, my)
735
+ cy = @top_line + my.to_i / @char_height
736
+
737
+ cx = x = 0
738
+ while cx < 2 * @cpl
739
+ x += @char_width
740
+
741
+ if x > mx
742
+ set_cursor_xy(cx / 2, cy)
743
+ set_cursor_on_lower_nibble(cx % 2 != 0)
744
+
745
+ cx = 2 * @cpl
746
+ end
747
+
748
+ cx += 1
749
+ x += @char_width if ( cx % (2 * @group_type) == 0 )
750
+ end
751
+ end
752
+
753
+ def ascii_to_pointer(mx, my)
754
+ cx = mx / @char_width
755
+ cy = @top_line + my / @char_height
756
+
757
+ set_cursor_xy(cx, cy)
758
+ end
759
+
760
+ def load_font(fontname)
761
+ desc = Pango::FontDescription.new(fontname)
762
+ context = Gdk::Pango.context
763
+ context.set_language(Gtk.default_language)
764
+
765
+ font = context.load_font(desc)
766
+
767
+ font.metrics(context.language)
768
+ end
769
+
770
+ def draw_shadow(area)
771
+ bw = self.border_width
772
+ x = bw
773
+ xt = widget_get_xt
774
+
775
+ if @show_offsets
776
+ self.style.paint_shadow(
777
+ self.window,
778
+ Gtk::STATE_NORMAL, Gtk::SHADOW_IN,
779
+ nil, self, nil,
780
+ bw, bw,
781
+ 8*@char_width + 2*xt, self.allocation.height - 2*bw
782
+ )
783
+
784
+ x += 8*@char_width + 2*xt
785
+ end
786
+
787
+ self.style.paint_shadow(
788
+ self.window,
789
+ Gtk::STATE_NORMAL, Gtk::SHADOW_IN,
790
+ nil, self, nil,
791
+ x, bw,
792
+ @xdisp_width + 2*xt, self.allocation.height - 2*bw
793
+ )
794
+
795
+ self.style.paint_shadow(
796
+ self.window,
797
+ Gtk::STATE_NORMAL, Gtk::SHADOW_IN,
798
+ nil, self, nil,
799
+ self.allocation.width - bw - @adisp_width - @scrollbar.requisition[0] - 2*xt, bw,
800
+ @adisp_width + 2*xt, self.allocation.height - 2*bw
801
+ )
802
+ end
803
+
804
+ def redraw_widget
805
+ return unless self.realized?
806
+
807
+ self.window.invalidate(nil, false)
808
+ end
809
+
810
+ def widget_get_xt
811
+ self.style.xthickness
812
+ end
813
+
814
+ def widget_get_yt
815
+ self.style.ythickness
816
+ end
817
+
818
+ def recalc_displays(width, height)
819
+ old_cpl = @cpl
820
+
821
+ w, h = @scrollbar.size_request
822
+ @xdisp_width = 1
823
+ @adisp_width = 1
824
+
825
+ total_width = width - 2 * self.border_width - 4 * widget_get_xt - w
826
+ total_width -= 2 * widget_get_xt + 8 * @char_width if @show_offsets
827
+
828
+ total_cpl = total_width / @char_width
829
+ if total_cpl == 0 or total_width < 0
830
+ @cpl = @lines = @vis_lines = 0
831
+ return
832
+ end
833
+
834
+ @cpl = 0
835
+ begin
836
+ break if @cpl % @group_type == 0 and total_cpl < @group_type * 3
837
+
838
+ @cpl += 1
839
+ total_cpl -= 3
840
+
841
+ total_cpl -= 1 if @cpl % @group_type == 0
842
+ end while total_cpl > 0
843
+
844
+ return if @cpl == 0
845
+
846
+ if @data.empty?
847
+ @lines = 1
848
+ else
849
+ @lines = @data.size / @cpl
850
+ @lines += 1 if @data.size % @cpl != 0
851
+ end
852
+
853
+ @vis_lines = (height - 2*self.border_width - 2*widget_get_yt).to_i / @char_height.to_i
854
+ @adisp_width = @cpl * @char_width + 1
855
+ xcpl = @cpl * 2 + (@cpl - 1) / @group_type
856
+ @xdisp_width = xcpl * @char_width + 1
857
+
858
+ @disp_buffer = ''
859
+
860
+ @adj.value = [@top_line * old_cpl / @cpl, @lines - @vis_lines].min
861
+ @adj.value = [ 0, @adj.value ].max
862
+ if @cursor_pos / @cpl < @adj.value or @cursor_pos / @cpl > @adj.value + @vis_lines - 1
863
+ @adj.value = [ @cursor_pos / @cpl, @lines - @vis_lines ].min
864
+ @adj.value = [ 0, @adj.value ].max
865
+ end
866
+
867
+ @adj.lower = 0
868
+ @adj.upper = @lines
869
+ @adj.step_increment = 1
870
+ @adj.page_increment = @vis_lines - 1
871
+ @adj.page_size = @vis_lines
872
+
873
+ @adj.signal_emit 'changed'
874
+ @adj.signal_emit 'value_changed'
875
+ end
876
+
877
+ def get_max_char_width(metrics)
878
+
879
+ layout = self.create_pango_layout('')
880
+ layout.set_font_description(@font_desc)
881
+ char_widths = [ 0 ]
882
+
883
+ (1..100).each do |i|
884
+ logical_rect = Pango::Rectangle.new(0, 0, 0, 0)
885
+ if is_displayable(i.chr)
886
+ layout.set_text(i.chr)
887
+ logical_rect = layout.pixel_extents[1]
888
+ end
889
+ char_widths << logical_rect.width
890
+ end
891
+
892
+ char_widths[48..122].max
893
+ end
894
+
895
+ def show_cursor
896
+ unless @cursor_shown
897
+ if @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized?
898
+ render_xc
899
+ render_ac
900
+ end
901
+
902
+ @cursor_shown = true
903
+ end
904
+ end
905
+
906
+ def hide_cursor
907
+ if @cursor_shown
908
+ if @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized?
909
+ render_byte(@cursor_pos)
910
+ end
911
+
912
+ @cursor_shown = false
913
+ end
914
+ end
915
+
916
+ def show_offsets_widget
917
+ @offsets = DrawingArea.new
918
+ @offsets.modify_font @font_desc
919
+ @olayout = @offsets.create_pango_layout('')
920
+
921
+ @offsets.events = Gdk::Event::EXPOSURE_MASK
922
+ @offsets.signal_connect 'expose_event' do |offsets, event|
923
+ imin = (event.area.y / @char_height).to_i
924
+ imax = ((event.area.y + event.area.height) / @char_height).to_i
925
+ imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0
926
+
927
+ imax = [ imax, @vis_lines ].min
928
+
929
+ render_offsets(imin, imax)
930
+ end
931
+
932
+ put @offsets, 0, 0
933
+ @offsets.show
934
+ end
935
+
936
+ def hide_offsets_widget
937
+ if @offsets
938
+ self.remove(@offsets)
939
+ @offsets = @offsets_gc = nil
940
+ end
941
+ end
942
+
943
+ def is_displayable(c)
944
+ if RUBY_VERSION < '1.9'
945
+ c = c[0]
946
+ else
947
+ c = c.ord
948
+ end
949
+
950
+ c >= 0x20 and c < 0x7f
951
+ end
952
+
953
+ def bytes_changed(s, e)
954
+ start_line = s / @cpl - @top_line
955
+ end_line = e / @cpl - @top_line
956
+
957
+ return if end_line < 0 or start_line > @vis_lines
958
+
959
+ start_line = [ 0, start_line ].max
960
+
961
+ render_hex_lines(start_line, end_line)
962
+ render_ascii_lines(start_line, end_line)
963
+ render_offsets(start_line, end_line) if @show_offsets
964
+ end
965
+
966
+ def render_hex_highlights(cursor_line)
967
+ xcpl = @cpl * 2 + @cpl / @group_type
968
+
969
+ @highlights.each do |hl|
970
+ next if (hl.start - hl.end).abs < hl.min_select
971
+
972
+ validate_highlight(hl)
973
+
974
+ s, e = [ hl.start, hl.end ].sort
975
+ sl, el = hl.start_line, hl.end_line
976
+
977
+ hl.style.attach(@xdisp.window) if hl.style
978
+ state = (@active_view == View::HEX) ? Gtk::STATE_SELECTED : Gtk::STATE_INSENSITIVE
979
+
980
+ if cursor_line == sl
981
+ cursor_off = 2 * (s % @cpl) + (s % @cpl) / @group_type
982
+ if cursor_line == el
983
+ len = 2 * (e % @cpl + 1) + (e % @cpl) / @group_type
984
+ else
985
+ len = xcpl
986
+ end
987
+
988
+ len -= cursor_off
989
+ (hl.style || self.style).paint_flat_box(
990
+ @xdisp.window,
991
+ state, Gtk::SHADOW_NONE,
992
+ nil, @xdisp, '',
993
+ cursor_off * @char_width, cursor_line * @char_height,
994
+ len * @char_width, @char_height
995
+ ) if len > 0
996
+
997
+ elsif cursor_line == el
998
+ cursor_off = 2 * (e % @cpl + 1) + (e % @cpl) / @group_type
999
+ (hl.style || self.style).paint_flat_box(
1000
+ @xdisp.window,
1001
+ state, Gtk::SHADOW_NONE,
1002
+ nil, @xdisp, '',
1003
+ 0, cursor_line * @char_height,
1004
+ cursor_off * @char_width, @char_height
1005
+ ) if cursor_off > 0
1006
+
1007
+ elsif cursor_line > sl and cursor_line < el
1008
+ (hl.style || self.style).paint_flat_box(
1009
+ @xdisp.window,
1010
+ state, Gtk::SHADOW_NONE,
1011
+ nil, @xdisp, '',
1012
+ 0, cursor_line * @char_height,
1013
+ xcpl * @char_width, @char_height
1014
+ )
1015
+ end
1016
+
1017
+ hl.style.attach(@adisp.window) if hl.style
1018
+ end
1019
+ end
1020
+
1021
+ def render_hex_lines(imin, imax)
1022
+ return unless self.realized? and @cpl != 0
1023
+
1024
+ cursor_line = @cursor_pos / @cpl - @top_line
1025
+
1026
+ @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL))
1027
+ @xdisp.window.draw_rectangle(
1028
+ @xdisp_gc,
1029
+ true,
1030
+ 0,
1031
+ imin * @char_height,
1032
+ @xdisp.allocation.width,
1033
+ (imax - imin + 1) * @char_height
1034
+ )
1035
+
1036
+ imax = [ imax, @vis_lines, @lines ].min
1037
+
1038
+ @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL))
1039
+
1040
+ frm_len = format_xblock((@top_line+imin) * @cpl, [(@top_line+imax+1) * @cpl, @data.size].min)
1041
+
1042
+ tmp = nil
1043
+ xcpl = @cpl*2 + @cpl/@group_type
1044
+ (imin..imax).each do |i|
1045
+ return unless (tmp = frm_len - ((i - imin) * xcpl)) > 0
1046
+
1047
+ render_hex_highlights(i)
1048
+ text = @disp_buffer[(i-imin) * xcpl, [xcpl, tmp].min]
1049
+ @xlayout.set_text(text)
1050
+ @xdisp.window.draw_layout(@xdisp_gc, 0, i * @char_height, @xlayout)
1051
+ end
1052
+
1053
+ render_xc if cursor_line >= imin and cursor_line <= imax and @cursor_shown
1054
+ end
1055
+
1056
+ def render_ascii_highlights(cursor_line)
1057
+ @highlights.each do |hl|
1058
+ next if (hl.start - hl.end).abs < hl.min_select
1059
+
1060
+ validate_highlight(hl)
1061
+
1062
+ s, e = [ hl.start, hl.end ].sort
1063
+ sl, el = hl.start_line, hl.end_line
1064
+
1065
+ hl.style.attach(@adisp.window) if hl.style
1066
+ state = (@active_view == View::ASCII) ? Gtk::STATE_SELECTED : Gtk::STATE_INSENSITIVE
1067
+
1068
+ if cursor_line == sl
1069
+ cursor_off = s % @cpl
1070
+ len =
1071
+ if cursor_line == el
1072
+ e - s + 1
1073
+ else
1074
+ @cpl - cursor_off
1075
+ end
1076
+
1077
+ (hl.style || self.style).paint_flat_box(
1078
+ @adisp.window,
1079
+ state, Gtk::SHADOW_NONE,
1080
+ nil, @adisp, '',
1081
+ cursor_off * @char_width, cursor_line * @char_height,
1082
+ len * @char_width, @char_height
1083
+ ) if len > 0
1084
+
1085
+ elsif cursor_line == el
1086
+ cursor_off = e % @cpl + 1
1087
+ (hl.style || self.style).paint_flat_box(
1088
+ @adisp.window,
1089
+ state, Gtk::SHADOW_NONE,
1090
+ nil, @adisp, '',
1091
+ 0, cursor_line * @char_height,
1092
+ cursor_off * @char_width, @char_height
1093
+ ) if cursor_off > 0
1094
+
1095
+ elsif cursor_line > sl and cursor_line < el
1096
+ (hl.style || self.style).paint_flat_box(
1097
+ @adisp.window,
1098
+ state, Gtk::SHADOW_NONE,
1099
+ nil, @adisp, '',
1100
+ 0, cursor_line * @char_height,
1101
+ @cpl * @char_width, @char_height
1102
+ )
1103
+ end
1104
+
1105
+ hl.style.attach(@adisp.window) if hl.style
1106
+ end
1107
+ end
1108
+
1109
+ def render_ascii_lines(imin, imax)
1110
+ return unless self.realized? and @cpl != 0
1111
+
1112
+ cursor_line = @cursor_pos / @cpl - @top_line
1113
+
1114
+ @adisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL))
1115
+ @adisp.window.draw_rectangle(
1116
+ @adisp_gc,
1117
+ true,
1118
+ 0,
1119
+ imin * @char_height,
1120
+ @adisp.allocation.width,
1121
+ (imax - imin + 1) * @char_height
1122
+ )
1123
+
1124
+ imax = [ imax, @vis_lines, @lines ].min
1125
+
1126
+ @adisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL))
1127
+
1128
+ frm_len = format_ablock((@top_line+imin) * @cpl, [(@top_line+imax+1) * @cpl, @data.size].min)
1129
+
1130
+ tmp = nil
1131
+ (imin..imax).each do |i|
1132
+ return unless (tmp = frm_len - ((i - imin) * @cpl)) > 0
1133
+
1134
+ render_ascii_highlights(i)
1135
+ text = @disp_buffer[(i-imin) * @cpl, [@cpl, tmp].min]
1136
+ @alayout.set_text(text)
1137
+ @adisp.window.draw_layout(@adisp_gc, 0, i * @char_height, @alayout)
1138
+ end
1139
+
1140
+ render_ac if cursor_line >= imin and cursor_line <= imax and @cursor_shown
1141
+ end
1142
+
1143
+ def render_offsets(imin, imax)
1144
+ return unless self.realized?
1145
+
1146
+ unless @offsets_gc
1147
+ @offsets_gc = Gdk::GC.new(@offsets.window)
1148
+ @offsets_gc.set_exposures(true)
1149
+ end
1150
+
1151
+ @offsets_gc.set_foreground(self.style.base(Gtk::STATE_INSENSITIVE))
1152
+ @offsets.window.draw_rectangle(
1153
+ @offsets_gc,
1154
+ true,
1155
+ 0, imin * @char_height,
1156
+ @offsets.allocation.width, (imax - imin + 1) * @char_height
1157
+ )
1158
+
1159
+ imax = [ imax, @vis_lines, @lines - @top_line - 1 ].min
1160
+ @offsets_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL))
1161
+
1162
+ (imin..imax).each do |i|
1163
+ text = "%08x" % ((@top_line + i) * @cpl + @starting_offset)
1164
+ @olayout.set_text(text)
1165
+ @offsets.window.draw_layout(@offsets_gc, 0, i * @char_height, @olayout)
1166
+ end
1167
+ end
1168
+
1169
+ def render_byte(pos)
1170
+ return unless @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized?
1171
+
1172
+ return unless (coords = get_xcoords(pos))
1173
+ cx, cy = coords
1174
+ c = format_xbyte(pos)
1175
+
1176
+ @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL))
1177
+ @xdisp.window.draw_rectangle(
1178
+ @xdisp_gc,
1179
+ true,
1180
+ cx, cy,
1181
+ 2 * @char_width, @char_height
1182
+ )
1183
+
1184
+ if pos < @data.size
1185
+ @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL))
1186
+ @xlayout.set_text(c)
1187
+ @xdisp.window.draw_layout(@xdisp_gc, cx, cy, @xlayout)
1188
+ end
1189
+
1190
+ return unless (coords = get_acoords(pos))
1191
+ cx, cy = coords
1192
+
1193
+ @adisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL))
1194
+ @adisp.window.draw_rectangle(
1195
+ @adisp_gc,
1196
+ true,
1197
+ cx, cy,
1198
+ @char_width, @char_height
1199
+ )
1200
+
1201
+ if pos < @data.size
1202
+ @adisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL))
1203
+ c = get_byte(pos)
1204
+ c = '.' unless is_displayable(c)
1205
+
1206
+ @alayout.set_text(c)
1207
+ @adisp.window.draw_layout(@adisp_gc, cx, cy, @alayout)
1208
+ end
1209
+ end
1210
+
1211
+ def render_xc
1212
+ return unless @xdisp.realized?
1213
+
1214
+ if coords = get_xcoords(@cursor_pos)
1215
+ cx, cy = coords
1216
+
1217
+ c = format_xbyte(@cursor_pos)
1218
+ if @lower_nibble
1219
+ cx += @char_width
1220
+ c = c[1,1]
1221
+ else
1222
+ c = c[0,1]
1223
+ end
1224
+
1225
+ @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_ACTIVE))
1226
+ @xdisp.window.draw_rectangle(
1227
+ @xdisp_gc,
1228
+ (@active_view == View::HEX),
1229
+ cx, cy,
1230
+ @char_width,
1231
+ @char_height - 1
1232
+ )
1233
+ @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_ACTIVE))
1234
+ @xlayout.set_text(c)
1235
+ @xdisp.window.draw_layout(@xdisp_gc, cx, cy, @xlayout)
1236
+ end
1237
+ end
1238
+
1239
+ def render_ac
1240
+ return unless @adisp.realized?
1241
+
1242
+ if coords = get_acoords(@cursor_pos)
1243
+ cx, cy = coords
1244
+
1245
+ c = get_byte(@cursor_pos)
1246
+ c = '.' unless is_displayable(c)
1247
+
1248
+ @adisp_gc.set_foreground(self.style.base(Gtk::STATE_ACTIVE))
1249
+ @adisp.window.draw_rectangle(
1250
+ @adisp_gc,
1251
+ (@active_view == View::ASCII),
1252
+ cx, cy,
1253
+ @char_width,
1254
+ @char_height - 1
1255
+ )
1256
+ @adisp_gc.set_foreground(self.style.text(Gtk::STATE_ACTIVE))
1257
+ @alayout.set_text(c)
1258
+ @adisp.window.draw_layout(@adisp_gc, cx, cy, @alayout)
1259
+ end
1260
+ end
1261
+
1262
+ def get_xcoords(pos)
1263
+ return nil if @cpl == 0
1264
+
1265
+ cy = pos / @cpl - @top_line
1266
+ return nil if cy < 0
1267
+
1268
+ cx = 2 * (pos % @cpl)
1269
+ spaces = (pos % @cpl) / @group_type
1270
+
1271
+ cx *= @char_width
1272
+ cy *= @char_height
1273
+ spaces *= @char_width
1274
+
1275
+ [cx + spaces, cy]
1276
+ end
1277
+
1278
+ def get_acoords(pos)
1279
+ return nil if @cpl == 0
1280
+
1281
+ cy = pos / @cpl - @top_line
1282
+ return nil if cy < 0
1283
+
1284
+ cy *= @char_height
1285
+ cx = @char_width * (pos % @cpl)
1286
+
1287
+ [cx, cy]
1288
+ end
1289
+
1290
+ def format_xblock(s, e)
1291
+ @disp_buffer = ''
1292
+
1293
+ (s+1..e).each do |i|
1294
+ @disp_buffer << get_byte(i - 1).unpack('H2')[0]
1295
+ @disp_buffer << ' ' if i % @group_type == 0
1296
+ end
1297
+
1298
+ @disp_buffer.size
1299
+ end
1300
+
1301
+ def format_ablock(s, e)
1302
+ @disp_buffer = ''
1303
+
1304
+ (s..e-1).each do |i|
1305
+ c = get_byte(i)
1306
+ c = '.' unless is_displayable(c)
1307
+ @disp_buffer << c
1308
+ end
1309
+
1310
+ @disp_buffer.size
1311
+ end
1312
+
1313
+ def get_byte(offset)
1314
+ if offset >= 0 and offset < @data.size
1315
+ @data[offset, 1]
1316
+ else
1317
+ 0.chr
1318
+ end
1319
+ end
1320
+
1321
+ def format_xbyte(pos)
1322
+ get_byte(pos).unpack('H2')[0]
1323
+ end
1324
+ end
1325
+
1326
+ end
1327
+
1328
+ __END__
1329
+ hexedit = Gtk::HexEditor.new(File.read '/bin/cat')
1330
+ hexedit.show_offsets(true)
1331
+ hexedit.set_cursor 2
1332
+ hexedit.set_cursor_on_lower_nibble true
1333
+ hexedit.set_font 'Terminus 12'
1334
+ hexedit.set_group_type Gtk::HexEditor::Group::LONG
1335
+
1336
+ window = Gtk::Window.new
1337
+ window.add(hexedit)
1338
+
1339
+ window.show_all
1340
+
1341
+ Gtk.main