ppcurses 0.0.25 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +50 -11
  3. data/lib/ppcurses/Screen.rb +128 -15
  4. data/lib/ppcurses/actions/BaseAction.rb +1 -2
  5. data/lib/ppcurses/actions/GetBooleanAction.rb +6 -6
  6. data/lib/ppcurses/actions/GetDataAction.rb +4 -4
  7. data/lib/ppcurses/actions/GetEnumeratedStringAction.rb +6 -6
  8. data/lib/ppcurses/actions/GetIntegerAction.rb +2 -2
  9. data/lib/ppcurses/actions/GetStringAction.rb +2 -0
  10. data/lib/ppcurses/actions/PromptAction.rb +2 -2
  11. data/lib/ppcurses/application.rb +219 -0
  12. data/lib/ppcurses/date/meta_month.rb +139 -0
  13. data/lib/ppcurses/form/button.rb +120 -0
  14. data/lib/ppcurses/form/combo_box.rb +93 -0
  15. data/lib/ppcurses/form/date_picker.rb +86 -0
  16. data/lib/ppcurses/form/form.rb +96 -0
  17. data/lib/ppcurses/form/input_element.rb +189 -0
  18. data/lib/ppcurses/form/radio_button_group.rb +56 -0
  19. data/lib/ppcurses/geometry.rb +23 -0
  20. data/lib/ppcurses/menu/BaseMenu.rb +8 -14
  21. data/lib/ppcurses/menu/Menu.rb +5 -8
  22. data/lib/ppcurses/menu/RadioMenu.rb +2 -5
  23. data/lib/ppcurses/menu/choice_menu.rb +33 -0
  24. data/lib/ppcurses/menu/date_menu.rb +97 -0
  25. data/lib/ppcurses/menu_bar.rb +102 -0
  26. data/lib/ppcurses/table_view.rb +58 -0
  27. data/lib/ppcurses/view.rb +45 -0
  28. data/lib/ppcurses/window/pp_window.rb +42 -0
  29. data/lib/ppcurses.rb +57 -3
  30. data/test/application/create_application.rb +23 -0
  31. data/test/date/printMetaMonth.rb +30 -0
  32. data/test/form/menu_opens_form.rb +15 -0
  33. data/test/form/simple_form.rb +48 -0
  34. data/test/form/test_combo.rb +20 -0
  35. data/test/form/test_date_picker.rb +19 -0
  36. data/test/getBooleanAction.rb +1 -1
  37. data/test/getDataAction.rb +6 -6
  38. data/test/getEnumStringAction.rb +4 -4
  39. data/test/getIntegerAction.rb +4 -4
  40. data/test/menu/changeMenuBorder.rb +1 -1
  41. data/test/menu/compositeMenu.rb +2 -2
  42. data/test/menu/displayMenu.rb +1 -1
  43. data/test/raw_screen/display_colours.rb +69 -0
  44. data/test/raw_screen/press_a_key.rb +18 -0
  45. data/test/table_view/testTableView.rb +16 -0
  46. data/test/threads/block_test.rb +63 -0
  47. data/test/threads/handle_resize.rb +43 -0
  48. metadata +37 -12
  49. data/lib/ppcurses/Constants.rb +0 -8
