tk_component 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,10 +35,10 @@ module TkComponent
35
35
  end
36
36
 
37
37
  def set_weights(row, col, weights = {})
38
- hw = weights[:h_weight]
39
- @row_weights[row] = ((rw = @row_weights[row]).present? ? [rw, hw].max : hw) if hw
40
- vw = weights[:v_weight]
41
- @column_weights[col] = ((cw = @column_weights[col]).present? ? [cw, vw].max : vw) if vw
38
+ vw = weights[:y_flex]
39
+ @row_weights[row] = ((rw = @row_weights[row]).present? ? [rw, vw].max : vw) if vw
40
+ hw = weights[:x_flex]
41
+ @column_weights[col] = ((cw = @column_weights[col]).present? ? [cw, hw].max : hw) if hw
42
42
  end
43
43
 
44
44
  def row_indexes
@@ -1,16 +1,19 @@
1
1
  module TkComponent
2
2
  module Builder
3
3
 
4
- TK_CMDS = %w(label entry button canvas text scale group).to_set.freeze
4
+ TK_CMDS = %w(label entry button radio_set radio_button canvas text scale group tree tree_node hscroll_bar vscroll_bar hpaned vpaned).to_set.freeze
5
5
  LAYOUT_CMDS = %w(frame hframe vframe row cell).to_set.freeze
6
- EVENT_CMDS = %w(on_change on_mouse_down on_mouse_up on_mouse_drag on_mouse_wheel on_click on_event).to_set.freeze
6
+ EVENT_CMDS = %w(on_change on_mouse_down on_mouse_up on_mouse_drag on_mouse_wheel on_click on_select on_item_open on_event).to_set.freeze
7
7
  TOKENS = (TK_CMDS + LAYOUT_CMDS + EVENT_CMDS).freeze
8
8
 
9
+ LAYOUT_OPTIONS = %i(column row rowspan columnspan sticky x_flex y_flex)
10
+
9
11
  class Node
10
12
  attr_accessor :name
11
13
  attr_accessor :options
12
14
  attr_accessor :sub_nodes
13
15
  attr_accessor :grid
16
+ attr_accessor :weights
14
17
  attr_accessor :grid_map
15
18
  attr_accessor :event_handlers
16
19
  attr_accessor :tk_item
@@ -29,14 +32,14 @@ module TkComponent
29
32
  @name = name
30
33
  @options = options.with_indifferent_access
31
34
  @sub_nodes = []
32
- @grid = {}
35
+ @grid = {}.with_indifferent_access
36
+ @weights = {}.with_indifferent_access
33
37
  @grid_map = GridMap.new
34
38
  @event_handlers = []
35
39
  @tk_item = nil
36
40
  end
37
41
 
38
42
  def short(level = 0)
39
- puts(" " * level + " #{@name}")
40
43
  @sub_nodes.each do |n|
41
44
  n.short(level + 4)
42
45
  end
@@ -44,10 +47,12 @@ module TkComponent
44
47
  end
45
48
 
46
49
  def insert_component(component_class, parent_component, options = {}, &block)
47
- c_node = node_from_command(:frame, &block)
50
+ layout_options = options.slice(*LAYOUT_OPTIONS)
51
+ c_node = node_from_command(:frame, layout_options, &block)
48
52
  comp = component_class.new(options.merge(parent: parent_component, parent_node: c_node))
49
53
  comp.generate(parent_component, options)
50
54
  parent_component.add_child(comp)
55
+ comp
51
56
  end
52
57
 
53
58
  def add_event_handler(name, lambda, options = {})
@@ -61,9 +66,25 @@ module TkComponent
61
66
  sub_nodes.each do |n|
62
67
  n.build(self, parent_component)
63
68
  end
69
+ apply_grid
70
+ built
71
+ end
72
+
73
+ def apply_grid
64
74
  self.tk_item.apply_internal_grid(grid_map)
65
75
  end
66
76
 
77
+ def built
78
+ self.tk_item.built
79
+ end
80
+
81
+ def rebuilt
82
+ end
83
+
84
+ def remove
85
+ self.tk_item.remove
86
+ end
87
+
67
88
  def prepare_option_events(component)
