life_game_viewer 0.9.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.
@@ -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