wads 0.1.2 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/wads/widgets.rb CHANGED
@@ -1,10 +1,17 @@
1
+ require 'singleton'
2
+ require 'logger'
1
3
  require_relative 'data_structures'
2
4
 
5
+ #
6
+ # All wads classes are contained within the wads module.
7
+ #
3
8
  module Wads
4
9
  COLOR_PEACH = Gosu::Color.argb(0xffe6b0aa)
5
10
  COLOR_LIGHT_PURPLE = Gosu::Color.argb(0xffd7bde2)
6
11
  COLOR_LIGHT_BLUE = Gosu::Color.argb(0xffa9cce3)
12
+ COLOR_VERY_LIGHT_BLUE = Gosu::Color.argb(0xffd0def5)
7
13
  COLOR_LIGHT_GREEN = Gosu::Color.argb(0xffa3e4d7)
14
+ COLOR_GREEN = COLOR_LIGHT_GREEN
8
15
  COLOR_LIGHT_YELLOW = Gosu::Color.argb(0xfff9e79f)
9
16
  COLOR_LIGHT_ORANGE = Gosu::Color.argb(0xffedbb99)
10
17
  COLOR_WHITE = Gosu::Color::WHITE
@@ -13,12 +20,16 @@ module Wads
13
20
  COLOR_LIME = Gosu::Color.argb(0xffDAF7A6)
14
21
  COLOR_YELLOW = Gosu::Color.argb(0xffFFC300)
15
22
  COLOR_MAROON = Gosu::Color.argb(0xffC70039)
23
+ COLOR_PURPLE = COLOR_MAROON
16
24
  COLOR_LIGHT_GRAY = Gosu::Color.argb(0xff2c3e50)
25
+ COLOR_LIGHTER_GRAY = Gosu::Color.argb(0xff364d63)
26
+ COLOR_LIGHTEST_GRAY = Gosu::Color.argb(0xff486684)
17
27
  COLOR_GRAY = Gosu::Color::GRAY
18
28
  COLOR_OFF_GRAY = Gosu::Color.argb(0xff566573)
19
29
  COLOR_LIGHT_BLACK = Gosu::Color.argb(0xff111111)
20
30
  COLOR_LIGHT_RED = Gosu::Color.argb(0xffe6b0aa)
21
31
  COLOR_CYAN = Gosu::Color::CYAN
32
+ COLOR_AQUA = COLOR_CYAN
22
33
  COLOR_HEADER_BLUE = Gosu::Color.argb(0xff089FCE)
23
34
  COLOR_HEADER_BRIGHT_BLUE = Gosu::Color.argb(0xff0FAADD)
24
35
  COLOR_BLUE = Gosu::Color::BLUE
@@ -27,6 +38,8 @@ module Wads
27
38
  COLOR_BLACK = Gosu::Color::BLACK
28
39
  COLOR_FORM_BUTTON = Gosu::Color.argb(0xcc2e4053)
29
40
  COLOR_ERROR_CODE_RED = Gosu::Color.argb(0xffe6b0aa)
41
+ COLOR_BORDER_BLUE = Gosu::Color.argb(0xff004D80)
42
+ COLOR_ALPHA = "alpha"
30
43
 
31
44
  Z_ORDER_BACKGROUND = 2
32
45
  Z_ORDER_BORDER = 3
@@ -42,33 +55,1070 @@ module Wads
42
55
  EVENT_TABLE_UNSELECT = "tableunselect"
43
56
  EVENT_TABLE_ROW_DELETE = "tablerowdelete"
44
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
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
+ end
809
+
810
+ # The layout sections are as follows:
811
+ #
812
+ # +-------------------------------------------------+
813
+ # + SECTION_NORTH +
814
+ # +-------------------------------------------------+
815
+ # + +
816
+ # + SECTION_CENTER +
817
+ # + +
818
+ # +-------------------------------------------------+
819
+ class HeaderContentLayout < SectionLayout
820
+ def initialize(x, y, width, height, parent_widget, args = {})
821
+ super
822
+ # Divide the height into 100, 100, and the middle gets everything else
823
+ # Right now we are using 100 pixels rather than a percentage for the borders
824
+ header_section_height = 100
825
+ if args[ARG_DESIRED_HEIGHT]
826
+ header_section_height = args[ARG_DESIRED_HEIGHT]
827
+ end
828
+ middle_section_y_start = y + header_section_height
829
+ height_middle_section = height - header_section_height
830
+ @container_map[SECTION_NORTH] = GuiContainer.new(x, y, width, header_section_height)
831
+ @container_map[SECTION_CENTER] = GuiContainer.new(x, middle_section_y_start, width, height_middle_section, FILL_VERTICAL_STACK)
832
+ end
833
+ end
834
+
835
+ # The layout sections are as follows:
836
+ #
837
+ # +-------------------------------------------------+
838
+ # + +
839
+ # + SECTION_CENTER +
840
+ # + +
841
+ # +-------------------------------------------------+
842
+ # + SECTION_SOUTH +
843
+ # +-------------------------------------------------+
844
+ class ContentFooterLayout < SectionLayout
845
+ def initialize(x, y, width, height, parent_widget, args = {})
846
+ super
847
+ # Divide the height into 100, 100, and the middle gets everything else
848
+ # Right now we are using 100 pixels rather than a percentage for the borders
849
+ bottom_section_height = 100
850
+ if args[ARG_DESIRED_HEIGHT]
851
+ bottom_section_height = args[ARG_DESIRED_HEIGHT]
852
+ end
853
+ bottom_section_y_start = y + height - bottom_section_height
854
+ middle_section_height = height - bottom_section_height
855
+ @container_map[SECTION_CENTER] = GuiContainer.new(x, y, width, middle_section_height, FILL_VERTICAL_STACK)
856
+ @container_map[SECTION_SOUTH] = GuiContainer.new(x, bottom_section_y_start,
857
+ width, bottom_section_height)
858
+ end
859
+ end
860
+
861
+ # The layout sections are as follows:
862
+ #
863
+ # +-------------------------------------------------+
864
+ # + | +
865
+ # + SECTION_WEST | SECTION_EAST +
866
+ # + | +
867
+ # +-------------------------------------------------+
868
+ #
869
+ class EastWestLayout < SectionLayout
870
+ def initialize(x, y, width, height, parent_widget, args = {})
871
+ super
872
+ west_section_width = width / 2
873
+ if args[ARG_PANEL_WIDTH]
874
+ west_section_width = args[ARG_PANEL_WIDTH]
875
+ end
876
+ east_section_width = width - west_section_width
877
+ @container_map[SECTION_WEST] = GuiContainer.new(x, y,
878
+ west_section_width, height,
879
+ FILL_FULL_SIZE)
880
+ @container_map[SECTION_EAST] = GuiContainer.new(x + west_section_width, y,
881
+ east_section_width, height,
882
+ FILL_FULL_SIZE)
883
+ end
884
+ end
885
+
886
+ # The layout sections are as follows:
887
+ #
888
+ # +-------------------------------------------------+
889
+ # + SECTION_NORTH +
890
+ # +-------------------------------------------------+
891
+ # + +
892
+ # + SECTION_CENTER +
893
+ # + +
894
+ # +-------------------------------------------------+
895
+ # + SECTION_SOUTH +
896
+ # +-------------------------------------------------+
897
+ class TopMiddleBottomLayout < SectionLayout
898
+ def initialize(x, y, width, height, parent_widget, args = {})
899
+ super
900
+ # Divide the height into 100, 100, and the middle gets everything else
901
+ # Right now we are using 100 pixels rather than a percentage for the borders
902
+ middle_section_y_start = y + 100
903
+ bottom_section_y_start = y + height - 100
904
+ height_middle_section = height - 200
905
+ @container_map[SECTION_NORTH] = GuiContainer.new(x, y, width, 100)
906
+ @container_map[SECTION_CENTER] = GuiContainer.new(x, middle_section_y_start,
907
+ width, height_middle_section, FILL_VERTICAL_STACK)
908
+ @container_map[SECTION_SOUTH] = GuiContainer.new(x, bottom_section_y_start, width, 100)
909
+ end
910
+ end
911
+
912
+ # The layout sections are as follows:
913
+ #
914
+ # +-------------------------------------------------+
915
+ # + SECTION_NORTH +
916
+ # +-------------------------------------------------+
917
+ # + | | +
918
+ # + SECTION_WEST | SECTION_CENTER | SECTION_EAST +
919
+ # + | | +
920
+ # +-------------------------------------------------+
921
+ # + SECTION_SOUTH +
922
+ # +-------------------------------------------------+
923
+ class BorderLayout < SectionLayout
924
+ def initialize(x, y, width, height, parent_widget, args = {})
925
+ super
926
+ # Divide the height into 100, 100, and the middle gets everything else
927
+ # Right now we are using 100 pixels rather than a percentage for the borders
928
+ middle_section_y_start = y + 100
929
+ bottom_section_y_start = y + height - 100
930
+
931
+ height_middle_section = bottom_section_y_start - middle_section_y_start
932
+
933
+ middle_section_x_start = x + 100
934
+ right_section_x_start = x + width - 100
935
+ width_middle_section = right_section_x_start - middle_section_x_start
936
+
937
+ @container_map[SECTION_NORTH] = GuiContainer.new(x, y, width, 100)
938
+ @container_map[SECTION_WEST] = GuiContainer.new(
939
+ x, middle_section_y_start, 100, height_middle_section, FILL_VERTICAL_STACK)
940
+ @container_map[SECTION_CENTER] = GuiContainer.new(
941
+ middle_section_x_start,
942
+ middle_section_y_start,
943
+ width_middle_section,
944
+ height_middle_section,
945
+ FILL_VERTICAL_STACK)
946
+ @container_map[SECTION_EAST] = GuiContainer.new(
947
+ right_section_x_start,
948
+ middle_section_y_start,
949
+ 100,
950
+ height_middle_section,
951
+ FILL_VERTICAL_STACK)
952
+ @container_map[SECTION_SOUTH] = GuiContainer.new(x, bottom_section_y_start, width, 100)
953
+ end
954
+ end
955
+
956
+ # The base class for all widgets. This class provides basic functionality for
957
+ # all gui widgets including maintaining the coordinates and layout used.
958
+ # A widget has a border and background, whose colors are defined by the theme.
959
+ # These can be turned off using the disable_border and disable_background methods.
960
+ # Widgets support a hierarchy of visible elements on the screen. For example,
961
+ # a parent widget may be a form, and it may contain many child widgets such as
962
+ # text labels, input fields, and a submit button. You can add children to a
963
+ # widget using the add or add_child methods. Children are automatically rendered
964
+ # so any child does not need an explicit call to its draw or render method.
965
+ # Children can be placed with x, y positioning relative to their parent for convenience
966
+ # (see the relative_x and relative_y methods).
967
+ #
968
+ # The draw and update methods are used by their Gosu counterparts.
969
+ # Typically there is one parent Wads widget used by a Gosu app, and it controls
970
+ # drawing all of the child widgets, invoking update on all widgets, and delegating
971
+ # user events. Widgets can override a render method for any specific drawing logic.
972
+ # It is worth showing the draw method here to amplify the point. You do not need
973
+ # to specifically call draw or render on any children. If you want to manage GUI
974
+ # elements outside of the widget hierarchy, then render is the best place to do it.
975
+ #
976
+ # Likewise, the update method recursively calls the handle_update method on all
977
+ # children in this widget's hierarchy.
978
+ #
979
+ # A commonly used method is contains_click(mouse_x, mouse_y) which returns
980
+ # whether this widget contained the mouse event. For a example, a button widget
981
+ # uses this method to determine if it was clicked.
982
+ #
45
983
  class Widget
46
984
  attr_accessor :x
47
985
  attr_accessor :y
48
986
  attr_accessor :base_z
49
- attr_accessor :color
50
- attr_accessor :background_color
51
- attr_accessor :border_color
987
+ attr_accessor :gui_theme
988
+ attr_accessor :layout
52
989
  attr_accessor :width
53
990
  attr_accessor :height
54
991
  attr_accessor :visible
55
- attr_accessor :font
56
992
  attr_accessor :children
57
993
  attr_accessor :overlay_widget
58
- attr_accessor :debug_off
994
+ attr_accessor :override_color
995
+ attr_accessor :is_selected
996
+ attr_accessor :text_input_fields
59
997
 
