metasm 1.0.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.
- data/BUGS +11 -0
- data/CREDITS +17 -0
- data/README +270 -0
- data/TODO +114 -0
- data/doc/code_organisation.txt +146 -0
- data/doc/const_missing.txt +16 -0
- data/doc/core_classes.txt +75 -0
- data/doc/feature_list.txt +53 -0
- data/doc/index.txt +59 -0
- data/doc/install_notes.txt +170 -0
- data/doc/style.css +3 -0
- data/doc/use_cases.txt +18 -0
- data/lib/metasm.rb +80 -0
- data/lib/metasm/arm.rb +12 -0
- data/lib/metasm/arm/debug.rb +39 -0
- data/lib/metasm/arm/decode.rb +167 -0
- data/lib/metasm/arm/encode.rb +77 -0
- data/lib/metasm/arm/main.rb +75 -0
- data/lib/metasm/arm/opcodes.rb +177 -0
- data/lib/metasm/arm/parse.rb +130 -0
- data/lib/metasm/arm/render.rb +55 -0
- data/lib/metasm/compile_c.rb +1457 -0
- data/lib/metasm/dalvik.rb +8 -0
- data/lib/metasm/dalvik/decode.rb +196 -0
- data/lib/metasm/dalvik/main.rb +60 -0
- data/lib/metasm/dalvik/opcodes.rb +366 -0
- data/lib/metasm/decode.rb +213 -0
- data/lib/metasm/decompile.rb +2659 -0
- data/lib/metasm/disassemble.rb +2068 -0
- data/lib/metasm/disassemble_api.rb +1280 -0
- data/lib/metasm/dynldr.rb +1329 -0
- data/lib/metasm/encode.rb +333 -0
- data/lib/metasm/exe_format/a_out.rb +194 -0
- data/lib/metasm/exe_format/autoexe.rb +82 -0
- data/lib/metasm/exe_format/bflt.rb +189 -0
- data/lib/metasm/exe_format/coff.rb +455 -0
- data/lib/metasm/exe_format/coff_decode.rb +901 -0
- data/lib/metasm/exe_format/coff_encode.rb +1078 -0
- data/lib/metasm/exe_format/dex.rb +457 -0
- data/lib/metasm/exe_format/dol.rb +145 -0
- data/lib/metasm/exe_format/elf.rb +923 -0
- data/lib/metasm/exe_format/elf_decode.rb +979 -0
- data/lib/metasm/exe_format/elf_encode.rb +1375 -0
- data/lib/metasm/exe_format/macho.rb +827 -0
- data/lib/metasm/exe_format/main.rb +228 -0
- data/lib/metasm/exe_format/mz.rb +164 -0
- data/lib/metasm/exe_format/nds.rb +172 -0
- data/lib/metasm/exe_format/pe.rb +437 -0
- data/lib/metasm/exe_format/serialstruct.rb +246 -0
- data/lib/metasm/exe_format/shellcode.rb +114 -0
- data/lib/metasm/exe_format/xcoff.rb +167 -0
- data/lib/metasm/gui.rb +23 -0
- data/lib/metasm/gui/cstruct.rb +373 -0
- data/lib/metasm/gui/dasm_coverage.rb +199 -0
- data/lib/metasm/gui/dasm_decomp.rb +369 -0
- data/lib/metasm/gui/dasm_funcgraph.rb +103 -0
- data/lib/metasm/gui/dasm_graph.rb +1354 -0
- data/lib/metasm/gui/dasm_hex.rb +543 -0
- data/lib/metasm/gui/dasm_listing.rb +599 -0
- data/lib/metasm/gui/dasm_main.rb +906 -0
- data/lib/metasm/gui/dasm_opcodes.rb +291 -0
- data/lib/metasm/gui/debug.rb +1228 -0
- data/lib/metasm/gui/gtk.rb +884 -0
- data/lib/metasm/gui/qt.rb +495 -0
- data/lib/metasm/gui/win32.rb +3004 -0
- data/lib/metasm/gui/x11.rb +621 -0
- data/lib/metasm/ia32.rb +14 -0
- data/lib/metasm/ia32/compile_c.rb +1523 -0
- data/lib/metasm/ia32/debug.rb +193 -0
- data/lib/metasm/ia32/decode.rb +1167 -0
- data/lib/metasm/ia32/decompile.rb +564 -0
- data/lib/metasm/ia32/encode.rb +314 -0
- data/lib/metasm/ia32/main.rb +233 -0
- data/lib/metasm/ia32/opcodes.rb +872 -0
- data/lib/metasm/ia32/parse.rb +327 -0
- data/lib/metasm/ia32/render.rb +91 -0
- data/lib/metasm/main.rb +1193 -0
- data/lib/metasm/mips.rb +11 -0
- data/lib/metasm/mips/compile_c.rb +7 -0
- data/lib/metasm/mips/decode.rb +253 -0
- data/lib/metasm/mips/encode.rb +51 -0
- data/lib/metasm/mips/main.rb +72 -0
- data/lib/metasm/mips/opcodes.rb +443 -0
- data/lib/metasm/mips/parse.rb +51 -0
- data/lib/metasm/mips/render.rb +43 -0
- data/lib/metasm/os/gnu_exports.rb +270 -0
- data/lib/metasm/os/linux.rb +1112 -0
- data/lib/metasm/os/main.rb +1686 -0
- data/lib/metasm/os/remote.rb +527 -0
- data/lib/metasm/os/windows.rb +2027 -0
- data/lib/metasm/os/windows_exports.rb +745 -0
- data/lib/metasm/parse.rb +876 -0
- data/lib/metasm/parse_c.rb +3938 -0
- data/lib/metasm/pic16c/decode.rb +42 -0
- data/lib/metasm/pic16c/main.rb +17 -0
- data/lib/metasm/pic16c/opcodes.rb +68 -0
- data/lib/metasm/ppc.rb +11 -0
- data/lib/metasm/ppc/decode.rb +264 -0
- data/lib/metasm/ppc/decompile.rb +251 -0
- data/lib/metasm/ppc/encode.rb +51 -0
- data/lib/metasm/ppc/main.rb +129 -0
- data/lib/metasm/ppc/opcodes.rb +410 -0
- data/lib/metasm/ppc/parse.rb +52 -0
- data/lib/metasm/preprocessor.rb +1277 -0
- data/lib/metasm/render.rb +130 -0
- data/lib/metasm/sh4.rb +8 -0
- data/lib/metasm/sh4/decode.rb +336 -0
- data/lib/metasm/sh4/main.rb +292 -0
- data/lib/metasm/sh4/opcodes.rb +381 -0
- data/lib/metasm/x86_64.rb +12 -0
- data/lib/metasm/x86_64/compile_c.rb +1025 -0
- data/lib/metasm/x86_64/debug.rb +59 -0
- data/lib/metasm/x86_64/decode.rb +268 -0
- data/lib/metasm/x86_64/encode.rb +264 -0
- data/lib/metasm/x86_64/main.rb +135 -0
- data/lib/metasm/x86_64/opcodes.rb +118 -0
- data/lib/metasm/x86_64/parse.rb +68 -0
- data/misc/bottleneck.rb +61 -0
- data/misc/cheader-findpppath.rb +58 -0
- data/misc/hexdiff.rb +74 -0
- data/misc/hexdump.rb +55 -0
- data/misc/metasm-all.rb +13 -0
- data/misc/objdiff.rb +47 -0
- data/misc/objscan.rb +40 -0
- data/misc/pdfparse.rb +661 -0
- data/misc/ppc_pdf2oplist.rb +192 -0
- data/misc/tcp_proxy_hex.rb +84 -0
- data/misc/txt2html.rb +440 -0
- data/samples/a.out.rb +31 -0
- data/samples/asmsyntax.rb +77 -0
- data/samples/bindiff.rb +555 -0
- data/samples/compilation-steps.rb +49 -0
- data/samples/cparser_makestackoffset.rb +55 -0
- data/samples/dasm-backtrack.rb +38 -0
- data/samples/dasmnavig.rb +318 -0
- data/samples/dbg-apihook.rb +228 -0
- data/samples/dbghelp.rb +143 -0
- data/samples/disassemble-gui.rb +102 -0
- data/samples/disassemble.rb +133 -0
- data/samples/dump_upx.rb +95 -0
- data/samples/dynamic_ruby.rb +1929 -0
- data/samples/elf_list_needed.rb +46 -0
- data/samples/elf_listexports.rb +33 -0
- data/samples/elfencode.rb +25 -0
- data/samples/exeencode.rb +128 -0
- data/samples/factorize-headers-elfimports.rb +77 -0
- data/samples/factorize-headers-peimports.rb +109 -0
- data/samples/factorize-headers.rb +43 -0
- data/samples/gdbclient.rb +583 -0
- data/samples/generate_libsigs.rb +102 -0
- data/samples/hotfix_gtk_dbg.rb +59 -0
- data/samples/install_win_env.rb +78 -0
- data/samples/lindebug.rb +924 -0
- data/samples/linux_injectsyscall.rb +95 -0
- data/samples/machoencode.rb +31 -0
- data/samples/metasm-shell.rb +91 -0
- data/samples/pe-hook.rb +69 -0
- data/samples/pe-ia32-cpuid.rb +203 -0
- data/samples/pe-mips.rb +35 -0
- data/samples/pe-shutdown.rb +78 -0
- data/samples/pe-testrelocs.rb +51 -0
- data/samples/pe-testrsrc.rb +24 -0
- data/samples/pe_listexports.rb +31 -0
- data/samples/peencode.rb +19 -0
- data/samples/peldr.rb +494 -0
- data/samples/preprocess-flatten.rb +19 -0
- data/samples/r0trace.rb +308 -0
- data/samples/rubstop.rb +399 -0
- data/samples/scan_pt_gnu_stack.rb +54 -0
- data/samples/scanpeexports.rb +62 -0
- data/samples/shellcode-c.rb +40 -0
- data/samples/shellcode-dynlink.rb +146 -0
- data/samples/source.asm +34 -0
- data/samples/struct_offset.rb +47 -0
- data/samples/testpe.rb +32 -0
- data/samples/testraw.rb +45 -0
- data/samples/win32genloader.rb +132 -0
- data/samples/win32hooker-advanced.rb +169 -0
- data/samples/win32hooker.rb +96 -0
- data/samples/win32livedasm.rb +33 -0
- data/samples/win32remotescan.rb +133 -0
- data/samples/wintrace.rb +92 -0
- data/tests/all.rb +8 -0
- data/tests/dasm.rb +39 -0
- data/tests/dynldr.rb +35 -0
- data/tests/encodeddata.rb +132 -0
- data/tests/ia32.rb +82 -0
- data/tests/mips.rb +116 -0
- data/tests/parse_c.rb +239 -0
- data/tests/preprocessor.rb +269 -0
- data/tests/x86_64.rb +62 -0
- metadata +255 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
# This file is part of Metasm, the Ruby assembly manipulation suite
|
|
2
|
+
# Copyright (C) 2006-2009 Yoann GUILLOT
|
|
3
|
+
#
|
|
4
|
+
# Licence is LGPL, see LICENCE in the top-level directory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
require 'gtk2'
|
|
8
|
+
|
|
9
|
+
module Metasm
|
|
10
|
+
module Gui
|
|
11
|
+
|
|
12
|
+
module Protect
|
|
13
|
+
@@lasterror = Time.now
|
|
14
|
+
def protect
|
|
15
|
+
yield
|
|
16
|
+
rescue Object
|
|
17
|
+
puts $!.message, $!.backtrace # also dump on stdout, for c/c
|
|
18
|
+
delay = Time.now-@@lasterror
|
|
19
|
+
sleep 1-delay if delay < 1 # msgbox flood protection
|
|
20
|
+
@@lasterror = Time.now
|
|
21
|
+
messagebox([$!.message, $!.backtrace].join("\n"), $!.class.name)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module Msgbox
|
|
26
|
+
include Protect
|
|
27
|
+
|
|
28
|
+
def toplevel
|
|
29
|
+
if self.kind_of? Gtk::Window
|
|
30
|
+
self
|
|
31
|
+
else
|
|
32
|
+
super()
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# shows a message box (non-modal)
|
|
37
|
+
# args: message, title/optionhash
|
|
38
|
+
def messagebox(*a)
|
|
39
|
+
MessageBox.new(toplevel, *a)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# asks for user input, yields the result (unless 'cancel' clicked)
|
|
43
|
+
# args: prompt, :text => default text, :title => title
|
|
44
|
+
def inputbox(*a)
|
|
45
|
+
InputBox.new(toplevel, *a) { |*ya| protect { yield(*ya) } }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# asks to chose a file to open, yields filename
|
|
49
|
+
# args: title, :path => path
|
|
50
|
+
def openfile(*a)
|
|
51
|
+
OpenFile.new(toplevel, *a) { |*ya| protect { yield(*ya) } }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# same as openfile, but for writing a (new) file
|
|
55
|
+
def savefile(*a)
|
|
56
|
+
SaveFile.new(toplevel, *a) { |*ya| protect { yield(*ya) } }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# displays a popup showing a table, yields the selected row
|
|
60
|
+
# args: title, [[col0 title, col1 title...], [col0 val0, col1 val0...], [val1], [val2]...]
|
|
61
|
+
def listwindow(*a)
|
|
62
|
+
ListWindow.new(toplevel, *a) { |*ya| protect { yield(*ya) } }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# clipboard = Gtk::Clipboard.get(Gdk::Selection::CLIPBOARD)
|
|
67
|
+
# text = clipboard.wait_for_text
|
|
68
|
+
# clipboard.text = 'foo'
|
|
69
|
+
|
|
70
|
+
# a widget that holds many other widgets, and displays only one of them at a time
|
|
71
|
+
class ContainerChoiceWidget < Gtk::Notebook
|
|
72
|
+
include Msgbox
|
|
73
|
+
|
|
74
|
+
attr_accessor :views, :view_indexes
|
|
75
|
+
def initialize(*a, &b)
|
|
76
|
+
super()
|
|
77
|
+
self.show_border = false
|
|
78
|
+
self.show_tabs = false
|
|
79
|
+
@views = {}
|
|
80
|
+
@view_indexes = []
|
|
81
|
+
|
|
82
|
+
signal_connect('realize') { initialize_visible } if respond_to? :initialize_visible
|
|
83
|
+
|
|
84
|
+
initialize_widget(*a, &b)
|
|
85
|
+
|
|
86
|
+
show_all
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def view(i)
|
|
90
|
+
@views[i]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def showview(i)
|
|
94
|
+
set_page @view_indexes.index(i)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def addview(name, w)
|
|
98
|
+
@view_indexes << name
|
|
99
|
+
@views[name] = w
|
|
100
|
+
append_page(w, Gtk::Label.new(name.to_s))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def curview
|
|
104
|
+
@views[curview_index]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def curview_index
|
|
108
|
+
return if page == -1
|
|
109
|
+
@view_indexes[page]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class ContainerVBoxWidget < Gtk::VBox
|
|
114
|
+
include Msgbox
|
|
115
|
+
|
|
116
|
+
def initialize(*a, &b)
|
|
117
|
+
super()
|
|
118
|
+
|
|
119
|
+
signal_connect('realize') { initialize_visible } if respond_to? :initialize_visible
|
|
120
|
+
|
|
121
|
+
signal_connect('size_request') { |w, alloc| resize(*alloc) } if respond_to? :resize
|
|
122
|
+
|
|
123
|
+
self.spacing = 2
|
|
124
|
+
|
|
125
|
+
initialize_widget(*a, &b)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def resize_child(cld, w, h)
|
|
129
|
+
pk = query_child_packing(cld)
|
|
130
|
+
if h <= 0
|
|
131
|
+
pk[0] = true
|
|
132
|
+
h = 1
|
|
133
|
+
else
|
|
134
|
+
pk[0] = false
|
|
135
|
+
end
|
|
136
|
+
return if h == cld.allocation.height
|
|
137
|
+
set_child_packing(cld, *pk)
|
|
138
|
+
cld.set_height_request(h)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def redraw
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class DrawableWidget < Gtk::DrawingArea
|
|
146
|
+
include Msgbox
|
|
147
|
+
|
|
148
|
+
attr_accessor :parent_widget, :caret_x, :caret_y, :hl_word
|
|
149
|
+
# this hash is used to determine the colors of the Gui elements (background, caret, ...)
|
|
150
|
+
# modifications to it are only useful before the widget is first rendered (IE before Gui.main)
|
|
151
|
+
attr_accessor :default_color_association
|
|
152
|
+
|
|
153
|
+
# keypress event keyval traduction table
|
|
154
|
+
Keyboard_trad = Gdk::Keyval.constants.grep(/^GDK_/).inject({}) { |h, cst|
|
|
155
|
+
v = Gdk::Keyval.const_get(cst)
|
|
156
|
+
key = cst.to_s.sub(/^GDK_/, '').sub(/^KP_/, '')
|
|
157
|
+
if key.length == 1
|
|
158
|
+
key = key[0] # ?a, ?b etc
|
|
159
|
+
else
|
|
160
|
+
key = key.downcase.to_sym
|
|
161
|
+
key = {
|
|
162
|
+
:page_up => :pgup, :page_down => :pgdown, :next => :pgdown,
|
|
163
|
+
:escape => :esc, :return => :enter, :l1 => :f11, :l2 => :f12,
|
|
164
|
+
:prior => :pgup,
|
|
165
|
+
|
|
166
|
+
:space => ?\ ,
|
|
167
|
+
:asciitilde => ?~, :quoteleft => ?`,
|
|
168
|
+
:exclam => ?!, :at => ?@,
|
|
169
|
+
:numbersign => ?#, :dollar => ?$,
|
|
170
|
+
:percent => ?%, :asciicircum => ?^,
|
|
171
|
+
:ampersand => ?&, :asterisk => ?*,
|
|
172
|
+
:parenleft => ?(, :parenright => ?),
|
|
173
|
+
:bracketleft => ?[, :bracketright => ?],
|
|
174
|
+
:braceleft => ?{, :braceright => ?},
|
|
175
|
+
:less => ?<, :greater => ?>,
|
|
176
|
+
:quotedbl => ?", :quoteright => ?',
|
|
177
|
+
:coma => ?,, :period => ?.,
|
|
178
|
+
:colon => ?:, :semicolon => ?;,
|
|
179
|
+
:slash => ?/, :equal => ?=,
|
|
180
|
+
:plus => ?+, :minus => ?-,
|
|
181
|
+
:question => ??, :backslash => ?\\,
|
|
182
|
+
:underscore => ?_, :bar => ?|,
|
|
183
|
+
:comma => ?,,
|
|
184
|
+
:divide => ?/, :multiply => ?*,
|
|
185
|
+
:subtract => ?-, :add => ?+
|
|
186
|
+
}.fetch(key, key)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
h.update v => key
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def initialize(*a, &b)
|
|
193
|
+
@parent_widget = nil
|
|
194
|
+
|
|
195
|
+
@caret_x = @caret_y = 0 # text cursor position
|
|
196
|
+
@oldcaret_x = @oldcaret_y = 1
|
|
197
|
+
@hl_word = nil
|
|
198
|
+
|
|
199
|
+
@layout = Pango::Layout.new Gdk::Pango.context # text rendering
|
|
200
|
+
|
|
201
|
+
@color = {}
|
|
202
|
+
@default_color_association = {:background => :palegrey}
|
|
203
|
+
|
|
204
|
+
super()
|
|
205
|
+
|
|
206
|
+
# events callbacks
|
|
207
|
+
signal_connect('expose_event') {
|
|
208
|
+
@w = window ; @gc = Gdk::GC.new(@w)
|
|
209
|
+
protect { paint }
|
|
210
|
+
@w = @gc = nil
|
|
211
|
+
true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
signal_connect('size_allocate') { |w, alloc|
|
|
215
|
+
protect { resized(alloc.width, alloc.height) }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
signal_connect('button_press_event') { |w, ev|
|
|
219
|
+
@last_kb_ev = ev
|
|
220
|
+
if keyboard_state(:control)
|
|
221
|
+
next protect { click_ctrl(ev.x, ev.y) } if ev.event_type == Gdk::Event::Type::BUTTON_PRESS and ev.button == 1 and respond_to? :click_ctrl
|
|
222
|
+
next
|
|
223
|
+
end
|
|
224
|
+
case ev.event_type
|
|
225
|
+
when Gdk::Event::Type::BUTTON_PRESS
|
|
226
|
+
grab_focus
|
|
227
|
+
case ev.button
|
|
228
|
+
when 1; protect { click(ev.x, ev.y) } if respond_to? :click
|
|
229
|
+
when 3; protect { rightclick(ev.x, ev.y) } if respond_to? :rightclick
|
|
230
|
+
end
|
|
231
|
+
when Gdk::Event::Type::BUTTON2_PRESS
|
|
232
|
+
case ev.button
|
|
233
|
+
when 1; protect { doubleclick(ev.x, ev.y) } if respond_to? :doubleclick
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
signal_connect('motion_notify_event') { |w, ev|
|
|
239
|
+
@last_kb_ev = ev
|
|
240
|
+
if keyboard_state(:control)
|
|
241
|
+
protect { mousemove_ctrl(ev.x, ev.y) } if respond_to? :mousemove_ctrl
|
|
242
|
+
else
|
|
243
|
+
protect { mousemove(ev.x, ev.y) }
|
|
244
|
+
end
|
|
245
|
+
} if respond_to? :mousemove
|
|
246
|
+
|
|
247
|
+
signal_connect('button_release_event') { |w, ev|
|
|
248
|
+
protect { mouserelease(ev.x, ev.y) } if ev.button == 1
|
|
249
|
+
} if respond_to? :mouserelease
|
|
250
|
+
|
|
251
|
+
signal_connect('scroll_event') { |w, ev|
|
|
252
|
+
dir = case ev.direction
|
|
253
|
+
when Gdk::EventScroll::Direction::UP; :up
|
|
254
|
+
when Gdk::EventScroll::Direction::DOWN; :down
|
|
255
|
+
else next
|
|
256
|
+
end
|
|
257
|
+
@last_kb_ev = ev
|
|
258
|
+
if keyboard_state(:control)
|
|
259
|
+
protect { mouse_wheel_ctrl(dir, ev.x, ev.y) } if respond_to? :mouse_wheel_ctrl
|
|
260
|
+
else
|
|
261
|
+
protect { mouse_wheel(dir, ev.x, ev.y) }
|
|
262
|
+
end
|
|
263
|
+
} if respond_to? :mouse_wheel
|
|
264
|
+
|
|
265
|
+
signal_connect('key_press_event') { |w, ev|
|
|
266
|
+
@last_kb_ev = ev
|
|
267
|
+
key = Keyboard_trad[ev.keyval]
|
|
268
|
+
if keyboard_state(:control)
|
|
269
|
+
protect { keypress_ctrl(key) or (@parent_widget and @parent_widget.keypress_ctrl(key)) }
|
|
270
|
+
else
|
|
271
|
+
protect { keypress(key) or (@parent_widget and @parent_widget.keypress(key)) }
|
|
272
|
+
end
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
signal_connect('realize') {
|
|
276
|
+
{ :white => 'fff', :palegrey => 'ddd', :black => '000', :grey => '444',
|
|
277
|
+
:red => 'f00', :darkred => '800', :palered => 'fcc',
|
|
278
|
+
:green => '0f0', :darkgreen => '080', :palegreen => 'cfc',
|
|
279
|
+
:blue => '00f', :darkblue => '008', :paleblue => 'ccf',
|
|
280
|
+
:yellow => 'ff0', :darkyellow => '440', :paleyellow => 'ffc',
|
|
281
|
+
}.each { |tag, val|
|
|
282
|
+
@color[tag] = color(val)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
set_color_association @default_color_association
|
|
286
|
+
|
|
287
|
+
initialize_visible if respond_to? :initialize_visible
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
initialize_widget(*a, &b)
|
|
291
|
+
|
|
292
|
+
# receive keyboard/mouse signals
|
|
293
|
+
set_events Gdk::Event::ALL_EVENTS_MASK
|
|
294
|
+
set_can_focus true
|
|
295
|
+
set_font 'courier 10'
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def initialize_widget
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# create a color from a 'rgb' description
|
|
303
|
+
def color(val)
|
|
304
|
+
if not @color[val]
|
|
305
|
+
@color[val] = Gdk::Color.new(*val.unpack('CCC').map { |c| (c.chr*4).hex })
|
|
306
|
+
window.colormap.alloc_color(@color[val], true, true)
|
|
307
|
+
end
|
|
308
|
+
@color[val]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def set_caret_from_click(x, y)
|
|
312
|
+
@caret_x = (x-1).to_i / @font_width
|
|
313
|
+
@caret_y = y.to_i / @font_height
|
|
314
|
+
update_caret
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# change the font of the widget
|
|
318
|
+
# arg is a Gtk Fontdescription string (eg 'courier 10')
|
|
319
|
+
def set_font(descr)
|
|
320
|
+
@layout.font_description = Pango::FontDescription.new(descr)
|
|
321
|
+
@layout.text = 'x'
|
|
322
|
+
@font_width, @font_height = @layout.pixel_size
|
|
323
|
+
gui_update
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# change the color association
|
|
327
|
+
# arg is a hash function symbol => color symbol
|
|
328
|
+
# color must be allocated
|
|
329
|
+
# check #initialize/sig('realize') for initial function/color list
|
|
330
|
+
def set_color_association(hash)
|
|
331
|
+
hash.each { |k, v| @color[k] = color(v) }
|
|
332
|
+
modify_bg Gtk::STATE_NORMAL, @color[:background]
|
|
333
|
+
gui_update
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# update @hl_word from a line & offset, return nil if unchanged
|
|
337
|
+
def update_hl_word(line, offset)
|
|
338
|
+
return if not line
|
|
339
|
+
word = line[0...offset].to_s[/\w*$/] << line[offset..-1].to_s[/^\w*/]
|
|
340
|
+
word = nil if word == ''
|
|
341
|
+
@hl_word = word if @hl_word != word
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def paint
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# invalidate the whole widget area
|
|
348
|
+
def redraw
|
|
349
|
+
invalidate(0, 0, 1000000, 1000000)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def invalidate_caret(cx, cy, x=0, y=0)
|
|
353
|
+
invalidate(x + cx*@font_width, y + cy*@font_height, 2, @font_height)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def invalidate(x, y, w, h)
|
|
357
|
+
return if not window
|
|
358
|
+
window.invalidate Gdk::Rectangle.new(x, y, w, h), false
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def width
|
|
362
|
+
allocation.width
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def height
|
|
366
|
+
allocation.height
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def resized(w, h)
|
|
370
|
+
redraw
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def keypress(key)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def keypress_ctrl(key)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def gui_update
|
|
380
|
+
redraw
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def draw_color(col)
|
|
384
|
+
@gc.set_foreground color(col)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def draw_rectangle(x, y, w, h)
|
|
388
|
+
@w.draw_rectangle(@gc, true, x, y, w, h)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def draw_rectangle_color(col, x, y, w, h)
|
|
392
|
+
draw_color(col)
|
|
393
|
+
draw_rectangle(x, y, w, h)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def draw_line(x, y, ex, ey)
|
|
397
|
+
@w.draw_line(@gc, x, y, ex, ey)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def draw_line_color(col, x, y, ex, ey)
|
|
401
|
+
draw_color(col)
|
|
402
|
+
draw_line(x, y, ex, ey)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def draw_string(x, y, str)
|
|
406
|
+
@layout.text = str
|
|
407
|
+
@w.draw_layout(@gc, x, y, @layout)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def draw_string_color(col, x, y, str)
|
|
411
|
+
draw_color(col)
|
|
412
|
+
draw_string(x, y, str)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def clipboard_copy(buf)
|
|
416
|
+
clipboard = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
|
|
417
|
+
clipboard.text = buf
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def clipboard_paste
|
|
421
|
+
clipboard = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
|
|
422
|
+
clipboard.wait_for_text
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def keyboard_state(query=nil)
|
|
426
|
+
case query
|
|
427
|
+
when :control, :ctrl
|
|
428
|
+
ev = @last_kb_ev and ev.state & Gdk::Window::CONTROL_MASK == Gdk::Window::CONTROL_MASK
|
|
429
|
+
when :shift
|
|
430
|
+
ev = @last_kb_ev and ev.state & Gdk::Window::SHIFT_MASK == Gdk::Window::SHIFT_MASK
|
|
431
|
+
when :alt
|
|
432
|
+
ev = @last_kb_ev and ev.state & Gdk::Window::MOD1_MASK == Gdk::Window::MOD1_MASK
|
|
433
|
+
else
|
|
434
|
+
[:control, :shift, :alt].find_all { |s| keyboard_state(s) }
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
module WindowPos
|
|
440
|
+
def x; position[0]; end
|
|
441
|
+
def x=(nx); move(nx, position[1]); end
|
|
442
|
+
def y; position[1]; end
|
|
443
|
+
def y=(ny); move(position[0], ny); end
|
|
444
|
+
def width; size[0] ; end
|
|
445
|
+
def width=(nw); resize(nw, size[1]); end
|
|
446
|
+
def height; size[1] ; end
|
|
447
|
+
def height=(nh); resize(size[0], nh); end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
class MessageBox < Gtk::MessageDialog
|
|
451
|
+
include WindowPos
|
|
452
|
+
def initialize(owner, str, opts={})
|
|
453
|
+
owner = nil if owner and (not owner.kind_of? Gtk::Window or owner.destroyed?)
|
|
454
|
+
owner ||= Gtk::Window.toplevels.first
|
|
455
|
+
opts = {:title => opts} if opts.kind_of? String
|
|
456
|
+
super(owner, Gtk::Dialog::DESTROY_WITH_PARENT, INFO, BUTTONS_CLOSE, str)
|
|
457
|
+
self.title = opts[:title] if opts[:title]
|
|
458
|
+
signal_connect('response') { destroy }
|
|
459
|
+
show_all
|
|
460
|
+
present # bring the window to the foreground & set focus
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
class InputBox < Gtk::Dialog
|
|
465
|
+
include WindowPos
|
|
466
|
+
attr_accessor :label, :textwidget
|
|
467
|
+
|
|
468
|
+
# shows a simplitic input box (eg window with a 1-line textbox + OK button), yields the text
|
|
469
|
+
# TODO history, dropdown, autocomplete, contexts, 3D stereo surround, etc
|
|
470
|
+
def initialize(owner, str, opts={})
|
|
471
|
+
owner ||= Gtk::Window.toplevels.first
|
|
472
|
+
super(nil, owner, Gtk::Dialog::DESTROY_WITH_PARENT,
|
|
473
|
+
[Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_REJECT])
|
|
474
|
+
self.title = opts[:title] if opts[:title]
|
|
475
|
+
|
|
476
|
+
@label = Gtk::Label.new(str)
|
|
477
|
+
@textwidget = Gtk::TextView.new
|
|
478
|
+
if opts[:text]
|
|
479
|
+
@textwidget.buffer.text = opts[:text].to_s
|
|
480
|
+
@textwidget.buffer.move_mark('selection_bound', @textwidget.buffer.start_iter)
|
|
481
|
+
@textwidget.buffer.move_mark('insert', @textwidget.buffer.end_iter)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
@textwidget.signal_connect('key_press_event') { |w, ev|
|
|
485
|
+
key = DrawableWidget::Keyboard_trad[ev.keyval]
|
|
486
|
+
case key
|
|
487
|
+
when :escape; response(RESPONSE_REJECT) ; true
|
|
488
|
+
when :enter; response(RESPONSE_ACCEPT) ; true
|
|
489
|
+
end
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
signal_connect('response') { |win, id|
|
|
493
|
+
resp = @textwidget.buffer.text if id == RESPONSE_ACCEPT
|
|
494
|
+
destroy
|
|
495
|
+
yield resp.strip if resp
|
|
496
|
+
true
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
vbox.pack_start label, false, false, 8
|
|
500
|
+
vbox.pack_start @textwidget, false, false, 8
|
|
501
|
+
|
|
502
|
+
Gtk::Drag.dest_set(self,
|
|
503
|
+
Gtk::Drag::DEST_DEFAULT_MOTION |
|
|
504
|
+
Gtk::Drag::DEST_DEFAULT_DROP,
|
|
505
|
+
[['text/plain', 0, 0], ['text/uri-list', 0, 0]],
|
|
506
|
+
Gdk::DragContext::ACTION_COPY | Gdk::DragContext::ACTION_MOVE)
|
|
507
|
+
|
|
508
|
+
signal_connect('drag_data_received') { |w, dc, x, y, data, info, time|
|
|
509
|
+
dc.targets.each { |target|
|
|
510
|
+
next if target.name != 'text/plain' and target.name != 'text/uri-list'
|
|
511
|
+
data.data.each_line { |l|
|
|
512
|
+
l = l.chomp.sub(%r{^file://}, '')
|
|
513
|
+
self.text = l
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
Gtk::Drag.finish(dc, true, false, time)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
show_all
|
|
521
|
+
present
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def text ; @textwidget.buffer.text ; end
|
|
525
|
+
def text=(nt) ; @textwidget.buffer.text = nt ; end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
class OpenFile < Gtk::FileChooserDialog
|
|
529
|
+
include WindowPos
|
|
530
|
+
@@currentfolder = nil
|
|
531
|
+
|
|
532
|
+
# shows an asynchronous FileChooser window, yields the chosen filename
|
|
533
|
+
# TODO save last path
|
|
534
|
+
def initialize(owner, title, opts={})
|
|
535
|
+
owner ||= Gtk::Window.toplevels.first
|
|
536
|
+
super(title, owner, Gtk::FileChooser::ACTION_OPEN, nil,
|
|
537
|
+
[Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT])
|
|
538
|
+
f = opts[:path] || @@currentfolder
|
|
539
|
+
self.current_folder = f if f
|
|
540
|
+
signal_connect('response') { |win, id|
|
|
541
|
+
if id == Gtk::Dialog::RESPONSE_ACCEPT
|
|
542
|
+
file = filename
|
|
543
|
+
@@currentfolder = File.dirname(file)
|
|
544
|
+
end
|
|
545
|
+
destroy
|
|
546
|
+
yield file if file
|
|
547
|
+
true
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
show_all
|
|
551
|
+
present
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
class SaveFile < Gtk::FileChooserDialog
|
|
556
|
+
include WindowPos
|
|
557
|
+
@@currentfolder = nil
|
|
558
|
+
|
|
559
|
+
# shows an asynchronous FileChooser window, yields the chosen filename
|
|
560
|
+
# TODO save last path
|
|
561
|
+
def initialize(owner, title, opts={})
|
|
562
|
+
owner ||= Gtk::Window.toplevels.first
|
|
563
|
+
super(title, owner, Gtk::FileChooser::ACTION_SAVE, nil,
|
|
564
|
+
[Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT])
|
|
565
|
+
f = opts[:path] || @@currentfolder
|
|
566
|
+
self.current_folder = f if f
|
|
567
|
+
signal_connect('response') { |win, id|
|
|
568
|
+
if id == Gtk::Dialog::RESPONSE_ACCEPT
|
|
569
|
+
file = filename
|
|
570
|
+
@@currentfolder = File.dirname(file)
|
|
571
|
+
end
|
|
572
|
+
destroy
|
|
573
|
+
yield file if file
|
|
574
|
+
true
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
show_all
|
|
578
|
+
present
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
class ListWindow < Gtk::Dialog
|
|
583
|
+
include WindowPos
|
|
584
|
+
# shows a window with a list of items
|
|
585
|
+
# the list is an array of arrays, displayed as String
|
|
586
|
+
# the first array is the column names
|
|
587
|
+
# each item clicked yields the block with the selected iterator, double-click also close the popup
|
|
588
|
+
def initialize(owner, title, list, h={})
|
|
589
|
+
owner ||= Gtk::Window.toplevels.first
|
|
590
|
+
super(title, owner, Gtk::Dialog::DESTROY_WITH_PARENT)
|
|
591
|
+
|
|
592
|
+
cols = list.shift
|
|
593
|
+
|
|
594
|
+
treeview = Gtk::TreeView.new
|
|
595
|
+
treeview.model = Gtk::ListStore.new(*[String]*cols.length)
|
|
596
|
+
treeview.selection.mode = Gtk::SELECTION_NONE
|
|
597
|
+
if @color_callback = h[:color_callback]
|
|
598
|
+
@drawable = DrawableWidget.new # needed for color()...
|
|
599
|
+
@drawable.signal_emit('realize')
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
cols.each_with_index { |col, i|
|
|
603
|
+
crt = Gtk::CellRendererText.new
|
|
604
|
+
tvc = Gtk::TreeViewColumn.new(col, crt)
|
|
605
|
+
tvc.sort_column_id = i
|
|
606
|
+
tvc.set_cell_data_func(crt) { |_tvc, _crt, model, iter|
|
|
607
|
+
_crt.text = iter[i]
|
|
608
|
+
if @color_callback
|
|
609
|
+
fu = (0...cols.length).map { |ii| iter[ii] }
|
|
610
|
+
fg, bg = @color_callback[fu]
|
|
611
|
+
fg ||= :black
|
|
612
|
+
bg ||= :white
|
|
613
|
+
_crt.foreground = @drawable.color(fg).to_s
|
|
614
|
+
_crt.cell_background = @drawable.color(bg).to_s
|
|
615
|
+
end
|
|
616
|
+
}
|
|
617
|
+
treeview.append_column tvc
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
list.each { |e|
|
|
621
|
+
iter = treeview.model.append
|
|
622
|
+
e.each_with_index { |v, i| iter[i] = v.to_s }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
treeview.model.set_sort_column_id(0)
|
|
626
|
+
|
|
627
|
+
treeview.signal_connect('cursor_changed') { |x|
|
|
628
|
+
if iter = treeview.selection.selected
|
|
629
|
+
yield iter
|
|
630
|
+
end
|
|
631
|
+
}
|
|
632
|
+
treeview.signal_connect('row_activated') { destroy }
|
|
633
|
+
|
|
634
|
+
signal_connect('destroy') { h[:ondestroy].call } if h[:ondestroy]
|
|
635
|
+
|
|
636
|
+
remove vbox
|
|
637
|
+
add Gtk::ScrolledWindow.new.add(treeview)
|
|
638
|
+
toplevel.set_default_size cols.length*120, 400
|
|
639
|
+
|
|
640
|
+
show if not h[:noshow]
|
|
641
|
+
|
|
642
|
+
# so that the 1st line is not selected by default
|
|
643
|
+
treeview.selection.mode = Gtk::SELECTION_SINGLE
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def show
|
|
647
|
+
show_all
|
|
648
|
+
present
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
class Window < Gtk::Window
|
|
653
|
+
include WindowPos
|
|
654
|
+
include Msgbox
|
|
655
|
+
|
|
656
|
+
attr_accessor :menu
|
|
657
|
+
def initialize(*a, &b)
|
|
658
|
+
super()
|
|
659
|
+
|
|
660
|
+
signal_connect('destroy') { destroy_window }
|
|
661
|
+
|
|
662
|
+
@vbox = Gtk::VBox.new
|
|
663
|
+
add @vbox
|
|
664
|
+
|
|
665
|
+
@menu = []
|
|
666
|
+
@menubar = Gtk::MenuBar.new
|
|
667
|
+
@accel_group = Gtk::AccelGroup.new
|
|
668
|
+
|
|
669
|
+
@vbox.add @menubar, 'expand' => false
|
|
670
|
+
@child = nil
|
|
671
|
+
s = Gdk::Screen.default
|
|
672
|
+
set_default_size s.width*3/4, s.height*3/4
|
|
673
|
+
|
|
674
|
+
Gtk::Settings.default.gtk_menu_bar_accel = nil # disable F10 -> focus menubar
|
|
675
|
+
|
|
676
|
+
(@@mainwindow_list ||= []) << self
|
|
677
|
+
|
|
678
|
+
initialize_window(*a, &b)
|
|
679
|
+
build_menu
|
|
680
|
+
update_menu
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
Gtk::Drag.dest_set(self,
|
|
684
|
+
Gtk::Drag::DEST_DEFAULT_MOTION |
|
|
685
|
+
Gtk::Drag::DEST_DEFAULT_DROP,
|
|
686
|
+
[['text/plain', 0, 0], ['text/uri-list', 0, 0]],
|
|
687
|
+
Gdk::DragContext::ACTION_COPY | Gdk::DragContext::ACTION_MOVE)
|
|
688
|
+
|
|
689
|
+
signal_connect('drag_data_received') { |w, dc, x, y, data, info, time|
|
|
690
|
+
dc.targets.each { |target|
|
|
691
|
+
next if target.name != 'text/plain' and target.name != 'text/uri-list'
|
|
692
|
+
data.data.each_line { |l|
|
|
693
|
+
next if not @child or not @child.respond_to? :dragdropfile
|
|
694
|
+
l = l.chomp.sub(%r{^file://}, '')
|
|
695
|
+
protect { @child.dragdropfile(l) }
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
Gtk::Drag.finish(dc, true, false, time)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
show_all
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
def destroy_window
|
|
705
|
+
@@mainwindow_list.delete self
|
|
706
|
+
Gui.main_quit if @@mainwindow_list.empty? # XXX we don't call main_start ourself..
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def widget=(w)
|
|
710
|
+
@vbox.remove @child if @child
|
|
711
|
+
@child = w
|
|
712
|
+
@vbox.add w if w
|
|
713
|
+
show_all
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def widget
|
|
717
|
+
@child
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def build_menu
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def new_menu
|
|
724
|
+
[]
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# finds a menu by name (recursive)
|
|
728
|
+
# returns a valid arg for addsubmenu(ret)
|
|
729
|
+
def find_menu(name, from=@menu)
|
|
730
|
+
name = name.gsub('_', '')
|
|
731
|
+
if not l = from.find { |e| e.grep(::String).find { |es| es.gsub('_', '') == name } }
|
|
732
|
+
l = from.map { |e| e.grep(::Array).map { |ae| find_menu(name, ae) }.compact.first }.compact.first
|
|
733
|
+
end
|
|
734
|
+
l.grep(::Array).first if l
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# append stuff to a menu
|
|
738
|
+
# arglist:
|
|
739
|
+
# empty = menu separator
|
|
740
|
+
# string = menu entry display name (use a single '_' keyboard for shortcut, eg 'Sho_rtcut' => 'r')
|
|
741
|
+
# :check = menu entry is a checkbox type, add a true/false argument to specify initial value
|
|
742
|
+
# second string = keyboard shortcut (accelerator) - use '^' for Ctrl, and '<up>' for special keys
|
|
743
|
+
# a menu object = this entry will open a submenu (you must specify a name, and action is ignored)
|
|
744
|
+
# the method takes a block or a Proc argument that will be run whenever the menu item is selected
|
|
745
|
+
#
|
|
746
|
+
# use @menu to reference the top-level menu bar
|
|
747
|
+
# call update_menu when the menu is done
|
|
748
|
+
def addsubmenu(menu, *args, &action)
|
|
749
|
+
args << action if action
|
|
750
|
+
menu << args
|
|
751
|
+
menu.last
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# make the window's MenuBar reflect the content of @menu
|
|
755
|
+
def update_menu
|
|
756
|
+
# clear
|
|
757
|
+
@menubar.children.dup.each { |mc| @menubar.remove mc }
|
|
758
|
+
# populate the menubar using @menu
|
|
759
|
+
@menu.each { |e| create_menu_item(@menubar, e) }
|
|
760
|
+
@menubar.show_all
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def create_menu_item(menu, entry)
|
|
764
|
+
args = entry.dup
|
|
765
|
+
|
|
766
|
+
# recognise 'OPEN', 'SAVE' etc, with special icon/localisation
|
|
767
|
+
stock = (Gtk::Stock.constants.map { |c| c.to_s } & args).first
|
|
768
|
+
args.delete stock if stock
|
|
769
|
+
accel = args.grep(/^\^?(\w|<\w+>)$/).first
|
|
770
|
+
args.delete accel if accel
|
|
771
|
+
check = args.delete :check
|
|
772
|
+
action = args.grep(::Proc).first
|
|
773
|
+
args.delete action if action
|
|
774
|
+
if submenu = args.grep(::Array).first
|
|
775
|
+
args.delete submenu
|
|
776
|
+
sm = Gtk::Menu.new
|
|
777
|
+
submenu.each { |e| create_menu_item(sm, e) }
|
|
778
|
+
submenu = sm
|
|
779
|
+
end
|
|
780
|
+
label = args.shift
|
|
781
|
+
|
|
782
|
+
if stock
|
|
783
|
+
item = Gtk::ImageMenuItem.new(Gtk::Stock.const_get(stock))
|
|
784
|
+
begin
|
|
785
|
+
item.label = label if label
|
|
786
|
+
rescue
|
|
787
|
+
# in some version of gtk, no #label=
|
|
788
|
+
item = Gtk::MenuItem.new(label) if label
|
|
789
|
+
end
|
|
790
|
+
elsif check
|
|
791
|
+
item = Gtk::CheckMenuItem.new(label)
|
|
792
|
+
item.active = args.shift
|
|
793
|
+
elsif label
|
|
794
|
+
item = Gtk::MenuItem.new(label)
|
|
795
|
+
else
|
|
796
|
+
item = Gtk::MenuItem.new
|
|
797
|
+
end
|
|
798
|
+
item.set_submenu(submenu) if submenu
|
|
799
|
+
|
|
800
|
+
if accel
|
|
801
|
+
key = accel[-1]
|
|
802
|
+
if key == ?>
|
|
803
|
+
key = accel[/<(.*)>/, 1]
|
|
804
|
+
key = case key
|
|
805
|
+
when 'enter'; Gdk::Keyval::GDK_Return
|
|
806
|
+
when 'esc'; Gdk::Keyval::GDK_Escape
|
|
807
|
+
when 'tab'; Gdk::Keyval::GDK_Tab
|
|
808
|
+
when /^f(\d\d?)$/i; Gdk::Keyval.const_get("GDK_#{key.upcase}")
|
|
809
|
+
else ??
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
key = key.unpack('C')[0] if key.kind_of? String # yay rb19
|
|
813
|
+
item.add_accelerator('activate', @accel_group, key, (accel[0] == ?^ ? Gdk::Window::CONTROL_MASK : 0), Gtk::ACCEL_VISIBLE)
|
|
814
|
+
end
|
|
815
|
+
if action
|
|
816
|
+
a = action
|
|
817
|
+
if check
|
|
818
|
+
a = lambda { item.active = action.call(item.active?) }
|
|
819
|
+
end
|
|
820
|
+
item.signal_connect('activate') { protect { a.call(item) } }
|
|
821
|
+
end
|
|
822
|
+
menu.append item
|
|
823
|
+
item
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def initialize_window
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
class ToolWindow < Gtk::Dialog
|
|
831
|
+
include WindowPos
|
|
832
|
+
|
|
833
|
+
def initialize(parent=nil, *a, &b)
|
|
834
|
+
super('toolwin', parent, Gtk::Dialog::DESTROY_WITH_PARENT)
|
|
835
|
+
set_events Gdk::Event::ALL_EVENTS_MASK
|
|
836
|
+
set_can_focus true
|
|
837
|
+
@child = vbox
|
|
838
|
+
initialize_window(*a, &b)
|
|
839
|
+
show_all
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def widget=(w)
|
|
843
|
+
remove @child if @child
|
|
844
|
+
@child = w
|
|
845
|
+
add @child
|
|
846
|
+
if @child.respond_to? :initial_size
|
|
847
|
+
resize(*@child.initial_size)
|
|
848
|
+
end
|
|
849
|
+
show_all
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def widget
|
|
853
|
+
@child
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
# start the Gui main loop
|
|
858
|
+
def self.main
|
|
859
|
+
Gtk.main
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# ends the Gui main loop
|
|
863
|
+
def self.main_quit
|
|
864
|
+
Gtk.main_quit
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
# register a proc to be run whenever the gui loop is idle
|
|
868
|
+
# if the proc returns nil/false, delete it
|
|
869
|
+
def self.idle_add(&b)
|
|
870
|
+
Gtk.idle_add(&b)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# run a single iteration of the main_loop
|
|
874
|
+
# e.g. call this from time to time when doing heavy computation, to keep the UI somewhat responsive
|
|
875
|
+
def self.main_iter
|
|
876
|
+
Gtk.main_iteration_do(false)
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
require 'metasm/gui/dasm_main'
|
|
883
|
+
require 'metasm/gui/debug'
|
|
884
|
+
|