wads 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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