glimmer-dsl-swt 4.18.2.4 → 4.18.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +240 -30
  4. data/VERSION +1 -1
  5. data/glimmer-dsl-swt.gemspec +13 -7
  6. data/lib/ext/glimmer/config.rb +24 -7
  7. data/lib/glimmer/data_binding/table_items_binding.rb +8 -5
  8. data/lib/glimmer/data_binding/widget_binding.rb +22 -4
  9. data/lib/glimmer/dsl/swt/color_expression.rb +4 -4
  10. data/lib/glimmer/dsl/swt/data_binding_expression.rb +3 -3
  11. data/lib/glimmer/dsl/swt/dsl.rb +1 -0
  12. data/lib/glimmer/dsl/swt/multiply_expression.rb +53 -0
  13. data/lib/glimmer/dsl/swt/property_expression.rb +4 -2
  14. data/lib/glimmer/dsl/swt/shape_expression.rb +2 -4
  15. data/{samples/elaborate/tetris/view/game_over_dialog.rb → lib/glimmer/dsl/swt/transform_expression.rb} +29 -46
  16. data/lib/glimmer/dsl/swt/widget_expression.rb +2 -1
  17. data/lib/glimmer/swt/color_proxy.rb +28 -6
  18. data/lib/glimmer/swt/custom/drawable.rb +8 -0
  19. data/lib/glimmer/swt/custom/shape.rb +66 -26
  20. data/lib/glimmer/swt/directory_dialog_proxy.rb +3 -3
  21. data/lib/glimmer/swt/display_proxy.rb +26 -5
  22. data/lib/glimmer/swt/file_dialog_proxy.rb +3 -3
  23. data/lib/glimmer/swt/shell_proxy.rb +24 -4
  24. data/lib/glimmer/swt/table_proxy.rb +31 -7
  25. data/lib/glimmer/swt/transform_proxy.rb +109 -0
  26. data/lib/glimmer/swt/widget_listener_proxy.rb +14 -5
  27. data/lib/glimmer/swt/widget_proxy.rb +35 -20
  28. data/lib/glimmer/ui/custom_shell.rb +11 -9
  29. data/lib/glimmer/ui/custom_widget.rb +65 -39
  30. data/samples/elaborate/meta_sample.rb +81 -24
  31. data/samples/elaborate/tetris.rb +105 -44
  32. data/samples/elaborate/tetris/model/block.rb +1 -1
  33. data/samples/elaborate/tetris/model/game.rb +233 -137
  34. data/samples/elaborate/tetris/model/past_game.rb +26 -0
  35. data/samples/elaborate/tetris/model/tetromino.rb +46 -30
  36. data/samples/elaborate/tetris/view/block.rb +33 -5
  37. data/samples/elaborate/tetris/view/high_score_dialog.rb +133 -0
  38. data/samples/elaborate/tetris/view/playfield.rb +1 -1
  39. data/samples/elaborate/tetris/view/score_lane.rb +11 -11
  40. data/samples/elaborate/tetris/view/tetris_menu_bar.rb +121 -0
  41. data/samples/elaborate/tic_tac_toe.rb +4 -4
  42. data/samples/hello/hello_canvas_transform.rb +40 -0
  43. data/samples/hello/hello_link.rb +1 -1
  44. metadata +11 -5
@@ -25,62 +25,80 @@ require_relative 'tetris/model/game'
25
25
 
26
26
  require_relative 'tetris/view/playfield'
27
27
  require_relative 'tetris/view/score_lane'
28
- require_relative 'tetris/view/game_over_dialog'
28
+ require_relative 'tetris/view/high_score_dialog'
29
+ require_relative 'tetris/view/tetris_menu_bar'
29
30
 
30
31
  class Tetris
31
32
  include Glimmer::UI::CustomShell
32
33
 
33
34
  BLOCK_SIZE = 25