@@ -0,0 +1,56 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module PPCurses
4
+
5
+ RADIO_NOT_SELECTED = '◎'
6
+ RADIO_SELECTED = '◉'
7
+
8
+ class RadioButtonGroup < View
9
+
10
+ attr_accessor :selected
11
+ attr_reader :current_option
12
+
13
+ #
14
+ # label : a label for the radio button group
15
+ # options: an array of strings
16
+ #
17
+ def initialize( label, options )
18
+
19
+ @label = label
20
+ @options = options
21
+ @current_option = 0
22
+ end
23
+
24
+ def show(screen)
25
+ screen.attron(Curses::A_REVERSE) if @selected
26
+ screen.addstr(" #{@label}:")
27
+ screen.attroff(Curses::A_REVERSE) if @selected
28
+ @options.each_with_index do |option, index|
29
+ if index == @current_option
30
+ screen.addstr(" #{option} #{RADIO_SELECTED}")
31
+ else
32
+ screen.addstr(" #{option} #{RADIO_NOT_SELECTED}")
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+ def key_down( key )
39
+ if key == KEY_LEFT then @current_option = @current_option-1 end
40
+ if key == KEY_RIGHT then @current_option = @current_option+1 end
41
+
42
+ if @current_option < 0 then @current_option = @options.length-1 end
43
+ if @current_option > @options.length-1 then @current_option = 0 end
44
+ end
45
+
46
+ def set_curs_pos(screen)
47
+ Curses.curs_set(INVISIBLE)
48
+ end
49
+
50
+ def height
51
+ 1
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,23 @@
1
+ module PPCurses
2
+
3
+
4
+ class Point
5
+
6
+ attr_accessor :x, :y
7
+
8
+ def initialize( x, y )
9
+ @x = x
10
+ @y = y
11
+ end
12
+
13
+ def to_s
14
+ "x=#{@x} y=#{@y}"
15
+ end
16
+
17
+ end
18
+
19
+ class Rect
20
+
21
+ end
22
+
23
+ end
@@ -1,5 +1,3 @@
1
- require 'curses'
2
-
3
1
  module PPCurses
4
2
 
5
3
  #noinspection RubyResolve
@@ -11,20 +9,15 @@ module PPCurses
11
9
  attr_accessor :side_wall_char
12
10
  attr_accessor :top_bot_wall_char
13
11
 
14
- #
15
- # Current base ruby is 1.9.2. action_items could be an optional parameter
16
- # if the base was moved up to 2.0
17
- #
18
- def initialize(menu_items, action_items )
12
+
13
+ def initialize(menu_items, action_items=nil)
19
14
  @selection=0
20
15
  @max_menu_width = 0
21
16
 
22
17
  @side_wall_char = '|'
23
18
  @top_bot_wall_char = '-'
24
19
 
25
- sample = menu_items[0]
26
-
27
- case sample
20
+ case menu_items[0]
28
21
  when String
29
22
  # Case 1: menu_items is a list of strings, with an associated action list
30
23
  build_menu_items(menu_items, action_items)
@@ -61,14 +54,15 @@ module PPCurses
61
54
  }
62
55
  end
63
56
 
57
+ def set_origin( point )
58
+ @win.move_to_point( point )
59
+ end
60
+
64
61
  def create_window
65
62
  w_height = @menu_items.length + 4
66
63
  w_width = @max_menu_width + 4
67
- @win = Window.new(w_height,w_width,(lines-w_height) / 2, (cols-w_width)/2)
68
-
64
+ @win = PPCurses::Window.new(w_height,w_width,(Curses.lines-w_height) / 2, (Curses.cols-w_width)/2)
69
65
  @win.timeout=-1
70
- # Enables reading arrow keys in getch
71
- @win.keypad(true)
72
66
  end
73
67
 
74
68
  def set_sub_menu(menu)
@@ -1,13 +1,9 @@
1
- # Curses reference:
2
- # http://www.ruby-doc.org/stdlib-1.9.3/libdoc/curses/rdoc/Curses.html
3
-
4
- require_relative 'BaseMenu.rb'
5
- require 'curses'
6
-
7
1
  module PPCurses
8
2
  #noinspection RubyResolve
9
3
  class Menu < BaseMenu
10
4
 
5
+ attr_accessor :selection
6
+
11
7
  def show
12
8
  @win.box(self.side_wall_char, self.top_bot_wall_char)
13
9
  y = 2
@@ -15,9 +11,9 @@ module PPCurses
15
11
 
16
12
  (0...@menu_items.length).each { |i|
17
13
  @win.setpos(y, x)
18
- @win.attron(A_REVERSE) if @selection == i
14
+ @win.attron(Curses::A_REVERSE) if @selection == i
19
15
  @win.addstr(@menu_items[i].display_string)
20
- @win.attroff(A_REVERSE) if @selection == i
16
+ @win.attroff(Curses::A_REVERSE) if @selection == i
21
17
  y += 1
22
18
  }