68
89
  option_events = options.extract!(*EVENT_CMDS)
69
90
  option_events.each do |k, v|
@@ -74,6 +95,7 @@ module TkComponent
74
95
  end
75
96
 
76
97
  def prepare_grid
98
+ self.grid_map = GridMap.new
77
99
  return unless self.sub_nodes.any?
78
100
  current_row = -1
79
101
  current_col = -1
@@ -90,13 +112,15 @@ module TkComponent
90
112
  current_col = 0 if current_col < 0
91
113
  current_row, current_col = grid_map.get_next_cell(current_row, current_col, going_down)
92
114
  binding.pry if n.options.nil?
93
- grid = n.options.extract!(:column, :row, :rowspan, :columnspan, :sticky)
94
- n.grid = grid.merge(column: current_col, row: current_row)
95
- rowspan = grid[:rowspan] || 1
96
- columnspan = grid[:columnspan] || 1
115
+ n.grid = {}.with_indifferent_access if n.grid.nil?
116
+ n.grid.merge!(n.options.extract!(:column, :row, :rowspan, :columnspan, :sticky))
117
+ n.grid.merge!(column: current_col, row: current_row)
118
+ rowspan = n.grid[:rowspan] || 1
119
+ columnspan = n.grid[:columnspan] || 1
97
120
  grid_map.fill(current_row, current_col, rowspan, columnspan, true)
98
- weights = n.options.extract!(:h_weight, :v_weight)
99
- grid_map.set_weights(current_row, current_col, weights)
121
+ n.weights = {}.with_indifferent_access if n.weights.nil?
122
+ n.weights.merge!(n.options.extract!(:x_flex, :y_flex))
123
+ grid_map.set_weights(current_row, current_col, n.weights)
100
124
  n.prepare_grid
101
125
  final_sub_nodes << n
102
126
  end
@@ -11,34 +11,43 @@ module TkComponent
11
11
  end
12
12
 
13
13
  def initialize(parent_item, name, options = {}, grid = {}, event_handlers = [])
14
- tk_class = TK_CLASSES[name.to_sym]
15
- raise "Don't know how to create #{name}" unless tk_class
16
- @native_item = tk_class.new(parent_item.native_item)
14
+ @native_item = create_native_item(parent_item.native_item, name, options, grid, event_handlers)
17
15
  apply_options(options)
18
16
  set_grid(grid)
19
17
  set_event_handlers(event_handlers)
20
18
  end
21
19
 
22
- def apply_options(options)
20
+ def create_native_item(parent_native_item, name, options = {}, grid = {}, event_handlers = [])
21
+ native_item_class(parent_native_item, name, options, grid, event_handlers).new(parent_native_item)
22
+ end
23
+
24
+ def native_item_class(parent_native_item, name, options = {}, grid = {}, event_handlers = [])
25
+ tk_class = TK_CLASSES[name.to_sym]
26
+ raise "Don't know how to create #{name}" unless tk_class.present?
27
+ return tk_class
28
+ end
29
+
30
+ def remove
31
+ @native_item.destroy
32
+ end
33
+
34
+ def apply_options(options, to_item = self.native_item)
23
35
  options.each do |k,v|
24
- apply_option(k, v)
36
+ apply_option(k, v, to_item)
25
37
  end
26
38
  end
27
39
 
28
- def apply_option(option, value)
29
- self.native_item.public_send(option, value)
40
+ def apply_option(option, value, to_item = self.native_item)
41
+ to_item.public_send(option, value)
30
42
  end
31
43
 
32
- def set_grid(grid)
33
- self.native_item.grid(grid)
44
+ def set_grid(grid, to_item = self.native_item)
45
+ to_item.grid(grid)
34
46
  end
35
47
 
36
48
  def apply_internal_grid(grid_map)
37
- puts(grid_map)
38
49
  grid_map.column_indexes.each { |c| TkGrid.columnconfigure(self.native_item, c, weight: grid_map.column_weight(c)) }
