GtkSimpleLayout 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,488 @@
1
+ require 'gtk2'
2
+
3
+ module SimpleLayout
4
+
5
+ class LayoutError < Exception; end
6
+
7
+ class EventHandlerProxy
8
+ def initialize(host, evt, &block)
9
+ @host = host
10
+ @evt = evt
11
+ self << block if block
12
+ end
13
+ def <<(v)
14
+ if v && v.respond_to?('call')
15
+ @host.signal_connect(@evt) do |*args|
16
+ v.call(*args)
17
+ end
18
+ end
19
+ self
20
+ end
21
+ end
22
+
23
+ module ExtClassMethod
24
+ def inspector_opt(opt = nil)
25
+ @insp_opt ||= {
26
+ :enable => (ENV['INSPECTOR_ENABLE'] == '1'),
27
+ :border_width => (ENV['INSPECTOR_BORDER_WIDTH'] || 5)
28
+ }
29
+ @insp_opt.merge! opt if opt
30
+ @insp_opt
31
+ end
32
+
33
+ def layout_class_maps
34
+ @layout_class_maps_hash ||= {
35
+ 'image' => Gtk::Image,
36
+ 'label' => Gtk::Label,
37
+ 'progress_bar' => Gtk::ProgressBar,
38
+ 'status_bar' => Gtk::Statusbar,
39
+ 'button' => Gtk::Button,
40
+ 'check_button' => Gtk::CheckButton,
41
+ 'radio_button' => Gtk::RadioButton,
42
+ 'toggle_button' => Gtk::ToggleButton,
43
+ 'link_button' => Gtk::LinkButton,
44
+ 'entry' => Gtk::Entry,
45
+ 'hscale' => Gtk::HScale,
46
+ 'vscale' => Gtk::VScale,
47
+ 'spin_button' => Gtk::SpinButton,
48
+ 'text_view' => Gtk::TextView,
49
+ 'tree_view' => Gtk::TreeView,
50
+ 'cell_view' => Gtk::CellView,
51
+ 'icon_view' => Gtk::IconView,
52
+ 'combobox' => Gtk::ComboBox,
53
+ 'combobox_entry' => Gtk::ComboBoxEntry,
54
+ #'menu' => Gtk::Menu,
55
+ #'menubar' => Gtk::MenuBar,
56
+ 'toolbar' => Gtk::Toolbar,
57
+ 'toolitem' => Gtk::ToolItem,
58
+ 'separator_toolitem' => Gtk::SeparatorToolItem,
59
+ 'tool_button' => Gtk::ToolButton,
60
+ 'toggle_tool_button' => Gtk::ToggleToolButton,
61
+ 'radio_tool_button' => Gtk::RadioToolButton,
62
+ 'color_button' => Gtk::ColorButton,
63
+ 'color_selection' => Gtk::ColorSelection,
64
+ 'file_chooser_button' => Gtk::FileChooserButton,
65
+ 'file_chooser_widget' => Gtk::FileChooserWidget,
66
+ 'font_button' => Gtk::FontButton,
67
+ 'font_selection' => Gtk::FontSelection,
68
+ 'alignment' => Gtk::Alignment,
69
+ 'aspect_frame' => Gtk::AspectFrame,
70
+ 'hbox' => Gtk::HBox,
71
+ 'vbox' => Gtk::VBox,
72
+ 'hbutton_box' => Gtk::HButtonBox,
73
+ 'vbutton_box' => Gtk::VButtonBox,
74
+ 'hpaned' => Gtk::HPaned,
75
+ 'vpaned' => Gtk::VPaned,
76
+ 'layout' => Gtk::Layout,
77
+ 'notebook' => Gtk::Notebook,
78
+ 'table' => Gtk::Table,
79
+ 'expander' => Gtk::Expander,
80
+ 'frame' => Gtk::Frame,
81
+ 'hseparator' => Gtk::HSeparator,
82
+ 'vseparator' => Gtk::VSeparator,
83
+ 'hscrollbar' => Gtk::HScrollbar,
84
+ 'vscrollbar' => Gtk::VScrollbar,
85
+ 'scrolled_window' => Gtk::ScrolledWindow,
86
+ 'arrow' => Gtk::Arrow,
87
+ 'calendar' => Gtk::Calendar,
88
+ 'drawing_area' => Gtk::DrawingArea,
89
+ 'event_box' => Gtk::EventBox,
90
+ 'handle_box' => Gtk::HandleBox,
91
+ 'viewport' => Gtk::Viewport,
92
+ 'curve' => Gtk::Curve,
93
+ 'gamma_curve' => Gtk::GammaCurve,
94
+ 'hruler' => Gtk::HRuler,
95
+ 'vruler' => Gtk::VRuler,
96
+ }
97
+ end
98
+
99
+ end
100
+
101
+ module Base
102
+ def Base.included(base)
103
+ base.extend(ExtClassMethod)
104
+ base.layout_class_maps.each do |k, v|
105
+ define_method(k) do |*args, &block|
106
+ create_component(v, args, block)
107
+ end
108
+ end
109
+ end
110
+
111
+ public
112
+
113
+ # register automatic event handlers
114
+ def register_auto_events()
115
+ self.methods.each do |name|
116
+ if name =~ /^(.+)_on_(.+)$/
117
+ w, evt = $1, $2
118
+ w = component(w.to_sym)
119
+ w.signal_connect(evt) do |*args| self.send(name, *args) end if w
120
+ end
121
+ end
122
+ end
123
+
124
+ # expose the components as instance variables
125
+ def expose_components()
126
+ @components.each_key do |k|
127
+ unless self.respond_to?(k.to_s, true)
128
+ self.instance_eval("def #{k.to_s}; component(:#{k.to_s}) end")
129
+ else
130
+ raise LayoutError, "#{k} is conflit with method, please redifine component id"
131
+ end
132
+ end
133
+ end
134
+
135
+ # add a widget to container (and/or become a new container as well).
136
+ # do not call this function directly unless knowing what you are doing
137
+ def add_component(w, container, layout_opt = nil)
138
+ if @pass_on_stack.last.nil? || @pass_on_stack.last[0] == false
139
+ if container.is_a?(Gtk::Box)
140
+ layout_opt ||= [false, false, 0]
141
+ pack_method = 'pack_start'
142
+ if layout_opt.first.is_a?(Symbol)
143
+ pack_method = 'pack_end' if layout_opt.shift == :end
144
+ end
145
+ container.send(pack_method, w, *layout_opt)
146
+ elsif container.is_a?(Gtk::Fixed) || container.is_a?(Gtk::Layout)
147
+ layout_opt ||= [0, 0]
148
+ container.put w, *layout_opt
149
+ elsif container.is_a?(Gtk::MenuShell)
150
+ container.append w
151
+ elsif container.is_a?(Gtk::Toolbar)
152
+ container.insert(container.n_items, w)
153
+ elsif container.is_a?(Gtk::MenuToolButton)
154
+ container.menu = w
155
+ elsif container.is_a?(Gtk::Table)
156
+ # should use #grid or #grid_flx to add a child to Table
157
+ elsif container.is_a?(Gtk::Notebook)
158
+ # should use #page to add a child to Notebook
159
+ elsif container.is_a?(Gtk::Paned)
160
+ # should use #area_first or #area_second to add child to Paned
161
+ elsif container.is_a?(Gtk::Container)
162
+ layout_opt ||= []
163
+ container.add(w, *layout_opt)
164
+ end
165
+ else
166
+ fun_name, args = *(@pass_on_stack.last[1])
167
+ container.send(fun_name, w, *args)
168
+ end
169
+ end
170
+
171
+ # create a "with block" for setup common attributes
172
+ def with_attr(options = {}, &block)
173
+ if block
174
+ @common_attribute ||= []
175
+ @common_attribute.push options
176
+ cnt, _ = @containers.last
177
+ block.call(cnt)
178
+ @common_attribute.pop
179
+ end
180
+ end
181
+
182
+ # get component with given name
183
+ def component(name)
184
+ @components[name]
185
+ end
186
+
187
+ # return children array of a component or group
188
+ def component_children(name)
189
+ @component_children ||= {}
190
+ @component_children[name]
191
+ end
192
+
193
+ # group the children
194
+ def group(name)
195
+ cnt, misc = @containers.last
196
+ gs = (name ? [name].flatten : [])
197
+ gs.each{|g| @component_children[g] ||= [] }
198
+ m = { :groups => gs,
199
+ :virtual => true,
200
+ :sibling => misc[:sibling],
201
+ :insp => misc[:insp],
202
+ :layout => misc[:layout],
203
+ :options => misc[:options],
204
+ :name => nil,
205
+ }
206
+ @containers.push [cnt, m]
207
+ yield cnt if block_given?
208
+ @containers.pop
209
+ end
210
+
211
+ # for HPaned and VPaned container
212
+ def area_first(resize = true, shrink = true, &block)
213
+ container_pass_on(Gtk::Paned, 'pack1', resize, shrink, block)
214
+ end
215
+ def area_second(resize = true, shrink = true, &block)
216
+ container_pass_on(Gtk::Paned, 'pack2', resize, shrink, block)
217
+ end
218
+
219
+ # for Notebook container
220
+ def page(text = nil, &block)
221
+ container_pass_on(Gtk::Notebook, 'append_page', Gtk::Label.new(text), block)
222
+ end
223
+
224
+ # for Table container
225
+ def grid_flx(left, right, top, bottom, *args, &block)
226
+ args.push block
227
+ container_pass_on(Gtk::Table, 'attach', left, right, top, bottom, *args)
228
+ end
229
+ def grid(left, top, *args, &block)
230
+ args.push block
231
+ container_pass_on(Gtk::Table, 'attach', left, left + 1, top, top + 1, *args)
232
+ end
233
+
234
+ # menu stuff
235
+ def factory_menu_bar(name, options = {}, &block)
236
+ cb = Proc.new do |id, w|
237
+ id = id.gsub('_', '') if id.is_a?(String)
238
+ m = "menu_#{name}_on_active"
239
+ self.send(m, id, Gtk::ItemFactory.path_from_widget(w), w) if self.respond_to?(m)
240
+ end
241
+ @item_factory_stack ||= []
242
+ @item_factory_stack.push [cb, [], []]
243
+ block.call(name) if block
244
+ options[:id] ||= name.to_sym
245
+ _, _, items = @item_factory_stack.pop
246
+ accel_group = Gtk::AccelGroup.new
247
+ add_accel_group(accel_group)
248
+ fact = Gtk::ItemFactory.new(Gtk::ItemFactory::TYPE_MENU_BAR, "<#{name}>", accel_group)
249
+ fact.create_items(items)
250
+
251
+ # process item attributes
252
+ items.each do |x|
253
+ # TODO: ...
254
+ end
255
+ layout_component(fact.get_widget("<#{name}>"), options)
256
+ end
257
+
258
+ def factory_menu_item(name, options = {}, &block)
259
+ cb, stack, items = @item_factory_stack.last
260
+ branch = false
261
+ options[:type] ||= :Item
262
+ case name
263
+ when /^[-]+$/
264
+ options[:type] = :Separator
265
+ when /^<[-]+>$/
266
+ options[:type] = :Tearoff
267
+ when /^>>(.+)>>$/
268
+ name = $1
269
+ branch = true
270
+ options[:type] = :LastBranch
271
+ when /^<(.+)>$/
272
+ name = $1
273
+ branch = true
274
+ options[:type] = :Branch
275
+ end
276
+
277
+ image = options.delete(:image)
278
+ if image.is_a?(String)
279
+ options[:type] = :ImageItem
280
+ image = Gdk::Pixbuf.new(image)
281
+ elsif image.is_a?(Gdk::Pixbuf)
282
+ options[:type] = :ImageItem
283
+ elsif image
284
+ options[:type] = :StockItem
285
+ end
286
+
287
+ item = [ "#{stack.last}/#{name}",
288
+ "<#{options[:type].to_s}>",
289
+ options[:accel],
290
+ image,
291
+ cb,
292
+ options[:id] || name
293
+ ]
294
+ items << item
295
+ if branch
296
+ stack.push "#{stack.last}/#{name}"
297
+ block.call(name) if block
298
+ stack.pop if branch
299
+ end
300
+ item
301
+ end
302
+
303
+ # layout the new UI component (container or widget)
304
+ def layout_component(w, options = {}, &block)
305
+ @containers ||= []
306
+ @pass_on_stack ||= []
307
+ @components ||= {}
308
+ @common_attribute ||= []
309
+ @component_children ||= {}
310
+
311
+ add_singleton_event_map(w) # so that you can use: w.on_clicked{|*args| ... }
312
+
313
+ name = options.delete(:id)
314
+ group_name = options.delete(:gid) || name
315
+ layout_opt = options.delete(:layout)
316
+ keep_top_cnt = options.delete(:keep_top_container)
317
+
318
+ options.each do |k, v|
319
+ if v.is_a?(Array)
320
+ w.send(k.to_s, *v) if w.respond_to?(k.to_s)
321
+ else
322
+ w.send(k.to_s + '=', v) if w.respond_to?(k.to_s + '=')
323
+ end
324
+ end
325
+
326
+ @components[name] = w if name
327
+ gs = (group_name ? [group_name].flatten : [])
328
+ gs.each{|g| @component_children[g] ||= [] }
329
+
330
+ misc = nil
331
+ if @containers.size > 0
332
+ container, misc = @containers.last
333
+ misc[:groups].each{ |g| @component_children[g].push w }
334
+ misc[:sibling] += 1
335
+ end
336
+
337
+ unless container and container.is_a? Gtk::ScrolledWindow
338
+ insp_evb = make_inspect_evb(misc, w, name, layout_opt, options)
339
+ end
340
+
341
+ if block # if given block, it's a container as well
342
+ m = { :groups => gs,
343
+ :sibling => 0,
344
+ :insp => insp_evb,
345
+ :name => name,
346
+ :layout => layout_opt,
347
+ :options => options,
348
+ }
349
+ @containers.push [w, m]
350
+ @pass_on_stack.push [false, nil]
351
+ @common_attribute.push({})
352
+ block.call(w) if block
353
+ @common_attribute.pop
354
+ @pass_on_stack.pop
355
+ @containers.pop
356
+ end
357
+
358
+ if @containers.size > 0
359
+ add_component(insp_evb || w, container, layout_opt) # add myself to parent
360
+ else
361
+ add_component(insp_evb || w, self, layout_opt) unless keep_top_cnt # add top container to host
362
+ @components[:self] = self # add host as ':self'
363
+ end
364
+ w
365
+ end
366
+
367
+ private
368
+
369
+ def add_singleton_event_map(w)
370
+ class << w
371
+ alias_method :simple_layout_singleton_method_missing, :method_missing
372
+ def method_missing(sym, *args, &block)
373
+ if sym.to_s =~ /^on_([^=]+)(=*)$/
374
+ block ||= args.last
375
+ return EventHandlerProxy.new(self, $1, &block)
376
+ else
377
+ simple_layout_singleton_method_missing(sym, *args, &block)
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ # create the inspector eventbox for widget
384
+ def make_inspect_evb(cnt_misc, w, name, layout_opt, options)
385
+ insp_evb = nil
386
+ insp_opt = self.class.inspector_opt
387
+ if insp_opt[:enable]
388
+ rgb = 0xffff - @containers.size * 0x1000
389
+ insp_evb = evb = Gtk::EventBox.new
390
+ sub_evb = Gtk::EventBox.new
391
+ sub_evb.add w
392
+ evb.add sub_evb
393
+ sub_evb.border_width = insp_opt[:border_width]
394
+ evb.modify_bg Gtk::STATE_NORMAL, Gdk::Color.new(rgb, rgb, rgb)
395
+ evbs = []
396
+ tips = ""
397
+ @containers.size.times do |i|
398
+ cnt, m = @containers[i]
399
+ if m[:insp] && (not m[:virtual])
400
+ evbs << m[:insp]
401
+ tips << "<b>container[#{i}]: #{cnt.class}#{m[:name] ? " (#{m[:name]})" : ''}</b>\n"
402
+ tips << " layout: #{m[:layout].inspect}\n" if m[:layout]
403
+ tips << " options: #{m[:options].inspect}\n" if m[:options] && m[:options].size > 0
404
+ tips << " groups: #{m[:groups].inspect}\n" if m[:groups].size > 0
405
+ end
406
+ end
407
+ evbs << evb
408
+ tips << "<b>widget: #{w.class}#{name ? " (#{name})" : ''}</b>\n"
409
+ tips << " layout: #{layout_opt.inspect}\n" if layout_opt
410
+ tips << " options: #{options.inspect}\n" if options && options.size > 0
411
+ tips << " groups: #{cnt_misc[:groups].inspect}\n" if cnt_misc && cnt_misc[:groups].size > 0
412
+
413
+ evb.signal_connect('event') do |b, evt|
414
+ b.tooltip_markup = tips
415
+ case evt.event_type
416
+ when Gdk::Event::ENTER_NOTIFY, Gdk::Event::LEAVE_NOTIFY
417
+ evbs.size.times do |i|
418
+ rgb = 0xffff - i * 0x1000
419
+ if evt.event_type == Gdk::Event::ENTER_NOTIFY
420
+ evbs[i].modify_bg Gtk::STATE_NORMAL, Gdk::Color.new(rgb, rgb - 0x2000, rgb - 0x2000)
421
+ elsif evt.event_type == Gdk::Event::LEAVE_NOTIFY
422
+ evbs[i].modify_bg Gtk::STATE_NORMAL, Gdk::Color.new(rgb, rgb, rgb)
423
+ end
424
+ end
425
+ end
426
+ end
427
+ end
428
+ insp_evb
429
+ end
430
+
431
+ # create a new UI component (container or widget)
432
+ def create_component(component_class, args, block)
433
+ @common_attribute ||= []
434
+ options = {}
435
+ options = args.pop if args.last.is_a?(Hash)
436
+ options.merge! @common_attribute.last if @common_attribute.last
437
+
438
+ w = component_class.new(*args)
439
+ layout_component(w, options, &block)
440
+ end
441
+
442
+ def container_pass_on(container_class, fun_name, *args)
443
+ block = args.pop # the last arg is Proc or nil
444
+ cnt, _ = @containers.last
445
+ if cnt.is_a?(container_class)
446
+ @pass_on_stack.push [true, [fun_name, args]]
447
+ block.call(cnt) if block
448
+ @pass_on_stack.pop
449
+ else
450
+ raise LayoutError, "class #{container_class} expected"
451
+ end
452
+ end
453
+
454
+ alias_method :simple_layout_method_missing_alias, :method_missing
455
+
456
+ def method_missing(sym, *args, &block)
457
+ if sym.to_s =~ /^(.+)_in_(.+)$/
458
+ maps = self.class.layout_class_maps
459
+ inner, outter = $1, $2
460
+ if maps[inner] && maps[outter]
461
+ if args.last.is_a?(Hash)
462
+ options = {}
463
+ options = args.pop if args.last.is_a?(Hash)
464
+
465
+ # default args pass to inner component, execpt:
466
+ # :layout pass to outter :layout
467
+ # :inner_layout pass to inner :layout
468
+ # :outter_args pass to outter args
469
+ outter_args, outter_layout_opt, options[:layout] =
470
+ options.delete(:outter_args), options.delete(:layout), options.delete(:inner_layout)
471
+
472
+ outter_args = (outter_args ? [outter_args] : []) unless outter_args.is_a?(Array)
473
+ outter_args << {} unless outter_args.last.is_a?(Hash)
474
+ outter_args.last[:layout] ||= outter_layout_opt
475
+ args.push options # push back inner options
476
+ end
477
+
478
+ inner_proc = Proc.new do
479
+ create_component(maps[inner], args, block)
480
+ end
481
+ return create_component(maps[outter], outter_args || [], inner_proc)
482
+ end
483
+ end
484
+ simple_layout_method_missing_alias(sym, *args, &block)
485
+ end
486
+
487
+ end
488
+ end