23
19
 
@@ -27,6 +23,7 @@ module PPCurses
27
23
  end
28
24
 
29
25
  def set_global_action(action)
26
+ PPCurses.implements_protocol(action, %w(execute))
30
27
  @global_action = action
31
28
  end
32
29
 
@@ -1,12 +1,9 @@
1
-
2
- require_relative 'BaseMenu.rb'
3
-
4
1
  module PPCurses
5
2
  #noinspection RubyResolve
6
3
  class RadioMenu < BaseMenu
7
4
 
8
5
  # TODO - duplicate code from Menu ...
9
- def initialize( menu_items, action_items )
6
+ def initialize( menu_items, action_items=nil )
10
7
  @items = Array.new
11
8
  @actions = Array.new
12
9
 
@@ -28,7 +25,7 @@ module PPCurses
28
25
 
29
26
  w_width = @menu_length + 4
30
27
 
31
- @win = Window.new(3, w_width ,0, (cols - w_width) / 2)
28
+ @win = Window.new(3, w_width ,0, (Curses.cols - w_width) / 2)
32
29
 
33
30
  @win.timeout=-1
34
31
  # Enables reading arrow keys in getch
@@ -0,0 +1,33 @@
1
+ module PPCurses
2
+
3
+
4
+ class ChoiceMenu < Menu
5
+
6
+ attr_reader :selection
7
+ attr_reader :pressed_enter
8
+
9
+ def menu_selection
10
+ while 1
11
+ c = @win.getch
12
+
13
+ if c == ESCAPE
14
+ @pressed_enter = false
15
+ self.hide
16
+ break
17
+ end
18
+
19
+ if c == ENTER
20
+ @pressed_enter = true
21
+ self.hide
22
+ break
23
+ end
24
+
25
+ self.handle_menu_selection(c)
26
+
27
+ end
28
+ end
29
+
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,97 @@
1
+ module PPCurses
2
+
3
+
4
+ class DateMenu < ChoiceMenu
5
+
6
+ def initialize(day)
7
+ @meta_info = MetaMonth.new(day)
8
+ find_max_menu_width
9
+ create_window
10
+ end
11
+
12
+ def day
13
+ @meta_info.day
14
+ end
15
+
16
+
17
+ def find_max_menu_width
18
+ @max_menu_width = 0
19
+
20
+ str_array = @meta_info.month_str_array
21
+
22
+ (0...str_array.length).each { |i|
23
+ display = str_array[i]
24
+ @max_menu_width = display.length if display.length > @max_menu_width
25
+ }
26
+ end
27
+
28
+
29
+ def create_window
30
+ w_height = @meta_info.month_str_array.length + 4
31
+ w_width = @max_menu_width + 4
32
+ @win = PPCurses::Window.new(w_height,w_width,(Curses.lines-w_height) / 2, (Curses.cols-w_width)/2)
33
+ @win.timeout=-1
34
+ end
35
+
36
+
37
+ def show
38
+ y = 2
39
+ x = 2
40
+
41
+ str_array = @meta_info.month_str_array
42
+
43
+ str_array.each_with_index { |val, i|
44
+ @win.setpos(y, x)
45
+
46
+ if i != @meta_info.day_row
47
+ @win.addstr(val)
48
+ else
49
+ num_of_digits = @meta_info.day.day > 9 ? 2 : 1
50
+ @win.addstr(val[0, @meta_info.day_col])
51
+ @win.attron(Curses::A_REVERSE)
52
+ @win.addstr(val[@meta_info.day_col, num_of_digits])
53
+ @win.attroff(Curses::A_REVERSE)
54
+ @win.addstr(val[@meta_info.day_col + num_of_digits, val.length])
55
+ end
56
+
57
+ y += 1
58
+ }
59
+
60
+ @win.refresh
61
+
62
+ @sub_menu.show if @sub_menu
63
+ end
64
+
65
+ def handle_menu_selection(c)
66
+
67
+ curr_day = @meta_info.day
68
+ day_change = 0
69
+
70
+ if c == KEY_UP then day_change = -7 end
71
+ if c == KEY_DOWN then day_change = 7 end
72
+ if c == KEY_LEFT then day_change = -1 end
73
+ if c == KEY_RIGHT then day_change = 1 end
74
+
75
+ # Use vi key bindings for months and year
76
+ # browsing.
77
+ if c == 'l' then day_change = 30 end
78
+ if c == 'h' then day_change = -30 end
79
+ if c == 'j' then day_change = 365 end
80
+ if c == 'k' then day_change = -365 end
81
+
82
+ if day_change != 0
83
+ curr_day = Date.jd(curr_day.jd + day_change)
84
+ @meta_info.day = curr_day
85
+ self.show
86
+ return true
87
+ end
88
+
89
+ false
90
+ end
91
+
92
+
93
+
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,102 @@
1
+ module PPCurses
2
+
3
+
4
+
5
+ # The menubar is activated and deactivated by the ESCAPE key.
6
+ #
7
+ # Any responders further down the responder chain will never receive ESCAPE key events.
8
+ #
9
+ class MenuBar < Responder
10
+
11
+ def initialize
12
+ @menu_items = []
13
+ @selected = false
14
+ end
15
+
16
+ # Expects screen to be a PPCurses::Screen object
17
+ # Need to convert to work with a window or a view.
18
+ def show(screen)
19
+
20
+ screen.set_pos_by_point(ZERO_POINT)
21
+
22
+ if @selected
23
+ screen.attron(A_REVERSE)
24
+ else
25
+ screen.attron(A_UNDERLINE)
26
+ end
27
+
28
+ @menu_items.each do |menu_item|
29
+ screen.addstr( "#{menu_item} ")
30
+ end
31
+
32
+ p = screen.cur_point
33
+
34
+ screen.addstr( ' ' * (screen.width - p.x) )
35
+
36
+ if @selected
37
+ screen.attroff(A_REVERSE)
38
+ else
39
+ screen.attroff(A_UNDERLINE)
40
+ end
41
+
42
+ end
43
+
44
+
45
+ def add_menu_item(menu_item)
46
+ @menu_items.push(menu_item)
47
+ end
48
+
49
+
50
+ def key_down( key )
51
+
52
+ if key == ESCAPE
53
+ @selected = !@selected
54
+ return
55
+ end
56
+
57
+ if @selected
58
+ @menu_items.each do |menu_item|
59
+ if key == menu_item.key
60
+ menu_item.action.call
61
+ return
62
+ end
63
+ end
64
+ return
65
+ end
66
+
67
+ @next_responder.key_down(key) unless @next_responder.nil?
68
+
69
+ end
70
+
71
+ end
72
+
73
+ # Based on Cocoa NSMenuItem
74
+ #
75
+ # Current link, which probably won't be valid in the future ...
76
+ #
77
+ # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSMenuItem_Class/index.html#//apple_ref/occ/instm/NSMenuItem/action
78
+ # ----------------------------------------------------------
79
+ # MenuBarItems ...
80
+ #
81
+ # q:Quit d:Del u:Undel
82
+ #
83
+ class MenuBarItem
84
+
85
+ attr_accessor :key
86
+ attr_accessor :label
87
+
88
+ attr_accessor :action
89
+
90
+
91
+ def initialize( key, label )
92
+ @key = key
93
+ @label = label
94
+ end
95
+
96
+ def to_s
97
+ "#{@key}:#{@label}"
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,58 @@
1
+ module PPCurses
2
+ # Based loosely on ...
3
+ #
4
+ # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSTableView_Class/index.html#//apple_ref/occ/cl/NSTableView
5
+ #
6
+ #
7
+ class TableView < View
8
+
9
+ attr_accessor :data_source
10
+
11
+
12
+ def selected_row
13
+
14
+ end
15
+
16
+ # A data source must implement a formal protocol
17
+ #
18
+ # - def number_of_rows_in_table(tableView)
19
+ # - def object_value_for(tableView, tableColumn, rowIndex)
20
+ #
21
+ def data_source=(val)
22
+ PPCurses.implements_protocol( val, %w(number_of_rows_in_table object_value_for ))
23
+ @data_source = val
24
+ end
25
+
26
+ end
27
+
28
+
29
+ # Based loosely on ...
30
+ #
31
+ # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSTableDataSource_Protocol/index.html#//apple_ref/occ/intf/NSTableViewDataSource
32
+
33
+ class SingleColumnDataSource
34
+
35
+ def initialize(values)
36
+ @values = values
37
+ end
38
+
39
+ def number_of_rows_in_table
40
+ @values.length
41
+ end
42
+
43
+ def object_value_for(column, row_index)
44
+ @values[row_index]
45
+ end
46
+ end
47
+
48
+ class TableViewDataSource
49
+
50
+ def TableViewDataSource.from_string_array(values)
51
+ SingleColumnDataSource.new(values)
52
+ end
53
+
54
+ end
55
+
56
+
57
+
58
+ end
@@ -0,0 +1,45 @@
1
+ module PPCurses
2
+
3
+ # View Hierarchy information:
4
+ #
5
+ # https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaViewsGuide/WorkingWithAViewHierarchy/WorkingWithAViewHierarchy.html
6
+
7
+ # Based loosely on ...
8
+ #
9
+ # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/index.html#//apple_ref/occ/instp/NSView/subviews
10
+
11
+ class View < ResponderManager
12
+
13
+ ## Managing the View Hierarchy
14
+
15
+ # superview
16
+
17
+ # add_subview
18
+
19
+ # subviews
20
+
21
+ # set_subviews
22
+
23
+
24
+ ## Drawing
25
+
26
+ # Expects screen to be a PPCurses::Screen object
27
+ # Need to convert to work with a window or a view.
28
+ #
29
+ # The default implementation does nothing
30
+ def display(screen)
31
+
32
+ end
33
+
34
+
35
+ ## Key-view Loop Management
36
+
37
+ # canBecomeKeyView
38
+
39
+ # nextKeyView
40
+
41
+ #
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,42 @@
1
+ module PPCurses
2
+
3
+ class Window < Curses::Window
4
+
5
+ # TODO - use optional parameters. A rect or a Curses window to wrap.
6
+ def initialize(height, width, top, left)
7
+ super(height,width,top,left)
8
+
9
+ # Enables reading arrow keys in getch
10
+ keypad(true)
11
+
12
+ box('|', '-')
13
+ end
14
+
15
+
16
+
17
+ # EXPERIMENTAL/HACK
18
+ #
19
+ # The following could be used to wrap all getch calls
20
+ # and support window resizes when the getch is blocking
21
+ # all threads.
22
+ #
23
+ def get_ch_handle_signals
24
+ got_input = false
25
+ until got_input
26
+ begin
27
+ c = getch
28
+ got_input = true
29
+ rescue NoMethodError
30
+ # Assuming a SIGWINCH occurred -- reposition..
31
+ c = ''
32
+ end
33
+ end
34
+
35
+ c
36
+ end
37
+
38
+ end
39
+
40
+
41
+
42
+ end
data/lib/ppcurses.rb CHANGED
@@ -1,13 +1,52 @@
1
+ gem 'curses', '=1.0.1'
2
+ require 'curses'
3
+ require 'date'
1
4
 