39
50
  grid_map.row_indexes.each { |r| TkGrid.rowconfigure(self.native_item, r, weight: grid_map.row_weight(r)) }
40
- # grid_map.column_indexes.each { |c| TkGrid.columnconfigure(self.native_item, c, weight: 1) }
41
- # grid_map.row_indexes.each { |r| TkGrid.rowconfigure(self.native_item, r, weight: 1) }
42
51
  end
43
52
 
44
53
  def set_event_handlers(event_handlers)
@@ -55,10 +64,17 @@ module TkComponent
55
64
  Event.bind_event(event_handler.name, self, event_handler.options, event_handler.lambda)
56
65
  end
57
66
  end
67
+
68
+ def built
69
+ end
70
+
71
+ def focus
72
+ self.native_item.focus
73
+ end
58
74
  end
59
75
 
60
76
  module ValueTyping
61
- def apply_option(option, v)
77
+ def apply_option(option, v, to_item = self.native_item)
62
78
  case option.to_sym
63
79
  when :value
64
80
  self.value = v
@@ -90,15 +106,23 @@ module TkComponent
90
106
  attr_accessor :tk_variable
91
107
 
92
108
  def initialize(parent_item, name, options = {}, grid = {}, event_handlers = [])
93
- @tk_variable = TkVariable.new
109
+ create_variable
94
110
  super
95
- self.native_item.public_send(variable_name, @tk_variable)
111
+ apply_variable
96
112
  end
97
113
 
98
114
  def variable_name
99
115
  :variable
100
116
  end
101
117
 
118
+ def apply_variable
119
+ self.native_item&.public_send(variable_name, @tk_variable)
120
+ end
121
+
122
+ def create_variable
123
+ @tk_variable = TkVariable.new
124
+ end
125
+
102
126
  delegate :value, to: :tk_variable
103
127
  delegate :"value=", to: :tk_variable
104
128
  end
@@ -127,8 +151,83 @@ module TkComponent
127
151
  end
128
152
  end
129
153
 
154
+ class TkRadioSet < TkItemWithVariable
155
+ # The variable for the radio set is only to be used by radio buttons inside it
156
+ # Thus, we don't try to link it to the actual item
157
+ def apply_variable
158
+ end
159
+ end
160
+
161
+ class TkRadioButton < TkItemWithVariable
162
+ # We need to use the tk_variable created by the parent_item
163
+ # So we set it here and skip creation below
164
+ def initialize(parent_item, name, options = {}, grid = {}, event_handlers = [])
165
+ @tk_variable = parent_item.tk_variable
166
+ super
167
+ end
168
+
169
+ def create_variable
170
+ end
171
+
172
+ # It is unfortunate that native TK radio buttons use 'value' to
173
+ # spedify the value for each of them, colliding with the 'value'
174
+ # methods for our items with variables. Thus, we need to
175
+ # override the setting of the 'value' option to revert it to the
176
+ # default functionality
177
+ def apply_option(option, v, to_item = self.native_item)
178
+ case option.to_sym
179
+ when :value
180
+ to_item.public_send(option, v)
181
+ else
182
+ super
183
+ end
184
+ end
185
+ end
186
+
187
+ module Scrollable
188
+ ROOT_FRAME_OPTIONS = %i|width height relief borderwidth padx pady padding| + TkComponent::Builder::LAYOUT_OPTIONS
189
+
190
+ def initialize(parent_item, name, options = {}, grid = {}, event_handlers = [])
191
+ return super unless (s_options = options.delete(:scrollers)) && s_options.present? && s_options != 'none'
192
+ frame_item = TK_CLASSES[:frame].new(parent_item.native_item) # Containing frame
193
+ real_native_item = create_native_item(frame_item, name, options, grid, event_handlers)
194
+ f_options = options.extract!(*ROOT_FRAME_OPTIONS)
195
+ apply_options(f_options, frame_item) # Apply the applicable options to the enclosing frame
196
+ @native_item = real_native_item
197
+ apply_options(options, real_native_item)
198
+ set_grid(grid, frame_item)
199
+ real_native_item.grid( :column => 0, :row => 0, :sticky => 'nwes')
200
+ if s_options.include?('x')
201
+ h_scrollbar = TK_CLASSES[:hscroll_bar].new(frame_item)
202
+ h_scrollbar.orient('horizontal')
203
+ h_scrollbar.command proc { |*args| real_native_item.xview(*args) }
204
+ real_native_item['xscrollcommand'] = proc { |*args| h_scrollbar.set(*args) }
205
+ h_scrollbar.grid( :column => 0, :row => 1, :sticky => 'wes')
206
+ end
207
+ if s_options.include?('y')
208
+ v_scrollbar = TK_CLASSES[:vscroll_bar].new(frame_item)
209
+ v_scrollbar.orient('vertical')
210
+ v_scrollbar.command proc { |*args| real_native_item.yview(*args) }
211
+ real_native_item['yscrollcommand'] = proc { |*args| v_scrollbar.set(*args) }
212
+ v_scrollbar.grid( :column => 1, :row => 0, :sticky => 'nse')
213
+ end
214
+ TkGrid.columnconfigure(frame_item, 0, :weight => 1)
215
+ TkGrid.columnconfigure(frame_item, 1, :weight => 0) if v_scrollbar.present?
216
+ TkGrid.rowconfigure(frame_item, 0, :weight => 1)
217
+ TkGrid.rowconfigure(frame_item, 1, :weight => 0) if h_scrollbar.present?
218
+ @native_item = real_native_item
219
+ set_event_handlers(event_handlers)
220
+ end
221
+
222
+ # We need to remove the parent native item, as it's the container we put in place initially
223
+ def remove
224
+ @native_item.winfo_parent.destroy
225
+ end
226
+ end
227
+
130
228
  class TkText < TkItem
