life_game_viewer 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +233 -0
- data/Rakefile +2 -0
- data/bin/life_view_sample +6 -0
- data/lib/life_game_viewer/model/life_calculator.rb +103 -0
- data/lib/life_game_viewer/model/life_visualizer.rb +19 -0
- data/lib/life_game_viewer/model/model_validation.rb +41 -0
- data/lib/life_game_viewer/model/my_life_model.rb +56 -0
- data/lib/life_game_viewer/model/sample_life_model.rb +132 -0
- data/lib/life_game_viewer/version.rb +3 -0
- data/lib/life_game_viewer/view/actions.rb +193 -0
- data/lib/life_game_viewer/view/clipboard_helper.rb +55 -0
- data/lib/life_game_viewer/view/generations.rb +67 -0
- data/lib/life_game_viewer/view/life_game_viewer_frame.rb +293 -0
- data/lib/life_game_viewer/view/life_table_model.rb +97 -0
- data/lib/life_game_viewer/view/main.rb +16 -0
- data/lib/life_game_viewer.rb +27 -0
- data/life_game_viewer.gemspec +17 -0
- data/resources/images/alfred-e-neuman.jpg +0 -0
- data/spec/model/life_calculator_spec.rb +110 -0
- data/spec/model/life_visualizer_spec.rb +20 -0
- data/spec/model/model_validation_spec.rb +51 -0
- data/spec/model/sample_life_model_spec.rb +108 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/view/generations_spec.rb +19 -0
- metadata +79 -0
@@ -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
|
+
|