wads 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -2
- data/README.md +62 -2
- data/data/sample_graph.csv +11 -0
- data/data/starwars-episode-4-interactions.json +336 -0
- data/lib/wads/app.rb +41 -183
- data/lib/wads/data_structures.rb +705 -17
- data/lib/wads/textinput.rb +66 -15
- data/lib/wads/version.rb +1 -1
- data/lib/wads/widgets.rb +2827 -271
- data/lib/wads.rb +1 -1
- data/media/CircleAlpha.png +0 -0
- data/media/CircleAqua.png +0 -0
- data/media/CircleBlue.png +0 -0
- data/media/CircleGray.png +0 -0
- data/media/CircleGreen.png +0 -0
- data/media/CirclePurple.png +0 -0
- data/media/CircleRed.png +0 -0
- data/media/CircleWhite.png +0 -0
- data/media/CircleYellow.png +0 -0
- data/media/SampleGraph.png +0 -0
- data/media/StocksSample.png +0 -0
- data/media/WadsScreenshot.png +0 -0
- data/run-graph +3 -0
- data/run-star-wars +3 -0
- data/run-stocks +3 -0
- data/run-theme-test +3 -0
- data/samples/basic_gosu_with_graph_widget.rb +66 -0
- data/samples/graph.rb +72 -0
- data/samples/star_wars.rb +112 -0
- data/samples/stocks.rb +126 -0
- data/samples/theme_test.rb +256 -0
- metadata +26 -5
- data/run-sample-app +0 -3
- data/sample_app.rb +0 -52
data/lib/wads/widgets.rb
CHANGED
@@ -1,8 +1,17 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'logger'
|
3
|
+
require_relative 'data_structures'
|
4
|
+
|
5
|
+
#
|
6
|
+
# All wads classes are contained within the wads module.
|
7
|
+
#
|
1
8
|
module Wads
|
2
9
|
COLOR_PEACH = Gosu::Color.argb(0xffe6b0aa)
|
3
10
|
COLOR_LIGHT_PURPLE = Gosu::Color.argb(0xffd7bde2)
|
4
11
|
COLOR_LIGHT_BLUE = Gosu::Color.argb(0xffa9cce3)
|
12
|
+
COLOR_VERY_LIGHT_BLUE = Gosu::Color.argb(0xffd0def5)
|
5
13
|
COLOR_LIGHT_GREEN = Gosu::Color.argb(0xffa3e4d7)
|
14
|
+
COLOR_GREEN = COLOR_LIGHT_GREEN
|
6
15
|
COLOR_LIGHT_YELLOW = Gosu::Color.argb(0xfff9e79f)
|
7
16
|
COLOR_LIGHT_ORANGE = Gosu::Color.argb(0xffedbb99)
|
8
17
|
COLOR_WHITE = Gosu::Color::WHITE
|
@@ -11,12 +20,16 @@ module Wads
|
|
11
20
|
COLOR_LIME = Gosu::Color.argb(0xffDAF7A6)
|
12
21
|
COLOR_YELLOW = Gosu::Color.argb(0xffFFC300)
|
13
22
|
COLOR_MAROON = Gosu::Color.argb(0xffC70039)
|
23
|
+
COLOR_PURPLE = COLOR_MAROON
|
14
24
|
COLOR_LIGHT_GRAY = Gosu::Color.argb(0xff2c3e50)
|
25
|
+
COLOR_LIGHTER_GRAY = Gosu::Color.argb(0xff364d63)
|
26
|
+
COLOR_LIGHTEST_GRAY = Gosu::Color.argb(0xff486684)
|
15
27
|
COLOR_GRAY = Gosu::Color::GRAY
|
16
28
|
COLOR_OFF_GRAY = Gosu::Color.argb(0xff566573)
|
17
29
|
COLOR_LIGHT_BLACK = Gosu::Color.argb(0xff111111)
|
18
30
|
COLOR_LIGHT_RED = Gosu::Color.argb(0xffe6b0aa)
|
19
31
|
COLOR_CYAN = Gosu::Color::CYAN
|
32
|
+
COLOR_AQUA = COLOR_CYAN
|
20
33
|
COLOR_HEADER_BLUE = Gosu::Color.argb(0xff089FCE)
|
21
34
|
COLOR_HEADER_BRIGHT_BLUE = Gosu::Color.argb(0xff0FAADD)
|
22
35
|
COLOR_BLUE = Gosu::Color::BLUE
|
@@ -25,83 +38,1275 @@ module Wads
|
|
25
38
|
COLOR_BLACK = Gosu::Color::BLACK
|
26
39
|
COLOR_FORM_BUTTON = Gosu::Color.argb(0xcc2e4053)
|
27
40
|
COLOR_ERROR_CODE_RED = Gosu::Color.argb(0xffe6b0aa)
|
41
|
+
COLOR_BORDER_BLUE = Gosu::Color.argb(0xff004D80)
|
42
|
+
COLOR_ALPHA = "alpha"
|
28
43
|
|
29
44
|
Z_ORDER_BACKGROUND = 2
|
30
|
-
|
31
|
-
|
32
|
-
|
45
|
+
Z_ORDER_BORDER = 3
|
46
|
+
Z_ORDER_SELECTION_BACKGROUND = 4
|
47
|
+
Z_ORDER_GRAPHIC_ELEMENTS = 5
|
33
48
|
Z_ORDER_PLOT_POINTS = 6
|
34
|
-
|
35
|
-
|
36
|
-
|
49
|
+
Z_ORDER_FOCAL_ELEMENTS = 8
|
50
|
+
Z_ORDER_TEXT = 9
|
51
|
+
|
52
|
+
EVENT_OK = "ok"
|
53
|
+
EVENT_TEXT_INPUT = "textinput"
|
54
|
+
EVENT_TABLE_SELECT = "tableselect"
|
55
|
+
EVENT_TABLE_UNSELECT = "tableunselect"
|
56
|
+
EVENT_TABLE_ROW_DELETE = "tablerowdelete"
|
57
|
+
|
58
|
+
IMAGE_CIRCLE_SIZE = 104
|
59
|
+
|
60
|
+
ELEMENT_TEXT = "text"
|
61
|
+
ELEMENT_TEXT_INPUT = "text_input"
|
62
|
+
ELEMENT_BUTTON = "button"
|
63
|
+
ELEMENT_IMAGE = "image"
|
64
|
+
ELEMENT_TABLE = "table"
|
65
|
+
ELEMENT_HORIZONTAL_PANEL = "hpanel"
|
66
|
+
ELEMENT_VERTICAL_PANEL = "vpanel"
|
67
|
+
ELEMENT_MAX_PANEL = "maxpanel"
|
68
|
+
ELEMENT_DOCUMENT = "document"
|
69
|
+
ELEMENT_GRAPH = "graph"
|
70
|
+
ELEMENT_GENERIC = "generic"
|
71
|
+
ELEMENT_PLOT = "plot"
|
72
|
+
|
73
|
+
ARG_SECTION = "section"
|
74
|
+
ARG_COLOR = "color"
|
75
|
+
ARG_DESIRED_WIDTH = "desired_width"
|
76
|
+
ARG_DESIRED_HEIGHT = "desired_height"
|
77
|
+
ARG_PANEL_WIDTH = "panel_width"
|
78
|
+
ARG_LAYOUT = "layout"
|
79
|
+
ARG_TEXT_ALIGN = "text_align"
|
80
|
+
ARG_USE_LARGE_FONT = "large_font"
|
81
|
+
ARG_THEME = "theme"
|
82
|
+
|
83
|
+
TEXT_ALIGN_LEFT = "left"
|
84
|
+
TEXT_ALIGN_CENTER = "center"
|
85
|
+
TEXT_ALIGN_RIGHT = "right"
|
86
|
+
|
87
|
+
SECTION_TOP = "north"
|
88
|
+
SECTION_MIDDLE = "center"
|
89
|
+
SECTION_BOTTOM = "south"
|
90
|
+
SECTION_LEFT = "west"
|
91
|
+
SECTION_RIGHT = "east"
|
92
|
+
SECTION_NORTH = SECTION_TOP
|
93
|
+
SECTION_HEADER = SECTION_TOP
|
94
|
+
SECTION_SOUTH = SECTION_BOTTOM
|
95
|
+
SECTION_FOOTER = SECTION_BOTTOM
|
96
|
+
SECTION_WEST = "west"
|
97
|
+
SECTION_EAST = "east"
|
98
|
+
SECTION_CENTER = "center"
|
99
|
+
SECTION_CONTENT = SECTION_CENTER
|
100
|
+
|
101
|
+
LAYOUT_VERTICAL_COLUMN = "vcolumn"
|
102
|
+
LAYOUT_TOP_MIDDLE_BOTTOM = "top_middle_bottom"
|
103
|
+
LAYOUT_HEADER_CONTENT = "header_content"
|
104
|
+
LAYOUT_CONTENT_FOOTER = "content_footer"
|
105
|
+
LAYOUT_BORDER = "border"
|
106
|
+
LAYOUT_EAST_WEST = "east_west"
|
107
|
+
LAYOUT_LEFT_RIGHT = LAYOUT_EAST_WEST
|
108
|
+
|
109
|
+
FILL_VERTICAL_STACK = "fill_vertical"
|
110
|
+
FILL_HORIZONTAL_STACK = "fill_horizontal"
|
111
|
+
FILL_FULL_SIZE = "fill_full_size"
|
112
|
+
|
113
|
+
GRAPH_DISPLAY_ALL = "all"
|
114
|
+
GRAPH_DISPLAY_EXPLORER = "explorer"
|
115
|
+
GRAPH_DISPLAY_TREE = "tree"
|
116
|
+
|
117
|
+
#
|
118
|
+
# An instance of Coordinates references an x, y position on the screen
|
119
|
+
# as well as the width and height of the widget, thus providing the
|
120
|
+
# outer dimensions of a rectangular widget.
|
121
|
+
#
|
122
|
+
class Coordinates
|
123
|
+
attr_accessor :x
|
124
|
+
attr_accessor :y
|
125
|
+
attr_accessor :width
|
126
|
+
attr_accessor :height
|
127
|
+
def initialize(x, y, w, h)
|
128
|
+
@x = x
|
129
|
+
@y = y
|
130
|
+
@width = w
|
131
|
+
@height = h
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# An instance of GuiTheme directs wads widgets as to what colors and fonts
|
137
|
+
# should be used. This accomplishes two goals: one, we don't need to constantly
|
138
|
+
# pass around these instances. They can be globally accessed using WadsConfig.
|
139
|
+
# It also makes it easy to change the look and feel of your application.
|
140
|
+
#
|
141
|
+
class GuiTheme
|
142
|
+
attr_accessor :text_color
|
143
|
+
attr_accessor :graphic_elements_color
|
144
|
+
attr_accessor :border_color
|
145
|
+
attr_accessor :background_color
|
146
|
+
attr_accessor :selection_color
|
147
|
+
attr_accessor :use_icons
|
148
|
+
attr_accessor :font
|
149
|
+
attr_accessor :font_large
|
150
|
+
|
151
|
+
def initialize(text, graphics, border, background, selection, use_icons, font, font_large)
|
152
|
+
@text_color = text
|
153
|
+
@graphic_elements_color = graphics
|
154
|
+
@border_color = border
|
155
|
+
@background_color = background
|
156
|
+
@selection_color = selection
|
157
|
+
@use_icons = use_icons
|
158
|
+
@font = font
|
159
|
+
@font_large = font_large
|
160
|
+
end
|
161
|
+
|
162
|
+
def pixel_width_for_string(str)
|
163
|
+
@font.text_width(str)
|
164
|
+
end
|
165
|
+
|
166
|
+
def pixel_width_for_large_font(str)
|
167
|
+
@font_large.text_width(str)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# Theme with black text on a white background
|
173
|
+
#
|
174
|
+
class WadsBrightTheme < GuiTheme
|
175
|
+
def initialize
|
176
|
+
super(COLOR_BLACK, # text color
|
177
|
+
COLOR_HEADER_BRIGHT_BLUE, # graphic elements
|
178
|
+
COLOR_BORDER_BLUE, # border color
|
179
|
+
COLOR_WHITE, # background
|
180
|
+
COLOR_VERY_LIGHT_BLUE, # selected item
|
181
|
+
true, # use icons
|
182
|
+
Gosu::Font.new(22), # regular font
|
183
|
+
Gosu::Font.new(38)) # large font
|
184
|
+
end
|
185
|
+
end
|
37
186
|
|
187
|
+
class WadsDarkRedBrownTheme < GuiTheme
|
188
|
+
def initialize
|
189
|
+
super(COLOR_WHITE, # text color
|
190
|
+
Gosu::Color.argb(0xffD63D41), # graphic elements - dark red
|
191
|
+
Gosu::Color.argb(0xffEC5633), # border color - dark orange
|
192
|
+
Gosu::Color.argb(0xff52373B), # background - dark brown
|
193
|
+
Gosu::Color.argb(0xffEC5633), # selected item - dark orange
|
194
|
+
true, # use icons
|
195
|
+
Gosu::Font.new(22), # regular font
|
196
|
+
Gosu::Font.new(38)) # large font
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class WadsEarthTonesTheme < GuiTheme
|
201
|
+
def initialize
|
202
|
+
super(COLOR_WHITE, # text color
|
203
|
+
Gosu::Color.argb(0xffD0605E), # graphic elements
|
204
|
+
Gosu::Color.argb(0xffFF994C), # border color
|
205
|
+
Gosu::Color.argb(0xff98506D), # background
|
206
|
+
Gosu::Color.argb(0xffFF994C), # selected item
|
207
|
+
true, # use icons
|
208
|
+
Gosu::Font.new(22), # regular font
|
209
|
+
Gosu::Font.new(38)) # large font
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class WadsNatureTheme < GuiTheme
|
214
|
+
def initialize
|
215
|
+
super(COLOR_WHITE, # text color
|
216
|
+
Gosu::Color.argb(0xffA9B40B), # graphic elements
|
217
|
+
Gosu::Color.argb(0xffF38B01), # border color
|
218
|
+
Gosu::Color.argb(0xffFFC001), # background
|
219
|
+
Gosu::Color.argb(0xffF38B01), # selected item
|
220
|
+
true, # use icons
|
221
|
+
Gosu::Font.new(22, { :bold => true}), # regular font
|
222
|
+
Gosu::Font.new(38, { :bold => true})) # large font
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
class WadsPurpleTheme < GuiTheme
|
227
|
+
def initialize
|
228
|
+
super(COLOR_WHITE, # text color
|
229
|
+
Gosu::Color.argb(0xff5A23B4), # graphic elements
|
230
|
+
Gosu::Color.argb(0xffFE01EA), # border color
|
231
|
+
Gosu::Color.argb(0xffAA01FF), # background
|
232
|
+
Gosu::Color.argb(0xffFE01EA), # selected item
|
233
|
+
true, # use icons
|
234
|
+
Gosu::Font.new(22), # regular font
|
235
|
+
Gosu::Font.new(38, { :bold => true})) # large font
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class WadsAquaTheme < GuiTheme
|
240
|
+
def initialize
|
241
|
+
super(COLOR_WHITE, # text color
|
242
|
+
Gosu::Color.argb(0xff387CA3), # graphic elements
|
243
|
+
Gosu::Color.argb(0xff387CA3), # border color
|
244
|
+
Gosu::Color.argb(0xff52ADC8), # background
|
245
|
+
Gosu::Color.argb(0xff55C39E), # selected item
|
246
|
+
true, # use icons
|
247
|
+
Gosu::Font.new(22), # regular font
|
248
|
+
Gosu::Font.new(38, { :bold => true})) # large font
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
#
|
253
|
+
# Theme with white text on a black background that also does not use icons.
|
254
|
+
# Currently, icons are primarily used in the Graph display widget.
|
255
|
+
#
|
256
|
+
class WadsNoIconTheme < GuiTheme
|
257
|
+
def initialize
|
258
|
+
super(COLOR_WHITE, # text color
|
259
|
+
COLOR_HEADER_BRIGHT_BLUE, # graphic elements
|
260
|
+
COLOR_BORDER_BLUE, # border color
|
261
|
+
COLOR_BLACK, # background
|
262
|
+
COLOR_LIGHT_GRAY, # selected item
|
263
|
+
false, # use icons
|
264
|
+
Gosu::Font.new(22), # regular font
|
265
|
+
Gosu::Font.new(38)) # large font
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
# WadsConfig is the one singleton that provides access to resources
|
271
|
+
# used throughput the application, including fonts, themes, and layouts.
|
272
|
+
#
|
273
|
+
class WadsConfig
|
274
|
+
include Singleton
|
275
|
+
|
276
|
+
attr_accessor :logger
|
277
|
+
attr_accessor :window
|
278
|
+
|
279
|
+
def get_logger
|
280
|
+
if @logger.nil?
|
281
|
+
@logger = Logger.new(STDOUT)
|
282
|
+
end
|
283
|
+
@logger
|
284
|
+
end
|
285
|
+
|
286
|
+
#
|
287
|
+
# Wads uses the Ruby logger, and you can conrol the log level using this method.
|
288
|
+
# Valid values are 'debug', 'info', 'warn', 'error'
|
289
|
+
def set_log_level(level)
|
290
|
+
get_logger.level = level
|
291
|
+
end
|
292
|
+
|
293
|
+
def set_window(w)
|
294
|
+
@window = w
|
295
|
+
end
|
296
|
+
|
297
|
+
def get_window
|
298
|
+
if @window.nil?
|
299
|
+
raise "The WadsConfig.instance.set_window(window) needs to be invoked first"
|
300
|
+
end
|
301
|
+
@window
|
302
|
+
end
|
303
|
+
|
304
|
+
#
|
305
|
+
# Get the default theme which is white text on a black background
|
306
|
+
# that uses icons (primarily used in the Graph display widget currently)
|
307
|
+
#
|
308
|
+
def get_default_theme
|
309
|
+
if @default_theme.nil?
|
310
|
+
@default_theme = GuiTheme.new(COLOR_WHITE, # text color
|
311
|
+
COLOR_HEADER_BRIGHT_BLUE, # graphic elements
|
312
|
+
COLOR_BORDER_BLUE, # border color
|
313
|
+
COLOR_BLACK, # background
|
314
|
+
COLOR_LIGHT_GRAY, # selected item
|
315
|
+
true, # use icons
|
316
|
+
Gosu::Font.new(22), # regular font
|
317
|
+
Gosu::Font.new(38)) # large font
|
318
|
+
end
|
319
|
+
@default_theme
|
320
|
+
end
|
321
|
+
|
322
|
+
#
|
323
|
+
# Get a reference to the current theme. If one has not been set using
|
324
|
+
# set_current_theme(theme), the default theme will be used.
|
325
|
+
#
|
326
|
+
def current_theme
|
327
|
+
if @current_theme.nil?
|
328
|
+
@current_theme = get_default_theme
|
329
|
+
end
|
330
|
+
@current_theme
|
331
|
+
end
|
332
|
+
|
333
|
+
#
|
334
|
+
# Set the theme to be used by wads widgets
|
335
|
+
#
|
336
|
+
def set_current_theme(theme)
|
337
|
+
@current_theme = theme
|
338
|
+
end
|
339
|
+
|
340
|
+
#
|
341
|
+
# This method returns the default dimensions for the given widget type
|
342
|
+
# as a two value array of the form [width, height].
|
343
|
+
# This helps the layout manager allocate space to widgets within a layout
|
344
|
+
# and container. The string value max tells the layout to use all available
|
345
|
+
# space in that dimension (either x or y)
|
346
|
+
#
|
347
|
+
def default_dimensions(widget_type)
|
348
|
+
if @default_dimensions.nil?
|
349
|
+
@default_dimensions = {}
|
350
|
+
@default_dimensions[ELEMENT_TEXT] = [100, 20]
|
351
|
+
@default_dimensions[ELEMENT_TEXT_INPUT] = [100, 20]
|
352
|
+
@default_dimensions[ELEMENT_IMAGE] = [100, 100]
|
353
|
+
@default_dimensions[ELEMENT_TABLE] = ["max", "max"]
|
354
|
+
@default_dimensions[ELEMENT_HORIZONTAL_PANEL] = ["max", 100]
|
355
|
+
@default_dimensions[ELEMENT_VERTICAL_PANEL] = [100, "max"]
|
356
|
+
@default_dimensions[ELEMENT_MAX_PANEL] = ["max", "max"]
|
357
|
+
@default_dimensions[ELEMENT_DOCUMENT] = ["max", "max"]
|
358
|
+
@default_dimensions[ELEMENT_GRAPH] = ["max", "max"]
|
359
|
+
@default_dimensions[ELEMENT_BUTTON] = [100, 26]
|
360
|
+
@default_dimensions[ELEMENT_GENERIC] = ["max", "max"]
|
361
|
+
@default_dimensions[ELEMENT_PLOT] = ["max", "max"]
|
362
|
+
end
|
363
|
+
@default_dimensions[widget_type]
|
364
|
+
end
|
365
|
+
|
366
|
+
def create_layout_for_widget(widget, layout_type = nil, args = {})
|
367
|
+
create_layout(widget.x, widget.y, widget.width, widget.height, widget, layout_type, args)
|
368
|
+
end
|
369
|
+
|
370
|
+
def create_layout(x, y, width, height, widget, layout_type = nil, args = {})
|
371
|
+
if layout_type.nil?
|
372
|
+
if @default_layout_type.nil?
|
373
|
+
layout_type = LAYOUT_VERTICAL_COLUMN
|
374
|
+
else
|
375
|
+
layout_type = @default_layout_type
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
if not @default_layout_args.nil?
|
380
|
+
if args.nil?
|
381
|
+
args = @default_layout_args
|
382
|
+
else
|
383
|
+
args.merge(@default_layout_args)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
if layout_type == LAYOUT_VERTICAL_COLUMN
|
388
|
+
return VerticalColumnLayout.new(x, y, width, height, widget, args)
|
389
|
+
elsif layout_type == LAYOUT_TOP_MIDDLE_BOTTOM
|
390
|
+
return TopMiddleBottomLayout.new(x, y, width, height, widget, args)
|
391
|
+
elsif layout_type == LAYOUT_BORDER
|
392
|
+
return BorderLayout.new(x, y, width, height, widget, args)
|
393
|
+
elsif layout_type == LAYOUT_HEADER_CONTENT
|
394
|
+
return HeaderContentLayout.new(x, y, width, height, widget, args)
|
395
|
+
elsif layout_type == LAYOUT_CONTENT_FOOTER
|
396
|
+
return ContentFooterLayout.new(x, y, width, height, widget, args)
|
397
|
+
elsif layout_type == LAYOUT_EAST_WEST
|
398
|
+
return EastWestLayout.new(x, y, width, height, widget, args)
|
399
|
+
end
|
400
|
+
raise "#{layout_type} is an unsupported layout type"
|
401
|
+
end
|
402
|
+
|
403
|
+
def set_default_layout(layout_type, layout_args = {})
|
404
|
+
@default_layout_type = layout_type
|
405
|
+
@default_layout_args = layout_args
|
406
|
+
end
|
407
|
+
|
408
|
+
#
|
409
|
+
# Get a Gosu images instance for the specified color, i.e. COLOR_AQUA ir COLOR_BLUE
|
410
|
+
#
|
411
|
+
def circle(color)
|
412
|
+
create_circles
|
413
|
+
if color.nil?
|
414
|
+
return nil
|
415
|
+
end
|
416
|
+
img = @wads_image_circles[color]
|
417
|
+
if img.nil?
|
418
|
+
get_logger.error("ERROR: Did not find circle image with color #{color}")
|
419
|
+
end
|
420
|
+
img
|
421
|
+
end
|
422
|
+
|
423
|
+
def create_circles
|
424
|
+
return unless @wads_image_circles.nil?
|
425
|
+
@wads_image_circle_aqua = Gosu::Image.new("./media/CircleAqua.png")
|
426
|
+
@wads_image_circle_blue = Gosu::Image.new("./media/CircleBlue.png")
|
427
|
+
@wads_image_circle_green = Gosu::Image.new("./media/CircleGreen.png")
|
428
|
+
@wads_image_circle_purple = Gosu::Image.new("./media/CirclePurple.png")
|
429
|
+
@wads_image_circle_red = Gosu::Image.new("./media/CircleRed.png")
|
430
|
+
@wads_image_circle_yellow = Gosu::Image.new("./media/CircleYellow.png")
|
431
|
+
@wads_image_circle_gray = Gosu::Image.new("./media/CircleGray.png")
|
432
|
+
@wads_image_circle_white = Gosu::Image.new("./media/CircleWhite.png")
|
433
|
+
@wads_image_circle_alpha = Gosu::Image.new("./media/CircleAlpha.png")
|
434
|
+
@wads_image_circles = {}
|
435
|
+
@wads_image_circles[COLOR_AQUA] = @wads_image_circle_aqua
|
436
|
+
@wads_image_circles[COLOR_BLUE] = @wads_image_circle_blue
|
437
|
+
@wads_image_circles[COLOR_GREEN] = @wads_image_circle_green
|
438
|
+
@wads_image_circles[COLOR_PURPLE] = @wads_image_circle_purple
|
439
|
+
@wads_image_circles[COLOR_RED] = @wads_image_circle_red
|
440
|
+
@wads_image_circles[COLOR_YELLOW] = @wads_image_circle_yellow
|
441
|
+
@wads_image_circles[COLOR_GRAY] = @wads_image_circle_gray
|
442
|
+
@wads_image_circles[COLOR_WHITE] = @wads_image_circle_white
|
443
|
+
@wads_image_circles[COLOR_ALPHA] = @wads_image_circle_alpha
|
444
|
+
@wads_image_circles[4294956800] = @wads_image_circle_yellow
|
445
|
+
@wads_image_circles[4281893349] = @wads_image_circle_blue
|
446
|
+
@wads_image_circles[4294967295] = @wads_image_circle_gray
|
447
|
+
@wads_image_circles[4286611584] = @wads_image_circle_gray
|
448
|
+
@wads_image_circles[4282962380] = @wads_image_circle_aqua
|
449
|
+
@wads_image_circles[4294939648] = @wads_image_circle_red
|
450
|
+
@wads_image_circles[4292664540] = @wads_image_circle_white
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
#
|
455
|
+
# A Gui container is used to allocate space in the x, y two dimensional space to widgets
|
456
|
+
# and keep track of where the next widget in the container will be placed.
|
457
|
+
# The fill type is one of FILL_VERTICAL_STACK, FILL_HORIZONTAL_STACK, or FILL_FULL_SIZE.
|
458
|
+
# Layouts used containers to allocate space across the entire visible application.
|
459
|
+
#
|
460
|
+
class GuiContainer
|
461
|
+
attr_accessor :start_x
|
462
|
+
attr_accessor :start_y
|
463
|
+
attr_accessor :next_x
|
464
|
+
attr_accessor :next_y
|
465
|
+
attr_accessor :max_width
|
466
|
+
attr_accessor :max_height
|
467
|
+
attr_accessor :padding
|
468
|
+
attr_accessor :fill_type
|
469
|
+
attr_accessor :elements
|
470
|
+
|
471
|
+
def initialize(start_x, start_y, width, height, fill_type = FILL_HORIZONTAL_STACK, padding = 5)
|
472
|
+
@start_x = start_x
|
473
|
+
@start_y = start_y
|
474
|
+
@next_x = start_x
|
475
|
+
@next_y = start_y
|
476
|
+
@max_width = width
|
477
|
+
@max_height = height
|
478
|
+
@padding = padding
|
479
|
+
if [FILL_VERTICAL_STACK, FILL_HORIZONTAL_STACK, FILL_FULL_SIZE].include? fill_type
|
480
|
+
@fill_type = fill_type
|
481
|
+
else
|
482
|
+
raise "#{fill_type} is not a valid fill type"
|
483
|
+
end
|
484
|
+
@elements = []
|
485
|
+
end
|
486
|
+
|
487
|
+
def get_coordinates(element_type, args = {})
|
488
|
+
default_dim = WadsConfig.instance.default_dimensions(element_type)
|
489
|
+
if default_dim.nil?
|
490
|
+
raise "#{element_type} is an undefined element type"
|
491
|
+
end
|
492
|
+
default_width = default_dim[0]
|
493
|
+
default_height = default_dim[1]
|
494
|
+
specified_width = args[ARG_DESIRED_WIDTH]
|
495
|
+
if specified_width.nil?
|
496
|
+
if default_width == "max"
|
497
|
+
if fill_type == FILL_VERTICAL_STACK or fill_type == FILL_FULL_SIZE
|
498
|
+
the_width = max_width
|
499
|
+
else
|
500
|
+
the_width = (@start_x + @max_width) - @next_x
|
501
|
+
end
|
502
|
+
else
|
503
|
+
the_width = default_width
|
504
|
+
end
|
505
|
+
else
|
506
|
+
if specified_width > @max_width
|
507
|
+
the_width = @max_width
|
508
|
+
else
|
509
|
+
the_width = specified_width
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
specified_height = args[ARG_DESIRED_HEIGHT]
|
514
|
+
if specified_height.nil?
|
515
|
+
if default_height == "max"
|
516
|
+
if fill_type == FILL_VERTICAL_STACK
|
517
|
+
the_height = (@start_y + @max_height) - @next_y
|
518
|
+
else
|
519
|
+
the_height = max_height
|
520
|
+
end
|
521
|
+
else
|
522
|
+
the_height = default_height
|
523
|
+
end
|
524
|
+
else
|
525
|
+
if specified_height > @max_height
|
526
|
+
the_height = @max_height
|
527
|
+
else
|
528
|
+
the_height = specified_height
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Not all elements require padding
|
533
|
+
padding_exempt = [ELEMENT_IMAGE, ELEMENT_HORIZONTAL_PANEL, ELEMENT_PLOT,
|
534
|
+
ELEMENT_VERTICAL_PANEL, ELEMENT_GENERIC, ELEMENT_MAX_PANEL].include? element_type
|
535
|
+
if padding_exempt
|
536
|
+
# No padding
|
537
|
+
width_to_use = the_width
|
538
|
+
height_to_use = the_height
|
539
|
+
x_to_use = @next_x
|
540
|
+
y_to_use = @next_y
|
541
|
+
else
|
542
|
+
# Apply padding only if we are the max, i.e. the boundaries
|
543
|
+
x_to_use = @next_x + @padding
|
544
|
+
y_to_use = @next_y + @padding
|
545
|
+
if the_width == @max_width
|
546
|
+
width_to_use = the_width - (2 * @padding)
|
547
|
+
else
|
548
|
+
width_to_use = the_width
|
549
|
+
end
|
550
|
+
if the_height == @max_height
|
551
|
+
height_to_use = the_height - (2 * @padding)
|
552
|
+
else
|
553
|
+
height_to_use = the_height
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
# Text elements also honor ARG_TEXT_ALIGN
|
558
|
+
arg_text_align = args[ARG_TEXT_ALIGN]
|
559
|
+
if not arg_text_align.nil?
|
560
|
+
# left is the default, so check for center or right
|
561
|
+
if arg_text_align == TEXT_ALIGN_CENTER
|
562
|
+
x_to_use = @next_x + ((@max_width - specified_width) / 2)
|
563
|
+
elsif arg_text_align == TEXT_ALIGN_RIGHT
|
564
|
+
x_to_use = @next_x + @max_width - specified_width - @padding
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
coords = Coordinates.new(x_to_use, y_to_use,
|
569
|
+
width_to_use, height_to_use)
|
570
|
+
|
571
|
+
if fill_type == FILL_VERTICAL_STACK
|
572
|
+
@next_y = @next_y + the_height + (2 * @padding)
|
573
|
+
elsif fill_type == FILL_HORIZONTAL_STACK
|
574
|
+
@next_x = @next_x + the_width + (2 * @padding)
|
575
|
+
end
|
576
|
+
|
577
|
+
@elements << coords
|
578
|
+
coords
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
# The base class for all wads layouts. It has helper methods to add
|
583
|
+
# different types of widgets to the layout.
|
584
|
+
class WadsLayout
|
585
|
+
attr_accessor :border_coords
|
586
|
+
attr_accessor :parent_widget
|
587
|
+
attr_accessor :args
|
588
|
+
|
589
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
590
|
+
@border_coords = Coordinates.new(x, y, width, height)
|
591
|
+
@parent_widget = parent_widget
|
592
|
+
@args = args
|
593
|
+
end
|
594
|
+
|
595
|
+
def get_coordinates(element_type, args = {})
|
596
|
+
raise "You must use a subclass of WadsLayout"
|
597
|
+
end
|
598
|
+
|
599
|
+
def add_widget(widget, args = {})
|
600
|
+
# The widget already has an x, y position, so we need to move it
|
601
|
+
# based on the layout
|
602
|
+
coordinates = get_coordinates(ELEMENT_GENERIC, args)
|
603
|
+
widget.move_recursive_absolute(coordinates.x, coordinates.y)
|
604
|
+
widget.base_z = @parent_widget.base_z
|
605
|
+
@parent_widget.add_child(widget)
|
606
|
+
widget
|
607
|
+
end
|
608
|
+
|
609
|
+
def add_text(message, args = {})
|
610
|
+
default_dimensions = WadsConfig.instance.default_dimensions(ELEMENT_TEXT)
|
611
|
+
if args[ARG_USE_LARGE_FONT]
|
612
|
+
text_width = WadsConfig.instance.current_theme.pixel_width_for_large_font(message)
|
613
|
+
else
|
614
|
+
text_width = WadsConfig.instance.current_theme.pixel_width_for_string(message)
|
615
|
+
end
|
616
|
+
coordinates = get_coordinates(ELEMENT_TEXT,
|
617
|
+
{ ARG_DESIRED_WIDTH => text_width,
|
618
|
+
ARG_DESIRED_HEIGHT => default_dimensions[1]}.merge(args))
|
619
|
+
new_text = Text.new(coordinates.x, coordinates.y, message,
|
620
|
+
{ ARG_THEME => @parent_widget.gui_theme}.merge(args))
|
621
|
+
new_text.base_z = @parent_widget.base_z
|
622
|
+
@parent_widget.add_child(new_text)
|
623
|
+
new_text
|
624
|
+
end
|
625
|
+
|
626
|
+
def add_text_input(width, default_text = '', args = {})
|
627
|
+
coordinates = get_coordinates(ELEMENT_TEXT_INPUT,
|
628
|
+
{ ARG_DESIRED_WIDTH => width}.merge(args))
|
629
|
+
new_text_input = TextField.new(coordinates.x, coordinates.y, default_text, width)
|
630
|
+
new_text_input.base_z = @parent_widget.base_z
|
631
|
+
@parent_widget.add_child(new_text_input)
|
632
|
+
@parent_widget.text_input_fields << new_text_input
|
633
|
+
new_text_input
|
634
|
+
end
|
635
|
+
|
636
|
+
def add_image(filename, args = {})
|
637
|
+
img = Gosu::Image.new(filename)
|
638
|
+
coordinates = get_coordinates(ELEMENT_IMAGE,
|
639
|
+
{ ARG_DESIRED_WIDTH => img.width,
|
640
|
+
ARG_DESIRED_HEIGHT => img.height}.merge(args))
|
641
|
+
new_image = ImageWidget.new(coordinates.x, coordinates.y, img,
|
642
|
+
{ARG_THEME => @parent_widget.gui_theme}.merge(args))
|
643
|
+
new_image.base_z = @parent_widget.base_z
|
644
|
+
@parent_widget.add_child(new_image)
|
645
|
+
new_image
|
646
|
+
end
|
647
|
+
|
648
|
+
def add_button(label, args = {}, &block)
|
649
|
+
text_width = WadsConfig.instance.current_theme.pixel_width_for_string(label) + 20
|
650
|
+
coordinates = get_coordinates(ELEMENT_BUTTON,
|
651
|
+
{ ARG_DESIRED_WIDTH => text_width}.merge(args))
|
652
|
+
new_button = Button.new(coordinates.x, coordinates.y, label,
|
653
|
+
{ ARG_DESIRED_WIDTH => coordinates.width,
|
654
|
+
ARG_THEME => @parent_widget.gui_theme}.merge(args))
|
655
|
+
new_button.set_action(&block)
|
656
|
+
new_button.base_z = @parent_widget.base_z
|
657
|
+
@parent_widget.add_child(new_button)
|
658
|
+
new_button
|
659
|
+
end
|
660
|
+
|
661
|
+
def add_plot(args = {})
|
662
|
+
coordinates = get_coordinates(ELEMENT_PLOT, args)
|
663
|
+
new_plot = Plot.new(coordinates.x, coordinates.y,
|
664
|
+
coordinates.width, coordinates.height)
|
665
|
+
new_plot.base_z = @parent_widget.base_z
|
666
|
+
@parent_widget.add_child(new_plot)
|
667
|
+
new_plot
|
668
|
+
end
|
669
|
+
|
670
|
+
def add_document(content, args = {})
|
671
|
+
number_of_content_lines = content.lines.count
|
672
|
+
height = (number_of_content_lines * 26) + 4
|
673
|
+
coordinates = get_coordinates(ELEMENT_DOCUMENT,
|
674
|
+
{ ARG_DESIRED_HEIGHT => height}.merge(args))
|
675
|
+
new_doc = Document.new(coordinates.x, coordinates.y,
|
676
|
+
coordinates.width, coordinates.height,
|
677
|
+
content,
|
678
|
+
{ARG_THEME => @parent_widget.gui_theme}.merge(args))
|
679
|
+
new_doc.base_z = @parent_widget.base_z
|
680
|
+
@parent_widget.add_child(new_doc)
|
681
|
+
new_doc
|
682
|
+
end
|
683
|
+
|
684
|
+
def add_graph_display(graph, display_mode = GRAPH_DISPLAY_ALL, args = {})
|
685
|
+
coordinates = get_coordinates(ELEMENT_GRAPH, args)
|
686
|
+
new_graph = GraphWidget.new(coordinates.x, coordinates.y,
|
687
|
+
coordinates.width, coordinates.height,
|
688
|
+
graph, display_mode)
|
689
|
+
new_graph.base_z = @parent_widget.base_z
|
690
|
+
@parent_widget.add_child(new_graph)
|
691
|
+
new_graph
|
692
|
+
end
|
693
|
+
|
694
|
+
def add_single_select_table(column_headers, visible_rows, args = {})
|
695
|
+
calculated_height = 30 + (visible_rows * 30)
|
696
|
+
coordinates = get_coordinates(ELEMENT_TABLE,
|
697
|
+
{ ARG_DESIRED_HEIGHT => calculated_height}.merge(args))
|
698
|
+
new_table = SingleSelectTable.new(coordinates.x, coordinates.y,
|
699
|
+
coordinates.width, coordinates.height,
|
700
|
+
column_headers, visible_rows,
|
701
|
+
{ARG_THEME => @parent_widget.gui_theme}.merge(args))
|
702
|
+
new_table.base_z = @parent_widget.base_z
|
703
|
+
@parent_widget.add_child(new_table)
|
704
|
+
new_table
|
705
|
+
end
|
706
|
+
|
707
|
+
def add_multi_select_table(column_headers, visible_rows, args = {})
|
708
|
+
calculated_height = 30 + (visible_rows * 30)
|
709
|
+
coordinates = get_coordinates(ELEMENT_TABLE,
|
710
|
+
{ ARG_DESIRED_HEIGHT => calculated_height}.merge(args))
|
711
|
+
new_table = MultiSelectTable.new(coordinates.x, coordinates.y,
|
712
|
+
coordinates.width, coordinates.height,
|
713
|
+
column_headers, visible_rows,
|
714
|
+
{ARG_THEME => @parent_widget.gui_theme}.merge(args))
|
715
|
+
new_table.base_z = @parent_widget.base_z
|
716
|
+
@parent_widget.add_child(new_table)
|
717
|
+
new_table
|
718
|
+
end
|
719
|
+
|
720
|
+
def add_table(column_headers, visible_rows, args = {})
|
721
|
+
calculated_height = 30 + (visible_rows * 30)
|
722
|
+
coordinates = get_coordinates(ELEMENT_TABLE,
|
723
|
+
{ ARG_DESIRED_HEIGHT => calculated_height}.merge(args))
|
724
|
+
new_table = Table.new(coordinates.x, coordinates.y,
|
725
|
+
coordinates.width, coordinates.height,
|
726
|
+
column_headers, visible_rows,
|
727
|
+
{ARG_THEME => @parent_widget.gui_theme}.merge(args))
|
728
|
+
new_table.base_z = @parent_widget.base_z
|
729
|
+
@parent_widget.add_child(new_table)
|
730
|
+
new_table
|
731
|
+
end
|
732
|
+
|
733
|
+
def add_horizontal_panel(args = {})
|
734
|
+
internal_add_panel(ELEMENT_HORIZONTAL_PANEL, args)
|
735
|
+
end
|
736
|
+
|
737
|
+
def add_vertical_panel(args = {})
|
738
|
+
internal_add_panel(ELEMENT_VERTICAL_PANEL, args)
|
739
|
+
end
|
740
|
+
|
741
|
+
def add_max_panel(args = {})
|
742
|
+
internal_add_panel(ELEMENT_MAX_PANEL, args)
|
743
|
+
end
|
744
|
+
|
745
|
+
def internal_add_panel(orientation, args)
|
746
|
+
coordinates = get_coordinates(orientation, args)
|
747
|
+
new_panel = Panel.new(coordinates.x, coordinates.y,
|
748
|
+
coordinates.width, coordinates.height)
|
749
|
+
new_panel_layout = args[ARG_LAYOUT]
|
750
|
+
if new_panel_layout.nil?
|
751
|
+
new_panel_layout = LAYOUT_VERTICAL_COLUMN
|
752
|
+
end
|
753
|
+
new_panel.set_layout(new_panel_layout, args)
|
754
|
+
|
755
|
+
new_panel_theme = args[ARG_THEME]
|
756
|
+
new_panel.gui_theme = new_panel_theme unless new_panel_theme.nil?
|
757
|
+
|
758
|
+
new_panel.base_z = @parent_widget.base_z
|
759
|
+
@parent_widget.add_child(new_panel)
|
760
|
+
#new_panel.disable_border
|
761
|
+
new_panel
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
class VerticalColumnLayout < WadsLayout
|
766
|
+
attr_accessor :single_column_container
|
767
|
+
|
768
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
769
|
+
super
|
770
|
+
@single_column_container = GuiContainer.new(x, y, width, height, FILL_VERTICAL_STACK)
|
771
|
+
end
|
772
|
+
|
773
|
+
# This is the standard interface for layouts
|
774
|
+
def get_coordinates(element_type, args = {})
|
775
|
+
@single_column_container.get_coordinates(element_type, args)
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
# SectionLayout is an intermediate class in the layout class hierarchy
|
780
|
+
# that is used to divide the visible screen into different sections.
|
781
|
+
# The commonly used sections include SECTION_TOP or SECTION_NORTH,
|
782
|
+
# SECTION_MIDDLE or SECTION_CENTER, SECTION_BOTTOM or SECTION_SOUTH,
|
783
|
+
# SECTION_LEFT or SECTION_WEST, SECTION_RIGHT or SECTION_EAST.
|
784
|
+
class SectionLayout < WadsLayout
|
785
|
+
attr_accessor :container_map
|
786
|
+
|
787
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
788
|
+
super
|
789
|
+
@container_map = {}
|
790
|
+
end
|
791
|
+
|
792
|
+
#
|
793
|
+
# Get the coordinates for the given element type. A generic map of parameters
|
794
|
+
# is accepted, however the ARG_SECTION is required so the layout can determine
|
795
|
+
# which section or container is used.
|
796
|
+
#
|
797
|
+
def get_coordinates(element_type, args = {})
|
798
|
+
section = args[ARG_SECTION]
|
799
|
+
if section.nil?
|
800
|
+
raise "Layout addition requires the arg '#{ARG_SECTION}' with value #{@container_map.keys.join(', ')}"
|
801
|
+
end
|
802
|
+
container = @container_map[section]
|
803
|
+
if container.nil?
|
804
|
+
raise "Invalid section #{section}. Value values are #{@container_map.keys.join(', ')}"
|
805
|
+
end
|
806
|
+
container.get_coordinates(element_type, args)
|
807
|
+
end
|
808
|
+
|
809
|
+
#
|
810
|
+
# This is a convenience method that creates a panel divided into a left and right,
|
811
|
+
# or east and west section. It will take up the entire space of the specified
|
812
|
+
# ARG_SECTION in the args map.
|
813
|
+
#
|
814
|
+
def add_east_west_panel(args)
|
815
|
+
section = args[ARG_SECTION]
|
816
|
+
if section.nil?
|
817
|
+
raise "East west panels require the arg '#{ARG_SECTION}' with value #{@container_map.keys.join(', ')}"
|
818
|
+
end
|
819
|
+
container = @container_map[section]
|
820
|
+
new_panel = Panel.new(container.start_x, container.start_y,
|
821
|
+
container.max_width, container.max_height)
|
822
|
+
new_panel.set_layout(LAYOUT_EAST_WEST, args)
|
823
|
+
new_panel.base_z = @parent_widget.base_z
|
824
|
+
new_panel.disable_border
|
825
|
+
@parent_widget.add_child(new_panel)
|
826
|
+
new_panel
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
# The layout sections are as follows:
|
831
|
+
#
|
832
|
+
# +-------------------------------------------------+
|
833
|
+
# + SECTION_NORTH +
|
834
|
+
# +-------------------------------------------------+
|
835
|
+
# + +
|
836
|
+
# + SECTION_CENTER +
|
837
|
+
# + +
|
838
|
+
# +-------------------------------------------------+
|
839
|
+
class HeaderContentLayout < SectionLayout
|
840
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
841
|
+
super
|
842
|
+
# Divide the height into 100, 100, and the middle gets everything else
|
843
|
+
# Right now we are using 100 pixels rather than a percentage for the borders
|
844
|
+
middle_section_y_start = y + 100
|
845
|
+
height_middle_section = height - 100
|
846
|
+
@container_map[SECTION_NORTH] = GuiContainer.new(x, y, width, 100)
|
847
|
+
@container_map[SECTION_CENTER] = GuiContainer.new(x, middle_section_y_start, width, height_middle_section, FILL_VERTICAL_STACK)
|
848
|
+
end
|
849
|
+
end
|
850
|
+
|
851
|
+
# The layout sections are as follows:
|
852
|
+
#
|
853
|
+
# +-------------------------------------------------+
|
854
|
+
# + +
|
855
|
+
# + SECTION_CENTER +
|
856
|
+
# + +
|
857
|
+
# +-------------------------------------------------+
|
858
|
+
# + SECTION_SOUTH +
|
859
|
+
# +-------------------------------------------------+
|
860
|
+
class ContentFooterLayout < SectionLayout
|
861
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
862
|
+
super
|
863
|
+
# Divide the height into 100, 100, and the middle gets everything else
|
864
|
+
# Right now we are using 100 pixels rather than a percentage for the borders
|
865
|
+
bottom_section_height = 100
|
866
|
+
if args[ARG_DESIRED_HEIGHT]
|
867
|
+
bottom_section_height = args[ARG_DESIRED_HEIGHT]
|
868
|
+
end
|
869
|
+
bottom_section_y_start = y + height - bottom_section_height
|
870
|
+
middle_section_height = height - bottom_section_height
|
871
|
+
@container_map[SECTION_CENTER] = GuiContainer.new(x, y, width, middle_section_height, FILL_VERTICAL_STACK)
|
872
|
+
@container_map[SECTION_SOUTH] = GuiContainer.new(x, bottom_section_y_start,
|
873
|
+
width, bottom_section_height)
|
874
|
+
end
|
875
|
+
end
|
876
|
+
|
877
|
+
# The layout sections are as follows:
|
878
|
+
#
|
879
|
+
# +-------------------------------------------------+
|
880
|
+
# + | +
|
881
|
+
# + SECTION_WEST | SECTION_EAST +
|
882
|
+
# + | +
|
883
|
+
# +-------------------------------------------------+
|
884
|
+
#
|
885
|
+
class EastWestLayout < SectionLayout
|
886
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
887
|
+
super
|
888
|
+
west_section_width = width / 2
|
889
|
+
if args[ARG_PANEL_WIDTH]
|
890
|
+
west_section_width = args[ARG_PANEL_WIDTH]
|
891
|
+
end
|
892
|
+
east_section_width = width - west_section_width
|
893
|
+
@container_map[SECTION_WEST] = GuiContainer.new(x, y,
|
894
|
+
west_section_width, height,
|
895
|
+
FILL_FULL_SIZE)
|
896
|
+
@container_map[SECTION_EAST] = GuiContainer.new(x + west_section_width, y,
|
897
|
+
east_section_width, height,
|
898
|
+
FILL_FULL_SIZE)
|
899
|
+
end
|
900
|
+
end
|
901
|
+
|
902
|
+
# The layout sections are as follows:
|
903
|
+
#
|
904
|
+
# +-------------------------------------------------+
|
905
|
+
# + SECTION_NORTH +
|
906
|
+
# +-------------------------------------------------+
|
907
|
+
# + +
|
908
|
+
# + SECTION_CENTER +
|
909
|
+
# + +
|
910
|
+
# +-------------------------------------------------+
|
911
|
+
# + SECTION_SOUTH +
|
912
|
+
# +-------------------------------------------------+
|
913
|
+
class TopMiddleBottomLayout < SectionLayout
|
914
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
915
|
+
super
|
916
|
+
# Divide the height into 100, 100, and the middle gets everything else
|
917
|
+
# Right now we are using 100 pixels rather than a percentage for the borders
|
918
|
+
middle_section_y_start = y + 100
|
919
|
+
bottom_section_y_start = y + height - 100
|
920
|
+
height_middle_section = height - 200
|
921
|
+
@container_map[SECTION_NORTH] = GuiContainer.new(x, y, width, 100)
|
922
|
+
@container_map[SECTION_CENTER] = GuiContainer.new(x, middle_section_y_start,
|
923
|
+
width, height_middle_section, FILL_VERTICAL_STACK)
|
924
|
+
@container_map[SECTION_SOUTH] = GuiContainer.new(x, bottom_section_y_start, width, 100)
|
925
|
+
end
|
926
|
+
end
|
927
|
+
|
928
|
+
# The layout sections are as follows:
|
929
|
+
#
|
930
|
+
# +-------------------------------------------------+
|
931
|
+
# + SECTION_NORTH +
|
932
|
+
# +-------------------------------------------------+
|
933
|
+
# + | | +
|
934
|
+
# + SECTION_WEST | SECTION_CENTER | SECTION_EAST +
|
935
|
+
# + | | +
|
936
|
+
# +-------------------------------------------------+
|
937
|
+
# + SECTION_SOUTH +
|
938
|
+
# +-------------------------------------------------+
|
939
|
+
class BorderLayout < SectionLayout
|
940
|
+
def initialize(x, y, width, height, parent_widget, args = {})
|
941
|
+
super
|
942
|
+
# Divide the height into 100, 100, and the middle gets everything else
|
943
|
+
# Right now we are using 100 pixels rather than a percentage for the borders
|
944
|
+
middle_section_y_start = y + 100
|
945
|
+
bottom_section_y_start = y + height - 100
|
946
|
+
|
947
|
+
height_middle_section = bottom_section_y_start - middle_section_y_start
|
948
|
+
|
949
|
+
middle_section_x_start = x + 100
|
950
|
+
right_section_x_start = x + width - 100
|
951
|
+
width_middle_section = right_section_x_start - middle_section_x_start
|
952
|
+
|
953
|
+
@container_map[SECTION_NORTH] = GuiContainer.new(x, y, width, 100)
|
954
|
+
@container_map[SECTION_WEST] = GuiContainer.new(
|
955
|
+
x, middle_section_y_start, 100, height_middle_section, FILL_VERTICAL_STACK)
|
956
|
+
@container_map[SECTION_CENTER] = GuiContainer.new(
|
957
|
+
middle_section_x_start,
|
958
|
+
middle_section_y_start,
|
959
|
+
width_middle_section,
|
960
|
+
height_middle_section,
|
961
|
+
FILL_VERTICAL_STACK)
|
962
|
+
@container_map[SECTION_EAST] = GuiContainer.new(
|
963
|
+
right_section_x_start,
|
964
|
+
middle_section_y_start,
|
965
|
+
100,
|
966
|
+
height_middle_section,
|
967
|
+
FILL_VERTICAL_STACK)
|
968
|
+
@container_map[SECTION_SOUTH] = GuiContainer.new(x, bottom_section_y_start, width, 100)
|
969
|
+
end
|
970
|
+
end
|
971
|
+
|
972
|
+
# The base class for all widgets. This class provides basic functionality for
|
973
|
+
# all gui widgets including maintaining the coordinates and layout used.
|
974
|
+
# A widget has a border and background, whose colors are defined by the theme.
|
975
|
+
# These can be turned off using the disable_border and disable_background methods.
|
976
|
+
# Widgets support a hierarchy of visible elements on the screen. For example,
|
977
|
+
# a parent widget may be a form, and it may contain many child widgets such as
|
978
|
+
# text labels, input fields, and a submit button. You can add children to a
|
979
|
+
# widget using the add or add_child methods. Children are automatically rendered
|
980
|
+
# so any child does not need an explicit call to its draw or render method.
|
981
|
+
# Children can be placed with x, y positioning relative to their parent for convenience
|
982
|
+
# (see the relative_x and relative_y methods).
|
983
|
+
#
|
984
|
+
# The draw and update methods are used by their Gosu counterparts.
|
985
|
+
# Typically there is one parent Wads widget used by a Gosu app, and it controls
|
986
|
+
# drawing all of the child widgets, invoking update on all widgets, and delegating
|
987
|
+
# user events. Widgets can override a render method for any specific drawing logic.
|
988
|
+
# It is worth showing the draw method here to amplify the point. You do not need
|
989
|
+
# to specifically call draw or render on any children. If you want to manage GUI
|
990
|
+
# elements outside of the widget hierarchy, then render is the best place to do it.
|
991
|
+
#
|
992
|
+
# Likewise, the update method recursively calls the handle_update method on all
|
993
|
+
# children in this widget's hierarchy.
|
994
|
+
#
|
995
|
+
# A commonly used method is contains_click(mouse_x, mouse_y) which returns
|
996
|
+
# whether this widget contained the mouse event. For a example, a button widget
|
997
|
+
# uses this method to determine if it was clicked.
|
998
|
+
#
|
38
999
|
class Widget
|
39
1000
|
attr_accessor :x
|
40
1001
|
attr_accessor :y
|
41
|
-
attr_accessor :
|
1002
|
+
attr_accessor :base_z
|
1003
|
+
attr_accessor :gui_theme
|
1004
|
+
attr_accessor :layout
|
42
1005
|
attr_accessor :width
|
43
1006
|
attr_accessor :height
|
44
1007
|
attr_accessor :visible
|
45
1008
|
attr_accessor :children
|
46
|
-
attr_accessor :
|
47
|
-
attr_accessor :
|
48
|
-
attr_accessor :
|
1009
|
+
attr_accessor :overlay_widget
|
1010
|
+
attr_accessor :override_color
|
1011
|
+
attr_accessor :is_selected
|
1012
|
+
attr_accessor :text_input_fields
|
49
1013
|
|
50
|
-
def initialize(x, y,
|
51
|
-
|
52
|
-
|
53
|
-
@
|
54
|
-
|
55
|
-
|
1014
|
+
def initialize(x, y, width = 10, height = 10, layout = nil, theme = nil)
|
1015
|
+
set_absolute_position(x, y)
|
1016
|
+
set_dimensions(width, height)
|
1017
|
+
@base_z = 0
|
1018
|
+
if uses_layout
|
1019
|
+
if layout.nil?
|
1020
|
+
@layout = WadsConfig.instance.create_layout(x, y, width, height, self)
|
1021
|
+
else
|
1022
|
+
@layout = layout
|
1023
|
+
end
|
1024
|
+
end
|
1025
|
+
if theme.nil?
|
1026
|
+
@gui_theme = WadsConfig.instance.current_theme
|
1027
|
+
else
|
1028
|
+
@gui_theme = theme
|
1029
|
+
end
|
56
1030
|
@visible = true
|
57
1031
|
@children = []
|
1032
|
+
@show_background = true
|
1033
|
+
@show_border = true
|
1034
|
+
@is_selected = false
|
1035
|
+
@text_input_fields = []
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
def debug(message)
|
1039
|
+
WadsConfig.instance.get_logger.debug message
|
1040
|
+
end
|
1041
|
+
def info(message)
|
1042
|
+
WadsConfig.instance.get_logger.info message
|
1043
|
+
end
|
1044
|
+
def warn(message)
|
1045
|
+
WadsConfig.instance.get_logger.warn message
|
1046
|
+
end
|
1047
|
+
def error(message)
|
1048
|
+
WadsConfig.instance.get_logger.error message
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
def set_absolute_position(x, y)
|
1052
|
+
@x = x
|
1053
|
+
@y = y
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def set_dimensions(width, height)
|
1057
|
+
@width = width
|
1058
|
+
@height = height
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
def uses_layout
|
1062
|
+
true
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
def get_layout
|
1066
|
+
if not uses_layout
|
1067
|
+
raise "The widget #{self.class.name} does not support layouts"
|
1068
|
+
end
|
1069
|
+
if @layout.nil?
|
1070
|
+
raise "No layout was defined for #{self.class.name}"
|
1071
|
+
end
|
1072
|
+
@layout
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
def set_layout(layout_type, args = {})
|
1076
|
+
@layout = WadsConfig.instance.create_layout_for_widget(self, layout_type, args)
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
def add_panel(section, args = {})
|
1080
|
+
get_layout.add_max_panel({ ARG_SECTION => section,
|
1081
|
+
ARG_THEME => @gui_theme}.merge(args))
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
def get_theme
|
1085
|
+
@gui_theme
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
def set_theme(new_theme)
|
1089
|
+
@gui_theme = new_theme
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
def set_selected
|
1093
|
+
@is_selected = true
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
def unset_selected
|
1097
|
+
@is_selected = false
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
def graphics_color
|
1101
|
+
if @override_color
|
1102
|
+
return @override_color
|
1103
|
+
end
|
1104
|
+
@gui_theme.graphic_elements_color
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
def text_color
|
1108
|
+
if @override_color
|
1109
|
+
return @override_color
|
1110
|
+
end
|
1111
|
+
@gui_theme.text_color
|
1112
|
+
end
|
1113
|
+
|
1114
|
+
def selection_color
|
1115
|
+
@gui_theme.selection_color
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
def border_color
|
1119
|
+
@gui_theme.border_color
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
#
|
1123
|
+
# The z order is determined by taking the base_z and adding the widget specific value.
|
1124
|
+
# An overlay widget has a base_z that is +10 higher than the widget underneath it.
|
1125
|
+
# The widget_z method provides a relative ordering that is common for user interfaces.
|
1126
|
+
# For example, text is higher than graphic elements and backgrounds.
|
1127
|
+
#
|
1128
|
+
def z_order
|
1129
|
+
@base_z + widget_z
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
def relative_z_order(relative_order)
|
1133
|
+
@base_z + relative_order
|
58
1134
|
end
|
59
1135
|
|
1136
|
+
#
|
1137
|
+
# Add a child widget that will automatically be drawn by this widget and will received
|
1138
|
+
# delegated events. This is an alias for add_child
|
1139
|
+
#
|
1140
|
+
def add(child)
|
1141
|
+
add_child(child)
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
#
|
1145
|
+
# Add a child widget that will automatically be drawn by this widget and will received
|
1146
|
+
# delegated events.
|
1147
|
+
#
|
60
1148
|
def add_child(child)
|
61
1149
|
@children << child
|
62
1150
|
end
|
63
1151
|
|
1152
|
+
#
|
1153
|
+
# Remove the given child widget
|
1154
|
+
#
|
1155
|
+
def remove_child(child)
|
1156
|
+
@children.delete(child)
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
#
|
1160
|
+
# Remove a list of child widgets
|
1161
|
+
#
|
1162
|
+
def remove_children(list)
|
1163
|
+
list.each do |child|
|
1164
|
+
@children.delete(child)
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
#
|
1169
|
+
# Remove all children whose class name includes the given token.
|
1170
|
+
# This method can be used if you do not have a saved list of the
|
1171
|
+
# widgets you want to remove.
|
1172
|
+
#
|
1173
|
+
def remove_children_by_type(class_name_token)
|
1174
|
+
children_to_remove = []
|
1175
|
+
@children.each do |child|
|
1176
|
+
if child.class.name.include? class_name_token
|
1177
|
+
children_to_remove << child
|
1178
|
+
end
|
1179
|
+
end
|
1180
|
+
children_to_remove.each do |child|
|
1181
|
+
@children.delete(child)
|
1182
|
+
end
|
1183
|
+
end
|
1184
|
+
|
1185
|
+
#
|
1186
|
+
# Remove all children from this widget
|
1187
|
+
#
|
64
1188
|
def clear_children
|
65
1189
|
@children = []
|
66
1190
|
end
|
67
1191
|
|
68
|
-
|
69
|
-
|
1192
|
+
#
|
1193
|
+
# Drawing the background is on by default. Use this method to prevent drawing a background.
|
1194
|
+
#
|
1195
|
+
def disable_background
|
1196
|
+
@show_background = false
|
70
1197
|
end
|
71
1198
|
|
72
|
-
|
73
|
-
|
1199
|
+
#
|
1200
|
+
# Drawing the border is on by default. Use this method to prevent drawing a border.
|
1201
|
+
#
|
1202
|
+
def disable_border
|
1203
|
+
@show_border = false
|
74
1204
|
end
|
75
1205
|
|
76
|
-
|
77
|
-
|
1206
|
+
#
|
1207
|
+
# Turn back on drawing of the border
|
1208
|
+
#
|
1209
|
+
def enable_border
|
1210
|
+
@show_border = true
|
78
1211
|
end
|
79
1212
|
|
80
|
-
|
81
|
-
|
82
|
-
|
1213
|
+
#
|
1214
|
+
# Turn back on drawing of the background
|
1215
|
+
#
|
1216
|
+
def enable_background
|
1217
|
+
@show_background = true
|
83
1218
|
end
|
84
1219
|
|
1220
|
+
#
|
1221
|
+
# A convenience method, or alias, to return the left x coordinate of this widget.
|
1222
|
+
#
|
1223
|
+
def left_edge
|
1224
|
+
@x
|
1225
|
+
end
|
1226
|
+
|
1227
|
+
#
|
1228
|
+
# A convenience method to return the right x coordinate of this widget.
|
1229
|
+
#
|
85
1230
|
def right_edge
|
86
1231
|
@x + @width - 1
|
87
1232
|
end
|
88
|
-
|
1233
|
+
|
1234
|
+
#
|
1235
|
+
# A convenience method, or alias, to return the top y coordinate of this widget.
|
1236
|
+
#
|
1237
|
+
def top_edge
|
1238
|
+
@y
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
#
|
1242
|
+
# A convenience method to return the bottom y coordinate of this widget
|
1243
|
+
#
|
89
1244
|
def bottom_edge
|
90
1245
|
@y + @height - 1
|
91
1246
|
end
|
92
1247
|
|
1248
|
+
#
|
1249
|
+
# A convenience method to return the center x coordinate of this widget
|
1250
|
+
#
|
93
1251
|
def center_x
|
94
1252
|
@x + ((right_edge - @x) / 2)
|
95
1253
|
end
|
96
1254
|
|
1255
|
+
#
|
1256
|
+
# A convenience method to return the center y coordinate of this widget
|
1257
|
+
#
|
1258
|
+
def center_y
|
1259
|
+
@y + ((bottom_edge - @y) / 2)
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
#
|
1263
|
+
# Move this widget to an absolute x, y position on the screen.
|
1264
|
+
# It will automatically move all child widgets, however be warned that
|
1265
|
+
# if you are manually rendering any elements within your own render
|
1266
|
+
# logic, you will need to deal with that seperately as the base class
|
1267
|
+
# does not have access to its coordinates.
|
1268
|
+
#
|
1269
|
+
def move_recursive_absolute(new_x, new_y)
|
1270
|
+
delta_x = new_x - @x
|
1271
|
+
delta_y = new_y - @y
|
1272
|
+
move_recursive_delta(delta_x, delta_y)
|
1273
|
+
end
|
1274
|
+
|
1275
|
+
#
|
1276
|
+
# Move this widget to a relative number of x, y pixels on the screen.
|
1277
|
+
# It will automatically move all child widgets, however be warned that
|
1278
|
+
# if you are manually rendering any elements within your own render
|
1279
|
+
# logic, you will need to deal with that seperately as the base class
|
1280
|
+
# does not have access to its coordinates.
|
1281
|
+
#
|
1282
|
+
def move_recursive_delta(delta_x, delta_y)
|
1283
|
+
@x = @x + delta_x
|
1284
|
+
@y = @y + delta_y
|
1285
|
+
@children.each do |child|
|
1286
|
+
child.move_recursive_delta(delta_x, delta_y)
|
1287
|
+
end
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
#
|
1291
|
+
# The primary draw method, used by the main Gosu loop draw method.
|
1292
|
+
# A common usage pattern is to have a primary widget in your Gosu app
|
1293
|
+
# that calls this draw method. All children of this widget are then
|
1294
|
+
# automatically drawn by this method recursively.
|
1295
|
+
# Note that as a widget author, you should only implement/override the
|
1296
|
+
# render method. This is a framework implementation that will
|
1297
|
+
# handle child rendering and invoke render as a user-implemented
|
1298
|
+
# callback.
|
1299
|
+
#
|
97
1300
|
def draw
|
98
1301
|
if @visible
|
99
1302
|
render
|
100
|
-
if @
|
1303
|
+
if @is_selected
|
1304
|
+
draw_background(Z_ORDER_SELECTION_BACKGROUND, @gui_theme.selection_color)
|
1305
|
+
elsif @show_background
|
101
1306
|
draw_background
|
102
1307
|
end
|
103
|
-
if @
|
104
|
-
draw_border
|
1308
|
+
if @show_border
|
1309
|
+
draw_border
|
105
1310
|
end
|
106
1311
|
@children.each do |child|
|
107
1312
|
child.draw
|
@@ -109,66 +1314,566 @@ module Wads
|
|
109
1314
|
end
|
110
1315
|
end
|
111
1316
|
|
112
|
-
def draw_background
|
113
|
-
|
1317
|
+
def draw_background(z_override = nil, color_override = nil)
|
1318
|
+
if color_override.nil?
|
1319
|
+
bgcolor = @gui_theme.background_color
|
1320
|
+
else
|
1321
|
+
bgcolor = color_override
|
1322
|
+
end
|
1323
|
+
if z_override
|
1324
|
+
z = relative_z_order(z_override)
|
1325
|
+
else
|
1326
|
+
z = relative_z_order(Z_ORDER_BACKGROUND)
|
1327
|
+
end
|
1328
|
+
Gosu::draw_rect(@x + 1, @y + 1, @width - 3, @height - 3, bgcolor, z)
|
114
1329
|
end
|
115
1330
|
|
1331
|
+
def draw_border
|
1332
|
+
Gosu::draw_line @x, @y, @gui_theme.border_color, right_edge, @y, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
|
1333
|
+
Gosu::draw_line @x, @y, @gui_theme.border_color, @x, bottom_edge, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
|
1334
|
+
Gosu::draw_line @x,bottom_edge, @gui_theme.border_color, right_edge, bottom_edge, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
|
1335
|
+
Gosu::draw_line right_edge, @y, @gui_theme.border_color, right_edge, bottom_edge, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
def contains_click(mouse_x, mouse_y)
|
1339
|
+
mouse_x >= @x and mouse_x <= right_edge and mouse_y >= @y and mouse_y <= bottom_edge
|
1340
|
+
end
|
1341
|
+
|
1342
|
+
#
|
1343
|
+
# Return true if any part of the given widget overlaps on the screen with this widget
|
1344
|
+
# as defined by the rectangle from the upper left corner to the bottom right.
|
1345
|
+
# Note that your widget may not necessariliy draw pixels in this entire space.
|
1346
|
+
#
|
1347
|
+
def overlaps_with(other_widget)
|
1348
|
+
if other_widget.contains_click(@x, @y)
|
1349
|
+
return true
|
1350
|
+
end
|
1351
|
+
if other_widget.contains_click(right_edge, @y)
|
1352
|
+
return true
|
1353
|
+
end
|
1354
|
+
if other_widget.contains_click(right_edge, bottom_edge - 1)
|
1355
|
+
return true
|
1356
|
+
end
|
1357
|
+
if other_widget.contains_click(@x, bottom_edge - 1)
|
1358
|
+
return true
|
1359
|
+
end
|
1360
|
+
if other_widget.contains_click(center_x, center_y)
|
1361
|
+
return true
|
1362
|
+
end
|
1363
|
+
return false
|
1364
|
+
end
|
1365
|
+
|
1366
|
+
#
|
1367
|
+
# The framework implementation of the main Gosu update loop. This method
|
1368
|
+
# propagates the event to all child widgets as well.
|
1369
|
+
# As a widget author, do not override this method.
|
1370
|
+
# Your callback to implement is the handle_update(update_count, mouse_x, mouse_y) method.
|
1371
|
+
#
|
1372
|
+
def update(update_count, mouse_x, mouse_y)
|
1373
|
+
if @overlay_widget
|
1374
|
+
@overlay_widget.update(update_count, mouse_x, mouse_y)
|
1375
|
+
end
|
1376
|
+
handle_update(update_count, mouse_x, mouse_y)
|
1377
|
+
@children.each do |child|
|
1378
|
+
child.update(update_count, mouse_x, mouse_y)
|
1379
|
+
end
|
1380
|
+
end
|
1381
|
+
|
1382
|
+
#
|
1383
|
+
# The framework implementation of the main Gosu button down method.
|
1384
|
+
# This method separates out mouse events from keyboard events, and calls the appropriate
|
1385
|
+
# callback. As a widget author, do not override this method.
|
1386
|
+
# Your callbacks to implement are:
|
1387
|
+
# handle_mouse_down(mouse_x, mouse_y)
|
1388
|
+
# handle_right_mouse(mouse_x, mouse_y)
|
1389
|
+
# handle_key_press(id, mouse_x, mouse_y)
|
1390
|
+
#
|
1391
|
+
def button_down(id, mouse_x, mouse_y)
|
1392
|
+
if @overlay_widget
|
1393
|
+
result = @overlay_widget.button_down(id, mouse_x, mouse_y)
|
1394
|
+
if not result.nil? and result.is_a? WidgetResult
|
1395
|
+
intercept_widget_event(result)
|
1396
|
+
if result.close_widget
|
1397
|
+
# remove the overlay widget frmo children, set to null
|
1398
|
+
# hopefully this closes and gets us back to normal
|
1399
|
+
remove_child(@overlay_widget)
|
1400
|
+
@overlay_widget = nil
|
1401
|
+
end
|
1402
|
+
end
|
1403
|
+
return
|
1404
|
+
end
|
1405
|
+
|
1406
|
+
if id == Gosu::MsLeft
|
1407
|
+
# Special handling for text input fields
|
1408
|
+
# Mouse click: Select text field based on mouse position.
|
1409
|
+
WadsConfig.instance.get_window.text_input = @text_input_fields.find { |tf| tf.under_point?(mouse_x, mouse_y) }
|
1410
|
+
# Advanced: Move caret to clicked position
|
1411
|
+
WadsConfig.instance.get_window.text_input.move_caret(mouse_x) unless WadsConfig.instance.get_window.text_input.nil?
|
1412
|
+
|
1413
|
+
result = handle_mouse_down mouse_x, mouse_y
|
1414
|
+
elsif id == Gosu::MsRight
|
1415
|
+
result = handle_right_mouse mouse_x, mouse_y
|
1416
|
+
else
|
1417
|
+
result = handle_key_press id, mouse_x, mouse_y
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
if not result.nil? and result.is_a? WidgetResult
|
1421
|
+
return result
|
1422
|
+
end
|
1423
|
+
|
1424
|
+
@children.each do |child|
|
1425
|
+
if id == Gosu::MsLeft
|
1426
|
+
if child.contains_click(mouse_x, mouse_y)
|
1427
|
+
result = child.button_down id, mouse_x, mouse_y
|
1428
|
+
if not result.nil? and result.is_a? WidgetResult
|
1429
|
+
intercept_widget_event(result)
|
1430
|
+
return result
|
1431
|
+
end
|
1432
|
+
end
|
1433
|
+
else
|
1434
|
+
result = child.button_down id, mouse_x, mouse_y
|
1435
|
+
if not result.nil? and result.is_a? WidgetResult
|
1436
|
+
intercept_widget_event(result)
|
1437
|
+
return result
|
1438
|
+
end
|
1439
|
+
end
|
1440
|
+
end
|
1441
|
+
end
|
1442
|
+
|
1443
|
+
#
|
1444
|
+
# The framework implementation of the main Gosu button up method.
|
1445
|
+
# This method separates out mouse events from keyboard events.
|
1446
|
+
# Only the mouse up event is propagated through the child hierarchy.
|
1447
|
+
# As a widget author, do not override this method.
|
1448
|
+
# Your callback to implement is:
|
1449
|
+
# handle_mouse_up(mouse_x, mouse_y)
|
1450
|
+
#
|
1451
|
+
def button_up(id, mouse_x, mouse_y)
|
1452
|
+
if @overlay_widget
|
1453
|
+
return @overlay_widget.button_up(id, mouse_x, mouse_y)
|
1454
|
+
end
|
1455
|
+
|
1456
|
+
if id == Gosu::MsLeft
|
1457
|
+
result = handle_mouse_up mouse_x, mouse_y
|
1458
|
+
if not result.nil? and result.is_a? WidgetResult
|
1459
|
+
return result
|
1460
|
+
end
|
1461
|
+
end
|
1462
|
+
|
1463
|
+
@children.each do |child|
|
1464
|
+
if id == Gosu::MsLeft
|
1465
|
+
if child.contains_click(mouse_x, mouse_y)
|
1466
|
+
result = child.handle_mouse_up mouse_x, mouse_y
|
1467
|
+
if not result.nil? and result.is_a? WidgetResult
|
1468
|
+
return result
|
1469
|
+
end
|
1470
|
+
end
|
1471
|
+
end
|
1472
|
+
end
|
1473
|
+
end
|
1474
|
+
|
1475
|
+
#
|
1476
|
+
# Return the absolute x coordinate given the relative x pixel to this widget
|
1477
|
+
#
|
1478
|
+
def relative_x(x)
|
1479
|
+
x_pixel_to_screen(x)
|
1480
|
+
end
|
1481
|
+
|
1482
|
+
# An alias for relative_x
|
1483
|
+
def x_pixel_to_screen(x)
|
1484
|
+
@x + x
|
1485
|
+
end
|
1486
|
+
|
1487
|
+
#
|
1488
|
+
# Return the absolute y coordinate given the relative y pixel to this widget
|
1489
|
+
#
|
1490
|
+
def relative_y(y)
|
1491
|
+
y_pixel_to_screen(y)
|
1492
|
+
end
|
1493
|
+
|
1494
|
+
# An alias for relative_y
|
1495
|
+
def y_pixel_to_screen(y)
|
1496
|
+
@y + y
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
#
|
1500
|
+
# Add a child text widget using x, y positioning relative to this widget
|
1501
|
+
#
|
1502
|
+
def add_text(message, rel_x, rel_y, color = nil, use_large_font = false)
|
1503
|
+
new_text = Text.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), message,
|
1504
|
+
{ ARG_COLOR => color, ARG_USE_LARGE_FONT => use_large_font})
|
1505
|
+
new_text.base_z = @base_z
|
1506
|
+
new_text.gui_theme = @gui_theme
|
1507
|
+
add_child(new_text)
|
1508
|
+
new_text
|
1509
|
+
end
|
1510
|
+
|
1511
|
+
#
|
1512
|
+
# Add a child document widget using x, y positioning relative to this widget
|
1513
|
+
#
|
1514
|
+
def add_document(content, rel_x, rel_y, width, height)
|
1515
|
+
new_doc = Document.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
|
1516
|
+
width, height,
|
1517
|
+
content)
|
1518
|
+
new_doc.base_z = @base_z
|
1519
|
+
new_doc.gui_theme = @gui_theme
|
1520
|
+
add_child(new_doc)
|
1521
|
+
new_doc
|
1522
|
+
end
|
1523
|
+
|
1524
|
+
#
|
1525
|
+
# Add a child button widget using x, y positioning relative to this widget.
|
1526
|
+
# The width of the button will be determined based on the label text unless
|
1527
|
+
# specified in the optional parameter. The code to execute is provided as a
|
1528
|
+
# block, as shown in the example below.
|
1529
|
+
# add_button("Test Button", 10, 10) do
|
1530
|
+
# puts "User hit the test button"
|
1531
|
+
# end
|
1532
|
+
def add_button(label, rel_x, rel_y, width = nil, &block)
|
1533
|
+
if width.nil?
|
1534
|
+
args = {}
|
1535
|
+
else
|
1536
|
+
args = { ARG_DESIRED_WIDTH => width }
|
1537
|
+
end
|
1538
|
+
new_button = Button.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), label, args)
|
1539
|
+
new_button.set_action(&block)
|
1540
|
+
new_button.base_z = @base_z
|
1541
|
+
new_button.gui_theme = @gui_theme
|
1542
|
+
add_child(new_button)
|
1543
|
+
new_button
|
1544
|
+
end
|
1545
|
+
|
1546
|
+
#
|
1547
|
+
# Add a child delete button widget using x, y positioning relative to this widget.
|
1548
|
+
# A delete button is a regular button that is rendered as a red X, instead of a text label.
|
1549
|
+
#
|
1550
|
+
def add_delete_button(rel_x, rel_y, &block)
|
1551
|
+
new_delete_button = DeleteButton.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y))
|
1552
|
+
new_delete_button.set_action(&block)
|
1553
|
+
new_delete_button.base_z = @base_z
|
1554
|
+
new_delete_button.gui_theme = @gui_theme
|
1555
|
+
add_child(new_delete_button)
|
1556
|
+
new_delete_button
|
1557
|
+
end
|
1558
|
+
|
1559
|
+
#
|
1560
|
+
# Add a child table widget using x, y positioning relative to this widget.
|
1561
|
+
#
|
1562
|
+
def add_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
|
1563
|
+
new_table = Table.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
|
1564
|
+
width, height, column_headers, max_visible_rows)
|
1565
|
+
new_table.base_z = @base_z
|
1566
|
+
new_table.gui_theme = @gui_theme
|
1567
|
+
add_child(new_table)
|
1568
|
+
new_table
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
#
|
1572
|
+
# Add a child table widget using x, y positioning relative to this widget.
|
1573
|
+
# The user can select up to one and only one item in the table.
|
1574
|
+
#
|
1575
|
+
def add_single_select_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
|
1576
|
+
new_table = SingleSelectTable.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
|
1577
|
+
width, height, column_headers, max_visible_rows)
|
1578
|
+
new_table.base_z = @base_z
|
1579
|
+
new_table.gui_theme = @gui_theme
|
1580
|
+
add_child(new_table)
|
1581
|
+
new_table
|
1582
|
+
end
|
1583
|
+
|
1584
|
+
#
|
1585
|
+
# Add a child table widget using x, y positioning relative to this widget.
|
1586
|
+
# The user can zero to many items in the table.
|
1587
|
+
#
|
1588
|
+
def add_multi_select_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
|
1589
|
+
new_table = MultiSelectTable.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
|
1590
|
+
width, height, column_headers, max_visible_rows)
|
1591
|
+
new_table.base_z = @base_z
|
1592
|
+
new_table.gui_theme = @gui_theme
|
1593
|
+
add_child(new_table)
|
1594
|
+
new_table
|
1595
|
+
end
|
1596
|
+
|
1597
|
+
#
|
1598
|
+
# Add a child graph display widget using x, y positioning relative to this widget.
|
1599
|
+
#
|
1600
|
+
def add_graph_display(rel_x, rel_y, width, height, graph)
|
1601
|
+
new_graph = GraphWidget.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height, graph)
|
1602
|
+
new_graph.base_z = @base_z
|
1603
|
+
add_child(new_graph)
|
1604
|
+
new_graph
|
1605
|
+
end
|
1606
|
+
|
1607
|
+
#
|
1608
|
+
# Add a child plot display widget using x, y positioning relative to this widget.
|
1609
|
+
#
|
1610
|
+
def add_plot(rel_x, rel_y, width, height)
|
1611
|
+
new_plot = Plot.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height)
|
1612
|
+
new_plot.base_z = @base_z
|
1613
|
+
new_plot.gui_theme = @gui_theme
|
1614
|
+
add_child(new_plot)
|
1615
|
+
new_plot
|
1616
|
+
end
|
1617
|
+
|
1618
|
+
#
|
1619
|
+
# Add child axis lines widget using x, y positioning relative to this widget.
|
1620
|
+
#
|
1621
|
+
def add_axis_lines(rel_x, rel_y, width, height)
|
1622
|
+
new_axis_lines = AxisLines.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height)
|
1623
|
+
new_axis_lines.base_z = @base_z
|
1624
|
+
new_axis_lines.gui_theme = @gui_theme
|
1625
|
+
add_child(new_axis_lines)
|
1626
|
+
new_axis_lines
|
1627
|
+
end
|
1628
|
+
|
1629
|
+
#
|
1630
|
+
# Add a child image widget using x, y positioning relative to this widget.
|
1631
|
+
#
|
1632
|
+
def add_image(filename, rel_x, rel_y)
|
1633
|
+
new_image = ImageWidget.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), img)
|
1634
|
+
new_image.base_z = @base_z
|
1635
|
+
new_image.gui_theme = @gui_theme
|
1636
|
+
add_child(new_image)
|
1637
|
+
new_image
|
1638
|
+
end
|
1639
|
+
|
1640
|
+
#
|
1641
|
+
# Add an overlay widget that is drawn on top of (at a higher z level) this widget
|
1642
|
+
#
|
1643
|
+
def add_overlay(overlay)
|
1644
|
+
overlay.base_z = @base_z + 10
|
1645
|
+
add_child(overlay)
|
1646
|
+
@overlay_widget = overlay
|
1647
|
+
end
|
1648
|
+
|
1649
|
+
# For all child widgets, adjust the x coordinate
|
1650
|
+
# so that they are centered.
|
1651
|
+
def center_children
|
1652
|
+
if @children.empty?
|
1653
|
+
return
|
1654
|
+
end
|
1655
|
+
number_of_children = @children.size
|
1656
|
+
total_width_of_children = 0
|
1657
|
+
@children.each do |child|
|
1658
|
+
total_width_of_children = total_width_of_children + child.width + 5
|
1659
|
+
end
|
1660
|
+
total_width_of_children = total_width_of_children - 5
|
1661
|
+
|
1662
|
+
start_x = (@width - total_width_of_children) / 2
|
1663
|
+
@children.each do |child|
|
1664
|
+
child.x = start_x
|
1665
|
+
start_x = start_x + child.width + 5
|
1666
|
+
end
|
1667
|
+
end
|
1668
|
+
|
1669
|
+
#
|
1670
|
+
# Override this method in your subclass to process mouse down events.
|
1671
|
+
# The base implementation is empty
|
1672
|
+
#
|
1673
|
+
def handle_mouse_down mouse_x, mouse_y
|
1674
|
+
# empty base implementation
|
1675
|
+
end
|
1676
|
+
|
1677
|
+
#
|
1678
|
+
# Override this method in your subclass to process mouse up events.
|
1679
|
+
# The base implementation is empty
|
1680
|
+
#
|
1681
|
+
def handle_mouse_up mouse_x, mouse_y
|
1682
|
+
# empty base implementation
|
1683
|
+
end
|
1684
|
+
|
1685
|
+
#
|
1686
|
+
# Override this method in your subclass to process the right mouse click event.
|
1687
|
+
# Note we do not differentiate between up and down for the right mouse button.
|
1688
|
+
# The base implementation is empty
|
1689
|
+
#
|
1690
|
+
def handle_right_mouse mouse_x, mouse_y
|
1691
|
+
# empty base implementation
|
1692
|
+
end
|
1693
|
+
|
1694
|
+
#
|
1695
|
+
# Override this method in your subclass to process keyboard events.
|
1696
|
+
# The base implementation is empty.
|
1697
|
+
# Note that the mouse was not necessarily positioned over this widget.
|
1698
|
+
# You can check this using the contains_click(mouse_x, mouse_y) method
|
1699
|
+
# and decide if you want to process the event based on that, if desired.
|
1700
|
+
#
|
1701
|
+
def handle_key_press id, mouse_x, mouse_y
|
1702
|
+
# empty base implementation
|
1703
|
+
end
|
1704
|
+
|
1705
|
+
#
|
1706
|
+
# Override this method in your subclass to perform any logic needed
|
1707
|
+
# as part of the main Gosu update loop. In most cases, this method is
|
1708
|
+
# invoked 60 times per second.
|
1709
|
+
#
|
1710
|
+
def handle_update update_count, mouse_x, mouse_y
|
1711
|
+
# empty base implementation
|
1712
|
+
end
|
1713
|
+
|
1714
|
+
#
|
1715
|
+
# Override this method in your subclass to perform any custom rendering logic.
|
1716
|
+
# Note that child widgets are automatically drawn and you do not need to do
|
1717
|
+
# that yourself.
|
1718
|
+
#
|
116
1719
|
def render
|
117
1720
|
# Base implementation is empty
|
118
|
-
# Note that the draw method invoked by clients stills renders any added children
|
119
|
-
# render is for specific drawing done by the widget
|
120
1721
|
end
|
121
1722
|
|
122
|
-
|
123
|
-
|
124
|
-
|
1723
|
+
#
|
1724
|
+
# Return the relative z order compared to other widgets.
|
1725
|
+
# The absolute z order is the base plus this value.
|
1726
|
+
# Its calculated relative so that overlay widgets can be
|
1727
|
+
# on top of base displays.
|
1728
|
+
#
|
1729
|
+
def widget_z
|
1730
|
+
0
|
1731
|
+
end
|
1732
|
+
|
1733
|
+
def intercept_widget_event(result)
|
1734
|
+
# Base implementation just relays the event
|
1735
|
+
result
|
1736
|
+
end
|
1737
|
+
end
|
1738
|
+
|
1739
|
+
#
|
1740
|
+
# A panel is simply an alias for a widget, although you can optionally
|
1741
|
+
# treat them differently if you wish. Generally a panel is used to
|
1742
|
+
# apply a specific layout to a sub-section of the screen.
|
1743
|
+
#
|
1744
|
+
class Panel < Widget
|
1745
|
+
def initialize(x, y, w, h, layout = nil, theme = nil)
|
1746
|
+
super(x, y, w, h, layout, theme)
|
1747
|
+
end
|
1748
|
+
end
|
1749
|
+
|
1750
|
+
#
|
1751
|
+
# Displays an image on the screen at the specific x, y location. The image
|
1752
|
+
# can be scaled by setting the scale attribute. The image attribute to the
|
1753
|
+
# construcor can be the string file location or a Gosu::Image instance
|
1754
|
+
#
|
1755
|
+
class ImageWidget < Widget
|
1756
|
+
attr_accessor :img
|
1757
|
+
attr_accessor :scale
|
1758
|
+
|
1759
|
+
def initialize(x, y, image, args = {})
|
1760
|
+
super(x, y)
|
1761
|
+
if image.is_a? String
|
1762
|
+
@img = Gosu::Image.new(image)
|
1763
|
+
elsif image.is_a? Gosu::Image
|
1764
|
+
@img = image
|
1765
|
+
else
|
1766
|
+
raise "ImageWidget requires either a filename or a Gosu::Image object"
|
1767
|
+
end
|
1768
|
+
if args[ARG_THEME]
|
1769
|
+
@gui_theme = args[ARG_THEME]
|
125
1770
|
end
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
1771
|
+
@scale = 1
|
1772
|
+
disable_border
|
1773
|
+
disable_background
|
1774
|
+
set_dimensions(@img.width, @img.height)
|
130
1775
|
end
|
131
1776
|
|
132
|
-
def
|
133
|
-
|
1777
|
+
def render
|
1778
|
+
@img.draw @x, @y, z_order, @scale, @scale
|
1779
|
+
end
|
1780
|
+
|
1781
|
+
def widget_z
|
1782
|
+
Z_ORDER_FOCAL_ELEMENTS
|
134
1783
|
end
|
135
1784
|
end
|
136
1785
|
|
1786
|
+
#
|
1787
|
+
# Displays a text label on the screen at the specific x, y location.
|
1788
|
+
# The font specified by the current theme is used.
|
1789
|
+
# The theme text color is used, unless the color parameter specifies an override.
|
1790
|
+
# The small font is used by default, unless the use_large_font parameter is true.
|
1791
|
+
#
|
137
1792
|
class Text < Widget
|
138
|
-
attr_accessor :
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
@
|
1793
|
+
attr_accessor :label
|
1794
|
+
|
1795
|
+
def initialize(x, y, label, args = {})
|
1796
|
+
super(x, y)
|
1797
|
+
@label = label
|
1798
|
+
if args[ARG_THEME]
|
1799
|
+
@gui_theme = args[ARG_THEME]
|
1800
|
+
end
|
1801
|
+
if args[ARG_USE_LARGE_FONT]
|
1802
|
+
@use_large_font = args[ARG_USE_LARGE_FONT]
|
1803
|
+
end
|
1804
|
+
if args[ARG_COLOR]
|
1805
|
+
@override_color = args[ARG_COLOR]
|
1806
|
+
end
|
1807
|
+
disable_border
|
1808
|
+
if @use_large_font
|
1809
|
+
set_dimensions(@gui_theme.font_large.text_width(@label) + 10, 20)
|
1810
|
+
else
|
1811
|
+
set_dimensions(@gui_theme.font.text_width(@label) + 10, 20)
|
1812
|
+
end
|
1813
|
+
end
|
1814
|
+
|
1815
|
+
def set_text(new_text)
|
1816
|
+
@label = new_text
|
143
1817
|
end
|
1818
|
+
|
1819
|
+
def change_text(new_text)
|
1820
|
+
set_text(new_text)
|
1821
|
+
end
|
1822
|
+
|
144
1823
|
def render
|
145
|
-
|
1824
|
+
if @use_large_font
|
1825
|
+
get_theme.font_large.draw_text(@label, @x, @y, z_order, 1, 1, text_color)
|
1826
|
+
else
|
1827
|
+
get_theme.font.draw_text(@label, @x, @y, z_order, 1, 1, text_color)
|
1828
|
+
end
|
1829
|
+
end
|
1830
|
+
|
1831
|
+
def widget_z
|
1832
|
+
Z_ORDER_TEXT
|
146
1833
|
end
|
147
1834
|
end
|
148
1835
|
|
1836
|
+
#
|
1837
|
+
# An ErrorMessage is a subclass of text that uses a red color
|
1838
|
+
#
|
149
1839
|
class ErrorMessage < Text
|
150
|
-
|
151
|
-
|
152
|
-
super("ERROR: #{str}", x, y, font, COLOR_ERROR_CODE_RED)
|
153
|
-
set_dimensions(@font.text_width(@str) + 4, 36)
|
1840
|
+
def initialize(x, y, message)
|
1841
|
+
super(x, y, "ERROR: #{message}", COLOR_ERROR_CODE_RED)
|
154
1842
|
end
|
155
1843
|
end
|
156
1844
|
|
1845
|
+
#
|
1846
|
+
# A data point to be used in a Plot widget. This object holds
|
1847
|
+
# the x, y screen location as well as the data values for x, y.
|
1848
|
+
#
|
157
1849
|
class PlotPoint < Widget
|
1850
|
+
attr_accessor :data_x
|
1851
|
+
attr_accessor :data_y
|
158
1852
|
attr_accessor :data_point_size
|
159
1853
|
|
160
|
-
def initialize(x, y, color = COLOR_MAROON, size = 4)
|
161
|
-
super(x, y
|
1854
|
+
def initialize(x, y, data_x, data_y, color = COLOR_MAROON, size = 4)
|
1855
|
+
super(x, y)
|
1856
|
+
@override_color = color
|
1857
|
+
@data_x = data_x
|
1858
|
+
@data_y = data_y
|
162
1859
|
@data_point_size = size
|
163
1860
|
end
|
164
1861
|
|
165
|
-
def render
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
1862
|
+
def render(override_size = nil)
|
1863
|
+
size_to_draw = @data_point_size
|
1864
|
+
if override_size
|
1865
|
+
size_to_draw = override_size
|
1866
|
+
end
|
1867
|
+
half_size = size_to_draw / 2
|
1868
|
+
Gosu::draw_rect(@x - half_size, @y - half_size,
|
1869
|
+
size_to_draw, size_to_draw,
|
1870
|
+
graphics_color, z_order)
|
170
1871
|
end
|
171
1872
|
|
1873
|
+
def widget_z
|
1874
|
+
Z_ORDER_PLOT_POINTS
|
1875
|
+
end
|
1876
|
+
|
172
1877
|
def to_display
|
173
1878
|
"#{@x}, #{@y}"
|
174
1879
|
end
|
@@ -184,102 +1889,169 @@ module Wads
|
|
184
1889
|
end
|
185
1890
|
end
|
186
1891
|
|
1892
|
+
#
|
1893
|
+
# Displays a button at the specified x, y location.
|
1894
|
+
# The button width is based on the label text unless specified
|
1895
|
+
# using the optional parameter. The code to executeon a button
|
1896
|
+
# click is specified using the set_action method, however typical
|
1897
|
+
# using involves the widget or layout form of add_button. For example:
|
1898
|
+
# add_button("Test Button", 10, 10) do
|
1899
|
+
# puts "User hit the test button"
|
1900
|
+
# end
|
1901
|
+
|
187
1902
|
class Button < Widget
|
188
1903
|
attr_accessor :label
|
189
1904
|
attr_accessor :is_pressed
|
1905
|
+
attr_accessor :action_code
|
190
1906
|
|
191
|
-
def initialize(
|
192
|
-
super(x, y
|
193
|
-
set_font(font)
|
1907
|
+
def initialize(x, y, label, args = {})
|
1908
|
+
super(x, y)
|
194
1909
|
@label = label
|
195
|
-
|
196
|
-
|
197
|
-
|
1910
|
+
if args[ARG_THEME]
|
1911
|
+
@gui_theme = args[ARG_THEME]
|
1912
|
+
end
|
1913
|
+
@text_pixel_width = @gui_theme.font.text_width(@label)
|
1914
|
+
if args[ARG_DESIRED_WIDTH]
|
1915
|
+
@width = args[ARG_DESIRED_WIDTH]
|
198
1916
|
else
|
199
|
-
@width =
|
1917
|
+
@width = @text_pixel_width + 10
|
200
1918
|
end
|
201
1919
|
@height = 26
|
202
1920
|
@is_pressed = false
|
203
|
-
@
|
1921
|
+
@is_pressed_update_count = -100
|
204
1922
|
end
|
205
1923
|
|
206
1924
|
def render
|
207
|
-
draw_border(COLOR_WHITE)
|
208
1925
|
text_x = center_x - (@text_pixel_width / 2)
|
209
|
-
@font.draw_text(@label, text_x, @y,
|
1926
|
+
@gui_theme.font.draw_text(@label, text_x, @y, z_order, 1, 1, text_color)
|
1927
|
+
end
|
1928
|
+
|
1929
|
+
def widget_z
|
1930
|
+
Z_ORDER_TEXT
|
1931
|
+
end
|
1932
|
+
|
1933
|
+
def set_action(&block)
|
1934
|
+
@action_code = block
|
1935
|
+
end
|
1936
|
+
|
1937
|
+
def handle_mouse_down mouse_x, mouse_y
|
1938
|
+
@is_pressed = true
|
1939
|
+
if @action_code
|
1940
|
+
@action_code.call
|
1941
|
+
end
|
1942
|
+
end
|
1943
|
+
|
1944
|
+
def handle_update update_count, mouse_x, mouse_y
|
1945
|
+
if @is_pressed
|
1946
|
+
@is_pressed_update_count = update_count
|
1947
|
+
@is_pressed = false
|
1948
|
+
end
|
1949
|
+
|
1950
|
+
if update_count < @is_pressed_update_count + 15
|
1951
|
+
unset_selected
|
1952
|
+
elsif contains_click(mouse_x, mouse_y)
|
1953
|
+
set_selected
|
1954
|
+
else
|
1955
|
+
unset_selected
|
1956
|
+
end
|
1957
|
+
end
|
1958
|
+
end
|
1959
|
+
|
1960
|
+
#
|
1961
|
+
# A subclass of button that renders a red X instead of label text
|
1962
|
+
#
|
1963
|
+
class DeleteButton < Button
|
1964
|
+
def initialize(x, y, args = {})
|
1965
|
+
super(x, y, "ignore", {ARG_DESIRED_WIDTH => 50}.merge(args))
|
1966
|
+
set_dimensions(14, 14)
|
1967
|
+
add_child(Line.new(@x, @y, right_edge, bottom_edge, COLOR_ERROR_CODE_RED))
|
1968
|
+
add_child(Line.new(@x, bottom_edge, right_edge, @y, COLOR_ERROR_CODE_RED))
|
1969
|
+
end
|
1970
|
+
|
1971
|
+
def render
|
1972
|
+
# do nothing, just override the parent so we don't draw a label
|
210
1973
|
end
|
211
1974
|
end
|
212
1975
|
|
1976
|
+
#
|
1977
|
+
# Displays multiple lines of text content at the specified coordinates
|
1978
|
+
#
|
213
1979
|
class Document < Widget
|
214
1980
|
attr_accessor :lines
|
215
1981
|
|
216
|
-
def initialize(
|
217
|
-
super(x, y
|
218
|
-
set_font(font)
|
1982
|
+
def initialize(x, y, width, height, content, args = {})
|
1983
|
+
super(x, y)
|
219
1984
|
set_dimensions(width, height)
|
220
1985
|
@lines = content.split("\n")
|
1986
|
+
disable_border
|
1987
|
+
if args[ARG_THEME]
|
1988
|
+
@gui_theme = args[ARG_THEME]
|
1989
|
+
end
|
221
1990
|
end
|
222
1991
|
|
223
1992
|
def render
|
224
1993
|
y = @y + 4
|
225
1994
|
@lines.each do |line|
|
226
|
-
@font.draw_text(line, @x + 5, y,
|
1995
|
+
@gui_theme.font.draw_text(line, @x + 5, y, z_order, 1, 1, text_color)
|
227
1996
|
y = y + 26
|
228
1997
|
end
|
229
1998
|
end
|
1999
|
+
|
2000
|
+
def widget_z
|
2001
|
+
Z_ORDER_TEXT
|
2002
|
+
end
|
230
2003
|
end
|
231
2004
|
|
232
2005
|
class InfoBox < Widget
|
233
|
-
def initialize(
|
2006
|
+
def initialize(x, y, width, height, title, content, args = {})
|
234
2007
|
super(x, y)
|
235
|
-
set_font(font)
|
236
2008
|
set_dimensions(width, height)
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
2009
|
+
@base_z = 10
|
2010
|
+
if args[ARG_THEME]
|
2011
|
+
@gui_theme = args[ARG_THEME]
|
2012
|
+
end
|
2013
|
+
add_text(title, 5, 5)
|
2014
|
+
add_document(content, 5, 52, width, height - 52)
|
2015
|
+
ok_button = add_button("OK", (@width / 2) - 50, height - 26) do
|
2016
|
+
WidgetResult.new(true)
|
2017
|
+
end
|
2018
|
+
ok_button.width = 100
|
244
2019
|
end
|
245
2020
|
|
246
|
-
def
|
2021
|
+
def handle_key_press id, mouse_x, mouse_y
|
247
2022
|
if id == Gosu::KbEscape
|
248
2023
|
return WidgetResult.new(true)
|
249
|
-
elsif id == Gosu::MsLeft
|
250
|
-
if @ok_button.contains_click(mouse_x, mouse_y)
|
251
|
-
return WidgetResult.new(true)
|
252
|
-
end
|
253
2024
|
end
|
254
|
-
|
255
|
-
end
|
2025
|
+
end
|
256
2026
|
end
|
257
2027
|
|
258
2028
|
class Dialog < Widget
|
259
2029
|
attr_accessor :textinput
|
260
2030
|
|
261
|
-
def initialize(
|
262
|
-
super(x, y)
|
263
|
-
@
|
264
|
-
set_font(font)
|
265
|
-
set_dimensions(width, height)
|
266
|
-
set_background(0xff566573 )
|
267
|
-
set_border(COLOR_WHITE)
|
2031
|
+
def initialize(x, y, width, height, title, text_input_default)
|
2032
|
+
super(x, y, width, height)
|
2033
|
+
@base_z = 10
|
268
2034
|
@error_message = nil
|
269
2035
|
|
270
|
-
|
2036
|
+
add_text(title, 5, 5)
|
271
2037
|
# Forms automatically have some explanatory content
|
272
|
-
|
2038
|
+
add_document(content, 0, 56, width, height)
|
273
2039
|
|
274
2040
|
# Forms automatically get a text input widget
|
275
|
-
@textinput = TextField.new(
|
2041
|
+
@textinput = TextField.new(x + 10, bottom_edge - 80, text_input_default, 600)
|
2042
|
+
@textinput.base_z = 10
|
276
2043
|
add_child(@textinput)
|
277
2044
|
|
278
2045
|
# Forms automatically get OK and Cancel buttons
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
2046
|
+
ok_button = add_button("OK", (@width / 2) - 100, height - 32) do
|
2047
|
+
handle_ok
|
2048
|
+
end
|
2049
|
+
ok_button.width = 100
|
2050
|
+
|
2051
|
+
cancel_button = add_button("Cancel", (@width / 2) + 50, height - 32) do
|
2052
|
+
WidgetResult.new(true)
|
2053
|
+
end
|
2054
|
+
cancel_button.width = 100
|
283
2055
|
end
|
284
2056
|
|
285
2057
|
def content
|
@@ -290,7 +2062,8 @@ module Wads
|
|
290
2062
|
end
|
291
2063
|
|
292
2064
|
def add_error_message(msg)
|
293
|
-
@error_message = ErrorMessage.new(
|
2065
|
+
@error_message = ErrorMessage.new(x + 10, bottom_edge - 120, msg)
|
2066
|
+
@error_message.base_z = @base_z
|
294
2067
|
end
|
295
2068
|
|
296
2069
|
def render
|
@@ -302,21 +2075,7 @@ module Wads
|
|
302
2075
|
def handle_ok
|
303
2076
|
# Default behavior is to do nothing except tell the caller to
|
304
2077
|
# close the dialog
|
305
|
-
return WidgetResult.new(true)
|
306
|
-
end
|
307
|
-
|
308
|
-
def handle_cancel
|
309
|
-
# Default behavior is to do nothing except tell the caller to
|
310
|
-
# close the dialog
|
311
|
-
return WidgetResult.new(true)
|
312
|
-
end
|
313
|
-
|
314
|
-
def handle_up(mouse_x, mouse_y)
|
315
|
-
# empty implementation of up arrow
|
316
|
-
end
|
317
|
-
|
318
|
-
def handle_down(mouse_x, mouse_y)
|
319
|
-
# empty implementation of down arrow
|
2078
|
+
return WidgetResult.new(true, EVENT_OK)
|
320
2079
|
end
|
321
2080
|
|
322
2081
|
def handle_mouse_click(mouse_x, mouse_y)
|
@@ -324,40 +2083,30 @@ module Wads
|
|
324
2083
|
# of standard form elements in this dialog
|
325
2084
|
end
|
326
2085
|
|
327
|
-
def
|
328
|
-
#
|
329
|
-
|
2086
|
+
def handle_mouse_down mouse_x, mouse_y
|
2087
|
+
# Mouse click: Select text field based on mouse position.
|
2088
|
+
WadsConfig.instance.get_window.text_input = [@textinput].find { |tf| tf.under_point?(mouse_x, mouse_y) }
|
2089
|
+
# Advanced: Move caret to clicked position
|
2090
|
+
WadsConfig.instance.get_window.text_input.move_caret(mouse_x) unless WadsConfig.instance.get_window.text_input.nil?
|
2091
|
+
|
2092
|
+
handle_mouse_click(mouse_x, mouse_y)
|
330
2093
|
end
|
331
2094
|
|
332
|
-
def
|
2095
|
+
def handle_key_press id, mouse_x, mouse_y
|
333
2096
|
if id == Gosu::KbEscape
|
334
2097
|
return WidgetResult.new(true)
|
335
|
-
elsif id == Gosu::KbUp
|
336
|
-
handle_up(mouse_x, mouse_y)
|
337
|
-
elsif id == Gosu::KbDown
|
338
|
-
handle_down(mouse_x, mouse_y)
|
339
|
-
elsif id == Gosu::MsLeft
|
340
|
-
if @ok_button.contains_click(mouse_x, mouse_y)
|
341
|
-
return handle_ok
|
342
|
-
elsif @cancel_button.contains_click(mouse_x, mouse_y)
|
343
|
-
return handle_cancel
|
344
|
-
else
|
345
|
-
# Mouse click: Select text field based on mouse position.
|
346
|
-
@window.text_input = [@textinput].find { |tf| tf.under_point?(mouse_x, mouse_y) }
|
347
|
-
# Advanced: Move caret to clicked position
|
348
|
-
@window.text_input.move_caret(mouse_x) unless @window.text_input.nil?
|
349
|
-
|
350
|
-
handle_mouse_click(mouse_x, mouse_y)
|
351
|
-
end
|
352
|
-
else
|
353
|
-
if @window.text_input
|
354
|
-
text_input_updated(@textinput.text)
|
355
|
-
end
|
356
2098
|
end
|
357
|
-
WidgetResult.new(false)
|
358
2099
|
end
|
359
2100
|
end
|
360
2101
|
|
2102
|
+
#
|
2103
|
+
# A result object returned from handle methods that instructs the parent widget
|
2104
|
+
# what to do. A close_widget value of true instructs the recipient to close
|
2105
|
+
# either the overlay window or the entire app, based on the context of the receiver.
|
2106
|
+
# In the case of a form being submitted, the action may be "OK" and the form_data
|
2107
|
+
# contains the information supplied by the user.
|
2108
|
+
# WidgetResult is intentionally generic so it can support a wide variety of use cases.
|
2109
|
+
#
|
361
2110
|
class WidgetResult
|
362
2111
|
attr_accessor :close_widget
|
363
2112
|
attr_accessor :action
|
@@ -370,83 +2119,145 @@ module Wads
|
|
370
2119
|
end
|
371
2120
|
end
|
372
2121
|
|
2122
|
+
#
|
2123
|
+
# Renders a line from x, y to x2, y2. The theme graphics elements color
|
2124
|
+
# is used by default, unless specified using the optional parameter.
|
2125
|
+
#
|
373
2126
|
class Line < Widget
|
374
2127
|
attr_accessor :x2
|
375
2128
|
attr_accessor :y2
|
376
2129
|
|
377
|
-
def initialize(x, y, x2, y2, color =
|
378
|
-
super
|
2130
|
+
def initialize(x, y, x2, y2, color = nil)
|
2131
|
+
super(x, y)
|
2132
|
+
@override_color = color
|
379
2133
|
@x2 = x2
|
380
2134
|
@y2 = y2
|
2135
|
+
disable_border
|
2136
|
+
disable_background
|
381
2137
|
end
|
382
2138
|
|
383
2139
|
def render
|
384
|
-
Gosu::draw_line x, y,
|
2140
|
+
Gosu::draw_line x, y, graphics_color, x2, y2, graphics_color, z_order
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
def widget_z
|
2144
|
+
Z_ORDER_GRAPHIC_ELEMENTS
|
2145
|
+
end
|
2146
|
+
|
2147
|
+
def uses_layout
|
2148
|
+
false
|
385
2149
|
end
|
386
2150
|
end
|
387
2151
|
|
2152
|
+
#
|
2153
|
+
# A very specific widget used along with a Plot to draw the x and y axis lines.
|
2154
|
+
# Note that the labels are drawn using separate widgets.
|
2155
|
+
#
|
388
2156
|
class AxisLines < Widget
|
389
|
-
def initialize(x, y, width, height, color =
|
390
|
-
super
|
391
|
-
|
392
|
-
|
2157
|
+
def initialize(x, y, width, height, color = nil)
|
2158
|
+
super(x, y)
|
2159
|
+
set_dimensions(width, height)
|
2160
|
+
disable_border
|
2161
|
+
disable_background
|
393
2162
|
end
|
394
2163
|
|
395
2164
|
def render
|
396
|
-
add_child(Line.new(@x, @y, @x, bottom_edge,
|
397
|
-
add_child(Line.new(@x, bottom_edge, right_edge, bottom_edge,
|
2165
|
+
add_child(Line.new(@x, @y, @x, bottom_edge, graphics_color))
|
2166
|
+
add_child(Line.new(@x, bottom_edge, right_edge, bottom_edge, graphics_color))
|
2167
|
+
end
|
2168
|
+
|
2169
|
+
def uses_layout
|
2170
|
+
false
|
398
2171
|
end
|
399
2172
|
end
|
400
2173
|
|
2174
|
+
#
|
2175
|
+
# Labels and tic marks for the vertical axis on a plot
|
2176
|
+
#
|
401
2177
|
class VerticalAxisLabel < Widget
|
402
2178
|
attr_accessor :label
|
403
2179
|
|
404
|
-
def initialize(x, y, label,
|
405
|
-
super
|
406
|
-
set_font(font)
|
2180
|
+
def initialize(x, y, label, color = nil)
|
2181
|
+
super(x, y)
|
407
2182
|
@label = label
|
2183
|
+
@override_color = color
|
2184
|
+
text_pixel_width = @gui_theme.font.text_width(@label)
|
2185
|
+
add_text(@label, -text_pixel_width - 28, -12)
|
2186
|
+
disable_border
|
2187
|
+
disable_background
|
408
2188
|
end
|
409
2189
|
|
410
2190
|
def render
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
2191
|
+
Gosu::draw_line @x - 20, @y, graphics_color,
|
2192
|
+
@x, @y, graphics_color, z_order
|
2193
|
+
end
|
2194
|
+
|
2195
|
+
def widget_z
|
2196
|
+
Z_ORDER_GRAPHIC_ELEMENTS
|
2197
|
+
end
|
2198
|
+
|
2199
|
+
def uses_layout
|
2200
|
+
false
|
416
2201
|
end
|
417
2202
|
end
|
418
2203
|
|
2204
|
+
#
|
2205
|
+
# Labels and tic marks for the horizontal axis on a plot
|
2206
|
+
#
|
419
2207
|
class HorizontalAxisLabel < Widget
|
420
2208
|
attr_accessor :label
|
421
2209
|
|
422
|
-
def initialize(x, y, label,
|
423
|
-
super
|
424
|
-
set_font(font)
|
2210
|
+
def initialize(x, y, label, color = nil)
|
2211
|
+
super(x, y)
|
425
2212
|
@label = label
|
2213
|
+
@override_color = color
|
2214
|
+
text_pixel_width = @gui_theme.font.text_width(@label)
|
2215
|
+
add_text(@label, -(text_pixel_width / 2), 26)
|
2216
|
+
disable_border
|
2217
|
+
disable_background
|
426
2218
|
end
|
427
2219
|
|
428
2220
|
def render
|
429
|
-
|
430
|
-
|
431
|
-
|
2221
|
+
Gosu::draw_line @x, @y, graphics_color, @x, @y + 20, graphics_color, z_order
|
2222
|
+
end
|
2223
|
+
|
2224
|
+
def widget_z
|
2225
|
+
Z_ORDER_TEXT
|
2226
|
+
end
|
2227
|
+
|
2228
|
+
def uses_layout
|
2229
|
+
false
|
432
2230
|
end
|
433
2231
|
end
|
434
2232
|
|
2233
|
+
#
|
2234
|
+
# Displays a table of information at the given coordinates.
|
2235
|
+
# The headers are an array of text labels to display at the top of each column.
|
2236
|
+
# The max_visible_rows specifies how many rows are visible at once.
|
2237
|
+
# If there are more data rows than the max, the arrow keys can be used to
|
2238
|
+
# page up or down through the rows in the table.
|
2239
|
+
#
|
435
2240
|
class Table < Widget
|
436
2241
|
attr_accessor :data_rows
|
437
2242
|
attr_accessor :row_colors
|
438
2243
|
attr_accessor :headers
|
439
2244
|
attr_accessor :max_visible_rows
|
440
2245
|
attr_accessor :current_row
|
2246
|
+
attr_accessor :can_delete_rows
|
441
2247
|
|
442
|
-
def initialize(x, y, width, height, headers,
|
443
|
-
super(x, y
|
444
|
-
set_font(font)
|
2248
|
+
def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
|
2249
|
+
super(x, y)
|
445
2250
|
set_dimensions(width, height)
|
2251
|
+
if args[ARG_THEME]
|
2252
|
+
@gui_theme = args[ARG_THEME]
|
2253
|
+
end
|
446
2254
|
@headers = headers
|
447
2255
|
@current_row = 0
|
448
2256
|
@max_visible_rows = max_visible_rows
|
449
|
-
clear_rows
|
2257
|
+
clear_rows
|
2258
|
+
@can_delete_rows = false
|
2259
|
+
@delete_buttons = []
|
2260
|
+
@next_delete_button_y = 38
|
450
2261
|
end
|
451
2262
|
|
452
2263
|
def scroll_up
|
@@ -456,7 +2267,7 @@ module Wads
|
|
456
2267
|
end
|
457
2268
|
|
458
2269
|
def scroll_down
|
459
|
-
if @current_row < @data_rows.size
|
2270
|
+
if @current_row + @max_visible_rows < @data_rows.size
|
460
2271
|
@current_row = @current_row + @max_visible_rows
|
461
2272
|
end
|
462
2273
|
end
|
@@ -466,11 +2277,50 @@ module Wads
|
|
466
2277
|
@row_colors = []
|
467
2278
|
end
|
468
2279
|
|
469
|
-
def add_row(data_row, color =
|
2280
|
+
def add_row(data_row, color = text_color )
|
470
2281
|
@data_rows << data_row
|
471
2282
|
@row_colors << color
|
472
2283
|
end
|
473
2284
|
|
2285
|
+
def add_table_delete_button
|
2286
|
+
if @delete_buttons.size < @max_visible_rows
|
2287
|
+
new_button = add_delete_button(@width - 18, @next_delete_button_y) do
|
2288
|
+
# nothing to do here, handled in parent widget by event
|
2289
|
+
end
|
2290
|
+
@delete_buttons << new_button
|
2291
|
+
@next_delete_button_y = @next_delete_button_y + 30
|
2292
|
+
end
|
2293
|
+
end
|
2294
|
+
|
2295
|
+
def remove_table_delete_button
|
2296
|
+
if not @delete_buttons.empty?
|
2297
|
+
@delete_buttons.pop
|
2298
|
+
@children.pop
|
2299
|
+
@next_delete_button_y = @next_delete_button_y - 30
|
2300
|
+
end
|
2301
|
+
end
|
2302
|
+
|
2303
|
+
def handle_update update_count, mouse_x, mouse_y
|
2304
|
+
# How many visible data rows are there
|
2305
|
+
if @can_delete_rows
|
2306
|
+
number_of_visible_rows = @data_rows.size - @current_row
|
2307
|
+
if number_of_visible_rows > @max_visible_rows
|
2308
|
+
number_of_visible_rows = @max_visible_rows
|
2309
|
+
end
|
2310
|
+
if number_of_visible_rows > @delete_buttons.size
|
2311
|
+
number_to_add = number_of_visible_rows - @delete_buttons.size
|
2312
|
+
number_to_add.times do
|
2313
|
+
add_table_delete_button
|
2314
|
+
end
|
2315
|
+
elsif number_of_visible_rows < @delete_buttons.size
|
2316
|
+
number_to_remove = @delete_buttons.size - number_of_visible_rows
|
2317
|
+
number_to_remove.times do
|
2318
|
+
remove_table_delete_button
|
2319
|
+
end
|
2320
|
+
end
|
2321
|
+
end
|
2322
|
+
end
|
2323
|
+
|
474
2324
|
def number_of_rows
|
475
2325
|
@data_rows.size
|
476
2326
|
end
|
@@ -482,9 +2332,9 @@ module Wads
|
|
482
2332
|
column_widths = []
|
483
2333
|
number_of_columns = @data_rows[0].size
|
484
2334
|
(0..number_of_columns-1).each do |c|
|
485
|
-
max_length = @font.text_width(headers[c])
|
2335
|
+
max_length = @gui_theme.font.text_width(headers[c])
|
486
2336
|
(0..number_of_rows-1).each do |r|
|
487
|
-
text_pixel_width = @font.text_width(@data_rows[r][c])
|
2337
|
+
text_pixel_width = @gui_theme.font.text_width(@data_rows[r][c])
|
488
2338
|
if text_pixel_width > max_length
|
489
2339
|
max_length = text_pixel_width
|
490
2340
|
end
|
@@ -492,18 +2342,22 @@ module Wads
|
|
492
2342
|
column_widths[c] = max_length
|
493
2343
|
end
|
494
2344
|
|
2345
|
+
# Draw a horizontal line between header and data rows
|
495
2346
|
x = @x + 10
|
496
2347
|
if number_of_columns > 1
|
497
2348
|
(0..number_of_columns-2).each do |c|
|
498
2349
|
x = x + column_widths[c] + 20
|
499
|
-
Gosu::draw_line x, @y,
|
2350
|
+
Gosu::draw_line x, @y, graphics_color, x, @y + @height, graphics_color, z_order
|
500
2351
|
end
|
501
2352
|
end
|
502
2353
|
|
503
|
-
|
2354
|
+
# Draw the header row
|
2355
|
+
y = @y
|
2356
|
+
Gosu::draw_rect(@x + 1, y, @width - 3, 28, graphics_color, relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
|
2357
|
+
|
504
2358
|
x = @x + 20
|
505
2359
|
(0..number_of_columns-1).each do |c|
|
506
|
-
@font.draw_text(@headers[c], x, y,
|
2360
|
+
@gui_theme.font.draw_text(@headers[c], x, y + 3, z_order, 1, 1, text_color)
|
507
2361
|
x = x + column_widths[c] + 20
|
508
2362
|
end
|
509
2363
|
y = y + 30
|
@@ -515,7 +2369,7 @@ module Wads
|
|
515
2369
|
elsif count < @current_row + @max_visible_rows
|
516
2370
|
x = @x + 20
|
517
2371
|
(0..number_of_columns-1).each do |c|
|
518
|
-
@font.draw_text(row[c], x, y + 2,
|
2372
|
+
@gui_theme.font.draw_text(row[c], x, y + 2, z_order, 1, 1, @row_colors[count])
|
519
2373
|
x = x + column_widths[c] + 20
|
520
2374
|
end
|
521
2375
|
y = y + 30
|
@@ -532,48 +2386,126 @@ module Wads
|
|
532
2386
|
end
|
533
2387
|
row_number
|
534
2388
|
end
|
2389
|
+
|
2390
|
+
def widget_z
|
2391
|
+
Z_ORDER_TEXT
|
2392
|
+
end
|
2393
|
+
|
2394
|
+
def uses_layout
|
2395
|
+
false
|
2396
|
+
end
|
535
2397
|
end
|
536
2398
|
|
2399
|
+
#
|
2400
|
+
# A table where the user can select one row at a time.
|
2401
|
+
# The selected row has a background color specified by the selection color of the
|
2402
|
+
# current theme.
|
2403
|
+
#
|
537
2404
|
class SingleSelectTable < Table
|
538
2405
|
attr_accessor :selected_row
|
539
|
-
attr_accessor :selected_color
|
540
2406
|
|
541
|
-
def initialize(x, y, width, height, headers,
|
542
|
-
super(x, y, width, height, headers,
|
543
|
-
|
2407
|
+
def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
|
2408
|
+
super(x, y, width, height, headers, max_visible_rows, args)
|
2409
|
+
end
|
2410
|
+
|
2411
|
+
def is_row_selected(mouse_y)
|
2412
|
+
row_number = determine_row_number(mouse_y)
|
2413
|
+
if row_number.nil?
|
2414
|
+
return false
|
2415
|
+
end
|
2416
|
+
selected_row = @current_row + row_number
|
2417
|
+
@selected_row == selected_row
|
544
2418
|
end
|
545
2419
|
|
546
2420
|
def set_selected_row(mouse_y, column_number)
|
547
2421
|
row_number = determine_row_number(mouse_y)
|
548
2422
|
if not row_number.nil?
|
549
|
-
|
2423
|
+
new_selected_row = @current_row + row_number
|
2424
|
+
if @selected_row
|
2425
|
+
if @selected_row == new_selected_row
|
2426
|
+
return nil # You can't select the same row already selected
|
2427
|
+
end
|
2428
|
+
end
|
2429
|
+
@selected_row = new_selected_row
|
550
2430
|
@data_rows[@selected_row][column_number]
|
551
2431
|
end
|
552
2432
|
end
|
553
2433
|
|
2434
|
+
def unset_selected_row(mouse_y, column_number)
|
2435
|
+
row_number = determine_row_number(mouse_y)
|
2436
|
+
if not row_number.nil?
|
2437
|
+
this_selected_row = @current_row + row_number
|
2438
|
+
@selected_row = this_selected_row
|
2439
|
+
return @data_rows[this_selected_row][column_number]
|
2440
|
+
end
|
2441
|
+
nil
|
2442
|
+
end
|
2443
|
+
|
554
2444
|
def render
|
555
2445
|
super
|
556
2446
|
if @selected_row
|
557
2447
|
if @selected_row >= @current_row and @selected_row < @current_row + @max_visible_rows
|
558
2448
|
y = @y + 30 + ((@selected_row - @current_row) * 30)
|
559
|
-
Gosu::draw_rect(@x + 20, y, @width - 30, 28, @
|
2449
|
+
Gosu::draw_rect(@x + 20, y, @width - 30, 28, @gui_theme.selection_color, relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
|
2450
|
+
end
|
2451
|
+
end
|
2452
|
+
end
|
2453
|
+
|
2454
|
+
def widget_z
|
2455
|
+
Z_ORDER_TEXT
|
2456
|
+
end
|
2457
|
+
|
2458
|
+
def handle_mouse_down mouse_x, mouse_y
|
2459
|
+
if contains_click(mouse_x, mouse_y)
|
2460
|
+
row_number = determine_row_number(mouse_y)
|
2461
|
+
if row_number.nil?
|
2462
|
+
return WidgetResult.new(false)
|
2463
|
+
end
|
2464
|
+
# First check if its the delete button that got this
|
2465
|
+
delete_this_row = false
|
2466
|
+
@delete_buttons.each do |db|
|
2467
|
+
if db.contains_click(mouse_x, mouse_y)
|
2468
|
+
delete_this_row = true
|
2469
|
+
end
|
560
2470
|
end
|
2471
|
+
if delete_this_row
|
2472
|
+
if not row_number.nil?
|
2473
|
+
data_set_row_to_delete = @current_row + row_number
|
2474
|
+
data_set_name_to_delete = @data_rows[data_set_row_to_delete][1]
|
2475
|
+
@data_rows.delete_at(data_set_row_to_delete)
|
2476
|
+
return WidgetResult.new(false, EVENT_TABLE_ROW_DELETE, [data_set_name_to_delete])
|
2477
|
+
end
|
2478
|
+
else
|
2479
|
+
if is_row_selected(mouse_y)
|
2480
|
+
unset_selected_row(mouse_y, 0)
|
2481
|
+
return WidgetResult.new(false, EVENT_TABLE_UNSELECT, @data_rows[row_number])
|
2482
|
+
else
|
2483
|
+
set_selected_row(mouse_y, 0)
|
2484
|
+
return WidgetResult.new(false, EVENT_TABLE_SELECT, @data_rows[row_number])
|
2485
|
+
end
|
2486
|
+
end
|
561
2487
|
end
|
562
2488
|
end
|
563
2489
|
end
|
564
2490
|
|
2491
|
+
#
|
2492
|
+
# A table where the user can select multiple rows at a time.
|
2493
|
+
# Selected rows have a background color specified by the selection color of the
|
2494
|
+
# current theme.
|
2495
|
+
#
|
565
2496
|
class MultiSelectTable < Table
|
566
2497
|
attr_accessor :selected_rows
|
567
|
-
attr_accessor :selection_color
|
568
2498
|
|
569
|
-
def initialize(x, y, width, height, headers,
|
570
|
-
super(x, y, width, height, headers,
|
2499
|
+
def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
|
2500
|
+
super(x, y, width, height, headers, max_visible_rows, args)
|
571
2501
|
@selected_rows = []
|
572
|
-
|
573
|
-
|
574
|
-
|
2502
|
+
end
|
2503
|
+
|
575
2504
|
def is_row_selected(mouse_y)
|
576
2505
|
row_number = determine_row_number(mouse_y)
|
2506
|
+
if row_number.nil?
|
2507
|
+
return false
|
2508
|
+
end
|
577
2509
|
@selected_rows.include?(@current_row + row_number)
|
578
2510
|
end
|
579
2511
|
|
@@ -602,61 +2534,112 @@ module Wads
|
|
602
2534
|
y = @y + 30
|
603
2535
|
row_count = @current_row
|
604
2536
|
while row_count < @data_rows.size
|
605
|
-
if @selected_rows.include? row_count
|
606
|
-
|
2537
|
+
if @selected_rows.include? row_count
|
2538
|
+
width_of_selection_background = @width - 30
|
2539
|
+
if @can_delete_rows
|
2540
|
+
width_of_selection_background = width_of_selection_background - 20
|
2541
|
+
end
|
2542
|
+
Gosu::draw_rect(@x + 20, y, width_of_selection_background, 28,
|
2543
|
+
@gui_theme.selection_color,
|
2544
|
+
relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
|
607
2545
|
end
|
608
2546
|
y = y + 30
|
609
2547
|
row_count = row_count + 1
|
610
2548
|
end
|
611
2549
|
end
|
2550
|
+
|
2551
|
+
def widget_z
|
2552
|
+
Z_ORDER_TEXT
|
2553
|
+
end
|
2554
|
+
|
2555
|
+
def handle_mouse_down mouse_x, mouse_y
|
2556
|
+
if contains_click(mouse_x, mouse_y)
|
2557
|
+
row_number = determine_row_number(mouse_y)
|
2558
|
+
if row_number.nil?
|
2559
|
+
return WidgetResult.new(false)
|
2560
|
+
end
|
2561
|
+
# First check if its the delete button that got this
|
2562
|
+
delete_this_row = false
|
2563
|
+
@delete_buttons.each do |db|
|
2564
|
+
if db.contains_click(mouse_x, mouse_y)
|
2565
|
+
delete_this_row = true
|
2566
|
+
end
|
2567
|
+
end
|
2568
|
+
if delete_this_row
|
2569
|
+
if not row_number.nil?
|
2570
|
+
data_set_row_to_delete = @current_row + row_number
|
2571
|
+
data_set_name_to_delete = @data_rows[data_set_row_to_delete][1]
|
2572
|
+
@data_rows.delete_at(data_set_row_to_delete)
|
2573
|
+
return WidgetResult.new(false, EVENT_TABLE_ROW_DELETE, [data_set_name_to_delete])
|
2574
|
+
end
|
2575
|
+
else
|
2576
|
+
if is_row_selected(mouse_y)
|
2577
|
+
unset_selected_row(mouse_y, 0)
|
2578
|
+
return WidgetResult.new(false, EVENT_TABLE_UNSELECT, @data_rows[row_number])
|
2579
|
+
else
|
2580
|
+
set_selected_row(mouse_y, 0)
|
2581
|
+
return WidgetResult.new(false, EVENT_TABLE_SELECT, @data_rows[row_number])
|
2582
|
+
end
|
2583
|
+
end
|
2584
|
+
end
|
2585
|
+
end
|
612
2586
|
end
|
613
2587
|
|
2588
|
+
#
|
2589
|
+
# A two-dimensional graph display which plots a number of PlotPoint objects.
|
2590
|
+
# Options include grid lines that can be displayed, as well as whether lines
|
2591
|
+
# should be drawn connecting each point in a data set.
|
2592
|
+
#
|
614
2593
|
class Plot < Widget
|
615
2594
|
attr_accessor :points
|
616
2595
|
attr_accessor :visible_range
|
617
2596
|
attr_accessor :display_grid
|
618
2597
|
attr_accessor :display_lines
|
619
2598
|
attr_accessor :zoom_level
|
2599
|
+
attr_accessor :visibility_map
|
620
2600
|
|
621
|
-
def initialize(x, y, width, height
|
622
|
-
super
|
623
|
-
set_font(font)
|
2601
|
+
def initialize(x, y, width, height)
|
2602
|
+
super(x, y)
|
624
2603
|
set_dimensions(width, height)
|
625
2604
|
@display_grid = false
|
626
2605
|
@display_lines = true
|
627
|
-
@
|
628
|
-
@grid_line_color = COLOR_CYAN
|
2606
|
+
@grid_line_color = COLOR_LIGHT_GRAY
|
629
2607
|
@cursor_line_color = COLOR_DARK_GRAY
|
630
|
-
@zero_line_color =
|
2608
|
+
@zero_line_color = COLOR_HEADER_BRIGHT_BLUE
|
631
2609
|
@zoom_level = 1
|
2610
|
+
@data_point_size = 4
|
2611
|
+
# Hash of rendered points keyed by data set name, so we can toggle visibility
|
2612
|
+
@points_by_data_set_name = {}
|
2613
|
+
@visibility_map = {}
|
2614
|
+
disable_border
|
632
2615
|
end
|
633
2616
|
|
634
|
-
def
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
point.increase_size
|
639
|
-
end
|
2617
|
+
def toggle_visibility(data_set_name)
|
2618
|
+
is_visible = @visibility_map[data_set_name]
|
2619
|
+
if is_visible.nil?
|
2620
|
+
return
|
640
2621
|
end
|
2622
|
+
@visibility_map[data_set_name] = !is_visible
|
2623
|
+
end
|
2624
|
+
|
2625
|
+
def increase_data_point_size
|
2626
|
+
@data_point_size = @data_point_size + 2
|
641
2627
|
end
|
642
2628
|
|
643
2629
|
def decrease_data_point_size
|
644
|
-
@
|
645
|
-
|
646
|
-
data_set.rendered_points.each do |point|
|
647
|
-
point.decrease_size
|
648
|
-
end
|
2630
|
+
if @data_point_size > 2
|
2631
|
+
@data_point_size = @data_point_size - 2
|
649
2632
|
end
|
650
2633
|
end
|
651
2634
|
|
652
2635
|
def zoom_out
|
653
|
-
@zoom_level = @zoom_level + 0.
|
2636
|
+
@zoom_level = @zoom_level + 0.15
|
654
2637
|
visible_range.scale(@zoom_level)
|
655
2638
|
end
|
656
2639
|
|
657
2640
|
def zoom_in
|
658
2641
|
if @zoom_level > 0.11
|
659
|
-
@zoom_level = @zoom_level - 0.
|
2642
|
+
@zoom_level = @zoom_level - 0.15
|
660
2643
|
end
|
661
2644
|
visible_range.scale(@zoom_level)
|
662
2645
|
end
|
@@ -680,11 +2663,6 @@ module Wads
|
|
680
2663
|
def define_range(range)
|
681
2664
|
@visible_range = range
|
682
2665
|
@zoom_level = 1
|
683
|
-
@data_set_hash.keys.each do |key|
|
684
|
-
data_set = @data_set_hash[key]
|
685
|
-
puts "Calling derive values on #{key}"
|
686
|
-
data_set.derive_values(range, @data_set_hash)
|
687
|
-
end
|
688
2666
|
end
|
689
2667
|
|
690
2668
|
def range_set?
|
@@ -692,26 +2670,43 @@ module Wads
|
|
692
2670
|
end
|
693
2671
|
|
694
2672
|
def is_on_screen(point)
|
695
|
-
point.
|
2673
|
+
point.data_x >= @visible_range.left_x and point.data_x <= @visible_range.right_x and point.data_y >= @visible_range.bottom_y and point.data_y <= @visible_range.top_y
|
696
2674
|
end
|
697
2675
|
|
698
|
-
def
|
2676
|
+
def add_data_point(data_set_name, data_x, data_y, color = COLOR_MAROON)
|
699
2677
|
if range_set?
|
700
|
-
@
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
2678
|
+
rendered_points = @points_by_data_set_name[data_set_name]
|
2679
|
+
if rendered_points.nil?
|
2680
|
+
rendered_points = []
|
2681
|
+
@points_by_data_set_name[data_set_name] = rendered_points
|
2682
|
+
end
|
2683
|
+
rendered_points << PlotPoint.new(draw_x(data_x), draw_y(data_y),
|
2684
|
+
data_x, data_y,
|
2685
|
+
color)
|
2686
|
+
if @visibility_map[data_set_name].nil?
|
2687
|
+
@visibility_map[data_set_name] = true
|
2688
|
+
end
|
2689
|
+
else
|
2690
|
+
error("ERROR: range not set, cannot add data")
|
2691
|
+
end
|
2692
|
+
end
|
2693
|
+
|
2694
|
+
def add_data_set(data_set_name, rendered_points)
|
2695
|
+
if range_set?
|
2696
|
+
@points_by_data_set_name[data_set_name] = rendered_points
|
2697
|
+
if @visibility_map[data_set_name].nil?
|
2698
|
+
@visibility_map[data_set_name] = true
|
709
2699
|
end
|
710
2700
|
else
|
711
|
-
|
2701
|
+
error("ERROR: range not set, cannot add data")
|
712
2702
|
end
|
713
2703
|
end
|
714
2704
|
|
2705
|
+
def remove_data_set(data_set_name)
|
2706
|
+
@points_by_data_set_name.delete(data_set_name)
|
2707
|
+
@visibility_map.delete(data_set_name)
|
2708
|
+
end
|
2709
|
+
|
715
2710
|
def x_val_to_pixel(val)
|
716
2711
|
x_pct = (@visible_range.right_x - val).to_f / @visible_range.x_range
|
717
2712
|
@width - (@width.to_f * x_pct).round
|
@@ -722,14 +2717,6 @@ module Wads
|
|
722
2717
|
(@height.to_f * y_pct).round
|
723
2718
|
end
|
724
2719
|
|
725
|
-
def x_pixel_to_screen(x)
|
726
|
-
@x + x
|
727
|
-
end
|
728
|
-
|
729
|
-
def y_pixel_to_screen(y)
|
730
|
-
@y + y
|
731
|
-
end
|
732
|
-
|
733
2720
|
def draw_x(x)
|
734
2721
|
x_pixel_to_screen(x_val_to_pixel(x))
|
735
2722
|
end
|
@@ -739,63 +2726,63 @@ module Wads
|
|
739
2726
|
end
|
740
2727
|
|
741
2728
|
def render
|
742
|
-
@
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
point
|
2729
|
+
@points_by_data_set_name.keys.each do |key|
|
2730
|
+
if @visibility_map[key]
|
2731
|
+
data_set_points = @points_by_data_set_name[key]
|
2732
|
+
data_set_points.each do |point|
|
2733
|
+
if is_on_screen(point)
|
2734
|
+
point.render(@data_point_size)
|
2735
|
+
end
|
747
2736
|
end
|
748
2737
|
if @display_lines
|
749
|
-
display_lines_for_point_set(
|
2738
|
+
display_lines_for_point_set(data_set_points)
|
750
2739
|
end
|
751
2740
|
end
|
752
|
-
|
753
|
-
|
754
|
-
|
2741
|
+
if @display_grid and range_set?
|
2742
|
+
display_grid_lines
|
2743
|
+
end
|
755
2744
|
end
|
756
2745
|
end
|
757
2746
|
|
758
2747
|
def display_lines_for_point_set(points)
|
759
2748
|
if points.length > 1
|
760
2749
|
points.inject(points[0]) do |last, the_next|
|
761
|
-
|
762
|
-
|
2750
|
+
if last.x < the_next.x
|
2751
|
+
Gosu::draw_line last.x, last.y, last.graphics_color,
|
2752
|
+
the_next.x, the_next.y, last.graphics_color, relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
|
2753
|
+
end
|
763
2754
|
the_next
|
764
2755
|
end
|
765
2756
|
end
|
766
2757
|
end
|
767
2758
|
|
768
2759
|
def display_grid_lines
|
769
|
-
# TODO this is bnot working well for large ranges with the given increment of 1
|
770
|
-
# We don't want to draw hundreds of grid lines
|
771
2760
|
grid_widgets = []
|
772
2761
|
|
773
|
-
|
774
|
-
|
775
|
-
|
2762
|
+
x_lines = @visible_range.grid_line_x_values
|
2763
|
+
y_lines = @visible_range.grid_line_y_values
|
2764
|
+
first_x = draw_x(@visible_range.left_x)
|
2765
|
+
last_x = draw_x(@visible_range.right_x)
|
2766
|
+
first_y = draw_y(@visible_range.bottom_y)
|
2767
|
+
last_y = draw_y(@visible_range.top_y)
|
2768
|
+
|
2769
|
+
x_lines.each do |grid_x|
|
776
2770
|
dx = draw_x(grid_x)
|
777
|
-
dy = draw_y(grid_y)
|
778
|
-
last_x = draw_x(@visible_range.right_x)
|
779
2771
|
color = @grid_line_color
|
780
|
-
if
|
781
|
-
color = @zero_line_color
|
782
|
-
end
|
783
|
-
grid_widgets << Line.new(dx,
|
784
|
-
grid_y = grid_y + 1
|
2772
|
+
if grid_x == 0 and grid_x != @visible_range.left_x.to_i
|
2773
|
+
color = @zero_line_color
|
2774
|
+
end
|
2775
|
+
grid_widgets << Line.new(dx, first_y, dx, last_y, color)
|
785
2776
|
end
|
786
|
-
|
787
|
-
grid_y
|
788
|
-
while grid_x < @visible_range.right_x
|
789
|
-
dx = draw_x(grid_x)
|
2777
|
+
|
2778
|
+
y_lines.each do |grid_y|
|
790
2779
|
dy = draw_y(grid_y)
|
791
|
-
last_y = draw_y(@visible_range.top_y)
|
792
2780
|
color = @grid_line_color
|
793
|
-
if
|
794
|
-
color = @zero_line_color
|
2781
|
+
if grid_y == 0 and grid_y != @visible_range.bottom_y.to_i
|
2782
|
+
color = @zero_line_color
|
795
2783
|
end
|
796
|
-
grid_widgets << Line.new(
|
797
|
-
|
798
|
-
end
|
2784
|
+
grid_widgets << Line.new(first_x, dy, last_x, dy, color)
|
2785
|
+
end
|
799
2786
|
|
800
2787
|
grid_widgets.each do |gw|
|
801
2788
|
gw.draw
|
@@ -824,4 +2811,573 @@ module Wads
|
|
824
2811
|
[get_x_data_val(mouse_x), get_y_data_val(mouse_y)]
|
825
2812
|
end
|
826
2813
|
end
|
2814
|
+
|
2815
|
+
#
|
2816
|
+
# A graphical representation of a node in a graph using a button-style, i.e
|
2817
|
+
# a rectangular border with a text label.
|
2818
|
+
# The choice to use this display class is dictated by the use_icons attribute
|
2819
|
+
# of the current theme.
|
2820
|
+
# Like images, the size of node widgets can be scaled.
|
2821
|
+
#
|
2822
|
+
class NodeWidget < Button
|
2823
|
+
attr_accessor :data_node
|
2824
|
+
|
2825
|
+
def initialize(x, y, node, color = nil, initial_scale = 1, is_explorer = false)
|
2826
|
+
super(x, y, node.name)
|
2827
|
+
@orig_width = @width
|
2828
|
+
@orig_height = @height
|
2829
|
+
@data_node = node
|
2830
|
+
@override_color = color
|
2831
|
+
set_scale(initial_scale, @is_explorer)
|
2832
|
+
end
|
2833
|
+
|
2834
|
+
def is_background
|
2835
|
+
@scale <= 1 and @is_explorer
|
2836
|
+
end
|
2837
|
+
|
2838
|
+
def set_scale(value, is_explorer = false)
|
2839
|
+
@scale = value
|
2840
|
+
@is_explorer = is_explorer
|
2841
|
+
if value < 1
|
2842
|
+
value = 1
|
2843
|
+
end
|
2844
|
+
@width = @orig_width * @scale.to_f
|
2845
|
+
debug("In regular node widget Setting scale of #{@label} to #{@scale}")
|
2846
|
+
end
|
2847
|
+
|
2848
|
+
def get_text_widget
|
2849
|
+
nil
|
2850
|
+
end
|
2851
|
+
|
2852
|
+
def render
|
2853
|
+
super
|
2854
|
+
draw_background(Z_ORDER_FOCAL_ELEMENTS)
|
2855
|
+
#draw_shadow(COLOR_GRAY)
|
2856
|
+
end
|
2857
|
+
|
2858
|
+
def widget_z
|
2859
|
+
Z_ORDER_TEXT
|
2860
|
+
end
|
2861
|
+
end
|
2862
|
+
|
2863
|
+
#
|
2864
|
+
# A graphical representation of a node in a graph using circular icons
|
2865
|
+
# and adjacent text labels.
|
2866
|
+
# The choice to use this display class is dictated by the use_icons attribute
|
2867
|
+
# of the current theme.
|
2868
|
+
# Like images, the size of node widgets can be scaled.
|
2869
|
+
#
|
2870
|
+
class NodeIconWidget < Widget
|
2871
|
+
attr_accessor :data_node
|
2872
|
+
attr_accessor :image
|
2873
|
+
attr_accessor :scale
|
2874
|
+
attr_accessor :label
|
2875
|
+
attr_accessor :is_explorer
|
2876
|
+
|
2877
|
+
def initialize(x, y, node, color = nil, initial_scale = 1, is_explorer = false)
|
2878
|
+
super(x, y)
|
2879
|
+
@override_color = color
|
2880
|
+
@data_node = node
|
2881
|
+
@label = node.name
|
2882
|
+
circle_image = WadsConfig.instance.circle(color)
|
2883
|
+
if circle_image.nil?
|
2884
|
+
@image = WadsConfig.instance.circle(COLOR_BLUE)
|
2885
|
+
else
|
2886
|
+
@image = circle_image
|
2887
|
+
end
|
2888
|
+
@is_explorer = is_explorer
|
2889
|
+
set_scale(initial_scale, @is_explorer)
|
2890
|
+
disable_border
|
2891
|
+
end
|
2892
|
+
|
2893
|
+
def name
|
2894
|
+
@data_node.name
|
2895
|
+
end
|
2896
|
+
|
2897
|
+
def is_background
|
2898
|
+
@scale <= 0.1 and @is_explorer
|
2899
|
+
end
|
2900
|
+
|
2901
|
+
def set_scale(value, is_explorer = false)
|
2902
|
+
@is_explorer = is_explorer
|
2903
|
+
if value < 0.5
|
2904
|
+
value = 0.5
|
2905
|
+
end
|
2906
|
+
@scale = value / 10.to_f
|
2907
|
+
#debug("In node widget Setting scale of #{@label} to #{value} = #{@scale}")
|
2908
|
+
@width = IMAGE_CIRCLE_SIZE * scale.to_f
|
2909
|
+
@height = IMAGE_CIRCLE_SIZE * scale.to_f
|
2910
|
+
# Only in explorer mode do we dull out nodes on the outer edge
|
2911
|
+
if is_background
|
2912
|
+
@image = WadsConfig.instance.circle(COLOR_ALPHA)
|
2913
|
+
else
|
2914
|
+
text_pixel_width = @gui_theme.font.text_width(@label)
|
2915
|
+
clear_children # the text widget is the only child, so we can remove all
|
2916
|
+
add_text(@label, (@width / 2) - (text_pixel_width / 2), -20)
|
2917
|
+
end
|
2918
|
+
end
|
2919
|
+
|
2920
|
+
def get_text_widget
|
2921
|
+
if @children.size > 0
|
2922
|
+
return @children[0]
|
2923
|
+
end
|
2924
|
+
#raise "No text widget for NodeIconWidget"
|
2925
|
+
nil
|
2926
|
+
end
|
2927
|
+
|
2928
|
+
def render
|
2929
|
+
@image.draw @x, @y, relative_z_order(Z_ORDER_FOCAL_ELEMENTS), @scale, @scale
|
2930
|
+
end
|
2931
|
+
|
2932
|
+
def widget_z
|
2933
|
+
Z_ORDER_TEXT
|
2934
|
+
end
|
2935
|
+
end
|
2936
|
+
|
2937
|
+
#
|
2938
|
+
# Given a single node or a graph data structure, this widget displays
|
2939
|
+
# a visualization of the graph using one of the available node widget classes.
|
2940
|
+
# There are different display modes that control what nodes within the graph
|
2941
|
+
# are shown. The default display mode, GRAPH_DISPLAY_ALL, shows all nodes
|
2942
|
+
# as the name implies. GRAPH_DISPLAY_TREE assumes an acyclic graph and renders
|
2943
|
+
# the graph in a tree-like structure. GRAPH_DISPLAY_EXPLORER has a chosen
|
2944
|
+
# center focus node with connected nodes circled around it based on the depth
|
2945
|
+
# or distance from that node. This mode also allows the user to click on
|
2946
|
+
# different nodes to navigate the graph and change focus nodes.
|
2947
|
+
#
|
2948
|
+
class GraphWidget < Widget
|
2949
|
+
attr_accessor :graph
|
2950
|
+
attr_accessor :selected_node
|
2951
|
+
attr_accessor :selected_node_x_offset
|
2952
|
+
attr_accessor :selected_node_y_offset
|
2953
|
+
attr_accessor :size_by_connections
|
2954
|
+
attr_accessor :is_explorer
|
2955
|
+
|
2956
|
+
def initialize(x, y, width, height, graph, display_mode = GRAPH_DISPLAY_ALL)
|
2957
|
+
super(x, y)
|
2958
|
+
set_dimensions(width, height)
|
2959
|
+
if graph.is_a? Node
|
2960
|
+
@graph = Graph.new(graph)
|
2961
|
+
else
|
2962
|
+
@graph = graph
|
2963
|
+
end
|
2964
|
+
@size_by_connections = false
|
2965
|
+
@is_explorer = false
|
2966
|
+
if [GRAPH_DISPLAY_ALL, GRAPH_DISPLAY_TREE, GRAPH_DISPLAY_EXPLORER].include? display_mode
|
2967
|
+
debug("Displaying graph in #{display_mode} mode")
|
2968
|
+
else
|
2969
|
+
raise "#{display_mode} is not a valid display mode for Graph Widget"
|
2970
|
+
end
|
2971
|
+
if display_mode == GRAPH_DISPLAY_ALL
|
2972
|
+
set_all_nodes_for_display
|
2973
|
+
elsif display_mode == GRAPH_DISPLAY_TREE
|
2974
|
+
set_tree_display
|
2975
|
+
else
|
2976
|
+
set_explorer_display
|
2977
|
+
end
|
2978
|
+
end
|
2979
|
+
|
2980
|
+
def handle_update update_count, mouse_x, mouse_y
|
2981
|
+
if contains_click(mouse_x, mouse_y) and @selected_node
|
2982
|
+
@selected_node.move_recursive_absolute(mouse_x - @selected_node_x_offset,
|
2983
|
+
mouse_y - @selected_node_y_offset)
|
2984
|
+
end
|
2985
|
+
end
|
2986
|
+
|
2987
|
+
def handle_mouse_down mouse_x, mouse_y
|
2988
|
+
# check to see if any node was selected
|
2989
|
+
if @rendered_nodes
|
2990
|
+
@rendered_nodes.values.each do |rn|
|
2991
|
+
if rn.contains_click(mouse_x, mouse_y)
|
2992
|
+
@selected_node = rn
|
2993
|
+
@selected_node_x_offset = mouse_x - rn.x
|
2994
|
+
@selected_node_y_offset = mouse_y - rn.y
|
2995
|
+
@click_timestamp = Time.now
|
2996
|
+
end
|
2997
|
+
end
|
2998
|
+
end
|
2999
|
+
WidgetResult.new(false)
|
3000
|
+
end
|
3001
|
+
|
3002
|
+
def handle_mouse_up mouse_x, mouse_y
|
3003
|
+
if @selected_node
|
3004
|
+
if @is_explorer
|
3005
|
+
time_between_mouse_up_down = Time.now - @click_timestamp
|
3006
|
+
if time_between_mouse_up_down < 0.2
|
3007
|
+
# Treat this as a single click and make the selected
|
3008
|
+
# node the new center node of the graph
|
3009
|
+
set_explorer_display(@selected_node.data_node)
|
3010
|
+
end
|
3011
|
+
end
|
3012
|
+
@selected_node = nil
|
3013
|
+
end
|
3014
|
+
end
|
3015
|
+
|
3016
|
+
def set_explorer_display(center_node = nil)
|
3017
|
+
if center_node.nil?
|
3018
|
+
# If not specified, pick a center node as the one with the most connections
|
3019
|
+
center_node = @graph.node_with_most_connections
|
3020
|
+
end
|
3021
|
+
|
3022
|
+
@graph.reset_visited
|
3023
|
+
@visible_data_nodes = {}
|
3024
|
+
center_node.bfs(4) do |n|
|
3025
|
+
@visible_data_nodes[n.name] = n
|
3026
|
+
end
|
3027
|
+
|
3028
|
+
@size_by_connections = false
|
3029
|
+
@is_explorer = true
|
3030
|
+
|
3031
|
+
@rendered_nodes = {}
|
3032
|
+
populate_rendered_nodes
|
3033
|
+
|
3034
|
+
prevent_text_overlap
|
3035
|
+
end
|
3036
|
+
|
3037
|
+
def set_tree_display
|
3038
|
+
@graph.reset_visited
|
3039
|
+
@visible_data_nodes = @graph.node_map
|
3040
|
+
@rendered_nodes = {}
|
3041
|
+
|
3042
|
+
root_nodes = @graph.root_nodes
|
3043
|
+
number_of_root_nodes = root_nodes.size
|
3044
|
+
width_for_each_root_tree = @width / number_of_root_nodes
|
3045
|
+
|
3046
|
+
start_x = 0
|
3047
|
+
y_level = 20
|
3048
|
+
root_nodes.each do |root|
|
3049
|
+
set_tree_recursive(root, start_x, start_x + width_for_each_root_tree - 1, y_level)
|
3050
|
+
start_x = start_x + width_for_each_root_tree
|
3051
|
+
y_level = y_level + 40
|
3052
|
+
end
|
3053
|
+
|
3054
|
+
@rendered_nodes.values.each do |rn|
|
3055
|
+
rn.base_z = @base_z
|
3056
|
+
end
|
3057
|
+
|
3058
|
+
if @size_by_connections
|
3059
|
+
scale_node_size
|
3060
|
+
end
|
3061
|
+
|
3062
|
+
prevent_text_overlap
|
3063
|
+
end
|
3064
|
+
|
3065
|
+
def scale_node_size
|
3066
|
+
range = @graph.get_number_of_connections_range
|
3067
|
+
# There are six colors. Any number of scale sizes
|
3068
|
+
# Lets try 4 first as a max size.
|
3069
|
+
bins = range.bin_max_values(4)
|
3070
|
+
|
3071
|
+
# Set the scale for each node
|
3072
|
+
@visible_data_nodes.values.each do |node|
|
3073
|
+
num_links = node.number_of_links
|
3074
|
+
index = 0
|
3075
|
+
while index < bins.size
|
3076
|
+
if num_links <= bins[index]
|
3077
|
+
@rendered_nodes[node.name].set_scale(index + 1, @is_explorer)
|
3078
|
+
index = bins.size
|
3079
|
+
end
|
3080
|
+
index = index + 1
|
3081
|
+
end
|
3082
|
+
end
|
3083
|
+
end
|
3084
|
+
|
3085
|
+
def prevent_text_overlap
|
3086
|
+
@rendered_nodes.values.each do |rn|
|
3087
|
+
text = rn.get_text_widget
|
3088
|
+
if text
|
3089
|
+
if overlaps_with_a_node(text)
|
3090
|
+
move_text_for_node(rn)
|
3091
|
+
else
|
3092
|
+
move_in_bounds = false
|
3093
|
+
# We also check to see if the text is outside the edges of this widget
|
3094
|
+
if text.x < @x or text.right_edge > right_edge
|
3095
|
+
move_in_bounds = true
|
3096
|
+
elsif text.y < @y or text.bottom_edge > bottom_edge
|
3097
|
+
move_in_bounds = true
|
3098
|
+
end
|
3099
|
+
if move_in_bounds
|
3100
|
+
debug("#{text.label} was out of bounds")
|
3101
|
+
move_text_for_node(rn)
|
3102
|
+
end
|
3103
|
+
end
|
3104
|
+
end
|
3105
|
+
end
|
3106
|
+
end
|
3107
|
+
|
3108
|
+
def move_text_for_node(rendered_node)
|
3109
|
+
text = rendered_node.get_text_widget
|
3110
|
+
if text.nil?
|
3111
|
+
return
|
3112
|
+
end
|
3113
|
+
radians_between_attempts = DEG_360 / 24
|
3114
|
+
current_radians = 0.05
|
3115
|
+
done = false
|
3116
|
+
while not done
|
3117
|
+
# Use radians to spread the other nodes around the center node
|
3118
|
+
# TODO base the distance off of scale
|
3119
|
+
text_x = rendered_node.center_x + ((rendered_node.width / 2) * Math.cos(current_radians))
|
3120
|
+
text_y = rendered_node.center_y - ((rendered_node.height / 2) * Math.sin(current_radians))
|
3121
|
+
if text_x < @x
|
3122
|
+
text_x = @x + 1
|
3123
|
+
elsif text_x > right_edge - 20
|
3124
|
+
text_x = right_edge - 20
|
3125
|
+
end
|
3126
|
+
if text_y < @y
|
3127
|
+
text_y = @y + 1
|
3128
|
+
elsif text_y > bottom_edge - 26
|
3129
|
+
text_y = bottom_edge - 26
|
3130
|
+
end
|
3131
|
+
text.x = text_x
|
3132
|
+
text.y = text_y
|
3133
|
+
current_radians = current_radians + radians_between_attempts
|
3134
|
+
if overlaps_with_a_node(text)
|
3135
|
+
# check for done
|
3136
|
+
if current_radians > DEG_360
|
3137
|
+
done = true
|
3138
|
+
error("ERROR: could not find a spot to put the text")
|
3139
|
+
end
|
3140
|
+
else
|
3141
|
+
done = true
|
3142
|
+
end
|
3143
|
+
end
|
3144
|
+
end
|
3145
|
+
|
3146
|
+
def overlaps_with_a_node(text)
|
3147
|
+
@rendered_nodes.values.each do |rn|
|
3148
|
+
if text.label == rn.label
|
3149
|
+
# don't compare to yourself
|
3150
|
+
else
|
3151
|
+
if rn.overlaps_with(text)
|
3152
|
+
return true
|
3153
|
+
end
|
3154
|
+
end
|
3155
|
+
end
|
3156
|
+
false
|
3157
|
+
end
|
3158
|
+
|
3159
|
+
def set_tree_recursive(current_node, start_x, end_x, y_level)
|
3160
|
+
# Draw the current node, and then recursively divide up
|
3161
|
+
# and call again for each of the children
|
3162
|
+
if current_node.visited
|
3163
|
+
return
|
3164
|
+
end
|
3165
|
+
current_node.visited = true
|
3166
|
+
|
3167
|
+
if @gui_theme.use_icons
|
3168
|
+
@rendered_nodes[current_node.name] = NodeIconWidget.new(
|
3169
|
+
x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
|
3170
|
+
y_pixel_to_screen(y_level),
|
3171
|
+
current_node,
|
3172
|
+
get_node_color(current_node))
|
3173
|
+
else
|
3174
|
+
@rendered_nodes[current_node.name] = NodeWidget.new(
|
3175
|
+
x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
|
3176
|
+
y_pixel_to_screen(y_level),
|
3177
|
+
current_node,
|
3178
|
+
get_node_color(current_node))
|
3179
|
+
end
|
3180
|
+
|
3181
|
+
number_of_child_nodes = current_node.outputs.size
|
3182
|
+
if number_of_child_nodes == 0
|
3183
|
+
return
|
3184
|
+
end
|
3185
|
+
width_for_each_child_tree = (end_x - start_x) / number_of_child_nodes
|
3186
|
+
start_child_x = start_x + 5
|
3187
|
+
|
3188
|
+
current_node.outputs.each do |child|
|
3189
|
+
if child.is_a? Edge
|
3190
|
+
child = child.destination
|
3191
|
+
end
|
3192
|
+
set_tree_recursive(child, start_child_x, start_child_x + width_for_each_child_tree - 1, y_level + 40)
|
3193
|
+
start_child_x = start_child_x + width_for_each_child_tree
|
3194
|
+
end
|
3195
|
+
end
|
3196
|
+
|
3197
|
+
def set_all_nodes_for_display
|
3198
|
+
@visible_data_nodes = @graph.node_map
|
3199
|
+
@rendered_nodes = {}
|
3200
|
+
populate_rendered_nodes
|
3201
|
+
if @size_by_connections
|
3202
|
+
scale_node_size
|
3203
|
+
end
|
3204
|
+
prevent_text_overlap
|
3205
|
+
end
|
3206
|
+
|
3207
|
+
def get_node_color(node)
|
3208
|
+
color_tag = node.get_tag(COLOR_TAG)
|
3209
|
+
if color_tag.nil?
|
3210
|
+
return @color
|
3211
|
+
end
|
3212
|
+
color_tag
|
3213
|
+
end
|
3214
|
+
|
3215
|
+
def set_center_node(center_node, max_depth = -1)
|
3216
|
+
# Determine the list of nodes to draw
|
3217
|
+
@graph.reset_visited
|
3218
|
+
@visible_data_nodes = @graph.traverse_and_collect_nodes(center_node, max_depth)
|
3219
|
+
|
3220
|
+
# Convert the data nodes to rendered nodes
|
3221
|
+
# Start by putting the center node in the center, then draw others around it
|
3222
|
+
@rendered_nodes = {}
|
3223
|
+
if @gui_theme.use_icons
|
3224
|
+
@rendered_nodes[center_node.name] = NodeIconWidget.new(
|
3225
|
+
center_x, center_y, center_node, get_node_color(center_node))
|
3226
|
+
else
|
3227
|
+
@rendered_nodes[center_node.name] = NodeWidget.new(center_x, center_y,
|
3228
|
+
center_node, get_node_color(center_node), get_node_color(center_node))
|
3229
|
+
end
|
3230
|
+
|
3231
|
+
populate_rendered_nodes(center_node)
|
3232
|
+
|
3233
|
+
if @size_by_connections
|
3234
|
+
scale_node_size
|
3235
|
+
end
|
3236
|
+
prevent_text_overlap
|
3237
|
+
end
|
3238
|
+
|
3239
|
+
def populate_rendered_nodes(center_node = nil)
|
3240
|
+
# Spread out the other nodes around the center node
|
3241
|
+
# going in a circle at each depth level
|
3242
|
+
stats = Stats.new("NodesPerDepth")
|
3243
|
+
@visible_data_nodes.values.each do |n|
|
3244
|
+
stats.increment(n.depth)
|
3245
|
+
end
|
3246
|
+
current_radians = []
|
3247
|
+
radians_increment = []
|
3248
|
+
(1..4).each do |n|
|
3249
|
+
number_of_nodes_at_depth = stats.count(n)
|
3250
|
+
radians_increment[n] = DEG_360 / number_of_nodes_at_depth.to_f
|
3251
|
+
current_radians[n] = 0.05
|
3252
|
+
end
|
3253
|
+
|
3254
|
+
padding = 100
|
3255
|
+
size_of_x_band = (@width - padding) / 6
|
3256
|
+
size_of_y_band = (@height - padding) / 6
|
3257
|
+
random_x = size_of_x_band / 8
|
3258
|
+
random_y = size_of_y_band / 8
|
3259
|
+
half_random_x = random_x / 2
|
3260
|
+
half_random_y = random_y / 2
|
3261
|
+
|
3262
|
+
# Precompute the band center points
|
3263
|
+
# then reference by the scale or depth values below
|
3264
|
+
band_center_x = padding + (size_of_x_band / 2)
|
3265
|
+
band_center_y = padding + (size_of_y_band / 2)
|
3266
|
+
# depth 1 [0] - center node, distance should be zero. Should be only one
|
3267
|
+
# depth 2 [1] - band one
|
3268
|
+
# depth 3 [2] - band two
|
3269
|
+
# depth 4 [3] - band three
|
3270
|
+
bands_x = [0, band_center_x]
|
3271
|
+
bands_x << band_center_x + size_of_x_band
|
3272
|
+
bands_x << band_center_x + size_of_x_band + size_of_x_band
|
3273
|
+
|
3274
|
+
bands_y = [0, band_center_y]
|
3275
|
+
bands_y << band_center_y + size_of_y_band
|
3276
|
+
bands_y << band_center_y + size_of_y_band + size_of_y_band
|
3277
|
+
|
3278
|
+
@visible_data_nodes.each do |node_name, data_node|
|
3279
|
+
process_this_node = true
|
3280
|
+
if center_node
|
3281
|
+
if node_name == center_node.name
|
3282
|
+
process_this_node = false
|
3283
|
+
end
|
3284
|
+
end
|
3285
|
+
if process_this_node
|
3286
|
+
scale_to_use = 1
|
3287
|
+
if stats.count(1) > 0 and stats.count(2) == 0
|
3288
|
+
# if all nodes are depth 1, then size everything
|
3289
|
+
# as a small node
|
3290
|
+
elsif data_node.depth < 4
|
3291
|
+
scale_to_use = 5 - data_node.depth
|
3292
|
+
end
|
3293
|
+
if @is_explorer
|
3294
|
+
# TODO Layer the nodes around the center
|
3295
|
+
# We need a better multiplier based on the height and width
|
3296
|
+
# max distance x would be (@width / 2) - padding
|
3297
|
+
# divide that into three regions, layer 2, 3, and 4
|
3298
|
+
# get the center point for each of these regions, and do a random from there
|
3299
|
+
# scale to use determines which of the regions
|
3300
|
+
band_index = 4 - scale_to_use
|
3301
|
+
distance_from_center_x = bands_x[band_index] + rand(random_x) - half_random_x
|
3302
|
+
distance_from_center_y = bands_y[band_index] + rand(random_y) - half_random_y
|
3303
|
+
else
|
3304
|
+
distance_from_center_x = 80 + rand(200)
|
3305
|
+
distance_from_center_y = 40 + rand(100)
|
3306
|
+
end
|
3307
|
+
# Use radians to spread the other nodes around the center node
|
3308
|
+
radians_to_use = current_radians[data_node.depth]
|
3309
|
+
radians_to_use = radians_to_use + (rand(radians_increment[data_node.depth]) / 2)
|
3310
|
+
current_radians[data_node.depth] = current_radians[data_node.depth] + radians_increment[data_node.depth]
|
3311
|
+
node_x = center_x + (distance_from_center_x * Math.cos(radians_to_use))
|
3312
|
+
node_y = center_y - (distance_from_center_y * Math.sin(radians_to_use))
|
3313
|
+
if node_x < @x
|
3314
|
+
node_x = @x + 1
|
3315
|
+
elsif node_x > right_edge - 20
|
3316
|
+
node_x = right_edge - 20
|
3317
|
+
end
|
3318
|
+
if node_y < @y
|
3319
|
+
node_y = @y + 1
|
3320
|
+
elsif node_y > bottom_edge - 26
|
3321
|
+
node_y = bottom_edge - 26
|
3322
|
+
end
|
3323
|
+
|
3324
|
+
# Note we can link between data nodes and rendered nodes using the node name
|
3325
|
+
# We have a map of each
|
3326
|
+
if @gui_theme.use_icons
|
3327
|
+
@rendered_nodes[data_node.name] = NodeIconWidget.new(
|
3328
|
+
node_x,
|
3329
|
+
node_y,
|
3330
|
+
data_node,
|
3331
|
+
get_node_color(data_node),
|
3332
|
+
scale_to_use,
|
3333
|
+
@is_explorer)
|
3334
|
+
else
|
3335
|
+
@rendered_nodes[data_node.name] = NodeWidget.new(
|
3336
|
+
node_x,
|
3337
|
+
node_y,
|
3338
|
+
data_node,
|
3339
|
+
get_node_color(data_node),
|
3340
|
+
scale_to_use,
|
3341
|
+
@is_explorer)
|
3342
|
+
end
|
3343
|
+
end
|
3344
|
+
end
|
3345
|
+
@rendered_nodes.values.each do |rn|
|
3346
|
+
rn.base_z = @base_z
|
3347
|
+
end
|
3348
|
+
end
|
3349
|
+
|
3350
|
+
def render
|
3351
|
+
if @rendered_nodes
|
3352
|
+
@rendered_nodes.values.each do |vn|
|
3353
|
+
vn.draw
|
3354
|
+
end
|
3355
|
+
|
3356
|
+
# Draw the connections between nodes
|
3357
|
+
@visible_data_nodes.values.each do |data_node|
|
3358
|
+
data_node.outputs.each do |connected_data_node|
|
3359
|
+
if connected_data_node.is_a? Edge
|
3360
|
+
connected_data_node = connected_data_node.destination
|
3361
|
+
end
|
3362
|
+
rendered_node = @rendered_nodes[data_node.name]
|
3363
|
+
connected_rendered_node = @rendered_nodes[connected_data_node.name]
|
3364
|
+
if connected_rendered_node.nil?
|
3365
|
+
# Don't draw if it is not currently visible
|
3366
|
+
else
|
3367
|
+
if @is_explorer and (rendered_node.is_background or connected_rendered_node.is_background)
|
3368
|
+
# Use a dull gray color for the line
|
3369
|
+
Gosu::draw_line rendered_node.center_x, rendered_node.center_y, COLOR_LIGHT_GRAY,
|
3370
|
+
connected_rendered_node.center_x, connected_rendered_node.center_y, COLOR_LIGHT_GRAY,
|
3371
|
+
relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
|
3372
|
+
else
|
3373
|
+
Gosu::draw_line rendered_node.center_x, rendered_node.center_y, rendered_node.graphics_color,
|
3374
|
+
connected_rendered_node.center_x, connected_rendered_node.center_y, connected_rendered_node.graphics_color,
|
3375
|
+
relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
|
3376
|
+
end
|
3377
|
+
end
|
3378
|
+
end
|
3379
|
+
end
|
3380
|
+
end
|
3381
|
+
end
|
3382
|
+
end
|
827
3383
|
end
|