60
- def initialize(x, y, color = COLOR_CYAN)
61
- @x = x
62
- @y = y
63
- @color = color
64
- @width = 1
65
- @height = 1
998
+ def initialize(x, y, width = 10, height = 10, layout = nil, theme = nil)
999
+ set_absolute_position(x, y)
1000
+ set_dimensions(width, height)
1001
+ @base_z = 0
1002
+ if uses_layout
1003
+ if layout.nil?
1004
+ @layout = WadsConfig.instance.create_layout(x, y, width, height, self)
1005
+ else
1006
+ @layout = layout
1007
+ end
1008
+ end
1009
+ if theme.nil?
1010
+ @gui_theme = WadsConfig.instance.current_theme
1011
+ else
1012
+ @gui_theme = theme
1013
+ end
66
1014
  @visible = true
67
1015
  @children = []
68
- @base_z = 0
69
- @debug_off = true
1016
+ @show_background = true
1017
+ @show_border = true
1018
+ @is_selected = false
1019
+ @text_input_fields = []
1020
+ end
1021
+
1022
+ def pad(str, size, left_align = false)
1023
+ str = str.to_s
1024
+ if left_align
1025
+ str[0, size].ljust(size, ' ')
1026
+ else
1027
+ str[0, size].rjust(size, ' ')
1028
+ end
1029
+ end
1030
+ def debug(message)
1031
+ WadsConfig.instance.get_logger.debug message
1032
+ end
1033
+ def info(message)
1034
+ WadsConfig.instance.get_logger.info message
1035
+ end
1036
+ def warn(message)
1037
+ WadsConfig.instance.get_logger.warn message
1038
+ end
1039
+ def error(message)
1040
+ WadsConfig.instance.get_logger.error message
1041
+ end
1042
+
1043
+ def set_absolute_position(x, y)
1044
+ @x = x
1045
+ @y = y
1046
+ end
1047
+
1048
+ def set_dimensions(width, height)
1049
+ @width = width
1050
+ @height = height
1051
+ end
1052
+
1053
+ def uses_layout
1054
+ true
1055
+ end
1056
+
1057
+ def get_layout
1058
+ if not uses_layout
1059
+ raise "The widget #{self.class.name} does not support layouts"
1060
+ end
1061
+ if @layout.nil?
1062
+ raise "No layout was defined for #{self.class.name}"
1063
+ end
1064
+ @layout
1065
+ end
1066
+
1067
+ def set_layout(layout_type, args = {})
1068
+ @layout = WadsConfig.instance.create_layout_for_widget(self, layout_type, args)
1069
+ end
1070
+
1071
+ def add_panel(section, args = {})
1072
+ new_panel = get_layout.add_max_panel({ ARG_SECTION => section,
1073
+ ARG_THEME => @gui_theme}.merge(args))
1074
+ new_panel.disable_border
1075
+ new_panel
1076
+ end
1077
+
1078
+ def get_theme
1079
+ @gui_theme
1080
+ end
1081
+
1082
+ def set_theme(new_theme)
1083
+ @gui_theme = new_theme
70
1084
  end
71
1085
 
1086
+ def set_selected
1087
+ @is_selected = true
1088
+ end
1089
+
1090
+ def unset_selected
1091
+ @is_selected = false
1092
+ end
1093
+
1094
+ def graphics_color
1095
+ if @override_color
1096
+ return @override_color
1097
+ end
1098
+ @gui_theme.graphic_elements_color
1099
+ end
1100
+
1101
+ def text_color
1102
+ if @override_color
1103
+ return @override_color
1104
+ end
1105
+ @gui_theme.text_color
1106
+ end
1107
+
1108
+ def selection_color
1109
+ @gui_theme.selection_color
1110
+ end
1111
+
1112
+ def border_color
1113
+ @gui_theme.border_color
1114
+ end
1115
+
1116
+ #
1117
+ # The z order is determined by taking the base_z and adding the widget specific value.
1118
+ # An overlay widget has a base_z that is +10 higher than the widget underneath it.
1119
+ # The widget_z method provides a relative ordering that is common for user interfaces.
1120
+ # For example, text is higher than graphic elements and backgrounds.
1121
+ #
72
1122
  def z_order
73
1123
  @base_z + widget_z
74
1124
  end
@@ -77,63 +1127,180 @@ module Wads
77
1127
  @base_z + relative_order
78
1128
  end
79
1129
 
1130
+ #
1131
+ # Add a child widget that will automatically be drawn by this widget and will received
1132
+ # delegated events. This is an alias for add_child
1133
+ #
1134
+ def add(child)
1135
+ add_child(child)
1136
+ end
1137
+
1138
+ #
1139
+ # Add a child widget that will automatically be drawn by this widget and will received
1140
+ # delegated events.
1141
+ #
80
1142
  def add_child(child)
81
1143
  @children << child
82
1144
  end
83
1145
 
1146
+ #
1147
+ # Remove the given child widget
1148
+ #
84
1149
  def remove_child(child)
85
1150
  @children.delete(child)
86
1151
  end
87
1152
 
1153
+ #
1154
+ # Remove a list of child widgets
1155
+ #
1156
+ def remove_children(list)
1157
+ list.each do |child|
1158
+ @children.delete(child)
1159
+ end
1160
+ end
1161
+
1162
+ #
1163
+ # Remove all children whose class name includes the given token.
1164
+ # This method can be used if you do not have a saved list of the
1165
+ # widgets you want to remove.
1166
+ #
1167
+ def remove_children_by_type(class_name_token)
1168
+ children_to_remove = []
1169
+ @children.each do |child|
1170
+ if child.class.name.include? class_name_token
1171
+ children_to_remove << child
1172
+ end
1173
+ end
1174
+ children_to_remove.each do |child|
1175
+ @children.delete(child)
1176
+ end
1177
+ end
1178
+
1179
+ #
1180
+ # Remove all children from this widget
1181
+ #
88
1182
  def clear_children
89
1183
  @children = []
90
1184
  end
91
1185
 
92
- def set_background(bgcolor)
93
- @background_color = bgcolor
1186
+ #
1187
+ # Drawing the background is on by default. Use this method to prevent drawing a background.
1188
+ #
1189
+ def disable_background
1190
+ @show_background = false
94
1191
  end
95
1192
 
96
- def set_border(bcolor)
97
- @border_color = bcolor
1193
+ #
1194
+ # Drawing the border is on by default. Use this method to prevent drawing a border.
1195
+ #
1196
+ def disable_border
1197
+ @show_border = false
98
1198
  end
99
1199
 
100
- def set_font(font)
101
- @font = font
1200
+ #
1201
+ # Turn back on drawing of the border
1202
+ #
1203
+ def enable_border
1204
+ @show_border = true
102
1205
  end
103
1206
 
104
- def set_color(color)
105
- @color = color
1207
+ #
1208
+ # Turn back on drawing of the background
1209
+ #
1210
+ def enable_background
1211
+ @show_background = true
106
1212
  end
107
1213
 
108
- def set_dimensions(width, height)
109
- @width = width
110
- @height = height
1214
+ #
1215
+ # A convenience method, or alias, to return the left x coordinate of this widget.
1216
+ #
1217
+ def left_edge
1218
+ @x
111
1219
  end
112
1220
 
1221
+ #
1222
+ # A convenience method to return the right x coordinate of this widget.
1223
+ #
113
1224
  def right_edge
114
1225
  @x + @width - 1
115
1226
  end
116
-
1227
+
1228
+ #
1229
+ # A convenience method, or alias, to return the top y coordinate of this widget.
1230
+ #
1231
+ def top_edge
1232
+ @y
1233
+ end
1234
+
1235
+ #
1236
+ # A convenience method to return the bottom y coordinate of this widget
1237
+ #
117
1238
  def bottom_edge
118
1239
  @y + @height - 1
119
1240
  end
120
1241
 
1242
+ #
1243
+ # A convenience method to return the center x coordinate of this widget
1244
+ #
121
1245
  def center_x
122
1246
  @x + ((right_edge - @x) / 2)
123
1247
  end
124
1248
 
1249
+ #
1250
+ # A convenience method to return the center y coordinate of this widget
1251
+ #
125
1252
  def center_y
126
1253
  @y + ((bottom_edge - @y) / 2)
127
1254
  end
128
1255
 
1256
+ #
1257
+ # Move this widget to an absolute x, y position on the screen.
1258
+ # It will automatically move all child widgets, however be warned that
1259
+ # if you are manually rendering any elements within your own render
1260
+ # logic, you will need to deal with that seperately as the base class
1261
+ # does not have access to its coordinates.
1262
+ #
1263
+ def move_recursive_absolute(new_x, new_y)
1264
+ delta_x = new_x - @x
1265
+ delta_y = new_y - @y
1266
+ move_recursive_delta(delta_x, delta_y)
1267
+ end
1268
+
1269
+ #
1270
+ # Move this widget to a relative number of x, y pixels on the screen.
1271
+ # It will automatically move all child widgets, however be warned that
1272
+ # if you are manually rendering any elements within your own render
1273
+ # logic, you will need to deal with that seperately as the base class
1274
+ # does not have access to its coordinates.
1275
+ #
1276
+ def move_recursive_delta(delta_x, delta_y)
1277
+ @x = @x + delta_x
1278
+ @y = @y + delta_y
1279
+ @children.each do |child|
1280
+ child.move_recursive_delta(delta_x, delta_y)
1281
+ end
1282
+ end
1283
+
1284
+ #
1285
+ # The primary draw method, used by the main Gosu loop draw method.
1286
+ # A common usage pattern is to have a primary widget in your Gosu app
1287
+ # that calls this draw method. All children of this widget are then
1288
+ # automatically drawn by this method recursively.
1289
+ # Note that as a widget author, you should only implement/override the
1290
+ # render method. This is a framework implementation that will
1291
+ # handle child rendering and invoke render as a user-implemented
1292
+ # callback.
1293
+ #
129
1294
  def draw
130
1295
  if @visible
131
1296
  render
132
- if @background_color
1297
+ if @is_selected
1298
+ draw_background(Z_ORDER_SELECTION_BACKGROUND, @gui_theme.selection_color)
1299
+ elsif @show_background
133
1300
  draw_background
134
1301
  end
135
- if @border_color
136
- draw_border(@border_color)
1302
+ if @show_border
1303
+ draw_border
137
1304
  end
138
1305
  @children.each do |child|
139
1306
  child.draw
@@ -141,38 +1308,61 @@ module Wads
141
1308
  end
142
1309
  end
143
1310
 
144
- def draw_background(z_override = nil)
1311
+ def draw_background(z_override = nil, color_override = nil)
1312
+ if color_override.nil?
1313
+ bgcolor = @gui_theme.background_color
1314
+ else
1315
+ bgcolor = color_override
1316
+ end
145
1317
  if z_override
146
1318
  z = relative_z_order(z_override)
147
1319
  else
148
1320
  z = relative_z_order(Z_ORDER_BACKGROUND)
149
1321
  end
150
- Gosu::draw_rect(@x + 1, @y + 1, @width - 3, @height - 3, @background_color, z)
1322
+ Gosu::draw_rect(@x + 1, @y + 1, @width - 3, @height - 3, bgcolor, z)
151
1323
  end
152
1324
 
153
- def draw_shadow(color)
154
- Gosu::draw_line @x - 1, @y - 1, color, right_edge - 1, @y - 1, color, relative_z_order(Z_ORDER_BORDER)
155
- Gosu::draw_line @x - 1, @y - 1, color, @x - 1, bottom_edge - 1, color, relative_z_order(Z_ORDER_BORDER)
156
- end
157
-
158
- def draw_border(color = nil)
159
- if color.nil?
160
- if @border_color
161
- color = @border_color
162
- else
163
- color = @color
164
- end
165
- end
166
- Gosu::draw_line @x, @y, color, right_edge, @y, color, relative_z_order(Z_ORDER_BORDER)
167
- Gosu::draw_line @x, @y, color, @x, bottom_edge, color, relative_z_order(Z_ORDER_BORDER)
168
- Gosu::draw_line @x,bottom_edge, color, right_edge, bottom_edge, color, relative_z_order(Z_ORDER_BORDER)
169
- Gosu::draw_line right_edge, @y, color, right_edge, bottom_edge, color, relative_z_order(Z_ORDER_BORDER)
1325
+ def draw_border
1326
+ Gosu::draw_line @x, @y, @gui_theme.border_color, right_edge, @y, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
1327
+ Gosu::draw_line @x, @y, @gui_theme.border_color, @x, bottom_edge, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
1328
+ Gosu::draw_line @x,bottom_edge, @gui_theme.border_color, right_edge, bottom_edge, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
1329
+ Gosu::draw_line right_edge, @y, @gui_theme.border_color, right_edge, bottom_edge, @gui_theme.border_color, relative_z_order(Z_ORDER_BORDER)
170
1330
  end