34
- PLAYFIELD_WIDTH = 10
35
- PLAYFIELD_HEIGHT = 20
36
- PREVIEW_PLAYFIELD_WIDTH = 4
37
- PREVIEW_PLAYFIELD_HEIGHT = 2
35
+ FONT_NAME = 'Menlo'
36
+ FONT_TITLE_HEIGHT = 32
37
+ FONT_TITLE_STYLE = :bold
38
+
39
+ option :playfield_width, default: Model::Game::PLAYFIELD_WIDTH
40
+ option :playfield_height, default: Model::Game::PLAYFIELD_HEIGHT
41
+
42
+ attr_reader :game
38
43
 
39
44
  before_body {
45
+ @mutex = Mutex.new
46
+ @game = Model::Game.new(playfield_width, playfield_height)
47
+
48
+ @game.configure_beeper do
49
+ display.beep
50
+ end
51
+
52
+ Display.app_name = 'Glimmer Tetris'
40
53
  display {
41
- on_swt_keydown { |key_event|
54
+ @keyboard_listener = on_swt_keydown { |key_event|
42
55
  case key_event.keyCode
43
- when swt(:arrow_down)
44
- Model::Game.current_tetromino.down
45
- when swt(:arrow_left)
46
- Model::Game.current_tetromino.left
47
- when swt(:arrow_right)
48
- Model::Game.current_tetromino.right
49
- when swt(:shift)
56
+ when swt(:arrow_down), 's'.bytes.first
57
+ game.down!
58
+ when swt(:arrow_up)
59
+ game.down!(immediate: true)
60
+ when swt(:arrow_left), 'a'.bytes.first
61
+ game.left!
62
+ when swt(:arrow_right), 'd'.bytes.first
63
+ game.right!
64
+ when swt(:shift), swt(:alt)
50
65
  if key_event.keyLocation == swt(:right) # right shift key
51
- Model::Game.current_tetromino.rotate(:right)
66
+ game.rotate!(:right)
52
67
  elsif key_event.keyLocation == swt(:left) # left shift key
53
- Model::Game.current_tetromino.rotate(:left)
68
+ game.rotate!(:left)
54
69
  end
55
- when 'd'.bytes.first, swt(:arrow_up)
56
- Model::Game.current_tetromino.rotate(:right)
57
- when 'a'.bytes.first
58
- Model::Game.current_tetromino.rotate(:left)
70
+ when swt(:ctrl)
71
+ game.rotate!(:left)
59
72
  end
60
73
  }
61
- }
62
-
63
- Model::Game.configure_beeper do
64
- async_exec {
65
- display.beep
74
+
75
+ # if running in app mode, set the Mac app about dialog (ignored in platforms)
76
+ @about_observer = on_about {
77
+ show_about_dialog
66
78
  }
67
- end
79
+
80
+ @quit_observer = on_quit {
81
+ exit(0)
82
+ }
83
+ }
68
84
  }
69
85
 
70
86
  after_body {
71
- Model::Game.start
72
-
73
- Thread.new {
74
- loop {
75
- sleep(Model::Game.delay)
76
- sync_exec {
77
- unless Model::Game.game_over?
78
- Model::Game.current_tetromino.down
79
- game_over_dialog(parent_shell: body_root).open if Model::Game.current_tetromino.row <= 0 && Model::Game.current_tetromino.stopped?
80
- end
81
- }
82
- }
83
- }
87
+ @game_over_observer = observe(@game, :game_over) do |game_over|
88
+ if game_over
89
+ show_high_score_dialog
90
+ else
91
+ start_moving_tetrominos_down
92
+ end
93
+ end
94
+ @show_high_scores_observer = observe(@game, :show_high_scores) do |show_high_scores|
95
+ if show_high_scores
96
+ show_high_score_dialog
97
+ else
98
+ @high_score_dialog.close unless @high_score_dialog.nil? || @high_score_dialog.disposed? || !@high_score_dialog.visible?
99
+ end
100
+ end
101
+ @game.start!
84
102
  }
85
103
 
86
104
  body {
@@ -92,17 +110,60 @@ class Tetris
92
110
  margin_height 0
93
111
  horizontal_spacing 0
94
112
  }
95
-
113
+
96
114
  text 'Glimmer Tetris'
115
+ minimum_size 475, 500
97
116
  background :gray
98
-
99
- playfield(game_playfield: Model::Game.playfield, playfield_width: PLAYFIELD_WIDTH, playfield_height: PLAYFIELD_HEIGHT, block_size: BLOCK_SIZE)
100
-
101
- score_lane(block_size: BLOCK_SIZE) {
102
- layout_data(:fill, :fill, false, true)
117
+
118
+ tetris_menu_bar(game: game)
119
+
120
+ playfield(game_playfield: game.playfield, playfield_width: playfield_width, playfield_height: playfield_height, block_size: BLOCK_SIZE)
121
+
122
+ score_lane(game: game, block_size: BLOCK_SIZE) {
123
+ layout_data(:fill, :fill, true, true)
124
+ }
125
+
126
+ on_widget_disposed {
127
+ deregister_observers
103
128
  }
104
129
  }
105
130
  }
