=begin = File gtkhex.rb = Info This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2010 Guillaume Delugré All right reserved. PDF Walker is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PDF Walker is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with PDF Walker. If not, see . This work has been derived from the GHex project. Thanks to them. Original implementation: Jaka Mocnik =end require 'gtk2' module Gtk class HexEditor < Fixed module View HEX = 1 ASCII = 2 end module Group BYTE = 1 WORD = 2 LONG = 4 end class Highlight attr_accessor :start, :end attr_accessor :start_line, :end_line attr_accessor :style attr_accessor :min_select attr_accessor :valid end class AutoHighlight attr_accessor :search_view attr_accessor :search_string attr_accessor :search_len attr_accessor :color attr_accessor :view_min attr_accessor :view_max attr_accessor :highlights end DEFAULT_FONT = "Monospace 12" DEFAULT_CPL = 32 DEFAULT_LINES = 16 DISPLAY_BORDER = 4 SCROLL_TIMEOUT = 100 type_register @@primary = Clipboard.get(Gdk::Selection::PRIMARY) @@clipboard = Clipboard.get(Gdk::Selection::CLIPBOARD) def initialize(data = '') super() @data = data if RUBY_VERSION >= '1.9' @data.force_encoding('binary') end @scroll_timeout = -1 @disp_buffer = "" @starting_offset = 0 @xdisp_width = @adisp_width = 200 @xdisp_gc = @adisp_gc = nil @active_view = View::HEX @group_type = Group::BYTE @lines = @vis_lines = @top_line = @cpl = 0 @cursor_pos = 0 @lower_nibble = false @cursor_shown = false @button = 0 @insert = false @selecting = false @selection = Highlight.new @selection.start = @selection.end = 0 @selection.style = nil @selection.min_select = 1 @selection.valid = false @highlights = [ @selection ] @auto_highlight = nil @disp_font_metrics = load_font DEFAULT_FONT @font_desc = Pango::FontDescription.new DEFAULT_FONT @char_width = get_max_char_width(@disp_font_metrics) @char_height = Pango.pixels(@disp_font_metrics.ascent) + Pango.pixels(@disp_font_metrics.descent) + 2 self.can_focus = true self.events = Gdk::Event::KEY_PRESS_MASK self.border_width = DISPLAY_BORDER mouse_handler = lambda do |widget, event| if event.event_type == Gdk::Event::BUTTON_RELEASE and event.button == 1 if @scroll_timeout GLib::Source.remove @scroll_timeout @scroll_timeout = nil @scroll_dir = 0 end @selecting = false Gtk.grab_remove(widget) @button = 0 elsif event.event_type == Gdk::Event::BUTTON_PRESS and event.button == 1 self.grab_focus unless self.has_focus? Gtk.grab_add(widget) @button = event.button focus_view = (widget == @xdisp) ? View::HEX : View::ASCII if @active_view == focus_view if @active_view == View::HEX hex_to_pointer(event.x, event.y) else ascii_to_pointer(event.x, event.y) end unless @selecting @selecting = true set_selection(@cursor_pos, @cursor_pos) end else hide_cursor @active_view = focus_view show_cursor end elsif event.event_type == Gdk::Event::BUTTON_PRESS and event.button == 2 # TODO else @button = 0 end end @xdisp = DrawingArea.new @xdisp.modify_font @font_desc @xlayout = @xdisp.create_pango_layout('') @xdisp.events = Gdk::Event::EXPOSURE_MASK | Gdk::Event::BUTTON_PRESS_MASK | Gdk::Event::BUTTON_RELEASE_MASK | Gdk::Event::BUTTON_MOTION_MASK | Gdk::Event::SCROLL_MASK @xdisp.signal_connect 'realize' do @xdisp_gc = Gdk::GC.new(@xdisp.window) @xdisp_gc.set_exposures(true) end @xdisp.signal_connect 'expose_event' do |xdisp, event| imin = (event.area.y / @char_height).to_i imax = ((event.area.y + event.area.height) / @char_height).to_i imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0 imax = [ imax, @vis_lines ].min render_hex_lines(imin, imax) end @xdisp.signal_connect 'scroll_event' do |xdisp, event| @scrollbar.event(event) end @xdisp.signal_connect 'button_press_event' do |xdisp, event| mouse_handler[xdisp, event] end @xdisp.signal_connect 'button_release_event' do |xdisp, event| mouse_handler[xdisp, event] end @xdisp.signal_connect 'motion_notify_event' do |xdisp, event| w, x, y, m = xdisp.window.pointer if y < 0 @scroll_dir = -1 elsif y >= xdisp.allocation.height @scroll_dir = 1 else @scroll_dir = 0 end if @scroll_dir != 0 if @scroll_timeout == nil @scroll_timeout = GLib::Timeout.add(SCROLL_TIMEOUT) { if @scroll_dir < 0 set_cursor([ 0, @cursor_pos - @cpl ].max) elsif @scroll_dir > 0 set_cursor([ @data.size - 1, @cursor_pos + @cpl ].min) end true } next end else if @scroll_timeout != nil GLib::Source.remove @scroll_timeout @scroll_timeout = nil end end next if event.window != xdisp.window hex_to_pointer(x,y) if @active_view == View::HEX and @button == 1 end put @xdisp, 0, 0 @xdisp.show @adisp = DrawingArea.new @adisp.modify_font @font_desc @alayout = @adisp.create_pango_layout('') @adisp.events = Gdk::Event::EXPOSURE_MASK | Gdk::Event::BUTTON_PRESS_MASK | Gdk::Event::BUTTON_RELEASE_MASK | Gdk::Event::BUTTON_MOTION_MASK | Gdk::Event::SCROLL_MASK @adisp.signal_connect 'realize' do @adisp_gc = Gdk::GC.new(@adisp.window) @adisp_gc.set_exposures(true) end @adisp.signal_connect 'expose_event' do |adisp, event| imin = (event.area.y / @char_height).to_i imax = ((event.area.y + event.area.height) / @char_height).to_i imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0 imax = [ imax, @vis_lines ].min render_ascii_lines(imin, imax) end @adisp.signal_connect 'scroll_event' do |adisp, event| @scrollbar.event(event) end @adisp.signal_connect 'button_press_event' do |adisp, event| mouse_handler[adisp, event] end @adisp.signal_connect 'button_release_event' do |adisp, event| mouse_handler[adisp, event] end @adisp.signal_connect 'motion_notify_event' do |adisp, event| w, x, y, m = adisp.window.pointer if y < 0 @scroll_dir = -1 elsif y >= adisp.allocation.height @scroll_dir = 1 else @scroll_dir = 0 end if @scroll_dir != 0 if @scroll_timeout == nil @scroll_timeout = GLib::Timeout.add(SCROLL_TIMEOUT) { if @scroll_dir < 0 set_cursor([ 0, @cursor_pos - @cpl ].max) elsif @scroll_dir > 0 set_cursor([ @data.size - 1, @cursor_pos + @cpl ].min) end true } next end else if @scroll_timeout != nil GLib::Source.remove @scroll_timeout @scroll_timeout = nil end end next if event.window != adisp.window ascii_to_pointer(x,y) if @active_view == View::ASCII and @button == 1 end put @adisp, 0, 0 @adisp.show @adj = Gtk::Adjustment.new(0, 0, 0, 0, 0, 0) @scrollbar = VScrollbar.new(@adj) @adj.signal_connect 'value_changed' do |adj| unless @xdisp_gc.nil? or @adisp_gc.nil? or not @xdisp.drawable? or not @adisp.drawable? source_min = (adj.value.to_i - @top_line) * @char_height source_max = source_min + @xdisp.allocation.height dest_min = 0 dest_max = @xdisp.allocation.height rect = Gdk::Rectangle.new(0, 0, 0, 0) @top_line = adj.value.to_i if source_min < 0 rect.y = 0 rect.height = -source_min rect_height = [ rect.height, @xdisp.allocation.height ].min source_min = 0 dest_min = rect.height else rect.y = 2 * @xdisp.allocation.height - source_max rect.y = 0 if rect.y < 0 rect.height = @xdisp.allocation.height - rect.y source_max = @xdisp.allocation.height dest_max = rect.y end if source_min != source_max @xdisp.window.draw_drawable( @xdisp_gc, @xdisp.window, 0, source_min, 0, dest_min, @xdisp.allocation.width, source_max - source_min ) @adisp.window.draw_drawable( @adisp_gc, @adisp.window, 0, source_min, 0, dest_min, @adisp.allocation.width, source_max - source_min ) if @offsets if @offsets_gc.nil? @offsets_gc = Gdk::GC.new(@offsets.window) @offsets_gc.set_exposures(true) end @offsets.window.draw_drawable( @offsets_gc, @offsets.window, 0, source_min, 0, dest_min, @offsets.allocation.width, source_max - source_min ) end # TODO update_all_auto_highlights(true, true) invalidate_all_highlights rect.width = @xdisp.allocation.width @xdisp.window.invalidate(rect, false) rect.width = @adisp.allocation.width @adisp.window.invalidate(rect, false) if @offsets rect.width = @offsets.allocation.width @offsets.window.invalidate(rect, false) end end end end put @scrollbar, 0, 0 @scrollbar.show end def set_selection(s, e) e = [ e, @data.size ].min @@primary.clear if @selection.start != @selection.end os, oe = [ @selection.start, @selection.end ].sort @selection.start = [ 0, s ].max @selection.start = [ @selection.start, @data.size ].min @selection.end = [ e, @data.size ].min invalidate_highlight(@selection) ns, ne = [ @selection.start, @selection.end ].sort if ns != os and ne != oe bytes_changed([ns, os].min, [ne, oe].max) elsif ne != oe bytes_changed(*[ne, oe].sort) elsif ns != os bytes_changed(*[ns, os].sort) end if @selection.start != @selection.end if @active_view == View::HEX brk_len = 2 * @cpl + @cpl / @group_type format_xblock(s,e) (@disp_buffer.size / brk_len + 1).times do |i| @disp_buffer.insert(i * (brk_len + 1), $/) end else brk_len = @cpl format_ablock(s,e) end @@primary.set_text(@disp_buffer) end end def get_selection [ @selection.start, @selection.end ].sort end def clear_selection set_selection(0, 0) end def cursor @cursor_pos end def set_cursor(index) return if index < 0 or index > @data.size old_pos = @cursor_pos index -= 1 if @insert and index == @data.size index = [ 0, index ].max hide_cursor @cursor_pos = index return if @cpl == 0 y = index / @cpl if y >= @top_line + @vis_lines @adj.value = [ y - @vis_lines + 1, @lines - @vis_lines ].min @adj.value = [ 0, @adj.value ].max @adj.signal_emit 'value_changed' elsif y < @top_line @adj.value = y @adj.signal_emit 'value_changed' end @lower_nibble = false if index == @data.size if @selecting set_selection(@selection.start, @cursor_pos) bytes_changed(*[@cursor_pos, old_pos].sort) else# @selection.start != @selection.end s, e = [@selection.start, @selection.end].sort @selection.end = @selection.start = @cursor_pos bytes_changed(s, e) end self.signal_emit 'cursor_moved' bytes_changed(old_pos, old_pos) show_cursor end def set_cursor_xy(x, y) pos = y.to_i * @cpl + x.to_i return if y < 0 or y >= @lines or x < 0 or x >= @cpl or pos > @data.size set_cursor(pos) end def set_cursor_on_lower_nibble(bool) if @selecting bytes_changed(@cursor_pos, @cursor_pos) @lower_nibble = bool elsif @selection.start != @selection.end s, e = [ @selection.start, @selection.end ].sort @selection.start = @selection.end = 0 bytes_changed(s, e) @lower_nibble = bool else hide_cursor @lower_nibble = bool show_cursor end end def set_group_type(type) hide_cursor @group_type = type recalc_displays(self.allocation.width, self.allocation.height) self.queue_resize show_cursor end def show_offsets(bool) return unless @show_offsets ^ bool @show_offsets = bool if bool show_offsets_widget else hide_offsets_widget end end def set_font(fontname) @font_desc = Pango::FontDescription.new(fontname) @disp_font_metrics = load_font(fontname) @xdisp.modify_font(@font_desc) if @xdisp @adisp.modify_font(@font_desc) if @adisp @offsets.modify_font(@font_desc) if @offsets @char_width = get_max_char_width(@disp_font_metrics) @char_height = Pango.pixels(@disp_font_metrics.ascent) + Pango.pixels(@disp_font_metrics.descent) + 2 recalc_displays(self.allocation.width, self.allocation.height) redraw_widget end def set_data(data) prev_data_size = @data.size @data = data.dup recalc_displays(self.allocation.width, self.allocation.height) set_cursor 0 bytes_changed(0, [ prev_data_size, @data.size ].max) redraw_widget end def validate_highlight(hl) unless hl.valid hl.start_line = [ hl.start, hl.end ].min / @cpl - @top_line hl.end_line = [ hl.start, hl.end ].max / @cpl - @top_line hl.valid = true end end def invalidate_highlight(hl) hl.valid = false end def invalidate_all_highlights @highlights.each do |hl| invalidate_highlight(hl) end end private signal_new( 'data_changed', GLib::Signal::RUN_FIRST, nil, nil, String ) signal_new( 'cursor_moved', GLib::Signal::RUN_FIRST, nil, nil ) def signal_do_cursor_moved end def signal_do_data_changed(data) # TODO end def signal_do_realize super self.window.set_back_pixmap(nil, true) end def signal_do_size_allocate(alloc) hide_cursor recalc_displays(alloc.width, alloc.height) self.set_allocation(alloc.x, alloc.y, alloc.width, alloc.height) self.window.move_resize( alloc.x, alloc.y, alloc.width, alloc.height ) if self.realized? bw = self.border_width xt = widget_get_xt yt = widget_get_yt my_alloc = Gtk::Allocation.new(0, 0, 0, 0) my_alloc.x = bw + xt my_alloc.y = bw + yt my_alloc.height = [ alloc.height - 2*bw - 2*yt, 1 ].max if @show_offsets my_alloc.width = 8 * @char_width @offsets.size_allocate(my_alloc) @offsets.queue_draw my_alloc.x += 2*xt + my_alloc.width end my_alloc.width = @xdisp_width @xdisp.size_allocate(my_alloc) my_alloc.x = alloc.width - bw - @scrollbar.requisition[0] my_alloc.y = bw my_alloc.width = @scrollbar.requisition[0] my_alloc.height = [ alloc.height - 2*bw, 1 ].max @scrollbar.size_allocate(my_alloc) my_alloc.x -= @adisp_width + xt my_alloc.y = bw + yt my_alloc.width = @adisp_width my_alloc.height = [ alloc.height - 2*bw - 2*yt, 1 ].max @adisp.size_allocate(my_alloc) show_cursor end def signal_do_size_request(req) sb_width, sb_height = @scrollbar.size_request bw = self.border_width xt, yt = widget_get_xt, widget_get_yt width = 4*xt + 2*bw + sb_width + @char_width*(DEFAULT_CPL + (DEFAULT_CPL-1)/@group_type) width += 2*xt + 8*@char_width if @show_offsets height = DEFAULT_LINES * @char_height + 2*yt + 2*bw req[0] = width req[1] = height end def signal_do_expose_event(event) draw_shadow(event.area) super(event) true end def signal_do_key_press_event(event) old_cp = @cursor_pos hide_cursor @selecting = (event.state & Gdk::Window::SHIFT_MASK) != 0 ret = true case event.keyval when Gdk::Keyval::GDK_KP_Tab, Gdk::Keyval::GDK_Tab @active_view = (@active_view == View::HEX) ? View::ASCII : View::HEX when Gdk::Keyval::GDK_Up set_cursor(@cursor_pos - @cpl) when Gdk::Keyval::GDK_Down set_cursor(@cursor_pos + @cpl) when Gdk::Keyval::GDK_Page_Up set_cursor([0, @cursor_pos - @vis_lines * @cpl].max) when Gdk::Keyval::GDK_Page_Down set_cursor([@cursor_pos + @vis_lines * @cpl, @data.size].min) when Gdk::Keyval::GDK_Left if @active_view == View::HEX if @selecting set_cursor(@cursor_pos - 1) else @lower_nibble ^= 1 set_cursor(@cursor_pos - 1) if @lower_nibble end else set_cursor(@cursor_pos - 1) end when Gdk::Keyval::GDK_Right if @active_view == View::HEX if @selecting set_cursor(@cursor_pos + 1) else @lower_nibble ^= 1 set_cursor(@cursor_pos + 1) unless @lower_nibble end else set_cursor(@cursor_pos + 1) end when Gdk::Keyval::GDK_c, Gdk::Keyval::GDK_C if event.state & Gdk::Window::CONTROL_MASK != 0 s,e = @selection.start, @selection.end + 1 if @active_view == View::HEX brk_len = 2 * @cpl + @cpl / @group_type format_xblock(s,e) (@disp_buffer.size / brk_len + 1).times do |i| @disp_buffer.insert(i * (brk_len + 1), $/) end else brk_len = @cpl format_ablock(s,e) end @@clipboard.set_text(@disp_buffer) end else ret = false end show_cursor ret end def hex_to_pointer(mx, my) cy = @top_line + my.to_i / @char_height cx = x = 0 while cx < 2 * @cpl x += @char_width if x > mx set_cursor_xy(cx / 2, cy) set_cursor_on_lower_nibble(cx % 2 != 0) cx = 2 * @cpl end cx += 1 x += @char_width if ( cx % (2 * @group_type) == 0 ) end end def ascii_to_pointer(mx, my) cx = mx / @char_width cy = @top_line + my / @char_height set_cursor_xy(cx, cy) end def load_font(fontname) desc = Pango::FontDescription.new(fontname) context = Gdk::Pango.context context.set_language(Gtk.default_language) font = context.load_font(desc) font.metrics(context.language) end def draw_shadow(area) bw = self.border_width x = bw xt = widget_get_xt if @show_offsets self.style.paint_shadow( self.window, Gtk::STATE_NORMAL, Gtk::SHADOW_IN, nil, self, nil, bw, bw, 8*@char_width + 2*xt, self.allocation.height - 2*bw ) x += 8*@char_width + 2*xt end self.style.paint_shadow( self.window, Gtk::STATE_NORMAL, Gtk::SHADOW_IN, nil, self, nil, x, bw, @xdisp_width + 2*xt, self.allocation.height - 2*bw ) self.style.paint_shadow( self.window, Gtk::STATE_NORMAL, Gtk::SHADOW_IN, nil, self, nil, self.allocation.width - bw - @adisp_width - @scrollbar.requisition[0] - 2*xt, bw, @adisp_width + 2*xt, self.allocation.height - 2*bw ) end def redraw_widget return unless self.realized? self.window.invalidate(nil, false) end def widget_get_xt self.style.xthickness end def widget_get_yt self.style.ythickness end def recalc_displays(width, height) old_cpl = @cpl w, h = @scrollbar.size_request @xdisp_width = 1 @adisp_width = 1 total_width = width - 2 * self.border_width - 4 * widget_get_xt - w total_width -= 2 * widget_get_xt + 8 * @char_width if @show_offsets total_cpl = total_width / @char_width if total_cpl == 0 or total_width < 0 @cpl = @lines = @vis_lines = 0 return end @cpl = 0 begin break if @cpl % @group_type == 0 and total_cpl < @group_type * 3 @cpl += 1 total_cpl -= 3 total_cpl -= 1 if @cpl % @group_type == 0 end while total_cpl > 0 return if @cpl == 0 if @data.empty? @lines = 1 else @lines = @data.size / @cpl @lines += 1 if @data.size % @cpl != 0 end @vis_lines = (height - 2*self.border_width - 2*widget_get_yt).to_i / @char_height.to_i @adisp_width = @cpl * @char_width + 1 xcpl = @cpl * 2 + (@cpl - 1) / @group_type @xdisp_width = xcpl * @char_width + 1 @disp_buffer = '' @adj.value = [@top_line * old_cpl / @cpl, @lines - @vis_lines].min @adj.value = [ 0, @adj.value ].max if @cursor_pos / @cpl < @adj.value or @cursor_pos / @cpl > @adj.value + @vis_lines - 1 @adj.value = [ @cursor_pos / @cpl, @lines - @vis_lines ].min @adj.value = [ 0, @adj.value ].max end @adj.lower = 0 @adj.upper = @lines @adj.step_increment = 1 @adj.page_increment = @vis_lines - 1 @adj.page_size = @vis_lines @adj.signal_emit 'changed' @adj.signal_emit 'value_changed' end def get_max_char_width(metrics) layout = self.create_pango_layout('') layout.set_font_description(@font_desc) char_widths = [ 0 ] (1..100).each do |i| logical_rect = Pango::Rectangle.new(0, 0, 0, 0) if is_displayable(i.chr) layout.set_text(i.chr) logical_rect = layout.pixel_extents[1] end char_widths << logical_rect.width end char_widths[48..122].max end def show_cursor unless @cursor_shown if @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized? render_xc render_ac end @cursor_shown = true end end def hide_cursor if @cursor_shown if @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized? render_byte(@cursor_pos) end @cursor_shown = false end end def show_offsets_widget @offsets = DrawingArea.new @offsets.modify_font @font_desc @olayout = @offsets.create_pango_layout('') @offsets.events = Gdk::Event::EXPOSURE_MASK @offsets.signal_connect 'expose_event' do |offsets, event| imin = (event.area.y / @char_height).to_i imax = ((event.area.y + event.area.height) / @char_height).to_i imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0 imax = [ imax, @vis_lines ].min render_offsets(imin, imax) end put @offsets, 0, 0 @offsets.show end def hide_offsets_widget if @offsets self.remove(@offsets) @offsets = @offsets_gc = nil end end def is_displayable(c) if RUBY_VERSION < '1.9' c = c[0] else c = c.ord end c >= 0x20 and c < 0x7f end def bytes_changed(s, e) start_line = s / @cpl - @top_line end_line = e / @cpl - @top_line return if end_line < 0 or start_line > @vis_lines start_line = [ 0, start_line ].max render_hex_lines(start_line, end_line) render_ascii_lines(start_line, end_line) render_offsets(start_line, end_line) if @show_offsets end def render_hex_highlights(cursor_line) xcpl = @cpl * 2 + @cpl / @group_type @highlights.each do |hl| next if (hl.start - hl.end).abs < hl.min_select validate_highlight(hl) s, e = [ hl.start, hl.end ].sort sl, el = hl.start_line, hl.end_line hl.style.attach(@xdisp.window) if hl.style state = (@active_view == View::HEX) ? Gtk::STATE_SELECTED : Gtk::STATE_INSENSITIVE if cursor_line == sl cursor_off = 2 * (s % @cpl) + (s % @cpl) / @group_type if cursor_line == el len = 2 * (e % @cpl + 1) + (e % @cpl) / @group_type else len = xcpl end len -= cursor_off (hl.style || self.style).paint_flat_box( @xdisp.window, state, Gtk::SHADOW_NONE, nil, @xdisp, '', cursor_off * @char_width, cursor_line * @char_height, len * @char_width, @char_height ) if len > 0 elsif cursor_line == el cursor_off = 2 * (e % @cpl + 1) + (e % @cpl) / @group_type (hl.style || self.style).paint_flat_box( @xdisp.window, state, Gtk::SHADOW_NONE, nil, @xdisp, '', 0, cursor_line * @char_height, cursor_off * @char_width, @char_height ) if cursor_off > 0 elsif cursor_line > sl and cursor_line < el (hl.style || self.style).paint_flat_box( @xdisp.window, state, Gtk::SHADOW_NONE, nil, @xdisp, '', 0, cursor_line * @char_height, xcpl * @char_width, @char_height ) end hl.style.attach(@adisp.window) if hl.style end end def render_hex_lines(imin, imax) return unless self.realized? and @cpl != 0 cursor_line = @cursor_pos / @cpl - @top_line @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @xdisp.window.draw_rectangle( @xdisp_gc, true, 0, imin * @char_height, @xdisp.allocation.width, (imax - imin + 1) * @char_height ) imax = [ imax, @vis_lines, @lines ].min @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) frm_len = format_xblock((@top_line+imin) * @cpl, [(@top_line+imax+1) * @cpl, @data.size].min) tmp = nil xcpl = @cpl*2 + @cpl/@group_type (imin..imax).each do |i| return unless (tmp = frm_len - ((i - imin) * xcpl)) > 0 render_hex_highlights(i) text = @disp_buffer[(i-imin) * xcpl, [xcpl, tmp].min] @xlayout.set_text(text) @xdisp.window.draw_layout(@xdisp_gc, 0, i * @char_height, @xlayout) end render_xc if cursor_line >= imin and cursor_line <= imax and @cursor_shown end def render_ascii_highlights(cursor_line) @highlights.each do |hl| next if (hl.start - hl.end).abs < hl.min_select validate_highlight(hl) s, e = [ hl.start, hl.end ].sort sl, el = hl.start_line, hl.end_line hl.style.attach(@adisp.window) if hl.style state = (@active_view == View::ASCII) ? Gtk::STATE_SELECTED : Gtk::STATE_INSENSITIVE if cursor_line == sl cursor_off = s % @cpl len = if cursor_line == el e - s + 1 else @cpl - cursor_off end (hl.style || self.style).paint_flat_box( @adisp.window, state, Gtk::SHADOW_NONE, nil, @adisp, '', cursor_off * @char_width, cursor_line * @char_height, len * @char_width, @char_height ) if len > 0 elsif cursor_line == el cursor_off = e % @cpl + 1 (hl.style || self.style).paint_flat_box( @adisp.window, state, Gtk::SHADOW_NONE, nil, @adisp, '', 0, cursor_line * @char_height, cursor_off * @char_width, @char_height ) if cursor_off > 0 elsif cursor_line > sl and cursor_line < el (hl.style || self.style).paint_flat_box( @adisp.window, state, Gtk::SHADOW_NONE, nil, @adisp, '', 0, cursor_line * @char_height, @cpl * @char_width, @char_height ) end hl.style.attach(@adisp.window) if hl.style end end def render_ascii_lines(imin, imax) return unless self.realized? and @cpl != 0 cursor_line = @cursor_pos / @cpl - @top_line @adisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @adisp.window.draw_rectangle( @adisp_gc, true, 0, imin * @char_height, @adisp.allocation.width, (imax - imin + 1) * @char_height ) imax = [ imax, @vis_lines, @lines ].min @adisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) frm_len = format_ablock((@top_line+imin) * @cpl, [(@top_line+imax+1) * @cpl, @data.size].min) tmp = nil (imin..imax).each do |i| return unless (tmp = frm_len - ((i - imin) * @cpl)) > 0 render_ascii_highlights(i) text = @disp_buffer[(i-imin) * @cpl, [@cpl, tmp].min] @alayout.set_text(text) @adisp.window.draw_layout(@adisp_gc, 0, i * @char_height, @alayout) end render_ac if cursor_line >= imin and cursor_line <= imax and @cursor_shown end def render_offsets(imin, imax) return unless self.realized? unless @offsets_gc @offsets_gc = Gdk::GC.new(@offsets.window) @offsets_gc.set_exposures(true) end @offsets_gc.set_foreground(self.style.base(Gtk::STATE_INSENSITIVE)) @offsets.window.draw_rectangle( @offsets_gc, true, 0, imin * @char_height, @offsets.allocation.width, (imax - imin + 1) * @char_height ) imax = [ imax, @vis_lines, @lines - @top_line - 1 ].min @offsets_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) (imin..imax).each do |i| text = "%08x" % ((@top_line + i) * @cpl + @starting_offset) @olayout.set_text(text) @offsets.window.draw_layout(@offsets_gc, 0, i * @char_height, @olayout) end end def render_byte(pos) return unless @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized? return unless (coords = get_xcoords(pos)) cx, cy = coords c = format_xbyte(pos) @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @xdisp.window.draw_rectangle( @xdisp_gc, true, cx, cy, 2 * @char_width, @char_height ) if pos < @data.size @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) @xlayout.set_text(c) @xdisp.window.draw_layout(@xdisp_gc, cx, cy, @xlayout) end return unless (coords = get_acoords(pos)) cx, cy = coords @adisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @adisp.window.draw_rectangle( @adisp_gc, true, cx, cy, @char_width, @char_height ) if pos < @data.size @adisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) c = get_byte(pos) c = '.' unless is_displayable(c) @alayout.set_text(c) @adisp.window.draw_layout(@adisp_gc, cx, cy, @alayout) end end def render_xc return unless @xdisp.realized? if coords = get_xcoords(@cursor_pos) cx, cy = coords c = format_xbyte(@cursor_pos) if @lower_nibble cx += @char_width c = c[1,1] else c = c[0,1] end @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_ACTIVE)) @xdisp.window.draw_rectangle( @xdisp_gc, (@active_view == View::HEX), cx, cy, @char_width, @char_height - 1 ) @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_ACTIVE)) @xlayout.set_text(c) @xdisp.window.draw_layout(@xdisp_gc, cx, cy, @xlayout) end end def render_ac return unless @adisp.realized? if coords = get_acoords(@cursor_pos) cx, cy = coords c = get_byte(@cursor_pos) c = '.' unless is_displayable(c) @adisp_gc.set_foreground(self.style.base(Gtk::STATE_ACTIVE)) @adisp.window.draw_rectangle( @adisp_gc, (@active_view == View::ASCII), cx, cy, @char_width, @char_height - 1 ) @adisp_gc.set_foreground(self.style.text(Gtk::STATE_ACTIVE)) @alayout.set_text(c) @adisp.window.draw_layout(@adisp_gc, cx, cy, @alayout) end end def get_xcoords(pos) return nil if @cpl == 0 cy = pos / @cpl - @top_line return nil if cy < 0 cx = 2 * (pos % @cpl) spaces = (pos % @cpl) / @group_type cx *= @char_width cy *= @char_height spaces *= @char_width [cx + spaces, cy] end def get_acoords(pos) return nil if @cpl == 0 cy = pos / @cpl - @top_line return nil if cy < 0 cy *= @char_height cx = @char_width * (pos % @cpl) [cx, cy] end def format_xblock(s, e) @disp_buffer = '' (s+1..e).each do |i| @disp_buffer << get_byte(i - 1).unpack('H2')[0] @disp_buffer << ' ' if i % @group_type == 0 end @disp_buffer.size end def format_ablock(s, e) @disp_buffer = '' (s..e-1).each do |i| c = get_byte(i) c = '.' unless is_displayable(c) @disp_buffer << c end @disp_buffer.size end def get_byte(offset) if offset >= 0 and offset < @data.size @data[offset, 1] else 0.chr end end def format_xbyte(pos) get_byte(pos).unpack('H2')[0] end end end __END__ hexedit = Gtk::HexEditor.new(File.read '/bin/cat') hexedit.show_offsets(true) hexedit.set_cursor 2 hexedit.set_cursor_on_lower_nibble true hexedit.set_font 'Terminus 12' hexedit.set_group_type Gtk::HexEditor::Group::LONG window = Gtk::Window.new window.add(hexedit) window.show_all Gtk.main