life_game_viewer 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,193 @@
1
+ # Contains Action classes used by the application.
2
+
3
+ require_relative 'clipboard_helper'
4
+
5
+
6
+ # Java Imports:
7
+ java_import %w(
8
+ javax.swing.AbstractAction
9
+ )
10
+
11
+
12
+ # Generation Move Actions. Class hierarchy is:
13
+ #
14
+ # MoveAction
15
+ # -- ShowPastGenerationAction
16
+ # -- -- ShowFirstGenerationAction
17
+ # -- -- ShowPreviousGenerationAction
18
+ # -- ShowFutureGenerationAction
19
+ # -- -- ShowNextGenerationAction
20
+ # -- -- ShowLastGenerationAction
21
+
22
+
23
+ class MoveAction < AbstractAction
24
+
25
+ def initialize(table_model)
26
+ super(caption) # caption implemented by subclasses
27
+ @table_model = table_model
28
+
29
+ # should_be_enabled? below needs to be implemented by subclasses
30
+ enabled_updater = lambda do |current_generation_num|
31
+ self.enabled = should_be_enabled?
32
+ end
33
+
34
+ @table_model.add_current_num_change_handler(enabled_updater)
35
+
36
+ self.enabled = enabled_updater.call(nil)
37
+ end
38
+
39
+ def actionPerformed(event)
40
+ move # implemented by subclasses
41
+ @table_model.fire_table_data_changed
42
+ end
43
+ end
44
+
45
+
46
+
47
+
48
+ class ShowPastGenerationAction < MoveAction
49
+
50
+ def initialize(table_model)
51
+ super(table_model) # caption implemented by subclasses
52
+ end
53
+
54
+ def should_be_enabled?
55
+ ! @table_model.at_first_generation?
56
+ end
57
+ end
58
+
59
+
60
+
61
+
62
+ class ShowFirstGenerationAction < ShowPastGenerationAction
63
+
64
+ def initialize(table_model)
65
+ super(table_model)
66
+ end
67
+
68
+ def move
69
+ @table_model.go_to_first_generation
70
+ end
71
+
72
+ def caption
73
+ "First (1)"
74
+ end
75
+ end
76
+
77
+
78
+
79
+
80
+ class ShowPreviousGenerationAction < ShowPastGenerationAction
81
+
82
+ def initialize(table_model)
83
+ super(table_model)
84
+ end
85
+
86
+ def move
87
+ @table_model.go_to_previous_generation
88
+ end
89
+
90
+ def caption
91
+ "Previous (4)"
92
+ end
93
+ end
94
+
95
+
96
+
97
+
98
+ class ShowFutureGenerationAction < MoveAction
99
+
100
+ def initialize(table_model)
101
+ super(table_model) # caption implemented by subclasses
102
+ end
103
+
104
+ def should_be_enabled?
105
+ ! @table_model.at_last_generation?
106
+ end
107
+ end
108
+
109
+
110
+
111
+
112
+ class ShowNextGenerationAction < ShowFutureGenerationAction
113
+
114
+ def initialize(table_model)
115
+ super(table_model)
116
+ end
117
+
118
+ def move
119
+ @table_model.go_to_next_generation
120
+ end
121
+
122
+ def caption
123
+ "Next (7)"
124
+ end
125
+ end
126
+
127
+
128
+
129
+
130
+ class ShowLastGenerationAction < ShowFutureGenerationAction
131
+
132
+ def initialize(table_model)
133
+ super(table_model)
134
+ end
135
+
136
+ def move
137
+ @table_model.go_to_last_generation
138
+ end
139
+
140
+ def caption
141
+ "Last (0)"
142
+ end
143
+ end
144
+
145
+
146
+
147
+
148
+ class ExitAction < AbstractAction
149
+
150
+ # table_model param not used but needed for instantiation by create_button
151
+ def initialize(table_model)
152
+ super("Exit (Q)")
153
+ put_value(SHORT_DESCRIPTION, 'Press capital-Q to exit.')
154
+ end
155
+
156
+ def actionPerformed(event)
157
+ java.lang.System.exit(0)
158
+ end
159
+ end
160
+
161
+
162
+
163
+
164
+ class CopyToClipboardAction < AbstractAction
165
+
166
+ def initialize(table_model)
167
+ super("Copy (C)")
168
+ put_value(SHORT_DESCRIPTION, "Press #{ClipboardHelper.key_prefix}-C to copy board contents to clipboard.")
169
+ @table_model = table_model
170
+ end
171
+
172
+ def actionPerformed(event)
173
+ text = LifeVisualizer.new.to_display_string(@table_model.life_model)
174
+ ClipboardHelper.clipboard_text = text
175
+ end
176
+ end
177
+
178
+
179
+
180
+
181
+ class NewGameFromClipboardAction < AbstractAction
182
+
183
+ def initialize(table_model)
184
+ super("Paste (V)")
185
+ put_value(SHORT_DESCRIPTION, "Press #{ClipboardHelper.key_prefix}-V to create a new game from the clipboard contents.")
186
+ @table_model = table_model
187
+ end
188
+
189
+ def actionPerformed(event)
190
+ new_model = @table_model.life_model.class.send(:create_from_string, ClipboardHelper.clipboard_text)
191
+ @table_model.reset_model(new_model)
192
+ end
193
+ end
@@ -0,0 +1,55 @@
1
+ java_import 'java.awt.datatransfer.StringSelection'
2
+ java_import 'java.awt.datatransfer.DataFlavor'
3
+ java_import 'java.awt.datatransfer.ClipboardOwner'
4
+ java_import 'java.awt.Toolkit'
5
+
6
+
7
+ # Simplifies the use of the system clipboard access via Java.
8
+ class ClipboardHelper
9
+
10
+ include ClipboardOwner
11
+
12
+ def self.clipboard
13
+ Toolkit.default_toolkit.system_clipboard
14
+ end
15
+
16
+ def self.clipboard_text
17
+ transferable = clipboard.getContents(self)
18
+ transferable.getTransferData(DataFlavor::stringFlavor)
19
+ end
20
+
21
+ def self.clipboard_text=(text)
22
+ string_selection = StringSelection.new(text)
23
+ clipboard.setContents(string_selection, self)
24
+ end
25
+
26
+ def self.mac_os?
27
+ /^Mac/ === java.lang.System.properties['os.name']
28
+ end
29
+
30
+ # For getting descriptive text for, e.g. tooltips.
31
+ def self.key_prefix
32
+ mac_os? ? "Command" : "Ctrl"
33
+ end
34
+
35
+ # Gets the first Action object associated with the passed action_name.
36
+ def self.input_action_key(action_name)
37
+ map = UIManager.get("TextField.focusInputMap")
38
+ map.keys.select { |key| map.get(key) == action_name }.first
39
+ end
40
+
41
+ # Name of copy key for this OS.
42
+ def self.copy_key_name
43
+ input_action_key("copy-to-clipboard").to_string
44
+ end
45
+
46
+ # Name of paste key for this OS.
47
+ def self.paste_key_name
48
+ input_action_key("paste-from-clipboard").to_string
49
+ end
50
+
51
+ def self.lostOwnership(clipboard, contents)
52
+ # do nothing; this method called by Java when this object loses ownership
53
+ # of the clipboard.
54
+ end
55
+ end
@@ -0,0 +1,67 @@
1
+ class Generations < Array
2
+
3
+ attr_reader :last_num
4
+ attr_accessor :current_num
5
+
6
+ def initialize(life_model)
7
+ self << life_model
8
+ @current_num = 0
9
+ @last_num = nil
10
+ ensure_next_in_cache
11
+ end
12
+
13
+ def current
14
+ self[current_num]
15
+ end
16
+
17
+ def found_last_generation?
18
+ !!@last_num
19
+ end
20
+
21
+ def at_last_generation?
22
+ current_num == @last_num
23
+ end
24
+
25
+ def at_first_generation?
26
+ current_num == 0
27
+ end
28
+
29
+ def ensure_next_in_cache
30
+ at_end_of_array = (current_num == (size - 1))
31
+ need_to_get_new_model = at_end_of_array && (! found_last_generation?)
32
+ if need_to_get_new_model
33
+ tentative_next = current.next_generation_model
34
+ if tentative_next == current
35
+ @last_num = current_num
36
+ else
37
+ self << tentative_next
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ def next
44
+ raise "Next was called when at end of lineage." if at_last_generation?
45
+ self.current_num = current_num + 1
46
+ ensure_next_in_cache
47
+ current
48
+ end
49
+
50
+ def previous
51
+ raise "Previous was called when at first generation." if at_first_generation?
52
+ self.current_num = current_num - 1
53
+ current
54
+ end
55
+
56
+ def first
57
+ self.current_num = 0
58
+ current
59
+ end
60
+
61
+ def last
62
+ until at_last_generation?
63
+ self.next
64
+ end
65
+ current
66
+ end
67
+ end
@@ -0,0 +1,293 @@
1
+ # This file contains the definitions of all the Swing UI component
2
+ # classes. The one containing all the rest is the MainFrame class.
3
+
4
+ require 'java'
5
+
6
+ require_relative '../model/life_visualizer'
7
+ require_relative '../model/model_validation'
8
+ require_relative '../model/sample_life_model'
9
+ require_relative 'life_table_model'
10
+ require_relative 'actions'
11
+
12
+
13
+ # Java Imports:
14
+ java_import %w(
15
+ java.awt.BorderLayout
16
+ java.awt.Color
17
+ java.awt.Desktop
18
+ java.awt.Dimension
19
+ java.awt.Frame
20
+ java.awt.GridLayout
21
+ java.awt.Toolkit
22
+ java.awt.event.KeyEvent
23
+ java.awt.event.MouseAdapter
24
+ java.awt.event.WindowAdapter
25
+ java.net.URI
26
+ javax.swing.BorderFactory
27
+ javax.swing.Box
28
+ javax.swing.ImageIcon
29
+ javax.swing.JButton
30
+ javax.swing.JComponent
31
+ javax.swing.JFrame
32
+ javax.swing.JLabel
33
+ javax.swing.JPanel
34
+ javax.swing.JScrollPane
35
+ javax.swing.JTable
36
+ javax.swing.KeyStroke
37
+ javax.swing.UIManager
38
+ javax.swing.table.TableCellRenderer
39
+ )
40
+
41
+
42
+ class LifeGameViewerFrame < JFrame
43
+
44
+ attr_accessor :table_model
45
+
46
+ # Special class method for demonstration purposes; makes it
47
+ # trivially simple to view the program in action without
48
+ # having to add any custom behavior.
49
+ def self.view_sample
50
+ str = ''
51
+ 12.times { str << ('*-' * 6) << "\n" }
52
+ model = SampleLifeModel.create_from_string(str)
53
+ frame = LifeGameViewerFrame.new(model)
54
+ frame.visible = true
55
+ frame # return frame so it can be manipulated (.visible =, etc.)
56
+ end
57
+
58
+ def initialize(life_model)
59
+ model_validation_message = ModelValidation.new.methods_missing_message(life_model)
60
+ if model_validation_message
61
+ raise model_validation_message
62
+ end
63
+
64
+ super('The Game of Life')
65
+ @table_model = LifeTableModel.new(life_model)
66
+ self.default_close_operation = JFrame::EXIT_ON_CLOSE
67
+ add(JScrollPane.new(Table.new(table_model)), BorderLayout::CENTER)
68
+ add(HeaderPanel.new, BorderLayout::NORTH)
69
+ add(BottomPanel.new(@table_model, self), BorderLayout::SOUTH)
70
+ content_pane.border = BorderFactory.create_empty_border(12, 12, 12, 12)
71
+ pack
72
+ end
73
+
74
+ # This is the method that Swing will call to ask what size to
75
+ # attempt to set for this window.
76
+ def getPreferredSize
77
+ # use the default size calculation; this would of course also be accomplished
78
+ # by not implementing the method at all.
79
+ super
80
+
81
+ # Or, you can override it with specific pixel sizes (width, height)
82
+ # Dimension.new(700, 560)
83
+
84
+ # Or, use the line below to make it the full screen size:
85
+ # Toolkit.get_default_toolkit.screen_size
86
+ end
87
+ end
88
+
89
+
90
+
91
+ # The application's table, containing alive/dead board values.
92
+ class Table < JTable
93
+ def initialize(table_model)
94
+ super(table_model)
95
+ self.table_header = nil
96
+ self.show_grid = true
97
+ self.grid_color = Color::BLUE
98
+ set_default_renderer(java.lang.Object, CellRenderer.new)
99
+ self.row_height = 32
100
+ end
101
+ end
102
+
103
+
104
+
105
+ # Combines button panel and bottom text panel into a single panel.
106
+ class BottomPanel < JPanel
107
+ def initialize(table_model, ancestor_window)
108
+ layout = GridLayout.new(0, 1)
109
+ super(layout)
110
+ layout.vgap = 12
111
+ add(ButtonPanel.new(table_model, ancestor_window))
112
+ add(StatusAndLinksPanel.new(table_model))
113
+ end
114
+ end
115
+
116
+
117
+ # Panel for top of main application window containing a large title.
118
+ class HeaderPanel < JPanel
119
+ def initialize
120
+ super(BorderLayout.new)
121
+ label = JLabel.new("<html><h2>Conway's Game of Life Viewer</h2></html")
122
+ inner_panel = JPanel.new
123
+ inner_panel.add(label)
124
+ add(inner_panel, BorderLayout::CENTER)
125
+ end
126
+ end
127
+
128
+
129
+ # Subclassed by application buttons, contains their common functionality
130
+ # added to the Swing JButton class.
131
+ class Button < JButton
132
+ def initialize(action_class, keystroke_text, table_model)
133
+ action = action_class.send(:new, table_model)
134
+ super(action)
135
+ key = KeyStroke.getKeyStroke(keystroke_text)
136
+ get_input_map(JComponent::WHEN_IN_FOCUSED_WINDOW).put(key, keystroke_text)
137
+ get_action_map.put(keystroke_text, action)
138
+ end
139
+ end
140
+
141
+
142
+ # Panel containing horizontal row of buttons.
143
+ class ButtonPanel < JPanel
144
+ def initialize(table_model, ancestor_window)
145
+ super(GridLayout.new(1, 0))
146
+ add(Button.new(ShowFirstGenerationAction, KeyEvent::VK_1, table_model))
147
+ add(Button.new(ShowPreviousGenerationAction, KeyEvent::VK_4, table_model))
148
+
149
+ next_button = Button.new(ShowNextGenerationAction, KeyEvent::VK_7, table_model)
150
+ add(next_button)
151
+ ancestor_window.add_window_listener(InitialFocusSettingWindowListener.new(next_button))
152
+
153
+ add(Button.new(ShowLastGenerationAction, KeyEvent::VK_0, table_model))
154
+ add(Button.new(CopyToClipboardAction, ClipboardHelper.copy_key_name, table_model))
155
+ add(Button.new(NewGameFromClipboardAction, ClipboardHelper.paste_key_name, table_model))
156
+ add(Button.new(ExitAction, KeyEvent::VK_Q, table_model))
157
+ end
158
+ end
159
+
160
+
161
+
162
+ # Status label showing, e.g. "Current Generation: 1, Population: 42"
163
+ class StatusLabel < JLabel
164
+ def initialize(table_model)
165
+ super()
166
+ @update_text = lambda do |current_generation_num|
167
+ last_fragment = table_model.at_last_generation? ? " (last)" : ""
168
+ self.text = "Current Generation#{last_fragment}: #{current_generation_num}, Population: #{table_model.number_living}"
169
+ end
170
+ @update_text.call(0)
171
+ self.horizontal_alignment = JLabel::CENTER
172
+ table_model.add_current_num_change_handler(@update_text)
173
+ end
174
+ end
175
+
176
+
177
+ # This class is responsible for rendering the display of a table cell,
178
+ # which in our case is to display an image of Alfred E. Neuman if the
179
+ # underlying data value is true, else display nothing.
180
+ class CellRenderer
181
+
182
+ class LifeLabel < JLabel
183
+ def initialize
184
+ super
185
+ self.horizontal_alignment = JLabel::CENTER
186
+ self.vertical_alignment = JLabel::CENTER
187
+ self.opaque = true
188
+ end
189
+ end
190
+
191
+ def initialize
192
+ @label = LifeLabel.new
193
+ image_spec = File.expand_path(File.join(
194
+ File.dirname(__FILE__), '..', '..', '..', 'resources', 'images', 'alfred-e-neuman.jpg'))
195
+ @true_icon = ImageIcon.new(image_spec, 'Alfred E. Neuman')
196
+ end
197
+
198
+ # from TableCellRenderer interface
199
+ def getTableCellRendererComponent(table, value, is_selected, has_focus, row, column)
200
+ alive = value
201
+ @label.icon = alive ? @true_icon : nil
202
+ @label.tool_tip_text = "row #{row}, column #{column}, value is #{alive}"
203
+ @label
204
+ end
205
+ end
206
+
207
+
208
+ # When added as a listener to a given window, will cause the
209
+ # passed component to own focus when the window first opens.
210
+ class InitialFocusSettingWindowListener < WindowAdapter
211
+
212
+ def initialize(component_requesting_focus)
213
+ super()
214
+ @component_requesting_focus = component_requesting_focus
215
+ end
216
+
217
+ def windowOpened(event)
218
+ @component_requesting_focus.requestFocus
219
+ end
220
+ end
221
+
222
+
223
+
224
+ # JLabel that a) provides a clickable link that launches the default browser
225
+ # with the passed URL, b) makes the text appear like a hyperlink, and
226
+ # c) sets the tooltip text to be the URL.
227
+ class HyperlinkLabel < JLabel
228
+
229
+ def initialize(url, caption, tool_tip_text)
230
+ text = "<html><a href=#{url}>#{caption}</a></html>" # make it appear like a hyperlink
231
+ super(text)
232
+ self.tool_tip_text = tool_tip_text
233
+ add_mouse_listener(ClickAdapter.new(url))
234
+ end
235
+
236
+ class ClickAdapter < MouseAdapter
237
+
238
+ def initialize(url)
239
+ super()
240
+ @url = url
241
+ end
242
+
243
+ def mouseClicked(event)
244
+ Desktop.desktop.browse(URI.new(@url))
245
+ end
246
+ end
247
+ end
248
+
249
+
250
+ class LinkPanel < JPanel
251
+
252
+ def initialize
253
+
254
+ #Without the call to super below, I get the following error:
255
+ #RuntimeError: Java wrapper with no contents: LinkPanel
256
+ #see http://jira.codehaus.org/browse/JRUBY-4704; not fixed?
257
+ super
258
+
259
+ add(wikipedia_label)
260
+ add(JLabel.new(" | "))
261
+ add(github_label)
262
+ add(JLabel.new(" | "))
263
+ add(article_label)
264
+ end
265
+
266
+ def wikipedia_label
267
+ url = 'http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life'
268
+ HyperlinkLabel.new(url, "Wikipedia",
269
+ "Visit Conway's Game of Life page on Wikipedia at #{url}.")
270
+ end
271
+
272
+ def github_label
273
+ url = 'http://github.com/keithrbennett/life-game-viewer'
274
+ HyperlinkLabel.new(url, "Github",
275
+ "Visit the Github page for this project at #{url}.")
276
+ end
277
+
278
+ def article_label
279
+ url = 'http://www.bbs-software.com/blog/2012/09/05/conways-game-of-life-viewer/'
280
+ HyperlinkLabel.new(url, "Article",
281
+ "Visit the blog article about this project at #{url}.")
282
+ end
283
+ end
284
+
285
+
286
+
287
+ class StatusAndLinksPanel < JPanel
288
+ def initialize(table_model)
289
+ super(BorderLayout.new)
290
+ add(StatusLabel.new(table_model), BorderLayout::WEST)
291
+ add(LinkPanel.new, BorderLayout::EAST)
292
+ end
293
+ end
@@ -0,0 +1,97 @@
1
+ require 'java'
2
+
3
+ java_import javax.swing.table.AbstractTableModel
4
+ java_import javax.swing.JOptionPane
5
+
6
+ require_relative 'generations'
7
+
8
+ # This class is the model used to drive Swing's JTable.
9
+ # It contains a LifeModel to which it delegates most calls.
10
+ class LifeTableModel < AbstractTableModel
11
+
12
+ attr_accessor :life_model
13
+ attr_reader :generations
14
+
15
+
16
+ def initialize(life_model)
17
+ super()
18
+ @current_num_change_handlers = []
19
+ self.inner_model = life_model
20
+ end
21
+
22
+ def inner_model=(life_model)
23
+ @life_model = life_model
24
+ @generations = Generations.new(life_model)
25
+ end
26
+
27
+ def getRowCount
28
+ life_model.row_count
29
+ end
30
+
31
+ def getColumnCount
32
+ life_model.column_count
33
+ end
34
+
35
+ def getValueAt(row, col)
36
+ life_model.alive?(row, col)
37
+ end
38
+
39
+ def getColumnName(colnum)
40
+ nil
41
+ end
42
+
43
+ def at_first_generation?
44
+ generations.at_first_generation?
45
+ end
46
+
47
+ def at_last_generation?
48
+ generations.at_last_generation?
49
+ end
50
+
51
+ def number_living
52
+ life_model.number_living
53
+ end
54
+
55
+ def go_to_next_generation
56
+ if at_last_generation?
57
+ JOptionPane.show_message_dialog(nil, "Generation ##{generations.current_num} is the last non-repeating generation.")
58
+ else
59
+ self.life_model = generations.next
60
+ fire_current_number_changed
61
+ end
62
+ end
63
+
64
+ def go_to_previous_generation
65
+ raise "Should not have gotten here; already at first generation" if at_first_generation?
66
+ self.life_model = generations.previous
67
+ fire_current_number_changed
68
+ end
69
+
70
+ def go_to_first_generation
71
+ self.life_model = generations.first
72
+ fire_current_number_changed
73
+ end
74
+
75
+ def go_to_last_generation
76
+ self.life_model = generations.last
77
+ fire_current_number_changed
78
+ end
79
+
80
+ def add_current_num_change_handler(callable)
81
+ @current_num_change_handlers << callable
82
+ end
83
+
84
+ def reset_model(new_model)
85
+ self.inner_model = new_model
86
+ fire_table_structure_changed
87
+ fire_current_number_changed
88
+ end
89
+
90
+ def fire_current_number_changed
91
+ @current_num_change_handlers.each do |handler|
92
+ handler.call(generations.current_num)
93
+ end
94
+ end
95
+ end
96
+
97
+
@@ -0,0 +1,16 @@
1
+
2
+ # Main entry point into the application.
3
+ module LifeGameViewer
4
+ class Main
5
+
6
+
7
+ def self.view_sample
8
+ LifeGameViewerFrame.view_sample
9
+ end
10
+
11
+ def self.view(model)
12
+ LifeGameViewerFrame.new(model).visible = true
13
+ end
14
+
15
+ end
16
+ end