131
+
132
+ def start_moving_tetrominos_down
133
+ Thread.new do
134
+ @mutex.synchronize do
135
+ loop do
136
+ time = Time.now
137
+ sleep @game.delay
138
+ break if @game.game_over? || body_root.disposed?
139
+ sync_exec {
140
+ @game.down! unless @game.paused?
141
+ }
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ def show_high_score_dialog
148
+ return if @high_score_dialog&.visible?
149
+ @high_score_dialog = high_score_dialog(parent_shell: body_root, game: @game) if @high_score_dialog.nil? || @high_score_dialog.disposed?
150
+ @high_score_dialog.show
151
+ end
152
+
153
+ def show_about_dialog
154
+ message_box {
155
+ text 'Glimmer Tetris'
156
+ message "Glimmer Tetris\n\nGlimmer DSL for SWT Sample\n\nCopyright (c) 2007-2021 Andy Maleh"
157
+ }.open
158
+ end
159
+
160
+ def deregister_observers
161
+ @show_high_scores_observer&.deregister
162
+ @game_over_observer&.deregister
163
+ @keyboard_listener&.deregister
164
+ @about_observer&.deregister
165
+ @quit_observer&.deregister
166
+ end
106
167
  end
107
168
 
108
169
  Tetris.launch
@@ -33,7 +33,7 @@ class Tetris
33
33
 
34
34
  # Clears block color. `quietly` option indicates if it should not notify observers by setting value quietly via variable not attribute writer.
35
35
  def clear
36
- self.color = COLOR_CLEAR
36
+ self.color = COLOR_CLEAR unless self.color == COLOR_CLEAR
37
37
  end
38
38
 
39
39
  def clear?
@@ -19,167 +19,263 @@
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
+ require 'fileutils'
23
+ require 'etc'
24
+ require 'glimmer/data_binding/observer'
25
+ require 'glimmer/config'
26
+
22
27
  require_relative 'block'
23
28
  require_relative 'tetromino'
29
+ require_relative 'past_game'
24
30
 
25
31
  class Tetris
26
32
  module Model
27
33
  class Game
34
+ PLAYFIELD_WIDTH = 10
35
+ PLAYFIELD_HEIGHT = 20
36
+ PREVIEW_PLAYFIELD_WIDTH = 4
37
+ PREVIEW_PLAYFIELD_HEIGHT = 2
28
38
  SCORE_MULTIPLIER = {1 => 40, 2 => 100, 3 => 300, 4 => 1200}
29
39
 
