# This file is part of Metasm, the Ruby assembly manipulation suite # Copyright (C) 2006-2009 Yoann GUILLOT # # Licence is LGPL, see LICENCE in the top-level directory require 'Qt4' module Metasm module Gui module Protect @@lasterror = Time.now def protect yield rescue Object puts $!.message, $!.backtrace # also dump on stdout, for c/c delay = Time.now-@@lasterror sleep 1-delay if delay < 1 # msgbox flood protection @@lasterror = Time.now messagebox([$!.message, $!.backtrace].join("\n"), $!.class.name) end end module Msgbox include Protect # shows a message box (non-modal) # args: message, title/optionhash def messagebox(text, opts={}) opts = {:title => opts} if opts.kind_of? String mbox = Qt::MessageBox.new#(self) mbox.text = text mbox.window_title = opts[:title] if opts[:title] mbox.window_modality = Qt::NonModal mbox.show mbox end # asks for user input, yields the result (unless 'cancel' clicked) # args: prompt, :text => default text, :title => title def inputbox(prompt, opts={}) ibox = Qt::InputDialog.new#(self) ibox.label_text = prompt ibox.window_title = opts[:title] if opts[:title] ibox.text_value = opts[:text] if opts[:text] connect(ibox, SIGNAL('TextValueSelected(v)')) { |v| protect { yield v } } ibox.show ibox end @@dialogfilefolder = nil # asks to choose a file to open, yields filename # args: title, :path => path def openfile(title, opts={}) f = Qt::FileDialog.get_open_file_name(nil, title, @@dialogfilefolder) if f and f != '' @@dialogfilefolder = File.dirname(f) protect { yield f } end f # useless, dialog is modal end # same as openfile, but for writing a (new) file def savefile(title, opts={}) f = Qt::FileDialog.get_save_file_name(nil, title, @@dialogfilefolder) if f and f != '' @@dialogfilefolder = File.dirname(f) protect { yield f } end f # useless, dialog is modal end # shows a window with a list of items # the list is an array of arrays, displayed as String # the first array is the column names # each item clicked yields the block with the selected iterator, double-click also close the popup # args: title, [[col0 title, col1 title...], [col0 val0, col1 val0...], [val1], [val2]...] def listwindow(title, list, h={}) l = Qt::TreeWidget.new#(self) l.window_title = title cols = list.shift #l.column_count = cols.length l.header_labels = cols list.each { |e| i = Qt::TreeWidgetItem.new e.length.times { |idx| i.set_text(idx, e[idx].to_s) } l.add_top_level_item i } connect(l, SIGNAL('itemActivated(QTreeWidgetItem*,int)')) { |item, col| next if not item.is_selected next if not idx = l.index_of_top_level_item(item) protect { yield(list[idx].map { |e| e.to_s }) } #if iter = treeview.selection.selected } connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)')) { l.close } l.resize(cols.length*120, 400) l.show if not h[:noshow] l end end # a widget that holds many other widgets, and displays only one of them at a time class ContainerChoiceWidget < Qt::StackedWidget include Msgbox attr_accessor :views, :view_indexes def initialize(*a) super() @views = {} @view_indexes = [] initialize_widget(*a) initialize_visible if respond_to? :initialize_visible end def view(i) @views[i] end def showview(i) set_current_index @view_indexes.index(i) end def addview(name, w) @view_indexes << name @views[name] = w add_widget w end def curview @views[curview_index] end def curview_index @view_indexes[current_index] end end class ContainerVBoxWidget < Qt::VBoxLayout def initialize(*a) super() signal_connect('realize') { initialize_visible } if respond_to? :initialize_visible signal_connect('size_request') { |w, alloc| resize(*alloc) } if respond_to? :resize self.spacing = 2 initialize_widget(*a) end end class DrawableWidget < Qt::Widget include Msgbox attr_accessor :parent_widget, :caret_x, :caret_y, :hl_word # this hash is used to determine the colors of the GUI elements (background, caret, ...) # modifications to it are only useful before the widget is first rendered (IE before GUI.main) attr_accessor :default_color_association # keypress event keyval traduction table # RHA no way to enumerate all Key_* constants, they are handled in Qt.const_missing Keyboard_trad = %w[ Escape Tab Backtab Backspace Return Enter Insert Delete Pause Print SysReq Clear Home End Left Up Right Down PageUp PageDown Shift Control Meta Alt AltGr CapsLock NumLock ScrollLock F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31 F32 F33 F34 F35 Super_L Super_R Menu Hyper_L Hyper_R Help Direction_L Direction_R nobreakspace exclamdown cent sterling currency yen brokenbar section diaeresis copyright ordfeminine guillemotleft notsign hyphen registered macron degree plusminus twosuperior threesuperior acute mu paragraph periodcentered cedilla onesuperior masculine guillemotright onequarter onehalf threequarters questiondown Agrave Aacute Acircumflex Atilde Adiaeresis Aring AE Ccedilla Egrave Eacute Ecircumflex Ediaeresis Igrave Iacute Icircumflex Idiaeresis ETH Ntilde Ograve Oacute Ocircumflex Otilde Odiaeresis multiply Ooblique Ugrave Uacute Ucircumflex Udiaeresis Yacute THORN ssharp division ydiaeresis Multi_key Codeinput SingleCandidate MultipleCandidate PreviousCandidate Mode_switch Kanji Muhenkan Henkan Romaji Hiragana Katakana Hiragana_Katakana Zenkaku Hankaku Zenkaku_Hankaku Touroku Massyo Kana_Lock Kana_Shift Eisu_Shift Eisu_toggle Hangul Hangul_Start Hangul_End Hangul_Hanja Hangul_Jamo Hangul_Romaja Hangul_Jeonja Hangul_Banja Hangul_PreHanja Hangul_PostHanja Hangul_Special Dead_Grave Dead_Acute Dead_Circumflex Dead_Tilde Dead_Macron Dead_Breve Dead_Abovedot Dead_Diaeresis Dead_Abovering Dead_Doubleacute Dead_Caron Dead_Cedilla Dead_Ogonek Dead_Iota Dead_Voiced_Sound Dead_Semivoiced_Sound Dead_Belowdot Dead_Hook Dead_Horn Back Forward Stop Refresh VolumeDown VolumeMute VolumeUp BassBoost BassUp BassDown TrebleUp TrebleDown MediaPlay MediaStop MediaPrevious MediaNext MediaRecord HomePage Favorites Search Standby OpenUrl LaunchMail LaunchMedia Launch0 Launch1 Launch2 Launch3 Launch4 Launch5 Launch6 Launch7 Launch8 Launch9 LaunchA LaunchB LaunchC LaunchD LaunchE LaunchF MediaLast unknown Call Context1 Context2 Context3 Context4 Flip Hangup No Select Yes Execute Printer Play Sleep Zoom Cancel ].inject({}) { |h, cst| v = Qt.const_get("Key_#{cst}").to_i # AONETUHANOTEUHATNOHEU Qt::Enum != Integer key = cst.downcase.to_sym key = { :pageup => :pgup, :pagedown => :pgdown, :escape => :esc, :return => :enter }.fetch(key, key) h.update v => key } def initialize(*a) @parent_widget = nil @caret_x = @caret_y = 0 # text cursor position @oldcaret_x = @oldcaret_y = 1 @hl_word = nil #@layout = Pango::Layout.new Gdk::Pango.context # text rendering @color = {} @default_color_association = {:background => :palegrey} if a.last.kind_of? Qt::Widget super(a.last) else super() end { :white => 'fff', :palegrey => 'ddd', :black => '000', :grey => '444', :red => 'f00', :darkred => '800', :palered => 'fcc', :green => '0f0', :darkgreen => '080', :palegreen => 'cfc', :blue => '00f', :darkblue => '008', :paleblue => 'ccf', :yellow => 'ff0', :darkyellow => '440', :paleyellow => 'ffc', }.each { |tag, val| @color[tag] = color(val) } initialize_widget(*a) set_auto_fill_background true set_color_association @default_color_association set_focus_policy Qt::StrongFocus initialize_visible if respond_to? :initialize_visible set_font 'courier 10' end def keyPressEvent(key) val = key.key >= 128 ? Keyboard_trad[key.key] : key.text[0].ord # must use text[0] to differenciate downcase/upcase if key.modifiers.to_i & Qt::ControlModifier.to_i > 0 # AONETHUAAAAAAAAAAAAAAA protect { keypress_ctrl(val) } if respond_to? :keypress_ctrl else protect { keypress(val) } if respond_to? :keypress end end def mousePressEvent(ev) if ev.modifiers.to_i & Qt::ControlModifier.to_i > 0 protect { click_ctrl(ev.x, ev.y) } if respond_to? :click_ctrl else if ev.button == Qt::LeftButton protect { click(ev.x, ev.y) } elsif ev.button == Qt::RightButton protect { rightclick(ev.x, ev.y) } if respond_to? :rightclick end end end def mouseDoubleClickEvent(ev) protect { doubleclick(ev.x, ev.y) } if respond_to? :doubleclick end def mouseReleaseEvent(ev) if ev.button == Qt::LeftButton protect { mouserelease(ev.x, ev.y) } if respond_to? :mouserelease end end def mouseMoveEvent(ev) if ev.modifiers.to_i & Qt::ControlModifier.to_i > 0 protect { mousemove_ctrl(ev.x, ev.y) } if respond_to? :mousemove_ctrl else protect { mousemove(ev.x, ev.y) } if respond_to? :mousemove end end def wheelEvent(ev) dir = ev.delta > 0 ? :up : :down if ev.modifiers.to_i & Qt::ControlModifier.to_i > 0 protect { mouse_wheel_ctrl(dir, ev.x, ev.y) } if respond_to? :mouse_wheel_ctrl else protect { mouse_wheel(dir, ev.x, ev.y) } if respond_to? :mouse_wheel end end def resizeEvent(ev) protect { resized(ev.size.width, ev.size.height) } if respond_to? :resized end def grab_focus; set_focus end def paintEvent(*a) @painter = Qt::Painter.new(self) protect { paint } @painter.end @painter = nil end def paint end def gui_update redraw end # create a color from a 'rgb' description def color(val) @color[val] ||= Qt::Color.new(*val.unpack('CCC').map { |c| (c.chr*2).hex }) end def set_caret_from_click(x, y) @caret_x = (x-1).to_i / @font_width @caret_y = y.to_i / @font_height update_caret end # change the font of the widget # arg is a Gtk Fontdescription string (eg 'courier 10') def set_font(descr) descr, sz = descr.split super(Qt::Font.new(descr, sz.to_i)) @font_width = font_metrics.width('x') @font_height = font_metrics.line_spacing @font_descent = font_metrics.descent gui_update end # change the color association # arg is a hash function symbol => color symbol # color must be allocated # check #initialize/sig('realize') for initial function/color list def set_color_association(hash) hash.each { |k, v| @color[k] = color(v) } #set_background_role Qt::Palette::Window(color(:background)) palette.set_color(Qt::Palette::Window, color(:background)) gui_update end # update @hl_word from a line & offset, return nil if unchanged def update_hl_word(line, offset, mode=:asm) return if not line word = line[0...offset].to_s[/\w*$/] << line[offset..-1].to_s[/^\w*/] word = nil if word == '' if @hl_word != word if word if mode == :asm and defined?(@dasm) and @dasm re = @dasm.gui_hilight_word_regexp(word) else re = Regexp.escape word end @hl_word_re = /^(.*?)(\b(?:#{re})\b)/ end @hl_word = word end end # invalidate the whole widget area def redraw invalidate(0, 0, 1000000, 1000000) end def invalidate_caret(cx, cy, x=0, y=0) invalidate(x + cx*@font_width, y + cy*@font_height, 2, @font_height) end def invalidate(x, y, w, h) update x, y, w, h end def resized(w, h) redraw end def keypress(key) end def keypress_ctrl(key) end def draw_color(col) @col = color(col) @painter.set_brush Qt::Brush.new(@col) @painter.set_pen Qt::Pen.new(@col) end def draw_rectangle(x, y, w, h) @painter.fill_rect(x, y, w, h, @col) end def draw_rectangle_color(col, x, y, w, h) draw_color(col) draw_rectangle(x, y, w, h) end def draw_line(x, y, ex, ey) @painter.draw_line(x, y, ex, ey) end def draw_line_color(col, x, y, ex, ey) draw_color(col) draw_line(x, y, ex, ey) end def draw_string(x, y, str) @painter.draw_text(x, y-@font_descent, str) end def draw_string_color(col, x, y, str) draw_color(col) draw_string(x, y, str) end end class Window < Qt::MainWindow include Msgbox attr_accessor :menu def initialize(*a) super() #connect(self, SIGNAL(:destroy)) { destroy_window } @menu = menu_bar screen = Qt::Application.desktop resize screen.width*3/4, screen.height*3/4 initialize_window(*a) build_menu show end def build_menu end def destroy_window end def widget=(w) set_central_widget w end def title=(t); set_window_title(t) end def title; window_title end def new_menu Qt::Menu.new end def addsubmenu(menu, *args) accel = args.grep(/^\^?(\w|<\w+>)$/).first args.delete accel if accel check = args.delete :check submenu = args.grep(Qt::Menu).first args.delete submenu if submenu if label = args.shift label = label.capitalize if label == label.upcase # TODO icon on OPEN/CLOSE etc label = label.gsub('_', '&') end if submenu submenu.title = label menu.add_menu submenu return end if check # TODO #item = Gtk::CheckMenuItem.new(label) #item.active = args.shift item = Qt::Action.new(label, self) menu.add_action item elsif label item = Qt::Action.new(label, self) menu.add_action item else menu.add_separator end item.setShortcut accel.sub('^', 'Ctrl+') connect(item, SIGNAL(:triggered)) { protect { yield(item) } } if block_given? item end end @app = Qt::Application.new ARGV # start the GUI main loop def self.main @app.exec end # ends the GUI main loop def self.main_quit @app.quit # XXX segfault.. end # register a proc to be run whenever the gui loop is idle # if the proc returns nil/false, delete it def self.idle_add t = Qt::Timer.new t.connect(t, SIGNAL(:timeout)) { if not yield ; t.stop ; t = nil end } t.start end # run a single iteration of the main_loop # e.g. call this from time to time when doing heavy computation, to keep the UI somewhat responsive def self.main_iter Qt::Application.process_events end end end require 'metasm/gui/dasm_main' require 'metasm/gui/debug'