171
1331
 
172
1332
  def contains_click(mouse_x, mouse_y)
173
1333
  mouse_x >= @x and mouse_x <= right_edge and mouse_y >= @y and mouse_y <= bottom_edge
174
1334
  end
175
1335
 
1336
+ #
1337
+ # Return true if any part of the given widget overlaps on the screen with this widget
1338
+ # as defined by the rectangle from the upper left corner to the bottom right.
1339
+ # Note that your widget may not necessariliy draw pixels in this entire space.
1340
+ #
1341
+ def overlaps_with(other_widget)
1342
+ if other_widget.contains_click(@x, @y)
1343
+ return true
1344
+ end
1345
+ if other_widget.contains_click(right_edge, @y)
1346
+ return true
1347
+ end
1348
+ if other_widget.contains_click(right_edge, bottom_edge - 1)
1349
+ return true
1350
+ end
1351
+ if other_widget.contains_click(@x, bottom_edge - 1)
1352
+ return true
1353
+ end
1354
+ if other_widget.contains_click(center_x, center_y)
1355
+ return true
1356
+ end
1357
+ return false
1358
+ end
1359
+
1360
+ #
1361
+ # The framework implementation of the main Gosu update loop. This method
1362
+ # propagates the event to all child widgets as well.
1363
+ # As a widget author, do not override this method.
1364
+ # Your callback to implement is the handle_update(update_count, mouse_x, mouse_y) method.
1365
+ #
176
1366
  def update(update_count, mouse_x, mouse_y)
177
1367
  if @overlay_widget
178
1368
  @overlay_widget.update(update_count, mouse_x, mouse_y)
@@ -183,8 +1373,16 @@ module Wads
183
1373
  end
184
1374
  end
185
1375
 
1376
+ #
1377
+ # The framework implementation of the main Gosu button down method.
1378
+ # This method separates out mouse events from keyboard events, and calls the appropriate
1379
+ # callback. As a widget author, do not override this method.
1380
+ # Your callbacks to implement are:
1381
+ # handle_mouse_down(mouse_x, mouse_y)
1382
+ # handle_right_mouse(mouse_x, mouse_y)
1383
+ # handle_key_press(id, mouse_x, mouse_y)
1384
+ #
186
1385
  def button_down(id, mouse_x, mouse_y)
187
- puts "Base widget #{self.class.name} button down #{id}." unless @debug_off
188
1386
  if @overlay_widget
189
1387
  result = @overlay_widget.button_down(id, mouse_x, mouse_y)
190
1388
  if not result.nil? and result.is_a? WidgetResult
@@ -200,6 +1398,14 @@ module Wads
200
1398
  end
201
1399
 
202
1400
  if id == Gosu::MsLeft
1401
+ # Special handling for text input fields
1402
+ # Mouse click: Select text field based on mouse position.
1403
+ if not @text_input_fields.empty?
1404
+ WadsConfig.instance.get_window.text_input = @text_input_fields.find { |tf| tf.under_point?(mouse_x, mouse_y) }
1405
+ # Advanced: Move caret to clicked position
1406
+ WadsConfig.instance.get_window.text_input.move_caret(mouse_x) unless WadsConfig.instance.get_window.text_input.nil?
1407
+ end
1408
+
203
1409
  result = handle_mouse_down mouse_x, mouse_y
204
1410
  elsif id == Gosu::MsRight
205
1411
  result = handle_right_mouse mouse_x, mouse_y
@@ -208,7 +1414,6 @@ module Wads
208
1414
  end
209
1415
 
210
1416
  if not result.nil? and result.is_a? WidgetResult
211
- puts "Base widget #{self.class.name} returning own #{result}" unless @debug_off
212
1417
  return result
213
1418
  end
214
1419
 
@@ -218,7 +1423,6 @@ module Wads
218
1423
  result = child.button_down id, mouse_x, mouse_y
219
1424
  if not result.nil? and result.is_a? WidgetResult
220
1425
  intercept_widget_event(result)
221
- puts "Base widget #{self.class.name} returning child #{result}" unless @debug_off
222
1426
  return result
223
1427
  end
224
1428
  end
@@ -226,13 +1430,20 @@ module Wads
226
1430
  result = child.button_down id, mouse_x, mouse_y
227
1431
  if not result.nil? and result.is_a? WidgetResult
228
1432
  intercept_widget_event(result)
229
- puts "Base widget #{self.class.name} returning child #{result}" unless @debug_off
230
1433
  return result
231
1434
  end
232
1435
  end
233
1436
  end
234
1437
  end
235
1438
 
1439
+ #
1440
+ # The framework implementation of the main Gosu button up method.
1441
+ # This method separates out mouse events from keyboard events.
1442
+ # Only the mouse up event is propagated through the child hierarchy.
1443
+ # As a widget author, do not override this method.
1444
+ # Your callback to implement is:
1445
+ # handle_mouse_up(mouse_x, mouse_y)
1446
+ #
236
1447
  def button_up(id, mouse_x, mouse_y)
237
1448
  if @overlay_widget
238
1449
  return @overlay_widget.button_up(id, mouse_x, mouse_y)
@@ -257,133 +1468,269 @@ module Wads
257
1468
  end
258
1469
  end
259
1470
 
1471
+ #
1472
+ # Return the absolute x coordinate given the relative x pixel to this widget
1473
+ #
1474
+ def relative_x(x)
1475
+ x_pixel_to_screen(x)
1476
+ end
1477
+
1478
+ # An alias for relative_x
260
1479
  def x_pixel_to_screen(x)
261
1480
  @x + x
262
1481
  end
263
1482
 
1483
+ #
1484
+ # Return the absolute y coordinate given the relative y pixel to this widget
1485
+ #
1486
+ def relative_y(y)
1487
+ y_pixel_to_screen(y)
1488
+ end
1489
+
1490
+ # An alias for relative_y
264
1491
  def y_pixel_to_screen(y)
265
1492
  @y + y
266
1493
  end
267
1494
 
268
- def add_text(message, rel_x, rel_y)
269
- new_text = Text.new(message, x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), @font)
1495
+ #
1496
+ # Add a child text widget using x, y positioning relative to this widget
1497
+ #
1498
+ def add_text(message, rel_x, rel_y, color = nil, use_large_font = false)
1499
+ new_text = Text.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), message,
1500
+ { ARG_COLOR => color, ARG_USE_LARGE_FONT => use_large_font})
270
1501
  new_text.base_z = @base_z
1502
+ new_text.gui_theme = @gui_theme
271
1503
  add_child(new_text)
272
1504
  new_text
273
1505
  end
274
1506
 
1507
+ #
1508
+ # Add a child document widget using x, y positioning relative to this widget
1509
+ #
275
1510
  def add_document(content, rel_x, rel_y, width, height)
276
- new_doc = Document.new(content, x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height, @font)
1511
+ new_doc = Document.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
1512
+ width, height,
1513
+ content)
277
1514
  new_doc.base_z = @base_z
1515
+ new_doc.gui_theme = @gui_theme
278
1516
  add_child(new_doc)
279
1517
  new_doc
280
1518
  end
281
1519
 
1520
+ #
1521
+ # Add a child button widget using x, y positioning relative to this widget.
1522
+ # The width of the button will be determined based on the label text unless
1523
+ # specified in the optional parameter. The code to execute is provided as a
1524
+ # block, as shown in the example below.
1525
+ # add_button("Test Button", 10, 10) do
1526
+ # puts "User hit the test button"
1527
+ # end
282
1528
  def add_button(label, rel_x, rel_y, width = nil, &block)
283
- new_button = Button.new(label, x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), @font)
284
1529
  if width.nil?
285
- width = 60
1530
+ args = {}
1531
+ else
1532
+ args = { ARG_DESIRED_WIDTH => width }
286
1533
  end
287
- new_button.width = width
1534
+ new_button = Button.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), label, args)
288
1535
  new_button.set_action(&block)
289
1536
  new_button.base_z = @base_z
1537
+ new_button.gui_theme = @gui_theme
290
1538
  add_child(new_button)
291
1539
  new_button
292
1540
  end
293
1541
 
1542
+ #
1543
+ # Add a child delete button widget using x, y positioning relative to this widget.
1544
+ # A delete button is a regular button that is rendered as a red X, instead of a text label.
1545
+ #
294
1546
  def add_delete_button(rel_x, rel_y, &block)
295
1547
  new_delete_button = DeleteButton.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y))
296
1548
  new_delete_button.set_action(&block)
297
1549
  new_delete_button.base_z = @base_z
1550
+ new_delete_button.gui_theme = @gui_theme
298
1551
  add_child(new_delete_button)
299
1552
  new_delete_button
300
1553
  end
301
1554
 
302
- def add_table(rel_x, rel_y, width, height, column_headers, color = COLOR_WHITE, max_visible_rows = 10)
1555
+ #
1556
+ # Add a child table widget using x, y positioning relative to this widget.
1557
+ #
1558
+ def add_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
303
1559
  new_table = Table.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
304
- width, height, column_headers, @font, color, max_visible_rows)
1560
+ width, height, column_headers, max_visible_rows)
305
1561
  new_table.base_z = @base_z
1562
+ new_table.gui_theme = @gui_theme
306
1563
  add_child(new_table)
307
1564
  new_table
308
1565
  end
309
1566
 
310
- def add_single_select_table(rel_x, rel_y, width, height, column_headers, color = COLOR_WHITE, max_visible_rows = 10)
1567
+ #
1568
+ # Add a child table widget using x, y positioning relative to this widget.
1569
+ # The user can select up to one and only one item in the table.
1570
+ #
1571
+ def add_single_select_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
311
1572
  new_table = SingleSelectTable.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
312
- width, height, column_headers, @font, color, max_visible_rows)
1573
+ width, height, column_headers, max_visible_rows)
313
1574
  new_table.base_z = @base_z
1575
+ new_table.gui_theme = @gui_theme
314
1576
  add_child(new_table)
315
1577
  new_table
316
1578
  end
317
1579
 
318
- def add_multi_select_table(rel_x, rel_y, width, height, column_headers, color = COLOR_WHITE, max_visible_rows = 10)
1580
+ #
1581
+ # Add a child table widget using x, y positioning relative to this widget.
1582
+ # The user can zero to many items in the table.
1583
+ #
1584
+ def add_multi_select_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
319
1585
  new_table = MultiSelectTable.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
320
- width, height, column_headers, @font, color, max_visible_rows)
1586
+ width, height, column_headers, max_visible_rows)
321
1587
  new_table.base_z = @base_z
1588
+ new_table.gui_theme = @gui_theme
322
1589
  add_child(new_table)
323
1590
  new_table
324
1591
  end
325
1592
 
1593
+ #
1594
+ # Add a child graph display widget using x, y positioning relative to this widget.
1595
+ #
326
1596
  def add_graph_display(rel_x, rel_y, width, height, graph)
327
- new_graph = GraphWidget.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height, @font, @color, graph)
1597
+ new_graph = GraphWidget.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height, graph)
328
1598
  new_graph.base_z = @base_z
329
1599
  add_child(new_graph)
330
1600
  new_graph
331
1601
  end
332
1602
 
1603
+ #
1604
+ # Add a child plot display widget using x, y positioning relative to this widget.
1605
+ #
333
1606
  def add_plot(rel_x, rel_y, width, height)
334
- new_plot = Plot.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height, @font)
1607
+ new_plot = Plot.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height)
335
1608
  new_plot.base_z = @base_z
1609
+ new_plot.gui_theme = @gui_theme
336
1610
  add_child(new_plot)
337
1611
  new_plot
338
1612
  end
339
1613
 
340
- def add_axis_lines(rel_x, rel_y, width, height, color)
341
- new_axis_lines = AxisLines.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height, color)
1614
+ #
1615
+ # Add child axis lines widget using x, y positioning relative to this widget.
1616
+ #
1617
+ def add_axis_lines(rel_x, rel_y, width, height)
1618
+ new_axis_lines = AxisLines.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height)
342
1619
  new_axis_lines.base_z = @base_z
