glimmer-dsl-swt 4.20.0.3 → 4.20.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +12 -6
  4. data/VERSION +1 -1
  5. data/docs/reference/GLIMMER_SAMPLES.md +34 -0
  6. data/docs/reference/GLIMMER_STYLE_GUIDE.md +4 -3
  7. data/glimmer-dsl-swt.gemspec +0 -0
  8. data/lib/glimmer/data_binding/shine.rb +5 -1
  9. data/lib/glimmer/data_binding/table_items_binding.rb +2 -2
  10. data/lib/glimmer/data_binding/tree_items_binding.rb +2 -2
  11. data/lib/glimmer/dsl/swt/shine_data_binding_expression.rb +6 -3
  12. data/lib/glimmer/dsl/swt/table_items_data_binding_expression.rb +2 -2
  13. data/lib/glimmer/dsl/swt/tree_items_data_binding_expression.rb +17 -12
  14. data/lib/glimmer/dsl/swt/widget_listener_expression.rb +3 -3
  15. data/lib/glimmer/swt/custom/animation.rb +6 -0
  16. data/lib/glimmer/swt/display_proxy.rb +11 -8
  17. data/lib/glimmer/swt/proxy_properties.rb +2 -1
  18. data/lib/glimmer/swt/table_proxy.rb +15 -8
  19. data/lib/glimmer/swt/widget_proxy.rb +2 -2
  20. data/lib/glimmer/ui/custom_shell.rb +3 -3
  21. data/lib/glimmer/ui/custom_widget.rb +5 -2
  22. data/samples/elaborate/calculator.rb +116 -0
  23. data/samples/elaborate/calculator/model/command.rb +105 -0
  24. data/samples/elaborate/calculator/model/command/all_clear.rb +17 -0
  25. data/samples/elaborate/calculator/model/command/command_history.rb +0 -0
  26. data/samples/elaborate/calculator/model/command/equals.rb +18 -0
  27. data/samples/elaborate/calculator/model/command/number.rb +20 -0
  28. data/samples/elaborate/calculator/model/command/operation.rb +27 -0
  29. data/samples/elaborate/calculator/model/command/operation/add.rb +15 -0
  30. data/samples/elaborate/calculator/model/command/operation/divide.rb +15 -0
  31. data/samples/elaborate/calculator/model/command/operation/multiply.rb +15 -0
  32. data/samples/elaborate/calculator/model/command/operation/subtract.rb +15 -0
  33. data/samples/elaborate/calculator/model/command/point.rb +20 -0
  34. data/samples/elaborate/calculator/model/presenter.rb +30 -0
  35. data/samples/elaborate/contact_manager.rb +1 -1
  36. data/samples/elaborate/login.rb +15 -13
  37. data/samples/elaborate/tetris.rb +6 -6
  38. data/samples/elaborate/tetris/view/high_score_dialog.rb +1 -1
  39. data/samples/elaborate/timer.rb +233 -0
  40. data/samples/elaborate/timer/alarm1.wav +0 -0
  41. data/samples/elaborate/timer/sounds/alarm1.wav +0 -0
  42. data/samples/elaborate/user_profile.rb +4 -2
  43. data/samples/elaborate/weather.rb +164 -0
  44. data/samples/hello/hello_canvas_animation_data_binding.rb +1 -1
  45. data/samples/hello/hello_cool_bar.rb +4 -4
  46. data/samples/hello/hello_custom_shell.rb +1 -1
  47. data/samples/hello/hello_table.rb +5 -5
  48. data/samples/hello/hello_text.rb +120 -0
  49. data/samples/hello/hello_tool_bar.rb +4 -4
  50. data/samples/hello/hello_tree.rb +11 -11
  51. metadata +20 -2
@@ -0,0 +1,30 @@
1
+ require_relative 'command'
2
+
3
+ class Calculator
4
+ module Model
5
+ class Presenter
6
+ FORMATTER = {
7
+ nil => '0',
8
+ 'NaN' => 'Not a number'
9
+ }
10
+
11
+ attr_accessor :result
12
+
13
+ def initialize
14
+ self.result = '0'
15
+ end
16
+
17
+ def press(button)
18
+ command = Command.for(button)
19
+ if command
20
+ new_result = command.result
21
+ self.result = FORMATTER[new_result] || new_result
22
+ end
23
+ end
24
+
25
+ def purge_command_history
26
+ Command.purge_command_history
27
+ end
28
+ end
29
+ end
30
+ end
@@ -132,7 +132,7 @@ class ContactManager
132
132
  width 200