2
5
  module PPCurses
6
+
7
+ TAB = 9
8
+ ENTER = 10
9
+ ESCAPE = 27
10
+ DELETE = 127
11
+ SPACE_BAR = ' '
12
+ KEY_RIGHT = Curses::KEY_RIGHT
13
+ KEY_LEFT = Curses::KEY_LEFT
14
+ KEY_UP = Curses::KEY_UP
15
+ KEY_DOWN = Curses::KEY_DOWN
16
+
17
+
18
+ A_REVERSE = Curses::A_REVERSE
19
+ A_UNDERLINE = Curses::A_UNDERLINE
20
+
21
+ # To be used in conjunction with curs_set for more readable code e.g. Curses.curs_set(INVISIBLE)
22
+ INVISIBLE = 0
23
+ VISIBLE = 1
24
+
25
+ def PPCurses.implements_protocol( element, methods )
26
+ methods.each { |method|
27
+ unless element.respond_to?(method); raise TypeError, "** Method #{method} MUST be defined **" end
28
+ }
29
+ end
30
+
31
+
32
+ require_relative 'ppcurses/application.rb'
33
+ require_relative 'ppcurses/view.rb'
34
+ require_relative 'ppcurses/menu_bar.rb'
3
35
  require_relative 'ppcurses/Screen.rb'