30
- class << self
31
- attr_accessor :game_over, :preview_tetromino, :lines, :score, :level
32
- alias game_over? game_over
33
-
34
- def consider_adding_tetromino
35
- if tetrominoes.empty? || Game.current_tetromino.stopped?
36
- preview_tetromino.launch!
37
- preview_next_tetromino!
38
- end
39
- end
40
-
41
- def current_tetromino
42
- tetrominoes.last
43
- end
40
+ attr_reader :playfield_width, :playfield_height
41
+ attr_accessor :game_over, :paused, :preview_tetromino, :lines, :score, :level, :high_scores, :beeping, :added_high_score, :show_high_scores
42
+ alias game_over? game_over
43
+ alias paused? paused
44
+ alias beeping? beeping
45
+ alias added_high_score? added_high_score
46
+
47
+ def initialize(playfield_width = PLAYFIELD_WIDTH, playfield_height = PLAYFIELD_HEIGHT)
48
+ @playfield_width = playfield_width
49
+ @playfield_height = playfield_height
50
+ @high_scores = []
51
+ @show_high_scores = false
52
+ @beeping = true
53
+ load_high_scores!
54
+ end
55
+
56
+ def configure_beeper(&beeper)
57
+ @beeper = beeper
58
+ end
59
+
60
+ def game_in_progress?
61
+ !game_over? && !paused?
62
+ end
63
+
64
+ def start!
65
+ self.show_high_scores = false
66
+ self.paused = false
67
+ self.level = 1
68
+ self.score = 0
69
+ self.lines = 0
70
+ reset_playfield
71
+ reset_preview_playfield
72
+ reset_tetrominoes
73
+ preview_next_tetromino!
74
+ consider_adding_tetromino
75
+ self.game_over = false
76
+ end
77
+ alias restart! start!
78
+
79
+ def game_over!
80
+ add_high_score!
81
+ beep
82
+ self.game_over = true
83
+ end
44
84
 
45
- def tetrominoes
46
- @tetrominoes ||= reset_tetrominoes
85
+ def clear_high_scores!
86
+ high_scores.clear
87
+ end
88
+
89
+ def add_high_score!
90
+ self.added_high_score = true
91
+ high_scores.prepend(PastGame.new("Player #{high_scores.count + 1}", score, lines, level))
92
+ end
93
+
94
+ def save_high_scores!
95
+ high_score_file_content = @high_scores.map {|past_game| past_game.to_a.join("\t") }.join("\n")
96
+ FileUtils.mkdir_p(tetris_dir)
97
+ File.write(tetris_high_score_file, high_score_file_content)
98
+ rescue => e
99
+ # Fail safely by keeping high scores in memory if unable to access disk
100
+ Glimmer::Config.logger.error {"Failed to save high scores in: #{tetris_high_score_file}\n#{e.full_message}"}
101
+ end
102
+
103
+ def load_high_scores!
104
+ if File.exist?(tetris_high_score_file)
105
+ self.high_scores = File.read(tetris_high_score_file).split("\n").map {|line| PastGame.new(*line.split("\t")) }
47
106
  end
