wads 0.1.2 → 0.2.2

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