glimmer-dsl-swt 4.18.2.3 → 4.18.3.2

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