1620
+ new_axis_lines.gui_theme = @gui_theme
343
1621
  add_child(new_axis_lines)
344
1622
  new_axis_lines
345
1623
  end
346
1624
 
347
- def add_overlay(overlay)
348
- overlay.base_z = @base_z + 10
349
- add_child(overlay)
350
- @overlay_widget = overlay
1625
+ #
1626
+ # Add a child image widget using x, y positioning relative to this widget.
1627
+ #
1628
+ def add_image(filename, rel_x, rel_y)
1629
+ new_image = ImageWidget.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), img)
1630
+ new_image.base_z = @base_z
1631
+ new_image.gui_theme = @gui_theme
1632
+ add_child(new_image)
1633
+ new_image
1634
+ end
1635
+
1636
+ #
1637
+ # Add an overlay widget that is drawn on top of (at a higher z level) this widget
1638
+ #
1639
+ def add_overlay(overlay)
1640
+ overlay.base_z = @base_z + 10
1641
+ add_child(overlay)
1642
+ @overlay_widget = overlay
1643
+ end
1644
+
1645
+ # For all child widgets, adjust the x coordinate
1646
+ # so that they are centered.
1647
+ def center_children
1648
+ if @children.empty?
1649
+ return
1650
+ end
1651
+ number_of_children = @children.size
1652
+ total_width_of_children = 0
1653
+ @children.each do |child|
1654
+ total_width_of_children = total_width_of_children + child.width + 5
1655
+ end
1656
+ total_width_of_children = total_width_of_children - 5
1657
+
1658
+ start_x = (@width - total_width_of_children) / 2
1659
+ @children.each do |child|
1660
+ child.x = start_x
1661
+ start_x = start_x + child.width + 5
1662
+ end
351
1663
  end
352
1664
 
353
1665
  #
354
- # Callbacks for subclasses to override
1666
+ # Override this method in your subclass to process mouse down events.
1667
+ # The base implementation is empty
355
1668
  #
356
1669
  def handle_mouse_down mouse_x, mouse_y
357
1670
  # empty base implementation
358
1671
  end
359
1672
 
1673
+ #
1674
+ # Override this method in your subclass to process mouse up events.
1675
+ # The base implementation is empty
1676
+ #
360
1677
  def handle_mouse_up mouse_x, mouse_y
361
1678
  # empty base implementation
362
1679
  end
363
1680
 
1681
+ #
1682
+ # Override this method in your subclass to process the right mouse click event.
1683
+ # Note we do not differentiate between up and down for the right mouse button.
1684
+ # The base implementation is empty
1685
+ #
364
1686
  def handle_right_mouse mouse_x, mouse_y
365
1687
  # empty base implementation
366
1688
  end
367
1689
 
1690
+ #
1691
+ # Override this method in your subclass to process keyboard events.
1692
+ # The base implementation is empty.
1693
+ # Note that the mouse was not necessarily positioned over this widget.
1694
+ # You can check this using the contains_click(mouse_x, mouse_y) method
1695
+ # and decide if you want to process the event based on that, if desired.
1696
+ #
368
1697
  def handle_key_press id, mouse_x, mouse_y
369
1698
  # empty base implementation
370
1699
  end
371
1700
 
1701
+ #
1702
+ # This callback is invoked for any key registered by the
1703
+ # register_hold_down_key(id) method.
1704
+ #
1705
+ def handle_key_held_down id, mouse_x, mouse_y
1706
+ # empty base implementation
1707
+ end
1708
+
1709
+ #
1710
+ # Override this method in your subclass to perform any logic needed
1711
+ # as part of the main Gosu update loop. In most cases, this method is
1712
+ # invoked 60 times per second.
1713
+ #
372
1714
  def handle_update update_count, mouse_x, mouse_y
373
1715
  # empty base implementation
374
1716
  end
375
1717
 
1718
+ #
1719
+ # Override this method in your subclass to perform any custom rendering logic.
1720
+ # Note that child widgets are automatically drawn and you do not need to do
1721
+ # that yourself.
1722
+ #
376
1723
  def render
377
1724
  # Base implementation is empty
378
- # Note that the draw method invoked by clients stills renders any added children
379
- # render is for specific drawing done by the widget
380
1725
  end
381
1726
 
1727
+ #
1728
+ # Return the relative z order compared to other widgets.
1729
+ # The absolute z order is the base plus this value.
1730
+ # Its calculated relative so that overlay widgets can be
1731
+ # on top of base displays.
1732
+ #
382
1733
  def widget_z
383
- # The relative z order compared to other widgets
384
- # The absolute z order is the base plus this value.
385
- # Its calculated relative so that overlay widgets can be
386
- # on top of base displays.
387
1734
  0
388
1735
  end
389
1736
 
@@ -393,36 +1740,132 @@ module Wads
393
1740
  end
394
1741
  end
395
1742
 
1743
+ #
1744
+ # A panel is simply an alias for a widget, although you can optionally
1745
+ # treat them differently if you wish. Generally a panel is used to
1746
+ # apply a specific layout to a sub-section of the screen.
1747
+ #
1748
+ class Panel < Widget
1749
+ def initialize(x, y, w, h, layout = nil, theme = nil)
1750
+ super(x, y, w, h, layout, theme)
1751
+ end
1752
+ end
1753
+
1754
+ #
1755
+ # Displays an image on the screen at the specific x, y location. The image
1756
+ # can be scaled by setting the scale attribute. The image attribute to the
1757
+ # construcor can be the string file location or a Gosu::Image instance
1758
+ #
1759
+ class ImageWidget < Widget
1760
+ attr_accessor :img
1761
+ attr_accessor :scale
1762
+
1763
+ def initialize(x, y, image, args = {})
1764
+ super(x, y)
1765
+ if image.is_a? String
1766
+ @img = Gosu::Image.new(image)
1767
+ elsif image.is_a? Gosu::Image
1768
+ @img = image
1769
+ elsif image.is_a? Gosu::Color
1770
+ @img = nil
1771
+ @override_color = image
1772
+ else
1773
+ raise "ImageWidget requires either a filename or a Gosu::Image object"
1774
+ end
1775
+ if args[ARG_THEME]
1776
+ @gui_theme = args[ARG_THEME]
1777
+ end
1778
+ @scale = 1
1779
+ disable_border
1780
+ disable_background
1781
+ set_dimensions(@img.width, @img.height) if @img
1782
+ end
1783
+
1784
+ def render
1785
+ if @img.nil?
1786
+ # TODO draw a box
1787
+ Gosu::draw_rect(@x, @y, @width - 1, @height - 1, @override_color, relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS))
1788
+ else
1789
+ @img.draw @x, @y, z_order, @scale, @scale
1790
+ end
1791
+ end
1792
+
1793
+ def widget_z
1794
+ Z_ORDER_FOCAL_ELEMENTS
1795
+ end
1796
+ end
1797
+
1798
+ #
1799
+ # Displays a text label on the screen at the specific x, y location.
1800
+ # The font specified by the current theme is used.
1801
+ # The theme text color is used, unless the color parameter specifies an override.
1802
+ # The small font is used by default, unless the use_large_font parameter is true.
1803
+ #
396
1804
  class Text < Widget
397
- attr_accessor :str
398
- def initialize(str, x, y, font, color = COLOR_WHITE)
399
- super(x, y, color)
400
- set_font(font)
401
- @str = str
1805
+ attr_accessor :label
1806
+
1807
+ def initialize(x, y, label, args = {})
1808
+ super(x, y)
1809
+ @label = label
1810
+ if args[ARG_THEME]
1811
+ @gui_theme = args[ARG_THEME]
1812
+ end
1813
+ if args[ARG_USE_LARGE_FONT]
1814
+ @use_large_font = args[ARG_USE_LARGE_FONT]
1815
+ end
1816
+ if args[ARG_COLOR]
1817
+ @override_color = args[ARG_COLOR]
1818
+ end
1819
+ disable_border
1820
+ if @use_large_font
1821
+ set_dimensions(@gui_theme.font_large.text_width(@label) + 10, 20)
1822
+ else
1823
+ set_dimensions(@gui_theme.font.text_width(@label) + 10, 20)
1824
+ end
1825
+ end
1826
+
1827
+ def set_text(new_text)
1828
+ @label = new_text
1829
+ end
1830
+
1831
+ def change_text(new_text)
1832
+ set_text(new_text)
402
1833
  end
1834
+
403
1835
  def render
404
- @font.draw_text(@str, @x, @y, z_order, 1, 1, @color)
1836
+ if @use_large_font
1837
+ get_theme.font_large.draw_text(@label, @x, @y, z_order, 1, 1, text_color)
1838
+ else
1839
+ get_theme.font.draw_text(@label, @x, @y, z_order, 1, 1, text_color)
1840
+ end
405
1841
  end
1842
+
406
1843
  def widget_z
407
1844
  Z_ORDER_TEXT
408
1845
  end
409
1846
  end
410
1847
 
1848
+ #
1849
+ # An ErrorMessage is a subclass of text that uses a red color
1850
+ #
411
1851
  class ErrorMessage < Text
412
- attr_accessor :str
413
- def initialize(str, x, y, font)
414
- super("ERROR: #{str}", x, y, font, COLOR_ERROR_CODE_RED)
415
- set_dimensions(@font.text_width(@str) + 4, 36)
1852
+ def initialize(x, y, message)
1853
+ super(x, y, "ERROR: #{message}", COLOR_ERROR_CODE_RED)
416
1854
  end
417
1855
  end
418
1856
 
1857
+ #
1858
+ # A data point to be used in a Plot widget. This object holds
1859
+ # the x, y screen location as well as the data values for x, y.
1860
+ #
419
1861
  class PlotPoint < Widget
420
1862
  attr_accessor :data_x
421
1863
  attr_accessor :data_y
422
1864
  attr_accessor :data_point_size
423
1865
 
424
1866
  def initialize(x, y, data_x, data_y, color = COLOR_MAROON, size = 4)
425
- super(x, y, color)
1867
+ super(x, y)
1868
+ @override_color = color
426
1869
  @data_x = data_x
427
1870
  @data_y = data_y
428
1871
  @data_point_size = size
@@ -436,7 +1879,7 @@ module Wads
436
1879
  half_size = size_to_draw / 2
437
1880
  Gosu::draw_rect(@x - half_size, @y - half_size,
438
1881
  size_to_draw, size_to_draw,
439
- @color, z_order)
1882
+ graphics_color, z_order)
440
1883
  end
441
1884
 
442
1885
  def widget_z
@@ -458,32 +1901,41 @@ module Wads
458
1901
  end
459
1902
  end
460
1903
 
1904
+ #
1905
+ # Displays a button at the specified x, y location.
1906
+ # The button width is based on the label text unless specified
1907
+ # using the optional parameter. The code to executeon a button
1908
+ # click is specified using the set_action method, however typical
1909
+ # using involves the widget or layout form of add_button. For example:
1910
+ # add_button("Test Button", 10, 10) do
1911
+ # puts "User hit the test button"
1912
+ # end
1913
+
461
1914
  class Button < Widget
462
1915
  attr_accessor :label
463
- attr_accessor :text_color
464
1916
  attr_accessor :is_pressed
465
1917
  attr_accessor :action_code
466
1918
 
467
- def initialize(label, x, y, font, width = nil, color = COLOR_DARK_GRAY, text_color = COLOR_HEADER_BRIGHT_BLUE)
468
- super(x, y, color)
469
- set_font(font)
1919
+ def initialize(x, y, label, args = {})
1920
+ super(x, y)
470
1921
  @label = label
471
- @text_pixel_width = @font.text_width(@label)
472
- if width.nil?
473
- @width = @text_pixel_width + 10
1922
+ if args[ARG_THEME]
1923
+ @gui_theme = args[ARG_THEME]
1924
+ end
1925
+ @text_pixel_width = @gui_theme.font.text_width(@label)
1926
+ if args[ARG_DESIRED_WIDTH]
1927
+ @width = args[ARG_DESIRED_WIDTH]
474
1928
  else
475
- @width = width
1929
+ @width = @text_pixel_width + 10
476
1930
  end
477
1931
  @height = 26
478
- @text_color = text_color
479
1932
  @is_pressed = false
480
1933
  @is_pressed_update_count = -100
