glimmer-dsl-swt 4.20.0.3 → 4.20.2.1

Sign up to get free protection for your applications and to get access to all the features.
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