GtkSimpleLayout 0.2.1

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.
@@ -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