131
229
  include ValueTyping
230
+ include Scrollable
132
231
 
133
232
  def value
134
233
  native_item.get('1.0', 'end')
@@ -138,6 +237,24 @@ module TkComponent
138
237
  native_item.replace('1.0', 'end', text)
139
238
  end
140
239
 
240
+ def selected_text
241
+ ranges = native_item.tag_ranges('sel')
242
+ return nil if ranges.empty?
243
+ native_item.get(ranges.first.first, ranges.first.last)
244
+ end
245
+
246
+ def current_line
247
+ native_item.get('insert linestart', 'insert lineend')
248
+ end
249
+
250
+ def append_text(text)
251
+ native_item.insert('end', text)
252
+ end
253
+
254
+ def select_range(from, to)
255
+ native_item.tag_add('sel', from, to)
256
+ end
257
+
141
258
  def set_event_handler(event_handler)
142
259
  case event_handler.name
143
260
  when :change
@@ -159,11 +276,214 @@ module TkComponent
159
276
  end
160
277
  end
161
278
 
279
+ class TkTree < TkItem
280
+ include Scrollable
281
+ @column_defs = []
282
+
283
+ def apply_options(options, to_item = self.native_item)
284
+ super
285
+ return unless @column_defs.present?
286
+ cols = @column_defs.map { |c| c[:key] }
287
+ to_item.columns(cols[1..-1].join(' ')) unless cols == ['#0']
288
+ @column_defs.each.with_index do |cd, idx|
289
+ key = idx == 0 ? '#0' : cd[:key]
290
+ column_conf = cd.slice(:width, :anchor)
291
+ to_item.column_configure(key, column_conf) unless column_conf.empty?
292
+ heading_conf = cd.slice(:text)
293
+ to_item.heading_configure(key, heading_conf) unless heading_conf.empty?
294
+ end
295
+ end
296
+
297
+ def apply_option(option, v, to_item = self.native_item)
298
+ case option.to_sym
299
+ when :column_defs
300
+ @column_defs = v
301
+ when :heading
302
+ @column_defs = [ { key: '#0', text: v } ]
303
+ else
304
+ super
305
+ end
306
+ end
307
+
308
+ def set_event_handler(event_handler)
309
+ case event_handler.name
310
+ when :select
311
+ Event.bind_event('<TreeviewSelect>', self, event_handler.options, event_handler.lambda)
312
+ when :item_open
313
+ Event.bind_event('<TreeviewOpen>', self, event_handler.options, event_handler.lambda)
314
+ else
315
+ super
316
+ end
317
+ end
318
+
319
+ def scroll_to_selection
320
+ scroll_to_item(@native_item.selection.first)
321
+ end
322
+
323
+ # Right now it only works well for non-nested trees
324
+ def scroll_to_item(tree_item)
325
+ return unless tree_item.present?
326
+ items = @native_item.children('')
327
+ rel_pos = items.index(tree_item).to_f / items.size.to_f
328
+ @native_item.after(200) { @native_item.yview_moveto(rel_pos) }
329
+ end
330
+ end
331
+
332
+ class TkTreeNode < TkItem
333
+ def initialize(parent_item, name, options = {}, grid = {}, event_handlers = [])
334
+ item_options = options.dup
335
+ parent_node = item_options.delete(:parent) || ''
336
+ parent_native_item = (parent_node == '' ? '' : parent_node.native_item)
337
+ at = item_options.delete(:at)
338
+ selected = item_options.delete(:selected)
339
+ @native_item = parent_item.native_item.insert(parent_native_item, at, item_options)
340
+ parent_item.native_item.selection_add(@native_item) if selected
341
+ set_event_handlers(event_handlers)
342
+ end
343
+ end
344
+
345
+ class ScrollBar < TkItem
346
+ def apply_option(option, v, to_item = self.native_item)
347
+ case option.to_sym
348
+ when :linked_to
349
+ @linked_to = v
350
+ else
351
+ super
352
+ end
353
+ end
354
+
355
+ def set_event_handler(event_handler)
356
+ case event_handler.name
357
+ when :change
358
+ Event.Event.bind_command(event_handler.name, self, event_handler.options, event_handler.lambda)
359
+ else
360
+ super
361
+ end
362
+ end
363
+
364
+ def apply_options(options, to_item = self.native_item)
365
+ options.merge!(orient: orient)
366
+ super
367
+ end
368
+
369
+ def set_event_handlers(event_handlers)
370
+ bind_linked_to
371
+ super
372
+ end
373
+
374
+ def bind_linked_to
375
+ return unless @linked_to.present?
376
+ items = @linked_to.is_a?(Array) ? @linked_to.map(&:native_item) : [ @linked_to.native_item ]
377
+ self.native_item.command proc { |*args|
378
+ items.each do |item|
379
+ item.send(scroll_command, *args)
380
+ end
381
+ }
382
+ items.each do |item|
383
+ item.send(linked_scroll_command, proc { |*args| self.native_item.send(linked_scroll_event, *args) })
384
+ end
385
+ end
386
+
387
+ def orient
388
+ raise "#{self.class.to_s} shouldn't be instantiated directly. Use 'H' or 'V' subclasses"
389
+ end
390
+
391
+ def scroll_command
392
+ raise "#{self.class.to_s} shouldn't be instantiated directly. Use 'H' or 'V' subclasses"
393
+ end
394
+
395
+ def linked_scroll_command
396
+ raise "#{self.class.to_s} shouldn't be instantiated directly. Use 'H' or 'V' subclasses"
397
+ end
398
+
399
+ def linked_scroll_event
400
+ raise "#{self.class.to_s} shouldn't be instantiated directly. Use 'H' or 'V' subclasses"
401
+ end
402
+ end
403
+
404
+ class HScrollbar < ScrollBar
405
+ def orient
406
+ 'horizontal'
407
+ end
408
+
409
+ def scroll_command
410
+ :xview
411
+ end
412
+
413
+ def linked_scroll_command
414
+ :xscrollcommand
415
+ end
416
+
417
+ def linked_scroll_event
418
+ :set
419
+ end
420
+ end
421
+
422
+ class VScrollbar < ScrollBar
423
+ def orient
424
+ 'vertical'
425
+ end
426
+
427
+ def scroll_command
428
+ :yview
429
+ end
430
+
431
+ def linked_scroll_command
432
+ :yscrollcommand
433
+ end
434
+
435
+ def linked_scroll_event
436
+ :set
437
+ end
438
+ end
439
+
440
+ class PanedWindow < TkItem
441
+ def create_native_item(parent_native_item, name, options = {}, grid = {}, event_handlers = [])
442
+ native_item_class(parent_native_item, name, options, grid, event_handlers).new(parent_native_item, orient: orient)
443
+ end
444
+
445
+ def built
446
+ # We need to synchronize children items to the panned window
447
+ added_panes = self.native_item.winfo_children - self.native_item.panes
448
+ removed_panes = self.native_item.panes - self.native_item.winfo_children
449
+ added_panes.each do |child|
450
+ self.native_item.add(child, weight: 1)
451
+ end
452
+ removed_panes.each do |child|
453
+ self.native_item.forget(child)
454
+ end
455
+ end
456
+
457
+ def orient
458
+ raise "#{self.class.to_s} shouldn't be instantiated directly. Use 'H' or 'V' subclasses"
459
+ end
460
+ end
461
+
462
+ class HPanedWindow < PanedWindow
463
+ def orient
464
+ 'horizontal'
465
+ end
466
+ end
467
+
468
+ class VPanedWindow < PanedWindow
469
+ def orient
470
+ 'vertical'
471
+ end
472
+ end
473
+
162
474
  class TkWindow < TkItem