481
1934
  end
482
1935
 
483
1936
  def render
484
- draw_border(@color)
485
1937
  text_x = center_x - (@text_pixel_width / 2)
486
- @font.draw_text(@label, text_x, @y, z_order, 1, 1, @text_color)
1938
+ @gui_theme.font.draw_text(@label, text_x, @y, z_order, 1, 1, text_color)
487
1939
  end
488
1940
 
489
1941
  def widget_z
@@ -508,22 +1960,24 @@ module Wads
508
1960
  end
509
1961
 
510
1962
  if update_count < @is_pressed_update_count + 15
511
- set_background(nil)
1963
+ unset_selected
512
1964
  elsif contains_click(mouse_x, mouse_y)
513
- set_background(COLOR_LIGHT_GRAY)
1965
+ set_selected
514
1966
  else
515
- set_background(nil)
1967
+ unset_selected
516
1968
  end
517
1969
  end
518
1970
  end
519
1971
 
1972
+ #
1973
+ # A subclass of button that renders a red X instead of label text
1974
+ #
520
1975
  class DeleteButton < Button
521
- def initialize(x, y)
522
- super("ignore", x, y, Gosu::Font.new(22), 50, COLOR_GRAY)
1976
+ def initialize(x, y, args = {})
1977
+ super(x, y, "ignore", {ARG_DESIRED_WIDTH => 50}.merge(args))
523
1978
  set_dimensions(14, 14)
524
1979
  add_child(Line.new(@x, @y, right_edge, bottom_edge, COLOR_ERROR_CODE_RED))
525
1980
  add_child(Line.new(@x, bottom_edge, right_edge, @y, COLOR_ERROR_CODE_RED))
526
- set_border(@color)
527
1981
  end
528
1982
 
529
1983
  def render
@@ -531,20 +1985,26 @@ module Wads
531
1985
  end
532
1986
  end
533
1987
 
1988
+ #
1989
+ # Displays multiple lines of text content at the specified coordinates
1990
+ #
534
1991
  class Document < Widget
535
1992
  attr_accessor :lines
536
1993
 
537
- def initialize(content, x, y, width, height, font)
538
- super(x, y, COLOR_GRAY)
539
- set_font(font)
1994
+ def initialize(x, y, width, height, content, args = {})
1995
+ super(x, y)
540
1996
  set_dimensions(width, height)
541
1997
  @lines = content.split("\n")
1998
+ disable_border
1999
+ if args[ARG_THEME]
2000
+ @gui_theme = args[ARG_THEME]
2001
+ end
542
2002
  end
543
2003
 
544
2004
  def render
545
2005
  y = @y + 4
546
2006
  @lines.each do |line|
547
- @font.draw_text(line, @x + 5, y, z_order, 1, 1, COLOR_WHITE)
2007
+ @gui_theme.font.draw_text(line, @x + 5, y, z_order, 1, 1, text_color)
548
2008
  y = y + 26
549
2009
  end
550
2010
  end
@@ -555,19 +2015,18 @@ module Wads
555
2015
  end
556
2016
 
557
2017
  class InfoBox < Widget
558
- def initialize(title, content, x, y, font, width, height)
2018
+ def initialize(x, y, width, height, title, content, args = {})
559
2019
  super(x, y)
560
- set_font(font)
561
2020
  set_dimensions(width, height)
562
- set_border(COLOR_WHITE)
563
- set_background(COLOR_OFF_GRAY)
564
2021
  @base_z = 10
2022
+ if args[ARG_THEME]
2023
+ @gui_theme = args[ARG_THEME]
2024
+ end
565
2025
  add_text(title, 5, 5)
566
2026
  add_document(content, 5, 52, width, height - 52)
567
2027
  ok_button = add_button("OK", (@width / 2) - 50, height - 26) do
568
2028
  WidgetResult.new(true)
569
2029
  end
570
- ok_button.text_color = COLOR_WHITE
571
2030
  ok_button.width = 100
572
2031
  end
573
2032
 
@@ -581,13 +2040,8 @@ module Wads
581
2040
  class Dialog < Widget
582
2041
  attr_accessor :textinput
583
2042
 
584
- def initialize(window, font, x, y, width, height, title, text_input_default)
585
- super(x, y)
586
- @window = window
587
- set_font(font)
588
- set_dimensions(width, height)
589
- set_background(COLOR_OFF_GRAY)
590
- set_border(COLOR_WHITE)
2043
+ def initialize(x, y, width, height, title, text_input_default)
2044
+ super(x, y, width, height)
591
2045
  @base_z = 10
592
2046
  @error_message = nil
593
2047
 
@@ -596,7 +2050,7 @@ module Wads
596
2050
  add_document(content, 0, 56, width, height)
597
2051
 
598
2052
  # Forms automatically get a text input widget
599
- @textinput = TextField.new(@window, @font, x + 10, bottom_edge - 80, text_input_default, 600)
2053
+ @textinput = TextField.new(x + 10, bottom_edge - 80, text_input_default, 600)
600
2054
  @textinput.base_z = 10
601
2055
  add_child(@textinput)
602
2056
 
@@ -604,15 +2058,11 @@ module Wads
604
2058
  ok_button = add_button("OK", (@width / 2) - 100, height - 32) do
605
2059
  handle_ok
606
2060
  end
607
- ok_button.color = COLOR_FORM_BUTTON
608
- ok_button.text_color = COLOR_WHITE
609
2061
  ok_button.width = 100
610
2062
 
611
2063
  cancel_button = add_button("Cancel", (@width / 2) + 50, height - 32) do
612
2064
  WidgetResult.new(true)
613
2065
  end
614
- cancel_button.color = COLOR_FORM_BUTTON
615
- cancel_button.text_color = COLOR_WHITE
616
2066
  cancel_button.width = 100
617
2067
  end
618
2068
 
@@ -624,7 +2074,7 @@ module Wads
624
2074
  end
625
2075
 
626
2076
  def add_error_message(msg)
627
- @error_message = ErrorMessage.new(msg, x + 10, bottom_edge - 120, @font)
2077
+ @error_message = ErrorMessage.new(x + 10, bottom_edge - 120, msg)
628
2078
  @error_message.base_z = @base_z
629
2079
  end
630
2080
 
@@ -647,9 +2097,9 @@ module Wads
647
2097
 
648
2098
  def handle_mouse_down mouse_x, mouse_y
649
2099
  # Mouse click: Select text field based on mouse position.
650
- @window.text_input = [@textinput].find { |tf| tf.under_point?(mouse_x, mouse_y) }
2100
+ WadsConfig.instance.get_window.text_input = [@textinput].find { |tf| tf.under_point?(mouse_x, mouse_y) }
651
2101
  # Advanced: Move caret to clicked position
652
- @window.text_input.move_caret(mouse_x) unless @window.text_input.nil?
2102
+ WadsConfig.instance.get_window.text_input.move_caret(mouse_x) unless WadsConfig.instance.get_window.text_input.nil?
653
2103
 
654
2104
  handle_mouse_click(mouse_x, mouse_y)
655
2105
  end
@@ -661,6 +2111,14 @@ module Wads
661
2111
  end
662
2112
  end
663
2113
 
2114
+ #
2115
+ # A result object returned from handle methods that instructs the parent widget
2116
+ # what to do. A close_widget value of true instructs the recipient to close
2117
+ # either the overlay window or the entire app, based on the context of the receiver.
2118
+ # In the case of a form being submitted, the action may be "OK" and the form_data
2119
+ # contains the information supplied by the user.
2120
+ # WidgetResult is intentionally generic so it can support a wide variety of use cases.
2121
+ #
664
2122
  class WidgetResult
665
2123
  attr_accessor :close_widget
666
2124
  attr_accessor :action
@@ -673,80 +2131,124 @@ module Wads
673
2131
  end
674
2132
  end
675
2133
 
2134
+ #
2135
+ # Renders a line from x, y to x2, y2. The theme graphics elements color
2136
+ # is used by default, unless specified using the optional parameter.
2137
+ #
676
2138
  class Line < Widget
677
2139
  attr_accessor :x2
678
2140
  attr_accessor :y2
679
2141
 
680
- def initialize(x, y, x2, y2, color = COLOR_CYAN)
681
- super x, y, color
2142
+ def initialize(x, y, x2, y2, color = nil)
2143
+ super(x, y)
2144
+ @override_color = color
682
2145
  @x2 = x2
683
2146
  @y2 = y2
2147
+ disable_border
2148
+ disable_background
684
2149
  end
685
2150
 
686
2151
  def render
687
- Gosu::draw_line x, y, @color, x2, y2, @color, z_order
2152
+ Gosu::draw_line x, y, graphics_color, x2, y2, graphics_color, z_order
688
2153
  end
689
2154
 
690
2155
  def widget_z
691
2156
  Z_ORDER_GRAPHIC_ELEMENTS
692
2157
  end
2158
+
2159
+ def uses_layout
2160
+ false
2161
+ end
693
2162
  end
694
2163
 
2164
+ #
2165
+ # A very specific widget used along with a Plot to draw the x and y axis lines.
2166
+ # Note that the labels are drawn using separate widgets.
2167
+ #
695
2168
  class AxisLines < Widget
696
- def initialize(x, y, width, height, color = COLOR_CYAN)
697
- super x, y, color
698
- @width = width
699
- @height = height
2169
+ def initialize(x, y, width, height, color = nil)
2170
+ super(x, y)
2171
+ set_dimensions(width, height)
2172
+ disable_border
2173
+ disable_background
700
2174
  end
701
2175
 
702
2176
  def render
703
- add_child(Line.new(@x, @y, @x, bottom_edge, @color))
704
- add_child(Line.new(@x, bottom_edge, right_edge, bottom_edge, @color))
2177
+ add_child(Line.new(@x, @y, @x, bottom_edge, graphics_color))
2178
+ add_child(Line.new(@x, bottom_edge, right_edge, bottom_edge, graphics_color))
2179
+ end
2180
+
2181
+ def uses_layout
2182
+ false
705
2183
  end
706
2184
  end
707
2185
 
2186
+ #
2187
+ # Labels and tic marks for the vertical axis on a plot
2188
+ #
708
2189
  class VerticalAxisLabel < Widget
709
2190
  attr_accessor :label
710
2191
 
711
- def initialize(x, y, label, font, color = COLOR_CYAN)
712
- super x, y, color
713
- set_font(font)
2192
+ def initialize(x, y, label, color = nil)
2193
+ super(x, y)
714
2194
  @label = label
2195
+ @override_color = color
2196
+ text_pixel_width = @gui_theme.font.text_width(@label)
2197
+ add_text(@label, -text_pixel_width - 28, -12)
2198
+ disable_border
2199
+ disable_background
715
2200
  end
716
2201
 
717
2202
  def render
718
- text_pixel_width = @font.text_width(@label)
719
- Gosu::draw_line @x - 20, @y, @color,
720
- @x, @y, @color, z_order
721
-
722
- @font.draw_text(@label, @x - text_pixel_width - 28, @y - 12, 1, 1, 1, @color)
2203
+ Gosu::draw_line @x - 20, @y, graphics_color,
2204
+ @x, @y, graphics_color, z_order
723
2205
  end
724
2206
 
725
2207
  def widget_z
726
2208
  Z_ORDER_GRAPHIC_ELEMENTS
727
2209
  end
2210
+
2211
+ def uses_layout
2212
+ false
2213
+ end
728
2214
  end
729
2215
 
2216
+ #
2217
+ # Labels and tic marks for the horizontal axis on a plot
2218
+ #
730
2219
  class HorizontalAxisLabel < Widget
731
2220
  attr_accessor :label
732
2221
 
733
- def initialize(x, y, label, font, color = COLOR_CYAN)
734
- super x, y, color
735
- set_font(font)
2222
+ def initialize(x, y, label, color = nil)
2223
+ super(x, y)
736
2224
  @label = label
2225
+ @override_color = color
2226
+ text_pixel_width = @gui_theme.font.text_width(@label)
2227
+ add_text(@label, -(text_pixel_width / 2), 26)
2228
+ disable_border
2229
+ disable_background
737
2230
  end
738
2231
 
739
2232
  def render
740
- text_pixel_width = @font.text_width(@label)
741
- Gosu::draw_line @x, @y, @color, @x, @y + 20, @color
742
- @font.draw_text(@label, @x - (text_pixel_width / 2), @y + 26, z_order, 1, 1, @color)
2233
+ Gosu::draw_line @x, @y, graphics_color, @x, @y + 20, graphics_color, z_order
743
2234
  end