4
- require_relative 'ppcurses/Constants.rb'
5
- # Menus
36
+ require_relative 'ppcurses/geometry.rb'
37
+ require_relative 'ppcurses/table_view.rb'
38
+
39
+ # Dates ---------------------------------------------------------------------------------------------------------------
40
+ require_relative 'ppcurses/date/meta_month.rb'
41
+ # Menus ---------------------------------------------------------------------------------------------------------------
42
+ require_relative 'ppcurses/menu/BaseMenu.rb'
6
43
  require_relative 'ppcurses/menu/Menu.rb'
7
44
  require_relative 'ppcurses/menu/CompositeMenu.rb'
8
45
  require_relative 'ppcurses/menu/RadioMenu.rb'
9
46
  require_relative 'ppcurses/menu/menu_item.rb'
10
- # Actions
47
+ require_relative 'ppcurses/menu/choice_menu.rb'
48
+ require_relative 'ppcurses/menu/date_menu.rb'
49
+ # Actions -------------------------------------------------------------------------------------------------------------
11
50
  require_relative 'ppcurses/actions/ShowMenuAction.rb'
12
51
  require_relative 'ppcurses/actions/GetStringAction.rb'
13
52
  require_relative 'ppcurses/actions/GetBooleanAction.rb'
@@ -16,6 +55,21 @@ module PPCurses
16
55
  require_relative 'ppcurses/actions/GetDataAction.rb'