133
133
  }
134
134
 
135
- items bind(@contact_manager_presenter, :results), column_properties(:first_name, :last_name, :email)
135
+ items <=> [@contact_manager_presenter, :results, column_properties: [:first_name, :last_name, :email]]
136
136
 
137
137
  on_mouse_up { |event|
138
138
  table_proxy.edit_table_item(event.table_item, event.column_index)
@@ -35,24 +35,19 @@ class LoginPresenter
35
35
 
36
36
  def status=(status)
37
37
  @status = status
38
-
39
- notify_observers("logged_in")
40
- notify_observers("logged_out")
41
38
  end
42
39
 
43
40
  def valid?
44
41
  !@user_name.to_s.strip.empty? && !@password.to_s.strip.empty?
45
42
  end
46
43
 
47
- def logged_in
44
+ def logged_in?
48
45
  self.status == "Logged In"
49
46
  end
50
- alias logged_in? logged_in
51
47
 
52
- def logged_out
53
- !self.logged_in
48
+ def logged_out?
49
+ !self.logged_in?
54
50
  end
55
- alias logged_out? logged_out
56
51
 
57
52
  def login!
58
53
  return unless valid?
@@ -77,13 +72,15 @@ class Login
77
72
  body {
78
73
  shell {
79
74
  text "Login"
75
+
80
76
  composite {
81
77
  grid_layout 2, false #two columns with differing widths
82
78
 
83
79
  label { text "Username:" } # goes in column 1
84
80
  @user_name_text = text { # goes in column 2
85
81
  text <=> [@presenter, :user_name]
86
- enabled <= [@presenter, :logged_out]
82
+ enabled <= [@presenter, :logged_out?, computed_by: :status]
83
+
87
84
  on_key_pressed { |event|
88
85
  @password_text.set_focus if event.keyCode == swt(:cr)
89
86
  }
@@ -92,7 +89,8 @@ class Login
92
89
  label { text "Password:" }
93
90
  @password_text = text(:password, :border) {
94
91
  text <=> [@presenter, :password]
95
- enabled <= [@presenter, :logged_out]
92
+ enabled <= [@presenter, :logged_out?, computed_by: :status]
93
+
96
94
  on_key_pressed { |event|
97
95
  @presenter.login! if event.keyCode == swt(:cr)
98
96
  }
@@ -103,16 +101,20 @@ class Login
103
101
 
104
102
  button {
105
103
  text "Login"
106
- enabled <= [@presenter, :logged_out]
104
+ enabled <= [@presenter, :logged_out?, computed_by: :status]
105
+
107
106
  on_widget_selected { @presenter.login! }
108
107
  on_key_pressed { |event|
109
- @presenter.login! if event.keyCode == swt(:cr)
108
+ if event.keyCode == swt(:cr)
109
+ @presenter.login!
110
+ end
110
111
  }
111
112
  }
112
113
 
113
114
  button {
114
115
  text "Logout"
115
- enabled <= [@presenter, :logged_in]
116
+ enabled <= [@presenter, :logged_in?, computed_by: :status]
117
+
116
118
  on_widget_selected { @presenter.logout! }
117
119
  on_key_pressed { |event|
118
120
  if event.keyCode == swt(:cr)
@@ -54,7 +54,7 @@ class Tetris
54
54
  Display.app_name = 'Glimmer Tetris'
55
55
 
56
56
  display {
57
- @keyboard_down_listener = on_swt_keydown { |key_event|
57
+ on_swt_keydown { |key_event|
58
58
  case key_event.keyCode
59
59
  when swt(:arrow_down), 's'.bytes.first
60
60
  game.down! if OS.mac?
@@ -82,7 +82,7 @@ class Tetris
82
82
 
83
83
  # invoke game.down! on keyup with Windows/Linux since they seem to group-render similar events, preventing intermediate renders (causing invisiblity while holding keys)
84
84
  if !OS.mac?
85
- @keyboard_up_listener = on_swt_keyup { |key_event|
85
+ on_swt_keyup { |key_event|
86
86
  case key_event.keyCode
87
87
  when swt(:arrow_down), 's'.bytes.first
88
88
  game.down!
@@ -91,25 +91,25 @@ class Tetris
91
91
  end
92
92
 
93
93
  # if running in app mode, set the Mac app about dialog (ignored in platforms)
94
- @about_observer = on_about {
94
+ on_about {
95
95
  show_about_dialog
96
96
  }
97
97
 
98
- @quit_observer = on_quit {
98
+ on_quit {
99
99
  exit(0)
100
100
  }
101
101
  }
102
102
  }
103
103
 
104
104
  after_body {
105
- @game_over_observer = observe(@game, :game_over) do |game_over|
105
+ observe(@game, :game_over) do |game_over|
106
106
  if game_over
107
107
  show_high_score_dialog
108
108
  else
109
109
  start_moving_tetrominos_down
110
110
  end
111
111
  end
112
- @show_high_scores_observer = observe(@game, :show_high_scores) do |show_high_scores|
112
+ observe(@game, :show_high_scores) do |show_high_scores|
113
113
  if show_high_scores
114
114
  show_high_score_dialog
115
115
  else
@@ -40,7 +40,7 @@ class Tetris
40
40
  type :vertical
41
41
  center true
42
42
  }
43
- text 'Tetris'
43
+ text 'Glimmer Tetris'
44
44
 
45
45
  tetris_menu_bar(game: game)
46
46
 
@@ -0,0 +1,233 @@
1
+ require 'glimmer-dsl-swt'
2
+ require 'os'
3
+
4
+ class Timer
5
+ include Glimmer::UI::CustomShell
6
+
7
+ import 'javax.sound.sampled'
8
+
9
+ FILE_SOUND_ALARM = File.expand_path(File.join('timer', 'alarm1.wav'), __dir__)
10
+ COMMAND_KEY = OS.mac? ? :command : :ctrl
11
+
12
+ ## Add options like the following to configure CustomShell by outside consumers
13
+ #
14
+ # options :title, :background_color
15
+ # option :width, default: 320
16
+ # option :height, default: 240
17
+
18
+ attr_accessor :countdown, :min, :sec
19
+
20
+ ## Use before_body block to pre-initialize variables to use in body
21
+ #
22
+ #
23
+ before_body {
24
+ Display.setAppName('Glimmer Timer')
25
+
26
+ @display = display {
27
+ on_about {
28
+ display_about_dialog
29
+ }
30
+ on_preferences {
31
+ display_about_dialog
32
+ }
33
+ }
34
+
35
+ @min = 0
36
+ @sec = 0
37
+ }
38
+
39
+ ## Use after_body block to setup observers for widgets in body
40
+ #
41
+ after_body {
42
+ Thread.new {
43
+ loop {
44
+ sleep(1)
45
+ if @countdown
46
+ sync_exec {
47
+ @countdown_time = Time.new(1, 1, 1, 0, min, sec)
48
+ @countdown_time -= 1
49
+ self.min = @countdown_time.min
50
+ self.sec = @countdown_time.sec
51
+ if @countdown_time.min <= 0 && @countdown_time.sec <= 0
52
+ stop_countdown
53
+ play_countdown_done_sound
54
+ end
55
+ }
56
+ end
57
+ }
58
+ }
59
+ }
60
+
61
+ ## Add widget content inside custom shell body
62
+ ## Top-most widget must be a shell or another custom shell
63
+ #
64
+ body {
65
+ shell {
66
+ grid_layout # 1 column
67
+
68
+ # Replace example content below with custom shell content
69
+ minimum_size (OS.windows? ? 214 : 200), 114
70
+ image File.join(APP_ROOT, 'package', 'windows', "Timer.ico") if OS.windows?
71
+ text "Glimmer Timer"
72
+
73
+ timer_menu_bar
74
+
75
+ countdown_group
76
+ }
77
+ }
78
+
79
+ def timer_menu_bar
80
+ menu_bar {
81
+ menu {
82
+ text '&Action'
83
+
84
+ menu_item {
85
+ text '&Start'
86
+ accelerator COMMAND_KEY, 's'
87
+ enabled bind(self, :countdown, on_read: :!)
88
+
89
+ on_widget_selected {
90
+ start_countdown
91
+ }
92
+ }
93
+ menu_item {
94
+ text 'St&op'
95
+ enabled bind(self, :countdown)
96
+ accelerator COMMAND_KEY, 'o'
97
+
98
+ on_widget_selected {
99
+ stop_countdown
100
+ }
101
+ }
102
+ unless OS.mac?
103
+ menu_item(:separator)
104
+ menu_item {
105
+ text 'E&xit'
106
+ accelerator :alt, :f4
107
+
108
+ on_widget_selected {
109
+ exit(0)
110
+ }
111
+ }
112
+ end
113
+ }
114
+ menu {
115
+ text '&Help'
116
+
117
+ menu_item {
118
+ text '&About'
119
+ accelerator COMMAND_KEY, :shift, 'a'
120
+
121
+ on_widget_selected {
122
+ display_about_dialog
123
+ }
124
+ }
125
+ }
126
+ }
127
+ end
128
+
129
+ def countdown_group
130
+ group {
131
+ # has grid layout with 1 column by default
132
+ text 'Countdown'
133
+ font height: 20
134
+
135
+ countdown_group_field_composite
136
+
137
+ countdown_group_button_composite
138
+ }
139
+ end
140
+
141
+ def countdown_group_field_composite
142
+ composite {
143
+ row_layout {
144
+ margin_width 0
145
+ margin_height 0
146
+ }
147
+ @min_spinner = spinner {
148
+ text_limit 2
149
+ digits 0
150
+ maximum 59
151
+ selection bind(self, :min)
152
+ enabled bind(self, :countdown, on_read: :!)
153
+ on_widget_default_selected {
154
+ start_countdown
155
+ }
156
+ }
157
+ label {
158
+ text ':'
159
+ font(height: 18) if OS.mac?
160
+ }
161
+ @sec_spinner = spinner {
162
+ text_limit 2
163
+ digits 0
164
+ maximum 59
165
+ selection bind(self, :sec)
166
+ enabled bind(self, :countdown, on_read: :!)
167
+ on_widget_default_selected {
168
+ start_countdown
169
+ }
170
+ }
171
+ }
172
+ end
173
+
174
+ def countdown_group_button_composite
175
+ composite {
176
+ row_layout {
177
+ margin_width 0
178
+ margin_height 0
179
+ }
180
+ @start_button = button {
181
+ text '&Start'
182
+ enabled bind(self, :countdown, on_read: :!)
183
+ on_widget_selected {
184
+ start_countdown
185
+ }
186
+ on_key_pressed { |event|
187
+ start_countdown if event.keyCode == swt(:cr)
188
+ }
189
+ }
190
+ @stop_button = button {
191
+ text 'St&op'
192
+ enabled bind(self, :countdown)
193
+ on_widget_selected {
194
+ stop_countdown
195
+ }
196
+ on_key_pressed { |event|
197
+ stop_countdown if event.keyCode == swt(:cr)
198
+ }
199
+ }
200
+ }
201
+ end
202
+
203
+ def display_about_dialog
204
+ message_box(body_root) {
205
+ text 'About'
206
+ message "Glimmer Timer\n\nCopyright (c) 2007-2021 Andy Maleh"
207
+ }.open
208
+ end
209
+
210
+ def start_countdown
211
+ self.countdown = true
212
+ @stop_button.swt_widget.set_focus
213
+ end
214
+
215
+ def stop_countdown
216
+ self.countdown = false
217
+ @min_spinner.swt_widget.set_focus
218
+ end
219
+
220
+ def play_countdown_done_sound
221
+ begin
222
+ file_or_stream = java.io.File.new(FILE_SOUND_ALARM)
223
+ audio_stream = AudioSystem.get_audio_input_stream(file_or_stream)
224
+ clip = AudioSystem.clip
225
+ clip.open(audio_stream)
226
+ clip.start
227
+ rescue => e
228
+ Glimmer::Config.logger.error e.full_message
229
+ end
230
+ end
231
+ end
232
+
233
+ Timer.launch
Binary file
@@ -23,7 +23,9 @@ require 'glimmer-dsl-swt'
23
23
 
24
24
  include Glimmer
25
25
 
26
- shell {
26
+ # A version of this was featured in a dzone article as an intro to Glimmer syntax:
27
+ # https://dzone.com/articles/an-introduction-glimmer
28
+ shell { |shell_proxy|
27
29
  text "User Profile"
28
30
 
29
31
  composite {
@@ -72,7 +74,7 @@ shell {
72
74
  button {
73
75
  text "close"
74
76
  layout_data :left, :center, true, true
75
- on_widget_selected { exit(0) }
77
+ on_widget_selected { shell_proxy.close }
76
78
  }
77
79
  }
78
80
  }.open
@@ -0,0 +1,164 @@
1
+ # Copyright (c) 2007-2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer-dsl-swt'
23
+ require 'net/http'
24
+ require 'json'
25
+ require 'facets/string/titlecase'
26
+
27
+ class Weather
28
+ include Glimmer::UI::CustomShell
29
+
30
+ DEFAULT_FONT_HEIGHT = 30
31
+ DEFAULT_FOREGROUND = :white
32
+
33
+ attr_accessor :city, :temp, :temp_min, :temp_max, :feels_like, :humidity
34
+
35
+ before_body {
36
+ @weather_mutex = Mutex.new
37
+ self.city = 'Montreal, QC, CA'
38
+ fetch_weather!
39
+ }
40
+
41
+ after_body {
42
+ Thread.new do
43
+ loop do
44
+ sleep(10)
45
+ break if body_root.disposed?
46
+ fetch_weather!
47
+ end
48
+ end
49
+ }
50
+
51
+ body {
52
+ shell(:no_resize) {
53
+ grid_layout
54
+
55
+ text 'Glimmer Weather'
56
+ minimum_size 400, 300
57
+ background rgb(135, 176, 235)
58
+
59
+ text {
60
+ layout_data(:center, :center, true, true)
61
+
62
+ text <=> [self, :city]
63
+
64
+ on_key_pressed {|event|
65
+ if event.keyCode == swt(:cr) # carriage return
66
+ Thread.new do
67
+ fetch_weather!
68
+ end
69
+ end
70
+ }
71
+ }
72
+
73
+ tab_folder {
74
+ layout_data(:center, :center, true, true)
75
+
76
+ ['℃', '℉'].each do |temp_unit|
77
+ tab_item {
78
+ grid_layout 2, false
79
+
80
+ text temp_unit
81
+ background :transparent
82
+
83
+ rectangle(0, 0, [:default, -2], [:default, -2], 15, 15) {
84
+ foreground DEFAULT_FOREGROUND
85
+ }
86
+
87
+ %w[temp temp_min temp_max feels_like].each do |field_name|
88
+ temp_field(field_name, temp_unit)
89
+ end
90
+
91
+ humidity_field
92
+ }
93
+ end
94
+ }
95
+ }
96
+ }
97
+
98
+ def temp_field(field_name, temp_unit)
99
+ name_label(field_name)
100
+ label {
101
+ layout_data(:fill, :center, true, false)
102
+ text <= [self, field_name, on_read: ->(t) { "#{kelvin_to_temp_unit(t, temp_unit).to_f.round}°" }]
103
+ font height: DEFAULT_FONT_HEIGHT
104
+ foreground DEFAULT_FOREGROUND
105
+ }
106
+ end
107
+
108
+ def humidity_field
109
+ name_label('humidity')
110
+ label {
111
+ layout_data(:fill, :center, true, false)
112
+ text <= [self, 'humidity', on_read: ->(h) { "#{h.to_f.round}%" }]
113
+ font height: DEFAULT_FONT_HEIGHT
114
+ foreground DEFAULT_FOREGROUND
115
+ }
116
+ end
117
+
118
+ def name_label(field_name)
119
+ label {
120
+ layout_data :fill, :center, false, false
121
+ text field_name.titlecase
122
+ font height: DEFAULT_FONT_HEIGHT
123
+ foreground DEFAULT_FOREGROUND
124
+ }
125
+ end
126
+
127
+ def fetch_weather!
128
+ @weather_mutex.synchronize do
129
+ self.weather_data = JSON.parse(Net::HTTP.get('api.openweathermap.org', "/data/2.5/weather?q=#{city}&appid=1d16d70a9aec3570b5cbd27e6b421330"))
130
+ end
131
+ rescue
132
+ # No Op
133
+ end
134
+
135
+ def weather_data=(data)
136
+ @weather_data = data
137
+ main_data = data['main']
138
+ # temps come back in Kelvin
139
+ self.temp = main_data['temp']
140
+ self.temp_min = main_data['temp_min']
141
+ self.temp_max = main_data['temp_max']
142
+ self.feels_like = main_data['feels_like']
143
+ self.humidity = main_data['humidity']
144
+ end
145
+
146
+ def kelvin_to_temp_unit(kelvin, temp_unit)
147
+ temp_unit == '℃' ? kelvin_to_celsius(kelvin) : kelvin_to_fahrenheit(kelvin)
148
+ end
149
+
150
+ def kelvin_to_celsius(kelvin)
151
+ kelvin - 273.15
152
+ end
153
+
154
+ def celsius_to_fahrenheit(celsius)
155
+ (celsius * 9 / 5 ) + 32
156
+ end
157
+
158
+ def kelvin_to_fahrenheit(kelvin)
159
+ celsius_to_fahrenheit(kelvin_to_celsius(kelvin))
160
+ end
161
+
162
+ end
163
+
164
+ Weather.launch