wads 0.1.0 → 0.2.0

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