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.
- 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
|
+
|