ruby-gtkhex 0.1.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.
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