wads 0.1.3 → 0.2.0

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