744
2235
 
745
2236
  def widget_z
746
2237
  Z_ORDER_TEXT
747
2238
  end
2239
+
2240
+ def uses_layout
2241
+ false
2242
+ end
748
2243
  end
749
2244
 
2245
+ #
2246
+ # Displays a table of information at the given coordinates.
2247
+ # The headers are an array of text labels to display at the top of each column.
2248
+ # The max_visible_rows specifies how many rows are visible at once.
2249
+ # If there are more data rows than the max, the arrow keys can be used to
2250
+ # page up or down through the rows in the table.
2251
+ #
750
2252
  class Table < Widget
751
2253
  attr_accessor :data_rows
752
2254
  attr_accessor :row_colors
@@ -755,10 +2257,12 @@ module Wads
755
2257
  attr_accessor :current_row
756
2258
  attr_accessor :can_delete_rows
757
2259
 
758
- def initialize(x, y, width, height, headers, font, color = COLOR_GRAY, max_visible_rows = 10)
759
- super(x, y, color)
760
- set_font(font)
2260
+ def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
2261
+ super(x, y)
761
2262
  set_dimensions(width, height)
2263
+ if args[ARG_THEME]
2264
+ @gui_theme = args[ARG_THEME]
2265
+ end
762
2266
  @headers = headers
763
2267
  @current_row = 0
764
2268
  @max_visible_rows = max_visible_rows
@@ -785,7 +2289,7 @@ module Wads
785
2289
  @row_colors = []
786
2290
  end
787
2291
 
788
- def add_row(data_row, color = @color)
2292
+ def add_row(data_row, color = text_color )
789
2293
  @data_rows << data_row
790
2294
  @row_colors << color
791
2295
  end
@@ -840,9 +2344,9 @@ module Wads
840
2344
  column_widths = []
841
2345
  number_of_columns = @data_rows[0].size
842
2346
  (0..number_of_columns-1).each do |c|
843
- max_length = @font.text_width(headers[c])
2347
+ max_length = @gui_theme.font.text_width(headers[c])
844
2348
  (0..number_of_rows-1).each do |r|
845
- text_pixel_width = @font.text_width(@data_rows[r][c])
2349
+ text_pixel_width = @gui_theme.font.text_width(@data_rows[r][c])
846
2350
  if text_pixel_width > max_length
847
2351
  max_length = text_pixel_width
848
2352
  end
@@ -850,18 +2354,22 @@ module Wads
850
2354
  column_widths[c] = max_length
851
2355
  end
852
2356
 
2357
+ # Draw a horizontal line between header and data rows
853
2358
  x = @x + 10
854
2359
  if number_of_columns > 1
855
2360
  (0..number_of_columns-2).each do |c|
856
2361
  x = x + column_widths[c] + 20
857
- Gosu::draw_line x, @y, @color, x, @y + @height, @color, z_order
2362
+ Gosu::draw_line x, @y, graphics_color, x, @y + @height, graphics_color, z_order
858
2363
  end
859
2364
  end
860
2365
 