163
475
  def initialize(parent_item, name, options = {}, grid = {}, event_handlers = [])
164
- @native_item = TkRoot.new { title options[:title] }
476
+ if (options.delete(:root))
477
+ @native_item = TkRoot.new { title options[:title] }
478
+ else
479
+ @native_item = TkToplevel.new { title options[:title] }
480
+ end
165
481
  apply_options(options)
166
482
  end
483
+
484
+ def focus
485
+ self.native_item.set_focus
486
+ end
167
487
  end
168
488
 
169
489
  TK_CLASSES = {
@@ -174,10 +494,17 @@ module TkComponent
174
494
  label: Tk::Tile::Label,
175
495
  entry: Tk::Tile::Entry,
176
496
  button: Tk::Tile::Button,
497
+ radio_set: Tk::Tile::Frame,
498
+ radio_button: Tk::Tile::RadioButton,
177
499
  canvas: Tk::Canvas,
178
500
  text: ::TkText,
179
501
  scale: Tk::Tile::Scale,
180
- group: Tk::Tile::LabelFrame
502
+ group: Tk::Tile::LabelFrame,
503
+ tree: Tk::Tile::Treeview,
504
+ hscroll_bar: Tk::Tile::Scrollbar,
505
+ vscroll_bar: Tk::Tile::Scrollbar,
506
+ hpaned: Tk::Tile::Paned,
507
+ vpaned: Tk::Tile::Paned
181
508
  }
182
509
 
183
510
  ITEM_CLASSES = {
@@ -188,10 +515,19 @@ module TkComponent
188
515
  label: TkComponent::Builder::TkItem,
189
516
  entry: TkComponent::Builder::TkEntry,
190
517
  button: TkComponent::Builder::TkItem,
518
+ radio_set: TkComponent::Builder::TkRadioSet,
519
+ radio_button: TkComponent::Builder::TkRadioButton,
191
520
  canvas: TkComponent::Builder::TkItem,
192
521
  text: TkComponent::Builder::TkText,
193
522
  scale: TkComponent::Builder::TkScale,
194
- group: TkComponent::Builder::TkItem
523
+ group: TkComponent::Builder::TkItem,
524
+ tree: TkComponent::Builder::TkTree,
525
+ tree_node: TkComponent::Builder::TkTreeNode,
526
+ hscroll_bar: TkComponent::Builder::HScrollbar,
527
+ vscroll_bar: TkComponent::Builder::VScrollbar,
528
+ hpaned: TkComponent::Builder::HPanedWindow,
529
+ vpaned: TkComponent::Builder::VPanedWindow
530
+
195
531
  }
196
532
  end
197
533
  end