48
-
49
- # Returns blocks in the playfield
50
- def playfield
51
- @playfield ||= @original_playfield = PLAYFIELD_HEIGHT.times.map {
52
- PLAYFIELD_WIDTH.times.map {
53
- Block.new
54
- }
107
+ rescue => e
108
+ # Fail safely by keeping high scores in memory if unable to access disk
109
+ Glimmer::Config.logger.error {"Failed to load high scores from: #{tetris_high_score_file}\n#{e.full_message}"}
110
+ end
111
+
112
+ def tetris_dir
113
+ @tetris_dir ||= File.join(Etc.getpwuid.dir, '.glimmer-tetris')
114
+ end
115
+
116
+ def tetris_high_score_file
117
+ File.join(tetris_dir, "high_scores.txt")
118
+ end
119
+
120
+ def down!(immediate: false)
121
+ return unless game_in_progress?
122
+ current_tetromino.down!(immediate: immediate)
123
+ game_over! if current_tetromino.row <= 0 && current_tetromino.stopped?
124
+ end
125
+
126
+ def right!
127
+ return unless game_in_progress?
128
+ current_tetromino.right!
129
+ end
130
+
131
+ def left!
132
+ return unless game_in_progress?
133
+ current_tetromino.left!
134
+ end
135
+
136
+ def rotate!(direction)
137
+ return unless game_in_progress?
138
+ current_tetromino.rotate!(direction)
139
+ end
140
+
141
+ def current_tetromino
142
+ tetrominoes.last
143
+ end
144
+
145
+ def tetrominoes
146
+ @tetrominoes ||= reset_tetrominoes
147
+ end
148
+
149
+ # Returns blocks in the playfield
150
+ def playfield
151
+ @playfield ||= @original_playfield = @playfield_height.times.map {
152
+ @playfield_width.times.map {
153
+ Block.new
55
154
  }
56
- end
57
-
58
- def hypothetical(&block)
59
- @playfield = hypothetical_playfield
60
- block.call
61
- @playfield = @original_playfield
62
- end
63
-
64
- def hypothetical?
65
- @playfield != @original_playfield
66
- end
67
-
68
- def hypothetical_playfield
69
- PLAYFIELD_HEIGHT.times.map { |row|
70
- PLAYFIELD_WIDTH.times.map { |column|
71
- playfield[row][column].clone
72
- }
155
+ }
156
+ end
157
+
158
+ # Executes a hypothetical scenario without truly changing playfield permanently
159
+ def hypothetical(&block)
160
+ @playfield = hypothetical_playfield
161
+ block.call
162
+ @playfield = @original_playfield
163
+ end
164
+
165
+ # Returns whether currently executing a hypothetical scenario
166
+ def hypothetical?
167
+ @playfield != @original_playfield
168
+ end
169
+
170
+ def hypothetical_playfield
171
+ @playfield_height.times.map { |row|
172
+ @playfield_width.times.map { |column|
173
+ playfield[row][column].clone
73
174
  }
74
- end
75
-
76
- def preview_playfield
77
- @preview_playfield ||= PREVIEW_PLAYFIELD_HEIGHT.times.map {|row|
78
- PREVIEW_PLAYFIELD_WIDTH.times.map {|column|
79
- Block.new
80
- }
175
+ }
176
+ end
177
+
178
+ def preview_playfield
179
+ @preview_playfield ||= PREVIEW_PLAYFIELD_HEIGHT.times.map {|row|
180
+ PREVIEW_PLAYFIELD_WIDTH.times.map {|column|
181
+ Block.new
81
182
  }
82
- end
83
-
84
- def preview_next_tetromino!
85
- self.preview_tetromino = Tetromino.new
86
- end
87
-
88
- def calculate_score!(eliminated_lines)
89
- new_score = SCORE_MULTIPLIER[eliminated_lines] * (level + 1)
90
- self.score += new_score
91
- end
92
-
93
- def level_up!
94
- self.level += 1 if lines >= self.level*10
95
- end
96
-
97
- def delay
98
- [1.1 - (level.to_i * 0.1), 0.001].max
99
- end
100
-
101
- def consider_eliminating_lines
102
- eliminated_lines = 0
103
- playfield.each_with_index do |row, playfield_row|
104
- if row.all? {|block| !block.clear?}
105
- eliminated_lines += 1
106
- shift_blocks_down_above_row(playfield_row)
107
- end
108
- end
109
- if eliminated_lines > 0
110
- beep
111
- self.lines += eliminated_lines
112
- level_up!
113
- calculate_score!(eliminated_lines)
183
+ }
184
+ end
185
+
186
+ def preview_next_tetromino!
187
+ self.preview_tetromino = Tetromino.new(self)
188
+ end
189
+
190
+ def calculate_score!(eliminated_lines)
191
+ new_score = SCORE_MULTIPLIER[eliminated_lines] * (level + 1)
192
+ self.score += new_score
193
+ end
194
+
195
+ def level_up!
196
+ self.level += 1 if lines >= self.level*10
197
+ end
198
+
199
+ def delay
200
+ [1.1 - (level.to_i * 0.1), 0.001].max
201
+ end
202
+
203
+ def beep
204
+ @beeper&.call if beeping
205
+ end
206
+
207
+ def reset_tetrominoes
208
+ @tetrominoes = []
209
+ end
210
+
211
+ def reset_playfield
212
+ playfield.each do |row|
213
+ row.each do |block|
214
+ block.clear
114
215
  end
115
216
  end
116
-
117
- def beep
118
- @beeper&.call
119
- end
120
-
121
- def configure_beeper(&beeper)
122
- @beeper = beeper
123
- end
124
-
125
- def shift_blocks_down_above_row(row)
126
- row.downto(0) do |playfield_row|
127
- playfield[playfield_row].each_with_index do |block, playfield_column|
128
- previous_row = playfield[playfield_row - 1]
129
- previous_block = previous_row[playfield_column]
130
- block.color = previous_block.color
131
- end
217
+ end
218
+
219
+ def reset_preview_playfield
220
+ preview_playfield.each do |row|
221
+ row.each do |block|
222
+ block.clear
132
223
  end
133
- playfield[0].each(&:clear)
134
224
  end
135
-
136
- def start
137
- self.level = 1
138
- self.score = 0
139
- self.lines = @previoius_lines = 0
140
- reset_playfield
141
- reset_preview_playfield
142
- reset_tetrominoes
225
+ end
226
+
227
+ def consider_adding_tetromino
228
+ if tetrominoes.empty? || current_tetromino.stopped?
229
+ preview_tetromino.launch!
143
230
  preview_next_tetromino!
144
- consider_adding_tetromino
145
- self.game_over = false
146
- end
147
- alias restart start
148
-
149
- def reset_tetrominoes
150
- @tetrominoes = []
151
231
  end
152
-
153
- def reset_playfield
154
- playfield.each do |row|
155
- row.each do |block|
156
- block.clear
157
- end
232
+ end
233
+
234
+ def consider_eliminating_lines
235
+ eliminated_lines = 0
236
+ playfield.each_with_index do |row, playfield_row|
237
+ if row.all? {|block| !block.clear?}
238
+ eliminated_lines += 1
239
+ shift_blocks_down_above_row(playfield_row)
158
240
  end
159
241
  end
160
-
161
- def reset_preview_playfield
162
- preview_playfield.each do |row|
163
- row.each do |block|
164
- block.clear
165
- end
166
- end
242
+ if eliminated_lines > 0
243
+ beep
244
+ self.lines += eliminated_lines
245
+ level_up!
246
+ calculate_score!(eliminated_lines)
167
247
  end
168
-
169
- def playfield_remaining_heights(tetromino = nil)
170
- PLAYFIELD_WIDTH.times.map do |playfield_column|
171
- (playfield.each_with_index.detect do |row, playfield_row|
172
- !row[playfield_column].clear? &&
173
- (
174
- tetromino.nil? ||
175
- (bottom_most_block = tetromino.bottom_most_block_for_column(playfield_column)).nil? ||
176
- (playfield_row > tetromino.row + bottom_most_block[:row])
177
- )
178
- end || [nil, PLAYFIELD_HEIGHT])[1]
179
- end.to_a
248
+ end
249
+
250
+ def playfield_remaining_heights(tetromino = nil)
251
+ @playfield_width.times.map do |playfield_column|
252
+ bottom_most_block = tetromino.bottom_most_block_for_column(playfield_column)
253
+ (playfield.each_with_index.detect do |row, playfield_row|
254
+ !row[playfield_column].clear? &&
255
+ (
256
+ tetromino.nil? ||
257
+ bottom_most_block.nil? ||
258
+ (playfield_row > tetromino.row + bottom_most_block[:row_index])
259
+ )
260
+ end || [nil, @playfield_height])[1]
261
+ end.to_a
262
+ end
263
+
264
+ private
265
+
266
+ def shift_blocks_down_above_row(row)
267
+ row.downto(0) do |playfield_row|
268
+ playfield[playfield_row].each_with_index do |block, playfield_column|
269
+ previous_row = playfield[playfield_row - 1]
270
+ previous_block = previous_row[playfield_column]
271
+ block.color = previous_block.color unless block.color == previous_block.color
272
+ end
180
273
  end
274
+ playfield[0].each(&:clear)
181
275
  end
276
+
182
277
  end
278
+
183
279
  end
280
+
184
281
  end
185
-