17
56
  require_relative 'ppcurses/actions/NulAction.rb'
18
57
  require_relative 'ppcurses/actions/InsertSQLDataAction.rb'
58
+ # Forms ---------------------------------------------------------------------------------------------------------------
59
+ require_relative 'ppcurses/form/form.rb'
60
+ require_relative 'ppcurses/form/button.rb'
61
+ require_relative 'ppcurses/form/input_element.rb'
62
+ require_relative 'ppcurses/form/radio_button_group.rb'
63
+ require_relative 'ppcurses/form/combo_box.rb'
64
+ require_relative 'ppcurses/form/date_picker.rb'
65
+ # Windows ------------------------------------------------------------------------------------------------------------
66
+ require_relative 'ppcurses/window/pp_window.rb'
67
+
68
+
69
+ ZERO_POINT = Point.new(0,0)
70
+
71
+ NO = false
72
+ YES = true
19
73
  end
20
74
 
21
75
 
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require_relative '../../lib/ppcurses.rb'
5
+
6
+
7
+ class MyDelegate
8
+
9
+ def applicationDidFinishLaunching( app )
10
+
11
+ end
12
+
13
+ end
14
+
15
+
16
+
17
+ delegate = MyDelegate.new
18
+
19
+ app = PPCurses::Application.new
20
+ app.set_delegate(delegate)
21
+ app.launch
22
+
23
+
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require_relative '../../lib/ppcurses.rb'
5
+
6
+
7
+ class String
8
+ def reverse_color; "\033[7m#{self}\033[27m" end
9
+ end
10
+
11
+ cal = PPCurses::MetaMonth.new
12
+
13
+ cal.month_str_array.each_with_index { |val, i|
14
+
15
+ if i != cal.day_row
16
+ puts val
17
+ else
18
+ num_of_digits = cal.day.day > 9 ? 2 : 1
19
+
20
+ print val[0, cal.day_col]
21
+ print val[cal.day_col, num_of_digits].reverse_color
22
+ puts val[cal.day_col + num_of_digits, val.length]
23
+ end
24
+
25
+ }
26
+
27
+
28
+
29
+
30
+