861
- y = @y
2366
+ # Draw the header row
2367
+ y = @y
2368
+ Gosu::draw_rect(@x + 1, y, @width - 3, 28, graphics_color, relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
2369
+
862
2370
  x = @x + 20
863
2371
  (0..number_of_columns-1).each do |c|
864
- @font.draw_text(@headers[c], x, y, z_order, 1, 1, @color)
2372
+ @gui_theme.font.draw_text(@headers[c], x, y + 3, z_order, 1, 1, text_color)
865
2373
  x = x + column_widths[c] + 20
866
2374
  end
867
2375
  y = y + 30
@@ -873,7 +2381,7 @@ module Wads
873
2381
  elsif count < @current_row + @max_visible_rows
874
2382
  x = @x + 20
875
2383
  (0..number_of_columns-1).each do |c|
876
- @font.draw_text(row[c], x, y + 2, z_order, 1, 1, @row_colors[count])
2384
+ @gui_theme.font.draw_text(row[c], x, y + 2, z_order, 1, 1, @row_colors[count])
877
2385
  x = x + column_widths[c] + 20
878
2386
  end
879
2387
  y = y + 30
@@ -894,15 +2402,31 @@ module Wads
894
2402
  def widget_z
895
2403
  Z_ORDER_TEXT
896
2404
  end
2405
+
2406
+ def uses_layout
2407
+ false
2408
+ end
897
2409
  end
898
2410
 
2411
+ #
2412
+ # A table where the user can select one row at a time.
2413
+ # The selected row has a background color specified by the selection color of the
2414
+ # current theme.
2415
+ #
899
2416
  class SingleSelectTable < Table
900
2417
  attr_accessor :selected_row
901
- attr_accessor :selected_color
902
2418
 
903
- def initialize(x, y, width, height, headers, font, color = COLOR_GRAY, max_visible_rows = 10)
904
- super(x, y, width, height, headers, font, color, max_visible_rows)
905
- @selected_color = COLOR_BLACK
2419
+ def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
2420
+ super(x, y, width, height, headers, max_visible_rows, args)
2421
+ end
2422
+
2423
+ def is_row_selected(mouse_y)
2424
+ row_number = determine_row_number(mouse_y)
2425
+ if row_number.nil?
2426
+ return false
2427
+ end
2428
+ selected_row = @current_row + row_number
2429
+ @selected_row == selected_row
906
2430
  end
907
2431
 
908
2432
  def set_selected_row(mouse_y, column_number)
@@ -919,12 +2443,22 @@ module Wads
919
2443
  end
920
2444
  end
921
2445
 
2446
+ def unset_selected_row(mouse_y, column_number)
2447
+ row_number = determine_row_number(mouse_y)
2448
+ if not row_number.nil?
2449
+ this_selected_row = @current_row + row_number
2450
+ @selected_row = this_selected_row
2451
+ return @data_rows[this_selected_row][column_number]
2452
+ end
2453
+ nil
2454
+ end
2455
+
922
2456
  def render
923
2457
  super
924
2458
  if @selected_row
925
2459
  if @selected_row >= @current_row and @selected_row < @current_row + @max_visible_rows
926
2460
  y = @y + 30 + ((@selected_row - @current_row) * 30)
927
- Gosu::draw_rect(@x + 20, y, @width - 30, 28, @selected_color, relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
2461
+ Gosu::draw_rect(@x + 20, y, @width - 30, 28, @gui_theme.selection_color, relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
928
2462
  end
929
2463
  end
930
2464
  end
@@ -932,20 +2466,58 @@ module Wads
932
2466
  def widget_z
933
2467
  Z_ORDER_TEXT
934
2468
  end
2469
+
2470
+ def handle_mouse_down mouse_x, mouse_y
2471
+ if contains_click(mouse_x, mouse_y)
2472
+ row_number = determine_row_number(mouse_y)
2473
+ if row_number.nil?
2474
+ return WidgetResult.new(false)
2475
+ end
2476
+ # First check if its the delete button that got this
2477
+ delete_this_row = false
2478
+ @delete_buttons.each do |db|
2479
+ if db.contains_click(mouse_x, mouse_y)
2480
+ delete_this_row = true
2481
+ end
2482
+ end
2483
+ if delete_this_row
2484
+ if not row_number.nil?
2485
+ data_set_row_to_delete = @current_row + row_number
2486
+ data_set_name_to_delete = @data_rows[data_set_row_to_delete][1]
2487
+ @data_rows.delete_at(data_set_row_to_delete)
2488
+ return WidgetResult.new(false, EVENT_TABLE_ROW_DELETE, [data_set_name_to_delete])
2489
+ end
2490
+ else
2491
+ if is_row_selected(mouse_y)
2492
+ unset_selected_row(mouse_y, 0)
2493
+ return WidgetResult.new(false, EVENT_TABLE_UNSELECT, @data_rows[row_number])
2494
+ else
2495
+ set_selected_row(mouse_y, 0)
2496
+ return WidgetResult.new(false, EVENT_TABLE_SELECT, @data_rows[row_number])
2497
+ end
2498
+ end
2499
+ end
2500
+ end
935
2501
  end
936
2502
 
2503
+ #
2504
+ # A table where the user can select multiple rows at a time.
2505
+ # Selected rows have a background color specified by the selection color of the
2506
+ # current theme.
2507
+ #
937
2508
  class MultiSelectTable < Table
938
2509
  attr_accessor :selected_rows
939
- attr_accessor :selection_color
940
2510
 
941
- def initialize(x, y, width, height, headers, font, color = COLOR_GRAY, max_visible_rows = 10)
942
- super(x, y, width, height, headers, font, color, max_visible_rows)
2511
+ def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
2512
+ super(x, y, width, height, headers, max_visible_rows, args)
943
2513
  @selected_rows = []
944
- @selection_color = COLOR_LIGHT_GRAY
945
- end
946
-
2514
+ end
2515
+
947
2516
  def is_row_selected(mouse_y)
948
2517
  row_number = determine_row_number(mouse_y)
2518
+ if row_number.nil?
2519
+ return false
2520
+ end
949
2521
  @selected_rows.include?(@current_row + row_number)
950
2522
  end
951
2523
 
@@ -980,7 +2552,7 @@ module Wads
980
2552
  width_of_selection_background = width_of_selection_background - 20
981
2553
  end
982
2554
  Gosu::draw_rect(@x + 20, y, width_of_selection_background, 28,
983
- @selection_color,
2555
+ @gui_theme.selection_color,
984
2556
  relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
985
2557
  end
986
2558
  y = y + 30
@@ -995,6 +2567,9 @@ module Wads
995
2567
  def handle_mouse_down mouse_x, mouse_y
996
2568
  if contains_click(mouse_x, mouse_y)
997
2569
  row_number = determine_row_number(mouse_y)
2570
+ if row_number.nil?
2571
+ return WidgetResult.new(false)
2572
+ end
998
2573
  # First check if its the delete button that got this
999
2574
  delete_this_row = false
1000
2575
  @delete_buttons.each do |db|
@@ -1022,6 +2597,11 @@ module Wads
1022
2597
  end
1023
2598
  end
1024
2599
 
2600
+ #
2601
+ # A two-dimensional graph display which plots a number of PlotPoint objects.
2602
+ # Options include grid lines that can be displayed, as well as whether lines
2603
+ # should be drawn connecting each point in a data set.
2604
+ #
1025
2605
  class Plot < Widget
1026
2606
  attr_accessor :points
1027
2607
  attr_accessor :visible_range
@@ -1030,9 +2610,8 @@ module Wads
1030
2610
  attr_accessor :zoom_level
1031
2611
  attr_accessor :visibility_map
1032
2612
 
1033
- def initialize(x, y, width, height, font)
1034
- super x, y, color
1035
- set_font(font)
2613
+ def initialize(x, y, width, height)
2614
+ super(x, y)
1036
2615
  set_dimensions(width, height)
1037
2616
  @display_grid = false
1038
2617
  @display_lines = true
@@ -1044,6 +2623,7 @@ module Wads
1044
2623
  # Hash of rendered points keyed by data set name, so we can toggle visibility
1045
2624
  @points_by_data_set_name = {}
1046
2625
  @visibility_map = {}
2626
+ disable_border
1047
2627
  end
1048
2628
 
1049
2629
  def toggle_visibility(data_set_name)
@@ -1105,6 +2685,24 @@ module Wads
1105
2685
  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
1106
2686
  end
1107
2687
 
2688
+ def add_data_point(data_set_name, data_x, data_y, color = COLOR_MAROON)
2689
+ if range_set?
2690
+ rendered_points = @points_by_data_set_name[data_set_name]
2691
+ if rendered_points.nil?
2692
+ rendered_points = []
2693
+ @points_by_data_set_name[data_set_name] = rendered_points
2694
+ end
2695
+ rendered_points << PlotPoint.new(draw_x(data_x), draw_y(data_y),
2696
+ data_x, data_y,
2697
+ color)
2698
+ if @visibility_map[data_set_name].nil?
2699
+ @visibility_map[data_set_name] = true
2700
+ end
2701
+ else
2702
+ error("ERROR: range not set, cannot add data")
2703
+ end
2704
+ end
2705
+
1108
2706
  def add_data_set(data_set_name, rendered_points)
1109
2707
  if range_set?
1110
2708
  @points_by_data_set_name[data_set_name] = rendered_points
@@ -1112,7 +2710,7 @@ module Wads
1112
2710
  @visibility_map[data_set_name] = true
1113
2711
  end
1114
2712
  else
1115
- puts "ERROR: range not set, cannot add data"
2713
+ error("ERROR: range not set, cannot add data")
1116
2714
  end
1117
2715
  end
1118
2716
 
@@ -1162,8 +2760,8 @@ module Wads
1162
2760
  if points.length > 1
1163
2761
  points.inject(points[0]) do |last, the_next|
1164
2762
  if last.x < the_next.x
1165
- Gosu::draw_line last.x, last.y, last.color,
1166
- the_next.x, the_next.y, last.color, relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
2763
+ Gosu::draw_line last.x, last.y, last.graphics_color,
2764
+ the_next.x, the_next.y, last.graphics_color, relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
1167
2765
  end
1168
2766
  the_next
1169
2767
  end
@@ -1226,19 +2824,47 @@ module Wads
1226
2824
  end
1227
2825
  end
1228
2826
 
2827
+ #
2828
+ # A graphical representation of a node in a graph using a button-style, i.e
2829
+ # a rectangular border with a text label.
2830
+ # The choice to use this display class is dictated by the use_icons attribute
2831
+ # of the current theme.
2832
+ # Like images, the size of node widgets can be scaled.
2833
+ #
1229
2834
  class NodeWidget < Button
1230
2835
  attr_accessor :data_node
1231
2836
 
1232
- def initialize(node, x, y, font, border_color = COLOR_DARK_GRAY, text_color = COLOR_HEADER_BRIGHT_BLUE)
1233
- super(node.name, x, y, font, nil, border_color, text_color)
1234
- set_background(COLOR_BLACK)
2837
+ def initialize(x, y, node, color = nil, initial_scale = 1, is_explorer = false)
2838
+ super(x, y, node.name)
2839
+ @orig_width = @width
2840
+ @orig_height = @height
1235
2841
  @data_node = node
2842
+ @override_color = color
2843
+ set_scale(initial_scale, @is_explorer)
2844
+ end
2845
+
2846
+ def is_background
2847
+ @scale <= 1 and @is_explorer
2848
+ end
2849
+
2850
+ def set_scale(value, is_explorer = false)
2851
+ @scale = value
2852
+ @is_explorer = is_explorer
2853
+ if value < 1
2854
+ value = 1
2855
+ end
2856
+ @width = @orig_width * @scale.to_f
2857
+ debug("In regular node widget Setting scale of #{@label} to #{@scale}")
2858
+ end
2859
+
2860
+ def get_text_widget
2861
+ nil
1236
2862
  end
1237
2863
 
1238
2864
  def render
1239
2865
  super
1240
2866
  draw_background(Z_ORDER_FOCAL_ELEMENTS)
1241
- draw_shadow(COLOR_GRAY)
2867
+ #draw_shadow(COLOR_GRAY)
1242
2868
  end
1243
2869
 
1244
2870
  def widget_z
@@ -1246,26 +2872,127 @@ module Wads
1246
2872
  end
1247
2873
  end
1248
2874
 
2875
+ #
2876
+ # A graphical representation of a node in a graph using circular icons
2877
+ # and adjacent text labels.
2878
+ # The choice to use this display class is dictated by the use_icons attribute
2879
+ # of the current theme.
2880
+ # Like images, the size of node widgets can be scaled.
2881
+ #
2882
+ class NodeIconWidget < Widget
2883
+ attr_accessor :data_node
2884
+ attr_accessor :image
2885
+ attr_accessor :scale
2886
+ attr_accessor :label
2887
+ attr_accessor :is_explorer
2888
+
2889
+ def initialize(x, y, node, color = nil, initial_scale = 1, is_explorer = false)
2890
+ super(x, y)
2891
+ @override_color = color
2892
+ @data_node = node
2893
+ @label = node.name
2894
+ circle_image = WadsConfig.instance.circle(color)
2895
+ if circle_image.nil?
2896
+ @image = WadsConfig.instance.circle(COLOR_BLUE)
2897
+ else
2898
+ @image = circle_image
2899
+ end
2900
+ @is_explorer = is_explorer
2901
+ set_scale(initial_scale, @is_explorer)
2902
+ disable_border
2903
+ end
2904
+
2905
+ def name
2906
+ @data_node.name
2907
+ end
2908
+
2909
+ def is_background
2910
+ @scale <= 0.1 and @is_explorer
2911
+ end
2912
+
2913
+ def set_scale(value, is_explorer = false)
2914
+ @is_explorer = is_explorer
2915
+ if value < 0.5
2916
+ value = 0.5
2917
+ end
2918
+ @scale = value / 10.to_f
2919
+ #debug("In node widget Setting scale of #{@label} to #{value} = #{@scale}")
2920
+ @width = IMAGE_CIRCLE_SIZE * scale.to_f
2921
+ @height = IMAGE_CIRCLE_SIZE * scale.to_f
2922
+ # Only in explorer mode do we dull out nodes on the outer edge
2923
+ if is_background
2924
+ @image = WadsConfig.instance.circle(COLOR_ALPHA)
2925
+ else
2926
+ text_pixel_width = @gui_theme.font.text_width(@label)
2927
+ clear_children # the text widget is the only child, so we can remove all
2928
+ add_text(@label, (@width / 2) - (text_pixel_width / 2), -20)
2929
+ end
2930
+ end
2931
+
2932
+ def get_text_widget
2933
+ if @children.size > 0
2934
+ return @children[0]
2935
+ end
2936
+ #raise "No text widget for NodeIconWidget"
2937
+ nil
2938
+ end
2939
+
2940
+ def render
2941
+ @image.draw @x, @y, relative_z_order(Z_ORDER_FOCAL_ELEMENTS), @scale, @scale
2942
+ end
2943
+
2944
+ def widget_z
2945
+ Z_ORDER_TEXT
2946
+ end
2947
+ end
2948
+
2949
+ #
2950
+ # Given a single node or a graph data structure, this widget displays
2951
+ # a visualization of the graph using one of the available node widget classes.
2952
+ # There are different display modes that control what nodes within the graph
2953
+ # are shown. The default display mode, GRAPH_DISPLAY_ALL, shows all nodes
2954
+ # as the name implies. GRAPH_DISPLAY_TREE assumes an acyclic graph and renders
2955
+ # the graph in a tree-like structure. GRAPH_DISPLAY_EXPLORER has a chosen
2956
+ # center focus node with connected nodes circled around it based on the depth
2957
+ # or distance from that node. This mode also allows the user to click on
2958
+ # different nodes to navigate the graph and change focus nodes.
2959
+ #
1249
2960
  class GraphWidget < Widget
1250
2961
  attr_accessor :graph
1251
- attr_accessor :center_node
1252
2962
  attr_accessor :selected_node
1253
2963
  attr_accessor :selected_node_x_offset
1254
2964
  attr_accessor :selected_node_y_offset
2965
+ attr_accessor :size_by_connections
2966
+ attr_accessor :is_explorer
1255
2967
 
1256
- def initialize(x, y, width, height, font, color, graph)
1257
- super x, y, color
1258
- set_font(font)
2968
+ def initialize(x, y, width, height, graph, display_mode = GRAPH_DISPLAY_ALL)
2969
+ super(x, y)
1259
2970
  set_dimensions(width, height)
1260
- set_border(color)
1261
- @graph = graph
1262
- set_all_nodes_for_display
2971
+ if graph.is_a? Node
2972
+ @graph = Graph.new(graph)
2973
+ else
2974
+ @graph = graph
2975
+ end
2976
+ @size_by_connections = false
2977
+ @is_explorer = false
2978
+ if [GRAPH_DISPLAY_ALL, GRAPH_DISPLAY_TREE, GRAPH_DISPLAY_EXPLORER].include? display_mode
2979
+ debug("Displaying graph in #{display_mode} mode")
2980
+ else
2981
+ raise "#{display_mode} is not a valid display mode for Graph Widget"
2982
+ end
2983
+ if display_mode == GRAPH_DISPLAY_ALL
2984
+ set_all_nodes_for_display
2985
+ elsif display_mode == GRAPH_DISPLAY_TREE
2986
+ set_tree_display
2987
+ else
2988
+ set_explorer_display
2989
+ end
1263
2990
  end
1264
2991
 
1265
2992
  def handle_update update_count, mouse_x, mouse_y
1266
2993
  if contains_click(mouse_x, mouse_y) and @selected_node
1267
- @selected_node.x = mouse_x - @selected_node_x_offset
1268
- @selected_node.y = mouse_y - @selected_node_y_offset
2994
+ @selected_node.move_recursive_absolute(mouse_x - @selected_node_x_offset,
2995
+ mouse_y - @selected_node_y_offset)
1269
2996
  end
1270
2997
  end
1271
2998
 
@@ -1277,6 +3004,7 @@ module Wads
1277
3004
  @selected_node = rn
1278
3005
  @selected_node_x_offset = mouse_x - rn.x
1279
3006
  @selected_node_y_offset = mouse_y - rn.y
3007
+ @click_timestamp = Time.now
1280
3008
  end
1281
3009
  end
1282
3010
  end
@@ -1285,11 +3013,40 @@ module Wads
1285
3013
 
1286
3014
  def handle_mouse_up mouse_x, mouse_y
1287
3015
  if @selected_node
3016
+ if @is_explorer
3017
+ time_between_mouse_up_down = Time.now - @click_timestamp
3018
+ if time_between_mouse_up_down < 0.2
3019
+ # Treat this as a single click and make the selected
3020
+ # node the new center node of the graph
3021
+ set_explorer_display(@selected_node.data_node)
3022
+ end
3023
+ end
1288
3024
  @selected_node = nil
1289
3025
  end
1290
3026
  end
1291
3027
 
1292
- def set_tree_display(max_depth = -1)
3028
+ def set_explorer_display(center_node = nil)
3029
+ if center_node.nil?
3030
+ # If not specified, pick a center node as the one with the most connections
3031
+ center_node = @graph.node_with_most_connections
3032
+ end
3033
+
3034
+ @graph.reset_visited
3035
+ @visible_data_nodes = {}
3036
+ center_node.bfs(4) do |n|
3037
+ @visible_data_nodes[n.name] = n
3038
+ end
3039
+
3040
+ @size_by_connections = false
3041
+ @is_explorer = true
3042
+
3043
+ @rendered_nodes = {}
3044
+ populate_rendered_nodes
3045
+
3046
+ prevent_text_overlap
3047
+ end
3048
+
3049
+ def set_tree_display
1293
3050
  @graph.reset_visited
1294
3051
  @visible_data_nodes = @graph.node_map
1295
3052
  @rendered_nodes = {}
@@ -1299,7 +3056,7 @@ module Wads
1299
3056
  width_for_each_root_tree = @width / number_of_root_nodes
1300
3057
 
1301
3058
  start_x = 0
1302
- y_level = 10
3059
+ y_level = 20
1303
3060
  root_nodes.each do |root|
1304
3061
  set_tree_recursive(root, start_x, start_x + width_for_each_root_tree - 1, y_level)
1305
3062
  start_x = start_x + width_for_each_root_tree
@@ -1309,8 +3066,108 @@ module Wads
1309
3066
  @rendered_nodes.values.each do |rn|
1310
3067
  rn.base_z = @base_z
1311
3068
  end
3069
+
3070
+ if @size_by_connections
3071
+ scale_node_size
3072
+ end
3073
+
3074
+ prevent_text_overlap
3075
+ end
3076
+
3077
+ def scale_node_size
3078
+ range = @graph.get_number_of_connections_range
3079
+ # There are six colors. Any number of scale sizes
3080
+ # Lets try 4 first as a max size.
3081
+ bins = range.bin_max_values(4)
3082
+
3083
+ # Set the scale for each node
3084
+ @visible_data_nodes.values.each do |node|
3085
+ num_links = node.number_of_links
3086
+ index = 0
3087
+ while index < bins.size
3088
+ if num_links <= bins[index]
3089
+ @rendered_nodes[node.name].set_scale(index + 1, @is_explorer)
3090
+ index = bins.size
3091
+ end
3092
+ index = index + 1
3093
+ end
3094
+ end
3095
+ end
3096
+
3097
+ def prevent_text_overlap
3098
+ @rendered_nodes.values.each do |rn|
3099
+ text = rn.get_text_widget
3100
+ if text
3101
+ if overlaps_with_a_node(text)
3102
+ move_text_for_node(rn)
3103
+ else
3104
+ move_in_bounds = false
3105
+ # We also check to see if the text is outside the edges of this widget
3106
+ if text.x < @x or text.right_edge > right_edge
3107
+ move_in_bounds = true
3108
+ elsif text.y < @y or text.bottom_edge > bottom_edge
3109
+ move_in_bounds = true
3110
+ end
3111
+ if move_in_bounds
3112
+ debug("#{text.label} was out of bounds")
3113
+ move_text_for_node(rn)
3114
+ end
3115
+ end
3116
+ end
3117
+ end
3118
+ end
3119
+
3120
+ def move_text_for_node(rendered_node)
3121
+ text = rendered_node.get_text_widget
3122
+ if text.nil?
3123
+ return
3124
+ end
3125
+ radians_between_attempts = DEG_360 / 24
3126
+ current_radians = 0.05
3127
+ done = false
3128
+ while not done
3129
+ # Use radians to spread the other nodes around the center node
3130
+ # TODO base the distance off of scale
3131
+ text_x = rendered_node.center_x + ((rendered_node.width / 2) * Math.cos(current_radians))
3132
+ text_y = rendered_node.center_y - ((rendered_node.height / 2) * Math.sin(current_radians))
3133
+ if text_x < @x
3134
+ text_x = @x + 1
3135
+ elsif text_x > right_edge - 20
3136
+ text_x = right_edge - 20
3137
+ end
3138
+ if text_y < @y
3139
+ text_y = @y + 1
3140
+ elsif text_y > bottom_edge - 26
3141
+ text_y = bottom_edge - 26
3142
+ end
3143
+ text.x = text_x
3144
+ text.y = text_y
3145
+ current_radians = current_radians + radians_between_attempts
3146
+ if overlaps_with_a_node(text)
3147
+ # check for done
3148
+ if current_radians > DEG_360
3149
+ done = true
3150
+ error("ERROR: could not find a spot to put the text")
3151
+ end
3152
+ else
3153
+ done = true
3154
+ end
3155
+ end
1312
3156
  end
1313
3157
 
3158
+ def overlaps_with_a_node(text)
3159
+ @rendered_nodes.values.each do |rn|
3160
+ if text.label == rn.label
3161
+ # don't compare to yourself
3162
+ else
3163
+ if rn.overlaps_with(text)
3164
+ return true
3165
+ end
3166
+ end
3167
+ end
3168
+ false
3169
+ end
3170
+
1314
3171
  def set_tree_recursive(current_node, start_x, end_x, y_level)
1315
3172
  # Draw the current node, and then recursively divide up
1316
3173
  # and call again for each of the children
@@ -1319,13 +3176,19 @@ module Wads
1319
3176
  end
1320
3177
  current_node.visited = true
1321
3178
 
1322
- @rendered_nodes[current_node.name] = NodeWidget.new(
3179
+ if @gui_theme.use_icons
3180
+ @rendered_nodes[current_node.name] = NodeIconWidget.new(
3181
+ x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
3182
+ y_pixel_to_screen(y_level),
1323
3183
  current_node,
3184
+ get_node_color(current_node))
3185
+ else
3186
+ @rendered_nodes[current_node.name] = NodeWidget.new(
1324
3187
  x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
1325
3188
  y_pixel_to_screen(y_level),
1326
- @font,
1327
- get_node_color(current_node),
3189
+ current_node,
1328
3190
  get_node_color(current_node))
3191
+ end
1329
3192
 
1330
3193
  number_of_child_nodes = current_node.outputs.size
1331
3194
  if number_of_child_nodes == 0
@@ -1347,10 +3210,14 @@ module Wads
1347
3210
  @visible_data_nodes = @graph.node_map
1348
3211
  @rendered_nodes = {}
1349
3212
  populate_rendered_nodes
3213
+ if @size_by_connections
3214
+ scale_node_size
3215
+ end
3216
+ prevent_text_overlap
1350
3217
  end
1351
3218
 
1352
3219
  def get_node_color(node)
1353
- color_tag = node.get_tag("color")
3220
+ color_tag = node.get_tag(COLOR_TAG)
1354
3221
  if color_tag.nil?
1355
3222
  return @color
1356
3223
  end
@@ -1365,18 +3232,60 @@ module Wads
1365
3232
  # Convert the data nodes to rendered nodes
1366
3233
  # Start by putting the center node in the center, then draw others around it
1367
3234
  @rendered_nodes = {}
1368
- @rendered_nodes[center_node.name] = NodeWidget.new(center_node, center_x, center_y, @font,
1369
- get_node_color(center_node), get_node_color(center_node))
3235
+ if @gui_theme.use_icons
3236
+ @rendered_nodes[center_node.name] = NodeIconWidget.new(
3237
+ center_x, center_y, center_node, get_node_color(center_node))
3238
+ else
3239
+ @rendered_nodes[center_node.name] = NodeWidget.new(center_x, center_y,
3240
+ center_node, get_node_color(center_node), get_node_color(center_node))
3241
+ end
1370
3242
 
1371
3243
  populate_rendered_nodes(center_node)
3244
+
3245
+ if @size_by_connections
3246
+ scale_node_size
3247
+ end
3248
+ prevent_text_overlap
1372
3249
  end
1373
3250
 
1374
3251
  def populate_rendered_nodes(center_node = nil)
1375
3252
  # Spread out the other nodes around the center node
1376
- # going in a circle
1377
- number_of_visible_nodes = @visible_data_nodes.size
1378
- radians_between_nodes = DEG_360 / number_of_visible_nodes.to_f
1379
- current_radians = 0.05
3253
+ # going in a circle at each depth level
3254
+ stats = Stats.new("NodesPerDepth")
3255
+ @visible_data_nodes.values.each do |n|
3256
+ stats.increment(n.depth)
3257
+ end
3258
+ current_radians = []
3259
+ radians_increment = []
3260
+ (1..4).each do |n|
3261
+ number_of_nodes_at_depth = stats.count(n)
3262
+ radians_increment[n] = DEG_360 / number_of_nodes_at_depth.to_f
3263
+ current_radians[n] = 0.05
3264
+ end
3265
+
3266
+ padding = 100
3267
+ size_of_x_band = (@width - padding) / 6
3268
+ size_of_y_band = (@height - padding) / 6
3269
+ random_x = size_of_x_band / 8
3270
+ random_y = size_of_y_band / 8
3271
+ half_random_x = random_x / 2
3272
+ half_random_y = random_y / 2
3273
+
3274
+ # Precompute the band center points
3275
+ # then reference by the scale or depth values below
3276
+ band_center_x = padding + (size_of_x_band / 2)
3277
+ band_center_y = padding + (size_of_y_band / 2)
3278
+ # depth 1 [0] - center node, distance should be zero. Should be only one
3279
+ # depth 2 [1] - band one
3280
+ # depth 3 [2] - band two
3281
+ # depth 4 [3] - band three
3282
+ bands_x = [0, band_center_x]
3283
+ bands_x << band_center_x + size_of_x_band
3284
+ bands_x << band_center_x + size_of_x_band + size_of_x_band
3285
+
3286
+ bands_y = [0, band_center_y]
3287
+ bands_y << band_center_y + size_of_y_band
3288
+ bands_y << band_center_y + size_of_y_band + size_of_y_band
1380
3289
 
1381
3290
  @visible_data_nodes.each do |node_name, data_node|
1382
3291
  process_this_node = true
@@ -1386,10 +3295,33 @@ module Wads
1386
3295
  end
1387
3296
  end
1388
3297
  if process_this_node
3298
+ scale_to_use = 1
3299
+ if stats.count(1) > 0 and stats.count(2) == 0
3300
+ # if all nodes are depth 1, then size everything
3301
+ # as a small node
3302
+ elsif data_node.depth < 4
3303
+ scale_to_use = 5 - data_node.depth
3304
+ end
3305
+ if @is_explorer
3306
+ # TODO Layer the nodes around the center
3307
+ # We need a better multiplier based on the height and width
3308
+ # max distance x would be (@width / 2) - padding
3309
+ # divide that into three regions, layer 2, 3, and 4
3310
+ # get the center point for each of these regions, and do a random from there
3311
+ # scale to use determines which of the regions
3312
+ band_index = 4 - scale_to_use
3313
+ distance_from_center_x = bands_x[band_index] + rand(random_x) - half_random_x
3314
+ distance_from_center_y = bands_y[band_index] + rand(random_y) - half_random_y
3315
+ else
3316
+ distance_from_center_x = 80 + rand(200)
3317
+ distance_from_center_y = 40 + rand(100)
3318
+ end
1389
3319
  # Use radians to spread the other nodes around the center node
1390
- # For now, we will randomly place them
1391
- node_x = center_x + ((80 + rand(200)) * Math.cos(current_radians))
1392
- node_y = center_y - ((40 + rand(100)) * Math.sin(current_radians))
3320
+ radians_to_use = current_radians[data_node.depth]
3321
+ radians_to_use = radians_to_use + (rand(radians_increment[data_node.depth]) / 2)
3322
+ current_radians[data_node.depth] = current_radians[data_node.depth] + radians_increment[data_node.depth]
3323
+ node_x = center_x + (distance_from_center_x * Math.cos(radians_to_use))
3324
+ node_y = center_y - (distance_from_center_y * Math.sin(radians_to_use))
1393
3325
  if node_x < @x
1394
3326
  node_x = @x + 1
1395
3327
  elsif node_x > right_edge - 20
@@ -1400,17 +3332,26 @@ module Wads
1400
3332
  elsif node_y > bottom_edge - 26
1401
3333
  node_y = bottom_edge - 26
1402
3334
  end
1403
- current_radians = current_radians + radians_between_nodes
1404
3335
 
1405
3336
  # Note we can link between data nodes and rendered nodes using the node name
1406
3337
  # We have a map of each
1407
- @rendered_nodes[data_node.name] = NodeWidget.new(
3338
+ if @gui_theme.use_icons
3339
+ @rendered_nodes[data_node.name] = NodeIconWidget.new(
3340
+ node_x,
3341
+ node_y,
1408
3342
  data_node,
3343
+ get_node_color(data_node),
3344
+ scale_to_use,
3345
+ @is_explorer)
3346
+ else
3347
+ @rendered_nodes[data_node.name] = NodeWidget.new(
1409
3348
  node_x,
1410
3349
  node_y,
1411
- @font,
3350
+ data_node,
1412
3351
  get_node_color(data_node),
1413
- get_node_color(data_node))
3352
+ scale_to_use,
3353
+ @is_explorer)
3354
+ end
1414
3355
  end
1415
3356
  end
1416
3357
  @rendered_nodes.values.each do |rn|
@@ -1423,6 +3364,7 @@ module Wads
1423
3364
  @rendered_nodes.values.each do |vn|
1424
3365
  vn.draw
1425
3366
  end
3367
+
1426
3368
  # Draw the connections between nodes
1427
3369
  @visible_data_nodes.values.each do |data_node|
1428
3370
  data_node.outputs.each do |connected_data_node|
@@ -1434,9 +3376,16 @@ module Wads
1434
3376
  if connected_rendered_node.nil?
1435
3377
  # Don't draw if it is not currently visible
1436
3378
  else
1437
- Gosu::draw_line rendered_node.center_x, rendered_node.center_y, rendered_node.color,
1438
- connected_rendered_node.center_x, connected_rendered_node.center_y, connected_rendered_node.color,
3379
+ if @is_explorer and (rendered_node.is_background or connected_rendered_node.is_background)
3380
+ # Use a dull gray color for the line
3381
+ Gosu::draw_line rendered_node.center_x, rendered_node.center_y, COLOR_LIGHT_GRAY,
3382
+ connected_rendered_node.center_x, connected_rendered_node.center_y, COLOR_LIGHT_GRAY,
3383
+ relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
3384
+ else
3385
+ Gosu::draw_line rendered_node.center_x, rendered_node.center_y, rendered_node.graphics_color,
3386
+ connected_rendered_node.center_x, connected_rendered_node.center_y, connected_rendered_node.graphics_color,
1439
3387
  relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
3388
+ end
1440
3389
  end
1441
3390
  end
1442
3391
  end