wads 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/wads/widgets.rb CHANGED
@@ -1,10 +1,17 @@
1
+ require 'singleton'
2
+ require 'logger'
1
3
  require_relative 'data_structures'
2
4
 
5
+ #
6
+ # All wads classes are contained within the wads module.
7
+ #
3
8
  module Wads
4
9
  COLOR_PEACH = Gosu::Color.argb(0xffe6b0aa)
5
10
  COLOR_LIGHT_PURPLE = Gosu::Color.argb(0xffd7bde2)
6
11
  COLOR_LIGHT_BLUE = Gosu::Color.argb(0xffa9cce3)
12
+ COLOR_VERY_LIGHT_BLUE = Gosu::Color.argb(0xffd0def5)
7
13
  COLOR_LIGHT_GREEN = Gosu::Color.argb(0xffa3e4d7)
14
+ COLOR_GREEN = COLOR_LIGHT_GREEN
8
15
  COLOR_LIGHT_YELLOW = Gosu::Color.argb(0xfff9e79f)
9
16
  COLOR_LIGHT_ORANGE = Gosu::Color.argb(0xffedbb99)
10
17
  COLOR_WHITE = Gosu::Color::WHITE
@@ -13,12 +20,16 @@ module Wads
13
20
  COLOR_LIME = Gosu::Color.argb(0xffDAF7A6)
14
21
  COLOR_YELLOW = Gosu::Color.argb(0xffFFC300)
15
22
  COLOR_MAROON = Gosu::Color.argb(0xffC70039)
23
+ COLOR_PURPLE = COLOR_MAROON
16
24
  COLOR_LIGHT_GRAY = Gosu::Color.argb(0xff2c3e50)
25
+ COLOR_LIGHTER_GRAY = Gosu::Color.argb(0xff364d63)
26
+ COLOR_LIGHTEST_GRAY = Gosu::Color.argb(0xff486684)
17
27
  COLOR_GRAY = Gosu::Color::GRAY
18
28
  COLOR_OFF_GRAY = Gosu::Color.argb(0xff566573)
19
29
  COLOR_LIGHT_BLACK = Gosu::Color.argb(0xff111111)
20
30
  COLOR_LIGHT_RED = Gosu::Color.argb(0xffe6b0aa)
21
31
  COLOR_CYAN = Gosu::Color::CYAN
32
+ COLOR_AQUA = COLOR_CYAN
22
33
  COLOR_HEADER_BLUE = Gosu::Color.argb(0xff089FCE)
23
34
  COLOR_HEADER_BRIGHT_BLUE = Gosu::Color.argb(0xff0FAADD)
24
35
  COLOR_BLUE = Gosu::Color::BLUE
@@ -27,87 +38,1275 @@ module Wads
27
38
  COLOR_BLACK = Gosu::Color::BLACK
28
39
  COLOR_FORM_BUTTON = Gosu::Color.argb(0xcc2e4053)
29
40
  COLOR_ERROR_CODE_RED = Gosu::Color.argb(0xffe6b0aa)
41
+ COLOR_BORDER_BLUE = Gosu::Color.argb(0xff004D80)
42
+ COLOR_ALPHA = "alpha"
30
43
 
31
44
  Z_ORDER_BACKGROUND = 2
32
- Z_ORDER_WIDGET_BORDER = 3
45
+ Z_ORDER_BORDER = 3
33
46
  Z_ORDER_SELECTION_BACKGROUND = 4
34
47
  Z_ORDER_GRAPHIC_ELEMENTS = 5
35
48
  Z_ORDER_PLOT_POINTS = 6
36
- Z_ORDER_OVERLAY_BACKGROUND = 7
37
- Z_ORDER_OVERLAY_ELEMENTS = 8
38
- 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
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
39
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
+ #
40
999
  class Widget
41
1000
  attr_accessor :x
42
1001
  attr_accessor :y
43
- attr_accessor :color
1002
+ attr_accessor :base_z
1003
+ attr_accessor :gui_theme
1004
+ attr_accessor :layout
44
1005
  attr_accessor :width
45
1006
  attr_accessor :height
46
1007
  attr_accessor :visible
47
1008
  attr_accessor :children
48
- attr_accessor :background_color
49
- attr_accessor :border_color
50
- attr_accessor :font
1009
+ attr_accessor :overlay_widget
1010
+ attr_accessor :override_color
1011
+ attr_accessor :is_selected
1012
+ attr_accessor :text_input_fields
51
1013
 
52
- def initialize(x, y, color = COLOR_CYAN)
53
- @x = x
54
- @y = y
55
- @color = color
56
- @width = 1
57
- @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
58
1030
  @visible = true
59
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
60
1130
  end
61
1131
 
1132
+ def relative_z_order(relative_order)
1133
+ @base_z + relative_order
1134
+ end
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
+ #
62
1148
  def add_child(child)
63
1149
  @children << child
64
1150
  end
65
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
+ #
66
1188
  def clear_children
67
1189
  @children = []
68
1190
  end
69
1191
 
70
- def set_background(bgcolor)
71
- @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
72
1197
  end
73
1198
 
74
- def set_border(bcolor)
75
- @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
76
1204
  end
77
1205
 
78
- def set_font(font)
79
- @font = font
1206
+ #
1207
+ # Turn back on drawing of the border
1208
+ #
1209
+ def enable_border
1210
+ @show_border = true
80
1211
  end
81
1212
 
82
- def set_dimensions(width, height)
83
- @width = width
84
- @height = height
1213
+ #
1214
+ # Turn back on drawing of the background
1215
+ #
1216
+ def enable_background
1217
+ @show_background = true
85
1218
  end
86
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
+ #
87
1230
  def right_edge
88
1231
  @x + @width - 1
89
1232
  end
90
-
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
+ #
91
1244
  def bottom_edge
92
1245
  @y + @height - 1
93
1246
  end
94
1247
 
1248
+ #
1249
+ # A convenience method to return the center x coordinate of this widget
1250
+ #
95
1251
  def center_x
96
1252
  @x + ((right_edge - @x) / 2)
97
1253
  end
98
1254
 
1255
+ #
1256
+ # A convenience method to return the center y coordinate of this widget
1257
+ #
99
1258
  def center_y
100
1259
  @y + ((bottom_edge - @y) / 2)
101
1260
  end
102
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
+ #
103
1300
  def draw
104
1301
  if @visible
105
1302
  render
106
- if @background_color
1303
+ if @is_selected
1304
+ draw_background(Z_ORDER_SELECTION_BACKGROUND, @gui_theme.selection_color)
1305
+ elsif @show_background
107
1306
  draw_background
108
1307
  end
109
- if @border_color
110
- draw_border(@border_color)
1308
+ if @show_border
1309
+ draw_border
111
1310
  end
112
1311
  @children.each do |child|
113
1312
  child.draw
@@ -115,87 +1314,568 @@ module Wads
115
1314
  end
116
1315
  end
117
1316
 
118
- def draw_background(z_order = Z_ORDER_BACKGROUND)
119
- Gosu::draw_rect(@x + 1, @y + 1, @width - 3, @height - 3, @background_color, z_order)
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)
120
1329
  end
121
1330
 
122
- def draw_shadow(color, z_order = Z_ORDER_WIDGET_BORDER)
123
- Gosu::draw_line @x - 1, @y - 1, color, right_edge - 1, @y - 1, color, z_order
124
- Gosu::draw_line @x - 1, @y - 1, color, @x - 1, bottom_edge - 1, color, z_order
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)
125
1336
  end
126
1337
 
127
- def render
128
- # Base implementation is empty
129
- # Note that the draw method invoked by clients stills renders any added children
130
- # render is for specific drawing done by the widget
131
- end
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
132
1341
 
133
- def draw_border(color = nil, zorder = Z_ORDER_WIDGET_BORDER)
134
- if color.nil?
135
- if @border_color
136
- color = @border_color
137
- else
138
- color = @color
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
139
1402
  end
1403
+ return
140
1404
  end
141
- Gosu::draw_line @x, @y, color, right_edge, @y, color, zorder
142
- Gosu::draw_line @x, @y, color, @x, bottom_edge, color, zorder
143
- Gosu::draw_line @x,bottom_edge, color, right_edge, bottom_edge, color, zorder
144
- Gosu::draw_line right_edge, @y, color, right_edge, bottom_edge, color, zorder
1405
+
1406
+ if id == Gosu::MsLeft
1407
+ # Special handling for text input fields
1408
+ # Mouse click: Select text field based on mouse position.
1409
+ if not @text_input_fields.empty?
1410
+ WadsConfig.instance.get_window.text_input = @text_input_fields.find { |tf| tf.under_point?(mouse_x, mouse_y) }
1411
+ # Advanced: Move caret to clicked position
1412
+ WadsConfig.instance.get_window.text_input.move_caret(mouse_x) unless WadsConfig.instance.get_window.text_input.nil?
1413
+ end
1414
+
1415
+ result = handle_mouse_down mouse_x, mouse_y
1416
+ elsif id == Gosu::MsRight
1417
+ result = handle_right_mouse mouse_x, mouse_y
1418
+ else
1419
+ result = handle_key_press id, mouse_x, mouse_y
1420
+ end
1421
+
1422
+ if not result.nil? and result.is_a? WidgetResult
1423
+ return result
1424
+ end
1425
+
1426
+ @children.each do |child|
1427
+ if id == Gosu::MsLeft
1428
+ if child.contains_click(mouse_x, mouse_y)
1429
+ result = child.button_down id, mouse_x, mouse_y
1430
+ if not result.nil? and result.is_a? WidgetResult
1431
+ intercept_widget_event(result)
1432
+ return result
1433
+ end
1434
+ end
1435
+ else
1436
+ result = child.button_down id, mouse_x, mouse_y
1437
+ if not result.nil? and result.is_a? WidgetResult
1438
+ intercept_widget_event(result)
1439
+ return result
1440
+ end
1441
+ end
1442
+ end
145
1443
  end
146
1444
 
147
- def contains_click(mouse_x, mouse_y)
148
- mouse_x >= @x and mouse_x <= right_edge and mouse_y >= @y and mouse_y <= bottom_edge
1445
+ #
1446
+ # The framework implementation of the main Gosu button up method.
1447
+ # This method separates out mouse events from keyboard events.
1448
+ # Only the mouse up event is propagated through the child hierarchy.
1449
+ # As a widget author, do not override this method.
1450
+ # Your callback to implement is:
1451
+ # handle_mouse_up(mouse_x, mouse_y)
1452
+ #
1453
+ def button_up(id, mouse_x, mouse_y)
1454
+ if @overlay_widget
1455
+ return @overlay_widget.button_up(id, mouse_x, mouse_y)
1456
+ end
1457
+
1458
+ if id == Gosu::MsLeft
1459
+ result = handle_mouse_up mouse_x, mouse_y
1460
+ if not result.nil? and result.is_a? WidgetResult
1461
+ return result
1462
+ end
1463
+ end
1464
+
1465
+ @children.each do |child|
1466
+ if id == Gosu::MsLeft
1467
+ if child.contains_click(mouse_x, mouse_y)
1468
+ result = child.handle_mouse_up mouse_x, mouse_y
1469
+ if not result.nil? and result.is_a? WidgetResult
1470
+ return result
1471
+ end
1472
+ end
1473
+ end
1474
+ end
1475
+ end
1476
+
1477
+ #
1478
+ # Return the absolute x coordinate given the relative x pixel to this widget
1479
+ #
1480
+ def relative_x(x)
1481
+ x_pixel_to_screen(x)
1482
+ end
1483
+
1484
+ # An alias for relative_x
1485
+ def x_pixel_to_screen(x)
1486
+ @x + x
1487
+ end
1488
+
1489
+ #
1490
+ # Return the absolute y coordinate given the relative y pixel to this widget
1491
+ #
1492
+ def relative_y(y)
1493
+ y_pixel_to_screen(y)
1494
+ end
1495
+
1496
+ # An alias for relative_y
1497
+ def y_pixel_to_screen(y)
1498
+ @y + y
1499
+ end
1500
+
1501
+ #
1502
+ # Add a child text widget using x, y positioning relative to this widget
1503
+ #
1504
+ def add_text(message, rel_x, rel_y, color = nil, use_large_font = false)
1505
+ new_text = Text.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), message,
1506
+ { ARG_COLOR => color, ARG_USE_LARGE_FONT => use_large_font})
1507
+ new_text.base_z = @base_z
1508
+ new_text.gui_theme = @gui_theme
1509
+ add_child(new_text)
1510
+ new_text
1511
+ end
1512
+
1513
+ #
1514
+ # Add a child document widget using x, y positioning relative to this widget
1515
+ #
1516
+ def add_document(content, rel_x, rel_y, width, height)
1517
+ new_doc = Document.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
1518
+ width, height,
1519
+ content)
1520
+ new_doc.base_z = @base_z
1521
+ new_doc.gui_theme = @gui_theme
1522
+ add_child(new_doc)
1523
+ new_doc
1524
+ end
1525
+
1526
+ #
1527
+ # Add a child button widget using x, y positioning relative to this widget.
1528
+ # The width of the button will be determined based on the label text unless
1529
+ # specified in the optional parameter. The code to execute is provided as a
1530
+ # block, as shown in the example below.
1531
+ # add_button("Test Button", 10, 10) do
1532
+ # puts "User hit the test button"
1533
+ # end
1534
+ def add_button(label, rel_x, rel_y, width = nil, &block)
1535
+ if width.nil?
1536
+ args = {}
1537
+ else
1538
+ args = { ARG_DESIRED_WIDTH => width }
1539
+ end
1540
+ new_button = Button.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), label, args)
1541
+ new_button.set_action(&block)
1542
+ new_button.base_z = @base_z
1543
+ new_button.gui_theme = @gui_theme
1544
+ add_child(new_button)
1545
+ new_button
1546
+ end
1547
+
1548
+ #
1549
+ # Add a child delete button widget using x, y positioning relative to this widget.
1550
+ # A delete button is a regular button that is rendered as a red X, instead of a text label.
1551
+ #
1552
+ def add_delete_button(rel_x, rel_y, &block)
1553
+ new_delete_button = DeleteButton.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y))
1554
+ new_delete_button.set_action(&block)
1555
+ new_delete_button.base_z = @base_z
1556
+ new_delete_button.gui_theme = @gui_theme
1557
+ add_child(new_delete_button)
1558
+ new_delete_button
1559
+ end
1560
+
1561
+ #
1562
+ # Add a child table widget using x, y positioning relative to this widget.
1563
+ #
1564
+ def add_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
1565
+ new_table = Table.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
1566
+ width, height, column_headers, max_visible_rows)
1567
+ new_table.base_z = @base_z
1568
+ new_table.gui_theme = @gui_theme
1569
+ add_child(new_table)
1570
+ new_table
1571
+ end
1572
+
1573
+ #
1574
+ # Add a child table widget using x, y positioning relative to this widget.
1575
+ # The user can select up to one and only one item in the table.
1576
+ #
1577
+ def add_single_select_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
1578
+ new_table = SingleSelectTable.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
1579
+ width, height, column_headers, max_visible_rows)
1580
+ new_table.base_z = @base_z
1581
+ new_table.gui_theme = @gui_theme
1582
+ add_child(new_table)
1583
+ new_table
1584
+ end
1585
+
1586
+ #
1587
+ # Add a child table widget using x, y positioning relative to this widget.
1588
+ # The user can zero to many items in the table.
1589
+ #
1590
+ def add_multi_select_table(rel_x, rel_y, width, height, column_headers, max_visible_rows = 10)
1591
+ new_table = MultiSelectTable.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y),
1592
+ width, height, column_headers, max_visible_rows)
1593
+ new_table.base_z = @base_z
1594
+ new_table.gui_theme = @gui_theme
1595
+ add_child(new_table)
1596
+ new_table
1597
+ end
1598
+
1599
+ #
1600
+ # Add a child graph display widget using x, y positioning relative to this widget.
1601
+ #
1602
+ def add_graph_display(rel_x, rel_y, width, height, graph)
1603
+ new_graph = GraphWidget.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height, graph)
1604
+ new_graph.base_z = @base_z
1605
+ add_child(new_graph)
1606
+ new_graph
1607
+ end
1608
+
1609
+ #
1610
+ # Add a child plot display widget using x, y positioning relative to this widget.
1611
+ #
1612
+ def add_plot(rel_x, rel_y, width, height)
1613
+ new_plot = Plot.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height)
1614
+ new_plot.base_z = @base_z
1615
+ new_plot.gui_theme = @gui_theme
1616
+ add_child(new_plot)
1617
+ new_plot
1618
+ end
1619
+
1620
+ #
1621
+ # Add child axis lines widget using x, y positioning relative to this widget.
1622
+ #
1623
+ def add_axis_lines(rel_x, rel_y, width, height)
1624
+ new_axis_lines = AxisLines.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), width, height)
1625
+ new_axis_lines.base_z = @base_z
1626
+ new_axis_lines.gui_theme = @gui_theme
1627
+ add_child(new_axis_lines)
1628
+ new_axis_lines
1629
+ end
1630
+
1631
+ #
1632
+ # Add a child image widget using x, y positioning relative to this widget.
1633
+ #
1634
+ def add_image(filename, rel_x, rel_y)
1635
+ new_image = ImageWidget.new(x_pixel_to_screen(rel_x), y_pixel_to_screen(rel_y), img)
1636
+ new_image.base_z = @base_z
1637
+ new_image.gui_theme = @gui_theme
1638
+ add_child(new_image)
1639
+ new_image
1640
+ end
1641
+
1642
+ #
1643
+ # Add an overlay widget that is drawn on top of (at a higher z level) this widget
1644
+ #
1645
+ def add_overlay(overlay)
1646
+ overlay.base_z = @base_z + 10
1647
+ add_child(overlay)
1648
+ @overlay_widget = overlay
1649
+ end
1650
+
1651
+ # For all child widgets, adjust the x coordinate
1652
+ # so that they are centered.
1653
+ def center_children
1654
+ if @children.empty?
1655
+ return
1656
+ end
1657
+ number_of_children = @children.size
1658
+ total_width_of_children = 0
1659
+ @children.each do |child|
1660
+ total_width_of_children = total_width_of_children + child.width + 5
1661
+ end
1662
+ total_width_of_children = total_width_of_children - 5
1663
+
1664
+ start_x = (@width - total_width_of_children) / 2
1665
+ @children.each do |child|
1666
+ child.x = start_x
1667
+ start_x = start_x + child.width + 5
1668
+ end
1669
+ end
1670
+
1671
+ #
1672
+ # Override this method in your subclass to process mouse down events.
1673
+ # The base implementation is empty
1674
+ #
1675
+ def handle_mouse_down mouse_x, mouse_y
1676
+ # empty base implementation
149
1677
  end
150
1678
 
151
- def update update_count, mouse_x, mouse_y
1679
+ #
1680
+ # Override this method in your subclass to process mouse up events.
1681
+ # The base implementation is empty
1682
+ #
1683
+ def handle_mouse_up mouse_x, mouse_y
152
1684
  # empty base implementation
153
1685
  end
154
1686
 
155
- def button_down id, mouse_x, mouse_y
1687
+ #
1688
+ # Override this method in your subclass to process the right mouse click event.
1689
+ # Note we do not differentiate between up and down for the right mouse button.
1690
+ # The base implementation is empty
1691
+ #
1692
+ def handle_right_mouse mouse_x, mouse_y
156
1693
  # empty base implementation
157
1694
  end
158
1695
 
159
- def button_up id, mouse_x, mouse_y
1696
+ #
1697
+ # Override this method in your subclass to process keyboard events.
1698
+ # The base implementation is empty.
1699
+ # Note that the mouse was not necessarily positioned over this widget.
1700
+ # You can check this using the contains_click(mouse_x, mouse_y) method
1701
+ # and decide if you want to process the event based on that, if desired.
1702
+ #
1703
+ def handle_key_press id, mouse_x, mouse_y
160
1704
  # empty base implementation
161
1705
  end
1706
+
1707
+ #
1708
+ # Override this method in your subclass to perform any logic needed
1709
+ # as part of the main Gosu update loop. In most cases, this method is
1710
+ # invoked 60 times per second.
1711
+ #
1712
+ def handle_update update_count, mouse_x, mouse_y
1713
+ # empty base implementation
1714
+ end
1715
+
1716
+ #
1717
+ # Override this method in your subclass to perform any custom rendering logic.
1718
+ # Note that child widgets are automatically drawn and you do not need to do
1719
+ # that yourself.
1720
+ #
1721
+ def render
1722
+ # Base implementation is empty
1723
+ end
1724
+
1725
+ #
1726
+ # Return the relative z order compared to other widgets.
1727
+ # The absolute z order is the base plus this value.
1728
+ # Its calculated relative so that overlay widgets can be
1729
+ # on top of base displays.
1730
+ #
1731
+ def widget_z
1732
+ 0
1733
+ end
1734
+
1735
+ def intercept_widget_event(result)
1736
+ # Base implementation just relays the event
1737
+ result
1738
+ end
1739
+ end
1740
+
1741
+ #
1742
+ # A panel is simply an alias for a widget, although you can optionally
1743
+ # treat them differently if you wish. Generally a panel is used to
1744
+ # apply a specific layout to a sub-section of the screen.
1745
+ #
1746
+ class Panel < Widget
1747
+ def initialize(x, y, w, h, layout = nil, theme = nil)
1748
+ super(x, y, w, h, layout, theme)
1749
+ end
1750
+ end
1751
+
1752
+ #
1753
+ # Displays an image on the screen at the specific x, y location. The image
1754
+ # can be scaled by setting the scale attribute. The image attribute to the
1755
+ # construcor can be the string file location or a Gosu::Image instance
1756
+ #
1757
+ class ImageWidget < Widget
1758
+ attr_accessor :img
1759
+ attr_accessor :scale
1760
+
1761
+ def initialize(x, y, image, args = {})
1762
+ super(x, y)
1763
+ if image.is_a? String
1764
+ @img = Gosu::Image.new(image)
1765
+ elsif image.is_a? Gosu::Image
1766
+ @img = image
1767
+ else
1768
+ raise "ImageWidget requires either a filename or a Gosu::Image object"
1769
+ end
1770
+ if args[ARG_THEME]
1771
+ @gui_theme = args[ARG_THEME]
1772
+ end
1773
+ @scale = 1
1774
+ disable_border
1775
+ disable_background
1776
+ set_dimensions(@img.width, @img.height)
1777
+ end
1778
+
1779
+ def render
1780
+ @img.draw @x, @y, z_order, @scale, @scale
1781
+ end
1782
+
1783
+ def widget_z
1784
+ Z_ORDER_FOCAL_ELEMENTS
1785
+ end
162
1786
  end
163
1787
 
1788
+ #
1789
+ # Displays a text label on the screen at the specific x, y location.
1790
+ # The font specified by the current theme is used.
1791
+ # The theme text color is used, unless the color parameter specifies an override.
1792
+ # The small font is used by default, unless the use_large_font parameter is true.
1793
+ #
164
1794
  class Text < Widget
165
- attr_accessor :str
166
- def initialize(str, x, y, font, color = COLOR_WHITE)
167
- super(x, y, color)
168
- set_font(font)
169
- @str = str
1795
+ attr_accessor :label
1796
+
1797
+ def initialize(x, y, label, args = {})
1798
+ super(x, y)
1799
+ @label = label
1800
+ if args[ARG_THEME]
1801
+ @gui_theme = args[ARG_THEME]
1802
+ end
1803
+ if args[ARG_USE_LARGE_FONT]
1804
+ @use_large_font = args[ARG_USE_LARGE_FONT]
1805
+ end
1806
+ if args[ARG_COLOR]
1807
+ @override_color = args[ARG_COLOR]
1808
+ end
1809
+ disable_border
1810
+ if @use_large_font
1811
+ set_dimensions(@gui_theme.font_large.text_width(@label) + 10, 20)
1812
+ else
1813
+ set_dimensions(@gui_theme.font.text_width(@label) + 10, 20)
1814
+ end
1815
+ end
1816
+
1817
+ def set_text(new_text)
1818
+ @label = new_text
170
1819
  end
1820
+
1821
+ def change_text(new_text)
1822
+ set_text(new_text)
1823
+ end
1824
+
171
1825
  def render
172
- @font.draw_text(@str, @x, @y, Z_ORDER_TEXT, 1, 1, @color)
1826
+ if @use_large_font
1827
+ get_theme.font_large.draw_text(@label, @x, @y, z_order, 1, 1, text_color)
1828
+ else
1829
+ get_theme.font.draw_text(@label, @x, @y, z_order, 1, 1, text_color)
1830
+ end
1831
+ end
1832
+
1833
+ def widget_z
1834
+ Z_ORDER_TEXT
173
1835
  end
174
1836
  end
175
1837
 
1838
+ #
1839
+ # An ErrorMessage is a subclass of text that uses a red color
1840
+ #
176
1841
  class ErrorMessage < Text
177
- attr_accessor :str
178
- def initialize(str, x, y, font)
179
- super("ERROR: #{str}", x, y, font, COLOR_ERROR_CODE_RED)
180
- set_dimensions(@font.text_width(@str) + 4, 36)
1842
+ def initialize(x, y, message)
1843
+ super(x, y, "ERROR: #{message}", COLOR_ERROR_CODE_RED)
181
1844
  end
182
1845
  end
183
1846
 
1847
+ #
1848
+ # A data point to be used in a Plot widget. This object holds
1849
+ # the x, y screen location as well as the data values for x, y.
1850
+ #
184
1851
  class PlotPoint < Widget
1852
+ attr_accessor :data_x
1853
+ attr_accessor :data_y
185
1854
  attr_accessor :data_point_size
186
1855
 
187
- def initialize(x, y, color = COLOR_MAROON, size = 4)
188
- super(x, y, color)
1856
+ def initialize(x, y, data_x, data_y, color = COLOR_MAROON, size = 4)
1857
+ super(x, y)
1858
+ @override_color = color
1859
+ @data_x = data_x
1860
+ @data_y = data_y
189
1861
  @data_point_size = size
190
1862
  end
191
1863
 
192
- def render
193
- @half_size = @data_point_size / 2
194
- Gosu::draw_rect(@x - @half_size, @y - @half_size,
195
- @data_point_size, @data_point_size,
196
- @color, Z_ORDER_PLOT_POINTS)
1864
+ def render(override_size = nil)
1865
+ size_to_draw = @data_point_size
1866
+ if override_size
1867
+ size_to_draw = override_size
1868
+ end
1869
+ half_size = size_to_draw / 2
1870
+ Gosu::draw_rect(@x - half_size, @y - half_size,
1871
+ size_to_draw, size_to_draw,
1872
+ graphics_color, z_order)
197
1873
  end
198
1874
 
1875
+ def widget_z
1876
+ Z_ORDER_PLOT_POINTS
1877
+ end
1878
+
199
1879
  def to_display
200
1880
  "#{@x}, #{@y}"
201
1881
  end
@@ -211,102 +1891,169 @@ module Wads
211
1891
  end
212
1892
  end
213
1893
 
1894
+ #
1895
+ # Displays a button at the specified x, y location.
1896
+ # The button width is based on the label text unless specified
1897
+ # using the optional parameter. The code to executeon a button
1898
+ # click is specified using the set_action method, however typical
1899
+ # using involves the widget or layout form of add_button. For example:
1900
+ # add_button("Test Button", 10, 10) do
1901
+ # puts "User hit the test button"
1902
+ # end
1903
+
214
1904
  class Button < Widget
215
1905
  attr_accessor :label
216
1906
  attr_accessor :is_pressed
1907
+ attr_accessor :action_code
217
1908
 
218
- def initialize(label, x, y, font, width = nil, color = COLOR_DARK_GRAY, text_color = COLOR_HEADER_BRIGHT_BLUE)
219
- super(x, y, color)
220
- set_font(font)
1909
+ def initialize(x, y, label, args = {})
1910
+ super(x, y)
221
1911
  @label = label
222
- @text_pixel_width = @font.text_width(@label)
223
- if width.nil?
224
- @width = @text_pixel_width + 10
1912
+ if args[ARG_THEME]
1913
+ @gui_theme = args[ARG_THEME]
1914
+ end
1915
+ @text_pixel_width = @gui_theme.font.text_width(@label)
1916
+ if args[ARG_DESIRED_WIDTH]
1917
+ @width = args[ARG_DESIRED_WIDTH]
225
1918
  else
226
- @width = width
1919
+ @width = @text_pixel_width + 10
227
1920
  end
228
1921
  @height = 26
229
1922
  @is_pressed = false
230
- @text_color = text_color
1923
+ @is_pressed_update_count = -100
231
1924
  end
232
1925
 
233
1926
  def render
234
- draw_border(@color)
235
1927
  text_x = center_x - (@text_pixel_width / 2)
236
- @font.draw_text(@label, text_x, @y, Z_ORDER_TEXT, 1, 1, @text_color)
1928
+ @gui_theme.font.draw_text(@label, text_x, @y, z_order, 1, 1, text_color)
1929
+ end
1930
+
1931
+ def widget_z
1932
+ Z_ORDER_TEXT
1933
+ end
1934
+
1935
+ def set_action(&block)
1936
+ @action_code = block
1937
+ end
1938
+
1939
+ def handle_mouse_down mouse_x, mouse_y
1940
+ @is_pressed = true
1941
+ if @action_code
1942
+ @action_code.call
1943
+ end
1944
+ end
1945
+
1946
+ def handle_update update_count, mouse_x, mouse_y
1947
+ if @is_pressed
1948
+ @is_pressed_update_count = update_count
1949
+ @is_pressed = false
1950
+ end
1951
+
1952
+ if update_count < @is_pressed_update_count + 15
1953
+ unset_selected
1954
+ elsif contains_click(mouse_x, mouse_y)
1955
+ set_selected
1956
+ else
1957
+ unset_selected
1958
+ end
1959
+ end
1960
+ end
1961
+
1962
+ #
1963
+ # A subclass of button that renders a red X instead of label text
1964
+ #
1965
+ class DeleteButton < Button
1966
+ def initialize(x, y, args = {})
1967
+ super(x, y, "ignore", {ARG_DESIRED_WIDTH => 50}.merge(args))
1968
+ set_dimensions(14, 14)
1969
+ add_child(Line.new(@x, @y, right_edge, bottom_edge, COLOR_ERROR_CODE_RED))
1970
+ add_child(Line.new(@x, bottom_edge, right_edge, @y, COLOR_ERROR_CODE_RED))
1971
+ end
1972
+
1973
+ def render
1974
+ # do nothing, just override the parent so we don't draw a label
237
1975
  end
238
1976
  end
239
1977
 
1978
+ #
1979
+ # Displays multiple lines of text content at the specified coordinates
1980
+ #
240
1981
  class Document < Widget
241
1982
  attr_accessor :lines
242
1983
 
243
- def initialize(content, x, y, width, height, font)
244
- super(x, y, COLOR_GRAY)
245
- set_font(font)
1984
+ def initialize(x, y, width, height, content, args = {})
1985
+ super(x, y)
246
1986
  set_dimensions(width, height)
247
1987
  @lines = content.split("\n")
1988
+ disable_border
1989
+ if args[ARG_THEME]
1990
+ @gui_theme = args[ARG_THEME]
1991
+ end
248
1992
  end
249
1993
 
250
1994
  def render
251
1995
  y = @y + 4
252
1996
  @lines.each do |line|
253
- @font.draw_text(line, @x + 5, y, Z_ORDER_TEXT, 1, 1, COLOR_WHITE)
1997
+ @gui_theme.font.draw_text(line, @x + 5, y, z_order, 1, 1, text_color)
254
1998
  y = y + 26
255
1999
  end
256
2000
  end
2001
+
2002
+ def widget_z
2003
+ Z_ORDER_TEXT
2004
+ end
257
2005
  end
258
2006
 
259
2007
  class InfoBox < Widget
260
- def initialize(title, content, x, y, font, width, height)
2008
+ def initialize(x, y, width, height, title, content, args = {})
261
2009
  super(x, y)
262
- set_font(font)
263
2010
  set_dimensions(width, height)
264
- set_border(COLOR_WHITE)
265
- @title = title
266
- add_child(Text.new(title, x + 5, y + 5, Gosu::Font.new(32)))
267
- add_child(Document.new(content, x + 5, y + 52, width, height, font))
268
- @ok_button = Button.new("OK", center_x - 50, bottom_edge - 26, @font, 100, COLOR_FORM_BUTTON)
269
- add_child(@ok_button)
270
- set_background(COLOR_GRAY)
2011
+ @base_z = 10
2012
+ if args[ARG_THEME]
2013
+ @gui_theme = args[ARG_THEME]
2014
+ end
2015
+ add_text(title, 5, 5)
2016
+ add_document(content, 5, 52, width, height - 52)
2017
+ ok_button = add_button("OK", (@width / 2) - 50, height - 26) do
2018
+ WidgetResult.new(true)
2019
+ end
2020
+ ok_button.width = 100
271
2021
  end
272
2022
 
273
- def button_down id, mouse_x, mouse_y
2023
+ def handle_key_press id, mouse_x, mouse_y
274
2024
  if id == Gosu::KbEscape
275
2025
  return WidgetResult.new(true)
276
- elsif id == Gosu::MsLeft
277
- if @ok_button.contains_click(mouse_x, mouse_y)
278
- return WidgetResult.new(true)
279
- end
280
2026
  end
281
- WidgetResult.new(false)
282
- end
2027
+ end
283
2028
  end
284
2029
 
285
2030
  class Dialog < Widget
286
2031
  attr_accessor :textinput
287
2032
 
288
- def initialize(window, font, x, y, width, height, title, text_input_default)
289
- super(x, y)
290
- @window = window
291
- set_font(font)
292
- set_dimensions(width, height)
293
- set_background(0xff566573 )
294
- set_border(COLOR_WHITE)
2033
+ def initialize(x, y, width, height, title, text_input_default)
2034
+ super(x, y, width, height)
2035
+ @base_z = 10
295
2036
  @error_message = nil
296
2037
 
297
- add_child(Text.new(title, x + 5, y + 5, @font))
2038
+ add_text(title, 5, 5)
298
2039
  # Forms automatically have some explanatory content
299
- add_child(Document.new(content, x, y + 56, width, height, font))
2040
+ add_document(content, 0, 56, width, height)
300
2041
 
301
2042
  # Forms automatically get a text input widget
302
- @textinput = TextField.new(@window, @font, x + 10, bottom_edge - 80, text_input_default, 600)
2043
+ @textinput = TextField.new(x + 10, bottom_edge - 80, text_input_default, 600)
2044
+ @textinput.base_z = 10
303
2045
  add_child(@textinput)
304
2046
 
305
2047
  # Forms automatically get OK and Cancel buttons
306
- @ok_button = Button.new("OK", center_x - 100, bottom_edge - 26, @font, 100, COLOR_FORM_BUTTON, COLOR_WHITE)
307
- @cancel_button = Button.new("Cancel", center_x + 50, bottom_edge - 26, @font, 100, COLOR_FORM_BUTTON, COLOR_WHITE)
308
- add_child(@ok_button)
309
- add_child(@cancel_button)
2048
+ ok_button = add_button("OK", (@width / 2) - 100, height - 32) do
2049
+ handle_ok
2050
+ end
2051
+ ok_button.width = 100
2052
+
2053
+ cancel_button = add_button("Cancel", (@width / 2) + 50, height - 32) do
2054
+ WidgetResult.new(true)
2055
+ end
2056
+ cancel_button.width = 100
310
2057
  end
311
2058
 
312
2059
  def content
@@ -317,7 +2064,8 @@ module Wads
317
2064
  end
318
2065
 
319
2066
  def add_error_message(msg)
320
- @error_message = ErrorMessage.new(msg, x + 10, bottom_edge - 120, @font)
2067
+ @error_message = ErrorMessage.new(x + 10, bottom_edge - 120, msg)
2068
+ @error_message.base_z = @base_z
321
2069
  end
322
2070
 
323
2071
  def render
@@ -329,21 +2077,7 @@ module Wads
329
2077
  def handle_ok
330
2078
  # Default behavior is to do nothing except tell the caller to
331
2079
  # close the dialog
332
- return WidgetResult.new(true)
333
- end
334
-
335
- def handle_cancel
336
- # Default behavior is to do nothing except tell the caller to
337
- # close the dialog
338
- return WidgetResult.new(true)
339
- end
340
-
341
- def handle_up(mouse_x, mouse_y)
342
- # empty implementation of up arrow
343
- end
344
-
345
- def handle_down(mouse_x, mouse_y)
346
- # empty implementation of down arrow
2080
+ return WidgetResult.new(true, EVENT_OK)
347
2081
  end
348
2082
 
349
2083
  def handle_mouse_click(mouse_x, mouse_y)
@@ -351,40 +2085,30 @@ module Wads
351
2085
  # of standard form elements in this dialog
352
2086
  end
353
2087
 
354
- def text_input_updated(text)
355
- # empty implementation of text being updated
356
- # in text widget
2088
+ def handle_mouse_down mouse_x, mouse_y
2089
+ # Mouse click: Select text field based on mouse position.
2090
+ WadsConfig.instance.get_window.text_input = [@textinput].find { |tf| tf.under_point?(mouse_x, mouse_y) }
2091
+ # Advanced: Move caret to clicked position
2092
+ WadsConfig.instance.get_window.text_input.move_caret(mouse_x) unless WadsConfig.instance.get_window.text_input.nil?
2093
+
2094
+ handle_mouse_click(mouse_x, mouse_y)
357
2095
  end
358
2096
 
359
- def button_down id, mouse_x, mouse_y
2097
+ def handle_key_press id, mouse_x, mouse_y
360
2098
  if id == Gosu::KbEscape
361
2099
  return WidgetResult.new(true)
362
- elsif id == Gosu::KbUp
363
- handle_up(mouse_x, mouse_y)
364
- elsif id == Gosu::KbDown
365
- handle_down(mouse_x, mouse_y)
366
- elsif id == Gosu::MsLeft
367
- if @ok_button.contains_click(mouse_x, mouse_y)
368
- return handle_ok
369
- elsif @cancel_button.contains_click(mouse_x, mouse_y)
370
- return handle_cancel
371
- else
372
- # Mouse click: Select text field based on mouse position.
373
- @window.text_input = [@textinput].find { |tf| tf.under_point?(mouse_x, mouse_y) }
374
- # Advanced: Move caret to clicked position
375
- @window.text_input.move_caret(mouse_x) unless @window.text_input.nil?
376
-
377
- handle_mouse_click(mouse_x, mouse_y)
378
- end
379
- else
380
- if @window.text_input
381
- text_input_updated(@textinput.text)
382
- end
383
2100
  end
384
- WidgetResult.new(false)
385
2101
  end
386
2102
  end
387
2103
 
2104
+ #
2105
+ # A result object returned from handle methods that instructs the parent widget
2106
+ # what to do. A close_widget value of true instructs the recipient to close
2107
+ # either the overlay window or the entire app, based on the context of the receiver.
2108
+ # In the case of a form being submitted, the action may be "OK" and the form_data
2109
+ # contains the information supplied by the user.
2110
+ # WidgetResult is intentionally generic so it can support a wide variety of use cases.
2111
+ #
388
2112
  class WidgetResult
389
2113
  attr_accessor :close_widget
390
2114
  attr_accessor :action
@@ -397,83 +2121,145 @@ module Wads
397
2121
  end
398
2122
  end
399
2123
 
2124
+ #
2125
+ # Renders a line from x, y to x2, y2. The theme graphics elements color
2126
+ # is used by default, unless specified using the optional parameter.
2127
+ #
400
2128
  class Line < Widget
401
2129
  attr_accessor :x2
402
2130
  attr_accessor :y2
403
2131
 
404
- def initialize(x, y, x2, y2, color = COLOR_CYAN)
405
- super x, y, color
406
- @x2 = x2
407
- @y2 = y2
2132
+ def initialize(x, y, x2, y2, color = nil)
2133
+ super(x, y)
2134
+ @override_color = color
2135
+ @x2 = x2
2136
+ @y2 = y2
2137
+ disable_border
2138
+ disable_background
2139
+ end
2140
+
2141
+ def render
2142
+ Gosu::draw_line x, y, graphics_color, x2, y2, graphics_color, z_order
2143
+ end
2144
+
2145
+ def widget_z
2146
+ Z_ORDER_GRAPHIC_ELEMENTS
408
2147
  end
409
2148
 
410
- def render
411
- Gosu::draw_line x, y, @color, x2, y2, @color, Z_ORDER_GRAPHIC_ELEMENTS
2149
+ def uses_layout
2150
+ false
412
2151
  end
413
2152
  end
414
2153
 
2154
+ #
2155
+ # A very specific widget used along with a Plot to draw the x and y axis lines.
2156
+ # Note that the labels are drawn using separate widgets.
2157
+ #
415
2158
  class AxisLines < Widget
416
- def initialize(x, y, width, height, color = COLOR_CYAN)
417
- super x, y, color
418
- @width = width
419
- @height = height
2159
+ def initialize(x, y, width, height, color = nil)
2160
+ super(x, y)
2161
+ set_dimensions(width, height)
2162
+ disable_border
2163
+ disable_background
420
2164
  end
421
2165
 
422
2166
  def render
423
- add_child(Line.new(@x, @y, @x, bottom_edge, @color))
424
- add_child(Line.new(@x, bottom_edge, right_edge, bottom_edge, @color))
2167
+ add_child(Line.new(@x, @y, @x, bottom_edge, graphics_color))
2168
+ add_child(Line.new(@x, bottom_edge, right_edge, bottom_edge, graphics_color))
2169
+ end
2170
+
2171
+ def uses_layout
2172
+ false
425
2173
  end
426
2174
  end
427
2175
 
2176
+ #
2177
+ # Labels and tic marks for the vertical axis on a plot
2178
+ #
428
2179
  class VerticalAxisLabel < Widget
429
2180
  attr_accessor :label
430
2181
 
431
- def initialize(x, y, label, font, color = COLOR_CYAN)
432
- super x, y, color
433
- set_font(font)
2182
+ def initialize(x, y, label, color = nil)
2183
+ super(x, y)
434
2184
  @label = label
2185
+ @override_color = color
2186
+ text_pixel_width = @gui_theme.font.text_width(@label)
2187
+ add_text(@label, -text_pixel_width - 28, -12)
2188
+ disable_border
2189
+ disable_background
435
2190
  end
436
2191
 
437
2192
  def render
438
- text_pixel_width = @font.text_width(@label)
439
- Gosu::draw_line @x - 20, @y, @color,
440
- @x, @y, @color, Z_ORDER_GRAPHIC_ELEMENTS
441
-
442
- @font.draw_text(@label, @x - text_pixel_width - 28, @y - 12, 1, 1, 1, @color)
2193
+ Gosu::draw_line @x - 20, @y, graphics_color,
2194
+ @x, @y, graphics_color, z_order
2195
+ end
2196
+
2197
+ def widget_z
2198
+ Z_ORDER_GRAPHIC_ELEMENTS
2199
+ end
2200
+
2201
+ def uses_layout
2202
+ false
443
2203
  end
444
2204
  end
445
2205
 
2206
+ #
2207
+ # Labels and tic marks for the horizontal axis on a plot
2208
+ #
446
2209
  class HorizontalAxisLabel < Widget
447
2210
  attr_accessor :label
448
2211
 
449
- def initialize(x, y, label, font, color = COLOR_CYAN)
450
- super x, y, color
451
- set_font(font)
2212
+ def initialize(x, y, label, color = nil)
2213
+ super(x, y)
452
2214
  @label = label
2215
+ @override_color = color
2216
+ text_pixel_width = @gui_theme.font.text_width(@label)
2217
+ add_text(@label, -(text_pixel_width / 2), 26)
2218
+ disable_border
2219
+ disable_background
453
2220
  end
454
2221
 
455
2222
  def render
456
- text_pixel_width = @font.text_width(@label)
457
- Gosu::draw_line @x, @y, @color, @x, @y + 20, @color
458
- @font.draw_text(@label, @x - (text_pixel_width / 2), @y + 26, Z_ORDER_TEXT, 1, 1, @color)
2223
+ Gosu::draw_line @x, @y, graphics_color, @x, @y + 20, graphics_color, z_order
2224
+ end
2225
+
2226
+ def widget_z
2227
+ Z_ORDER_TEXT
2228
+ end
2229
+
2230
+ def uses_layout
2231
+ false
459
2232
  end
460
2233
  end
461
2234
 
2235
+ #
2236
+ # Displays a table of information at the given coordinates.
2237
+ # The headers are an array of text labels to display at the top of each column.
2238
+ # The max_visible_rows specifies how many rows are visible at once.
2239
+ # If there are more data rows than the max, the arrow keys can be used to
2240
+ # page up or down through the rows in the table.
2241
+ #
462
2242
  class Table < Widget
463
2243
  attr_accessor :data_rows
464
2244
  attr_accessor :row_colors
465
2245
  attr_accessor :headers
466
2246
  attr_accessor :max_visible_rows
467
2247
  attr_accessor :current_row
2248
+ attr_accessor :can_delete_rows
468
2249
 
469
- def initialize(x, y, width, height, headers, font, color = COLOR_GRAY, max_visible_rows = 10)
470
- super(x, y, color)
471
- set_font(font)
2250
+ def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
2251
+ super(x, y)
472
2252
  set_dimensions(width, height)
2253
+ if args[ARG_THEME]
2254
+ @gui_theme = args[ARG_THEME]
2255
+ end
473
2256
  @headers = headers
474
2257
  @current_row = 0
475
2258
  @max_visible_rows = max_visible_rows
476
- clear_rows
2259
+ clear_rows
2260
+ @can_delete_rows = false
2261
+ @delete_buttons = []
2262
+ @next_delete_button_y = 38
477
2263
  end
478
2264
 
479
2265
  def scroll_up
@@ -493,11 +2279,50 @@ module Wads
493
2279
  @row_colors = []
494
2280
  end
495
2281
 
496
- def add_row(data_row, color = @color)
2282
+ def add_row(data_row, color = text_color )
497
2283
  @data_rows << data_row
498
2284
  @row_colors << color
499
2285
  end
500
2286
 
2287
+ def add_table_delete_button
2288
+ if @delete_buttons.size < @max_visible_rows
2289
+ new_button = add_delete_button(@width - 18, @next_delete_button_y) do
2290
+ # nothing to do here, handled in parent widget by event
2291
+ end
2292
+ @delete_buttons << new_button
2293
+ @next_delete_button_y = @next_delete_button_y + 30
2294
+ end
2295
+ end
2296
+
2297
+ def remove_table_delete_button
2298
+ if not @delete_buttons.empty?
2299
+ @delete_buttons.pop
2300
+ @children.pop
2301
+ @next_delete_button_y = @next_delete_button_y - 30
2302
+ end
2303
+ end
2304
+
2305
+ def handle_update update_count, mouse_x, mouse_y
2306
+ # How many visible data rows are there
2307
+ if @can_delete_rows
2308
+ number_of_visible_rows = @data_rows.size - @current_row
2309
+ if number_of_visible_rows > @max_visible_rows
2310
+ number_of_visible_rows = @max_visible_rows
2311
+ end
2312
+ if number_of_visible_rows > @delete_buttons.size
2313
+ number_to_add = number_of_visible_rows - @delete_buttons.size
2314
+ number_to_add.times do
2315
+ add_table_delete_button
2316
+ end
2317
+ elsif number_of_visible_rows < @delete_buttons.size
2318
+ number_to_remove = @delete_buttons.size - number_of_visible_rows
2319
+ number_to_remove.times do
2320
+ remove_table_delete_button
2321
+ end
2322
+ end
2323
+ end
2324
+ end
2325
+
501
2326
  def number_of_rows
502
2327
  @data_rows.size
503
2328
  end
@@ -509,9 +2334,9 @@ module Wads
509
2334
  column_widths = []
510
2335
  number_of_columns = @data_rows[0].size
511
2336
  (0..number_of_columns-1).each do |c|
512
- max_length = @font.text_width(headers[c])
2337
+ max_length = @gui_theme.font.text_width(headers[c])
513
2338
  (0..number_of_rows-1).each do |r|
514
- text_pixel_width = @font.text_width(@data_rows[r][c])
2339
+ text_pixel_width = @gui_theme.font.text_width(@data_rows[r][c])
515
2340
  if text_pixel_width > max_length
516
2341
  max_length = text_pixel_width
517
2342
  end
@@ -519,18 +2344,22 @@ module Wads
519
2344
  column_widths[c] = max_length
520
2345
  end
521
2346
 
2347
+ # Draw a horizontal line between header and data rows
522
2348
  x = @x + 10
523
2349
  if number_of_columns > 1
524
2350
  (0..number_of_columns-2).each do |c|
525
2351
  x = x + column_widths[c] + 20
526
- Gosu::draw_line x, @y, @color, x, @y + @height, @color, Z_ORDER_GRAPHIC_ELEMENTS
2352
+ Gosu::draw_line x, @y, graphics_color, x, @y + @height, graphics_color, z_order
527
2353
  end
528
2354
  end
529
2355
 
530
- y = @y
2356
+ # Draw the header row
2357
+ y = @y
2358
+ Gosu::draw_rect(@x + 1, y, @width - 3, 28, graphics_color, relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
2359
+
531
2360
  x = @x + 20
532
2361
  (0..number_of_columns-1).each do |c|
533
- @font.draw_text(@headers[c], x, y, Z_ORDER_TEXT, 1, 1, @color)
2362
+ @gui_theme.font.draw_text(@headers[c], x, y + 3, z_order, 1, 1, text_color)
534
2363
  x = x + column_widths[c] + 20
535
2364
  end
536
2365
  y = y + 30
@@ -542,7 +2371,7 @@ module Wads
542
2371
  elsif count < @current_row + @max_visible_rows
543
2372
  x = @x + 20
544
2373
  (0..number_of_columns-1).each do |c|
545
- @font.draw_text(row[c], x, y + 2, Z_ORDER_TEXT, 1, 1, @row_colors[count])
2374
+ @gui_theme.font.draw_text(row[c], x, y + 2, z_order, 1, 1, @row_colors[count])
546
2375
  x = x + column_widths[c] + 20
547
2376
  end
548
2377
  y = y + 30
@@ -559,15 +2388,35 @@ module Wads
559
2388
  end
560
2389
  row_number
561
2390
  end
2391
+
2392
+ def widget_z
2393
+ Z_ORDER_TEXT
2394
+ end
2395
+
2396
+ def uses_layout
2397
+ false
2398
+ end
562
2399
  end
563
2400
 
2401
+ #
2402
+ # A table where the user can select one row at a time.
2403
+ # The selected row has a background color specified by the selection color of the
2404
+ # current theme.
2405
+ #
564
2406
  class SingleSelectTable < Table
565
2407
  attr_accessor :selected_row
566
- attr_accessor :selected_color
567
2408
 
568
- def initialize(x, y, width, height, headers, font, color = COLOR_GRAY, max_visible_rows = 10)
569
- super(x, y, width, height, headers, font, color, max_visible_rows)
570
- @selected_color = COLOR_BLACK
2409
+ def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
2410
+ super(x, y, width, height, headers, max_visible_rows, args)
2411
+ end
2412
+
2413
+ def is_row_selected(mouse_y)
2414
+ row_number = determine_row_number(mouse_y)
2415
+ if row_number.nil?
2416
+ return false
2417
+ end
2418
+ selected_row = @current_row + row_number
2419
+ @selected_row == selected_row
571
2420
  end
572
2421
 
573
2422
  def set_selected_row(mouse_y, column_number)
@@ -584,29 +2433,81 @@ module Wads
584
2433
  end
585
2434
  end
586
2435
 
2436
+ def unset_selected_row(mouse_y, column_number)
2437
+ row_number = determine_row_number(mouse_y)
2438
+ if not row_number.nil?
2439
+ this_selected_row = @current_row + row_number
2440
+ @selected_row = this_selected_row
2441
+ return @data_rows[this_selected_row][column_number]
2442
+ end
2443
+ nil
2444
+ end
2445
+
587
2446
  def render
588
2447
  super
589
2448
  if @selected_row
590
2449
  if @selected_row >= @current_row and @selected_row < @current_row + @max_visible_rows
591
2450
  y = @y + 30 + ((@selected_row - @current_row) * 30)
592
- Gosu::draw_rect(@x + 20, y, @width - 30, 28, @selected_color, Z_ORDER_SELECTION_BACKGROUND)
2451
+ Gosu::draw_rect(@x + 20, y, @width - 30, 28, @gui_theme.selection_color, relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
2452
+ end
2453
+ end
2454
+ end
2455
+
2456
+ def widget_z
2457
+ Z_ORDER_TEXT
2458
+ end
2459
+
2460
+ def handle_mouse_down mouse_x, mouse_y
2461
+ if contains_click(mouse_x, mouse_y)
2462
+ row_number = determine_row_number(mouse_y)
2463
+ if row_number.nil?
2464
+ return WidgetResult.new(false)
2465
+ end
2466
+ # First check if its the delete button that got this
2467
+ delete_this_row = false
2468
+ @delete_buttons.each do |db|
2469
+ if db.contains_click(mouse_x, mouse_y)
2470
+ delete_this_row = true
2471
+ end
593
2472
  end
2473
+ if delete_this_row
2474
+ if not row_number.nil?
2475
+ data_set_row_to_delete = @current_row + row_number
2476
+ data_set_name_to_delete = @data_rows[data_set_row_to_delete][1]
2477
+ @data_rows.delete_at(data_set_row_to_delete)
2478
+ return WidgetResult.new(false, EVENT_TABLE_ROW_DELETE, [data_set_name_to_delete])
2479
+ end
2480
+ else
2481
+ if is_row_selected(mouse_y)
2482
+ unset_selected_row(mouse_y, 0)
2483
+ return WidgetResult.new(false, EVENT_TABLE_UNSELECT, @data_rows[row_number])
2484
+ else
2485
+ set_selected_row(mouse_y, 0)
2486
+ return WidgetResult.new(false, EVENT_TABLE_SELECT, @data_rows[row_number])
2487
+ end
2488
+ end
594
2489
  end
595
2490
  end
596
2491
  end
597
2492
 
2493
+ #
2494
+ # A table where the user can select multiple rows at a time.
2495
+ # Selected rows have a background color specified by the selection color of the
2496
+ # current theme.
2497
+ #
598
2498
  class MultiSelectTable < Table
599
2499
  attr_accessor :selected_rows
600
- attr_accessor :selection_color
601
2500
 
602
- def initialize(x, y, width, height, headers, font, color = COLOR_GRAY, max_visible_rows = 10)
603
- super(x, y, width, height, headers, font, color, max_visible_rows)
2501
+ def initialize(x, y, width, height, headers, max_visible_rows = 10, args = {})
2502
+ super(x, y, width, height, headers, max_visible_rows, args)
604
2503
  @selected_rows = []
605
- @selection_color = COLOR_LIGHT_GRAY
606
- end
607
-
2504
+ end
2505
+
608
2506
  def is_row_selected(mouse_y)
609
2507
  row_number = determine_row_number(mouse_y)
2508
+ if row_number.nil?
2509
+ return false
2510
+ end
610
2511
  @selected_rows.include?(@current_row + row_number)
611
2512
  end
612
2513
 
@@ -635,61 +2536,112 @@ module Wads
635
2536
  y = @y + 30
636
2537
  row_count = @current_row
637
2538
  while row_count < @data_rows.size
638
- if @selected_rows.include? row_count
639
- Gosu::draw_rect(@x + 20, y, @width - 3, 28, @selection_color, Z_ORDER_SELECTION_BACKGROUND)
2539
+ if @selected_rows.include? row_count
2540
+ width_of_selection_background = @width - 30
2541
+ if @can_delete_rows
2542
+ width_of_selection_background = width_of_selection_background - 20
2543
+ end
2544
+ Gosu::draw_rect(@x + 20, y, width_of_selection_background, 28,
2545
+ @gui_theme.selection_color,
2546
+ relative_z_order(Z_ORDER_SELECTION_BACKGROUND))
640
2547
  end
641
2548
  y = y + 30
642
2549
  row_count = row_count + 1
643
2550
  end
644
2551
  end
2552
+
2553
+ def widget_z
2554
+ Z_ORDER_TEXT
2555
+ end
2556
+
2557
+ def handle_mouse_down mouse_x, mouse_y
2558
+ if contains_click(mouse_x, mouse_y)
2559
+ row_number = determine_row_number(mouse_y)
2560
+ if row_number.nil?
2561
+ return WidgetResult.new(false)
2562
+ end
2563
+ # First check if its the delete button that got this
2564
+ delete_this_row = false
2565
+ @delete_buttons.each do |db|
2566
+ if db.contains_click(mouse_x, mouse_y)
2567
+ delete_this_row = true
2568
+ end
2569
+ end
2570
+ if delete_this_row
2571
+ if not row_number.nil?
2572
+ data_set_row_to_delete = @current_row + row_number
2573
+ data_set_name_to_delete = @data_rows[data_set_row_to_delete][1]
2574
+ @data_rows.delete_at(data_set_row_to_delete)
2575
+ return WidgetResult.new(false, EVENT_TABLE_ROW_DELETE, [data_set_name_to_delete])
2576
+ end
2577
+ else
2578
+ if is_row_selected(mouse_y)
2579
+ unset_selected_row(mouse_y, 0)
2580
+ return WidgetResult.new(false, EVENT_TABLE_UNSELECT, @data_rows[row_number])
2581
+ else
2582
+ set_selected_row(mouse_y, 0)
2583
+ return WidgetResult.new(false, EVENT_TABLE_SELECT, @data_rows[row_number])
2584
+ end
2585
+ end
2586
+ end
2587
+ end
645
2588
  end
646
2589
 
2590
+ #
2591
+ # A two-dimensional graph display which plots a number of PlotPoint objects.
2592
+ # Options include grid lines that can be displayed, as well as whether lines
2593
+ # should be drawn connecting each point in a data set.
2594
+ #
647
2595
  class Plot < Widget
648
2596
  attr_accessor :points
649
2597
  attr_accessor :visible_range
650
2598
  attr_accessor :display_grid
651
2599
  attr_accessor :display_lines
652
2600
  attr_accessor :zoom_level
2601
+ attr_accessor :visibility_map
653
2602
 
654
- def initialize(x, y, width, height, font)
655
- super x, y, color
656
- set_font(font)
2603
+ def initialize(x, y, width, height)
2604
+ super(x, y)
657
2605
  set_dimensions(width, height)
658
2606
  @display_grid = false
659
2607
  @display_lines = true
660
- @data_set_hash = {}
661
- @grid_line_color = COLOR_CYAN
2608
+ @grid_line_color = COLOR_LIGHT_GRAY
662
2609
  @cursor_line_color = COLOR_DARK_GRAY
663
- @zero_line_color = COLOR_BLUE
2610
+ @zero_line_color = COLOR_HEADER_BRIGHT_BLUE
664
2611
  @zoom_level = 1
2612
+ @data_point_size = 4
2613
+ # Hash of rendered points keyed by data set name, so we can toggle visibility
2614
+ @points_by_data_set_name = {}
2615
+ @visibility_map = {}
2616
+ disable_border
665
2617
  end
666
2618
 
667
- def increase_data_point_size
668
- @data_set_hash.keys.each do |key|
669
- data_set = @data_set_hash[key]
670
- data_set.rendered_points.each do |point|
671
- point.increase_size
672
- end
2619
+ def toggle_visibility(data_set_name)
2620
+ is_visible = @visibility_map[data_set_name]
2621
+ if is_visible.nil?
2622
+ return
673
2623
  end
2624
+ @visibility_map[data_set_name] = !is_visible
2625
+ end
2626
+
2627
+ def increase_data_point_size
2628
+ @data_point_size = @data_point_size + 2
674
2629
  end
675
2630
 
676
2631
  def decrease_data_point_size
677
- @data_set_hash.keys.each do |key|
678
- data_set = @data_set_hash[key]
679
- data_set.rendered_points.each do |point|
680
- point.decrease_size
681
- end
2632
+ if @data_point_size > 2
2633
+ @data_point_size = @data_point_size - 2
682
2634
  end
683
2635
  end
684
2636
 
685
2637
  def zoom_out
686
- @zoom_level = @zoom_level + 0.1
2638
+ @zoom_level = @zoom_level + 0.15
687
2639
  visible_range.scale(@zoom_level)
688
2640
  end
689
2641
 
690
2642
  def zoom_in
691
2643
  if @zoom_level > 0.11
692
- @zoom_level = @zoom_level - 0.1
2644
+ @zoom_level = @zoom_level - 0.15
693
2645
  end
694
2646
  visible_range.scale(@zoom_level)
695
2647
  end
@@ -713,11 +2665,6 @@ module Wads
713
2665
  def define_range(range)
714
2666
  @visible_range = range
715
2667
  @zoom_level = 1
716
- @data_set_hash.keys.each do |key|
717
- data_set = @data_set_hash[key]
718
- puts "Calling derive values on #{key}"
719
- data_set.derive_values(range, @data_set_hash)
720
- end
721
2668
  end
722
2669
 
723
2670
  def range_set?
@@ -725,24 +2672,43 @@ module Wads
725
2672
  end
726
2673
 
727
2674
  def is_on_screen(point)
728
- 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
2675
+ 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
729
2676
  end
730
2677
 
731
- def add_data_set(data_set)
2678
+ def add_data_point(data_set_name, data_x, data_y, color = COLOR_MAROON)
732
2679
  if range_set?
733
- @data_set_hash[data_set.name] = data_set
734
- data_set.clear_rendered_points
735
- data_set.derive_values(@visible_range, @data_set_hash)
736
- data_set.data_points.each do |point|
737
- if is_on_screen(point)
738
- data_set.add_rendered_point PlotPoint.new(draw_x(point.x), draw_y(point.y), data_set.color, data_set.data_point_size)
739
- end
2680
+ rendered_points = @points_by_data_set_name[data_set_name]
2681
+ if rendered_points.nil?
2682
+ rendered_points = []
2683
+ @points_by_data_set_name[data_set_name] = rendered_points
2684
+ end
2685
+ rendered_points << PlotPoint.new(draw_x(data_x), draw_y(data_y),
2686
+ data_x, data_y,
2687
+ color)
2688
+ if @visibility_map[data_set_name].nil?
2689
+ @visibility_map[data_set_name] = true
740
2690
  end
741
2691
  else
742
- puts "ERROR: range not set, cannot add data"
2692
+ error("ERROR: range not set, cannot add data")
743
2693
  end
744
2694
  end
745
2695
 
2696
+ def add_data_set(data_set_name, rendered_points)
2697
+ if range_set?
2698
+ @points_by_data_set_name[data_set_name] = rendered_points
2699
+ if @visibility_map[data_set_name].nil?
2700
+ @visibility_map[data_set_name] = true
2701
+ end
2702
+ else
2703
+ error("ERROR: range not set, cannot add data")
2704
+ end
2705
+ end
2706
+
2707
+ def remove_data_set(data_set_name)
2708
+ @points_by_data_set_name.delete(data_set_name)
2709
+ @visibility_map.delete(data_set_name)
2710
+ end
2711
+
746
2712
  def x_val_to_pixel(val)
747
2713
  x_pct = (@visible_range.right_x - val).to_f / @visible_range.x_range
748
2714
  @width - (@width.to_f * x_pct).round
@@ -753,14 +2719,6 @@ module Wads
753
2719
  (@height.to_f * y_pct).round
754
2720
  end
755
2721
 
756
- def x_pixel_to_screen(x)
757
- @x + x
758
- end
759
-
760
- def y_pixel_to_screen(y)
761
- @y + y
762
- end
763
-
764
2722
  def draw_x(x)
765
2723
  x_pixel_to_screen(x_val_to_pixel(x))
766
2724
  end
@@ -770,63 +2728,63 @@ module Wads
770
2728
  end
771
2729
 
772
2730
  def render
773
- @data_set_hash.keys.each do |key|
774
- data_set = @data_set_hash[key]
775
- if data_set.visible
776
- data_set.rendered_points.each do |point|
777
- point.draw
2731
+ @points_by_data_set_name.keys.each do |key|
2732
+ if @visibility_map[key]
2733
+ data_set_points = @points_by_data_set_name[key]
2734
+ data_set_points.each do |point|
2735
+ if is_on_screen(point)
2736
+ point.render(@data_point_size)
2737
+ end
778
2738
  end
779
2739
  if @display_lines
780
- display_lines_for_point_set(data_set.rendered_points)
2740
+ display_lines_for_point_set(data_set_points)
781
2741
  end
782
2742
  end
783
- end
784
- if @display_grid and range_set?
785
- display_grid_lines
2743
+ if @display_grid and range_set?
2744
+ display_grid_lines
2745
+ end
786
2746
  end
787
2747
  end
788
2748
 
789
2749
  def display_lines_for_point_set(points)
790
2750
  if points.length > 1
791
2751
  points.inject(points[0]) do |last, the_next|
792
- Gosu::draw_line last.x, last.y, last.color,
793
- the_next.x, the_next.y, last.color, Z_ORDER_GRAPHIC_ELEMENTS
2752
+ if last.x < the_next.x
2753
+ Gosu::draw_line last.x, last.y, last.graphics_color,
2754
+ the_next.x, the_next.y, last.graphics_color, relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
2755
+ end
794
2756
  the_next
795
2757
  end
796
2758
  end
797
2759
  end
798
2760
 
799
2761
  def display_grid_lines
800
- # TODO this is bnot working well for large ranges with the given increment of 1
801
- # We don't want to draw hundreds of grid lines
802
2762
  grid_widgets = []
803
2763
 
804
- grid_x = @visible_range.left_x
805
- grid_y = @visible_range.bottom_y + 1
806
- while grid_y < @visible_range.top_y
2764
+ x_lines = @visible_range.grid_line_x_values
2765
+ y_lines = @visible_range.grid_line_y_values
2766
+ first_x = draw_x(@visible_range.left_x)
2767
+ last_x = draw_x(@visible_range.right_x)
2768
+ first_y = draw_y(@visible_range.bottom_y)
2769
+ last_y = draw_y(@visible_range.top_y)
2770
+
2771
+ x_lines.each do |grid_x|
807
2772
  dx = draw_x(grid_x)
808
- dy = draw_y(grid_y)
809
- last_x = draw_x(@visible_range.right_x)
810
2773
  color = @grid_line_color
811
- if grid_y == 0 and grid_y != @visible_range.bottom_y.to_i
812
- color = @zero_line_color
813
- end
814
- grid_widgets << Line.new(dx, dy, last_x, dy, color)
815
- grid_y = grid_y + 1
2774
+ if grid_x == 0 and grid_x != @visible_range.left_x.to_i
2775
+ color = @zero_line_color
2776
+ end
2777
+ grid_widgets << Line.new(dx, first_y, dx, last_y, color)
816
2778
  end
817
- grid_x = @visible_range.left_x + 1
818
- grid_y = @visible_range.bottom_y
819
- while grid_x < @visible_range.right_x
820
- dx = draw_x(grid_x)
2779
+
2780
+ y_lines.each do |grid_y|
821
2781
  dy = draw_y(grid_y)
822
- last_y = draw_y(@visible_range.top_y)
823
2782
  color = @grid_line_color
824
- if grid_x == 0 and grid_x != @visible_range.left_x.to_i
825
- color = @zero_line_color
2783
+ if grid_y == 0 and grid_y != @visible_range.bottom_y.to_i
2784
+ color = @zero_line_color
826
2785
  end
827
- grid_widgets << Line.new(dx, dy, dx, last_y, color)
828
- grid_x = grid_x + 1
829
- end
2786
+ grid_widgets << Line.new(first_x, dy, last_x, dy, color)
2787
+ end
830
2788
 
831
2789
  grid_widgets.each do |gw|
832
2790
  gw.draw
@@ -856,94 +2814,504 @@ module Wads
856
2814
  end
857
2815
  end
858
2816
 
2817
+ #
2818
+ # A graphical representation of a node in a graph using a button-style, i.e
2819
+ # a rectangular border with a text label.
2820
+ # The choice to use this display class is dictated by the use_icons attribute
2821
+ # of the current theme.
2822
+ # Like images, the size of node widgets can be scaled.
2823
+ #
859
2824
  class NodeWidget < Button
860
2825
  attr_accessor :data_node
861
2826
 
862
- def initialize(node, x, y, font, border_color = COLOR_DARK_GRAY, text_color = COLOR_HEADER_BRIGHT_BLUE)
863
- super(node.name, x, y, font, nil, border_color, text_color)
864
- set_background(COLOR_BLACK)
2827
+ def initialize(x, y, node, color = nil, initial_scale = 1, is_explorer = false)
2828
+ super(x, y, node.name)
2829
+ @orig_width = @width
2830
+ @orig_height = @height
865
2831
  @data_node = node
2832
+ @override_color = color
2833
+ set_scale(initial_scale, @is_explorer)
2834
+ end
2835
+
2836
+ def is_background
2837
+ @scale <= 1 and @is_explorer
2838
+ end
2839
+
2840
+ def set_scale(value, is_explorer = false)
2841
+ @scale = value
2842
+ @is_explorer = is_explorer
2843
+ if value < 1
2844
+ value = 1
2845
+ end
2846
+ @width = @orig_width * @scale.to_f
2847
+ debug("In regular node widget Setting scale of #{@label} to #{@scale}")
2848
+ end
2849
+
2850
+ def get_text_widget
2851
+ nil
866
2852
  end
867
2853
 
868
2854
  def render
869
2855
  super
870
- draw_background(Z_ORDER_OVERLAY_BACKGROUND)
871
- draw_shadow(COLOR_GRAY)
2856
+ draw_background(Z_ORDER_FOCAL_ELEMENTS)
2857
+ #draw_shadow(COLOR_GRAY)
2858
+ end
2859
+
2860
+ def widget_z
2861
+ Z_ORDER_TEXT
2862
+ end
2863
+ end
2864
+
2865
+ #
2866
+ # A graphical representation of a node in a graph using circular icons
2867
+ # and adjacent text labels.
2868
+ # The choice to use this display class is dictated by the use_icons attribute
2869
+ # of the current theme.
2870
+ # Like images, the size of node widgets can be scaled.
2871
+ #
2872
+ class NodeIconWidget < Widget
2873
+ attr_accessor :data_node
2874
+ attr_accessor :image
2875
+ attr_accessor :scale
2876
+ attr_accessor :label
2877
+ attr_accessor :is_explorer
2878
+
2879
+ def initialize(x, y, node, color = nil, initial_scale = 1, is_explorer = false)
2880
+ super(x, y)
2881
+ @override_color = color
2882
+ @data_node = node
2883
+ @label = node.name
2884
+ circle_image = WadsConfig.instance.circle(color)
2885
+ if circle_image.nil?
2886
+ @image = WadsConfig.instance.circle(COLOR_BLUE)
2887
+ else
2888
+ @image = circle_image
2889
+ end
2890
+ @is_explorer = is_explorer
2891
+ set_scale(initial_scale, @is_explorer)
2892
+ disable_border
2893
+ end
2894
+
2895
+ def name
2896
+ @data_node.name
2897
+ end
2898
+
2899
+ def is_background
2900
+ @scale <= 0.1 and @is_explorer
2901
+ end
2902
+
2903
+ def set_scale(value, is_explorer = false)
2904
+ @is_explorer = is_explorer
2905
+ if value < 0.5
2906
+ value = 0.5
2907
+ end
2908
+ @scale = value / 10.to_f
2909
+ #debug("In node widget Setting scale of #{@label} to #{value} = #{@scale}")
2910
+ @width = IMAGE_CIRCLE_SIZE * scale.to_f
2911
+ @height = IMAGE_CIRCLE_SIZE * scale.to_f
2912
+ # Only in explorer mode do we dull out nodes on the outer edge
2913
+ if is_background
2914
+ @image = WadsConfig.instance.circle(COLOR_ALPHA)
2915
+ else
2916
+ text_pixel_width = @gui_theme.font.text_width(@label)
2917
+ clear_children # the text widget is the only child, so we can remove all
2918
+ add_text(@label, (@width / 2) - (text_pixel_width / 2), -20)
2919
+ end
2920
+ end
2921
+
2922
+ def get_text_widget
2923
+ if @children.size > 0
2924
+ return @children[0]
2925
+ end
2926
+ #raise "No text widget for NodeIconWidget"
2927
+ nil
2928
+ end
2929
+
2930
+ def render
2931
+ @image.draw @x, @y, relative_z_order(Z_ORDER_FOCAL_ELEMENTS), @scale, @scale
2932
+ end
2933
+
2934
+ def widget_z
2935
+ Z_ORDER_TEXT
872
2936
  end
873
2937
  end
874
2938
 
2939
+ #
2940
+ # Given a single node or a graph data structure, this widget displays
2941
+ # a visualization of the graph using one of the available node widget classes.
2942
+ # There are different display modes that control what nodes within the graph
2943
+ # are shown. The default display mode, GRAPH_DISPLAY_ALL, shows all nodes
2944
+ # as the name implies. GRAPH_DISPLAY_TREE assumes an acyclic graph and renders
2945
+ # the graph in a tree-like structure. GRAPH_DISPLAY_EXPLORER has a chosen
2946
+ # center focus node with connected nodes circled around it based on the depth
2947
+ # or distance from that node. This mode also allows the user to click on
2948
+ # different nodes to navigate the graph and change focus nodes.
2949
+ #
875
2950
  class GraphWidget < Widget
876
2951
  attr_accessor :graph
877
- attr_accessor :center_node
878
- attr_accessor :depth
879
2952
  attr_accessor :selected_node
880
2953
  attr_accessor :selected_node_x_offset
881
2954
  attr_accessor :selected_node_y_offset
2955
+ attr_accessor :size_by_connections
2956
+ attr_accessor :is_explorer
882
2957
 
883
- def initialize(x, y, width, height, font, color, graph)
884
- super x, y, color
885
- set_font(font)
2958
+ def initialize(x, y, width, height, graph, display_mode = GRAPH_DISPLAY_ALL)
2959
+ super(x, y)
886
2960
  set_dimensions(width, height)
887
- set_border(color)
888
- @graph = graph
2961
+ if graph.is_a? Node
2962
+ @graph = Graph.new(graph)
2963
+ else
2964
+ @graph = graph
2965
+ end
2966
+ @size_by_connections = false
2967
+ @is_explorer = false
2968
+ if [GRAPH_DISPLAY_ALL, GRAPH_DISPLAY_TREE, GRAPH_DISPLAY_EXPLORER].include? display_mode
2969
+ debug("Displaying graph in #{display_mode} mode")
2970
+ else
2971
+ raise "#{display_mode} is not a valid display mode for Graph Widget"
2972
+ end
2973
+ if display_mode == GRAPH_DISPLAY_ALL
2974
+ set_all_nodes_for_display
2975
+ elsif display_mode == GRAPH_DISPLAY_TREE
2976
+ set_tree_display
2977
+ else
2978
+ set_explorer_display
2979
+ end
889
2980
  end
890
2981
 
891
- def update update_count, mouse_x, mouse_y
2982
+ def handle_update update_count, mouse_x, mouse_y
892
2983
  if contains_click(mouse_x, mouse_y) and @selected_node
893
- @selected_node.x = mouse_x - @selected_node_x_offset
894
- @selected_node.y = mouse_y - @selected_node_y_offset
2984
+ @selected_node.move_recursive_absolute(mouse_x - @selected_node_x_offset,
2985
+ mouse_y - @selected_node_y_offset)
895
2986
  end
896
2987
  end
897
2988
 
898
- def button_down id, mouse_x, mouse_y
899
- if id == Gosu::MsLeft
900
- # check to see if any node was selected
901
- if @rendered_nodes
902
- @rendered_nodes.values.each do |rn|
903
- if rn.contains_click(mouse_x, mouse_y)
904
- @selected_node = rn
905
- @selected_node_x_offset = mouse_x - rn.x
906
- @selected_node_y_offset = mouse_y - rn.y
907
- end
2989
+ def handle_mouse_down mouse_x, mouse_y
2990
+ # check to see if any node was selected
2991
+ if @rendered_nodes
2992
+ @rendered_nodes.values.each do |rn|
2993
+ if rn.contains_click(mouse_x, mouse_y)
2994
+ @selected_node = rn
2995
+ @selected_node_x_offset = mouse_x - rn.x
2996
+ @selected_node_y_offset = mouse_y - rn.y
2997
+ @click_timestamp = Time.now
908
2998
  end
909
2999
  end
910
3000
  end
911
3001
  WidgetResult.new(false)
912
3002
  end
913
3003
 
914
- def button_up id, mouse_x, mouse_y
915
- if id == Gosu::MsLeft
916
- if @selected_node
917
- @selected_node = nil
3004
+ def handle_mouse_up mouse_x, mouse_y
3005
+ if @selected_node
3006
+ if @is_explorer
3007
+ time_between_mouse_up_down = Time.now - @click_timestamp
3008
+ if time_between_mouse_up_down < 0.2
3009
+ # Treat this as a single click and make the selected
3010
+ # node the new center node of the graph
3011
+ set_explorer_display(@selected_node.data_node)
3012
+ end
3013
+ end
3014
+ @selected_node = nil
3015
+ end
3016
+ end
3017
+
3018
+ def set_explorer_display(center_node = nil)
3019
+ if center_node.nil?
3020
+ # If not specified, pick a center node as the one with the most connections
3021
+ center_node = @graph.node_with_most_connections
3022
+ end
3023
+
3024
+ @graph.reset_visited
3025
+ @visible_data_nodes = {}
3026
+ center_node.bfs(4) do |n|
3027
+ @visible_data_nodes[n.name] = n
3028
+ end
3029
+
3030
+ @size_by_connections = false
3031
+ @is_explorer = true
3032
+
3033
+ @rendered_nodes = {}
3034
+ populate_rendered_nodes
3035
+
3036
+ prevent_text_overlap
3037
+ end
3038
+
3039
+ def set_tree_display
3040
+ @graph.reset_visited
3041
+ @visible_data_nodes = @graph.node_map
3042
+ @rendered_nodes = {}
3043
+
3044
+ root_nodes = @graph.root_nodes
3045
+ number_of_root_nodes = root_nodes.size
3046
+ width_for_each_root_tree = @width / number_of_root_nodes
3047
+
3048
+ start_x = 0
3049
+ y_level = 20
3050
+ root_nodes.each do |root|
3051
+ set_tree_recursive(root, start_x, start_x + width_for_each_root_tree - 1, y_level)
3052
+ start_x = start_x + width_for_each_root_tree
3053
+ y_level = y_level + 40
3054
+ end
3055
+
3056
+ @rendered_nodes.values.each do |rn|
3057
+ rn.base_z = @base_z
3058
+ end
3059
+
3060
+ if @size_by_connections
3061
+ scale_node_size
3062
+ end
3063
+
3064
+ prevent_text_overlap
3065
+ end
3066
+
3067
+ def scale_node_size
3068
+ range = @graph.get_number_of_connections_range
3069
+ # There are six colors. Any number of scale sizes
3070
+ # Lets try 4 first as a max size.
3071
+ bins = range.bin_max_values(4)
3072
+
3073
+ # Set the scale for each node
3074
+ @visible_data_nodes.values.each do |node|
3075
+ num_links = node.number_of_links
3076
+ index = 0
3077
+ while index < bins.size
3078
+ if num_links <= bins[index]
3079
+ @rendered_nodes[node.name].set_scale(index + 1, @is_explorer)
3080
+ index = bins.size
3081
+ end
3082
+ index = index + 1
3083
+ end
3084
+ end
3085
+ end
3086
+
3087
+ def prevent_text_overlap
3088
+ @rendered_nodes.values.each do |rn|
3089
+ text = rn.get_text_widget
3090
+ if text
3091
+ if overlaps_with_a_node(text)
3092
+ move_text_for_node(rn)
3093
+ else
3094
+ move_in_bounds = false
3095
+ # We also check to see if the text is outside the edges of this widget
3096
+ if text.x < @x or text.right_edge > right_edge
3097
+ move_in_bounds = true
3098
+ elsif text.y < @y or text.bottom_edge > bottom_edge
3099
+ move_in_bounds = true
3100
+ end
3101
+ if move_in_bounds
3102
+ debug("#{text.label} was out of bounds")
3103
+ move_text_for_node(rn)
3104
+ end
3105
+ end
3106
+ end
3107
+ end
3108
+ end
3109
+
3110
+ def move_text_for_node(rendered_node)
3111
+ text = rendered_node.get_text_widget
3112
+ if text.nil?
3113
+ return
3114
+ end
3115
+ radians_between_attempts = DEG_360 / 24
3116
+ current_radians = 0.05
3117
+ done = false
3118
+ while not done
3119
+ # Use radians to spread the other nodes around the center node
3120
+ # TODO base the distance off of scale
3121
+ text_x = rendered_node.center_x + ((rendered_node.width / 2) * Math.cos(current_radians))
3122
+ text_y = rendered_node.center_y - ((rendered_node.height / 2) * Math.sin(current_radians))
3123
+ if text_x < @x
3124
+ text_x = @x + 1
3125
+ elsif text_x > right_edge - 20
3126
+ text_x = right_edge - 20
918
3127
  end
3128
+ if text_y < @y
3129
+ text_y = @y + 1
3130
+ elsif text_y > bottom_edge - 26
3131
+ text_y = bottom_edge - 26
3132
+ end
3133
+ text.x = text_x
3134
+ text.y = text_y
3135
+ current_radians = current_radians + radians_between_attempts
3136
+ if overlaps_with_a_node(text)
3137
+ # check for done
3138
+ if current_radians > DEG_360
3139
+ done = true
3140
+ error("ERROR: could not find a spot to put the text")
3141
+ end
3142
+ else
3143
+ done = true
3144
+ end
3145
+ end
3146
+ end
3147
+
3148
+ def overlaps_with_a_node(text)
3149
+ @rendered_nodes.values.each do |rn|
3150
+ if text.label == rn.label
3151
+ # don't compare to yourself
3152
+ else
3153
+ if rn.overlaps_with(text)
3154
+ return true
3155
+ end
3156
+ end
3157
+ end
3158
+ false
3159
+ end
3160
+
3161
+ def set_tree_recursive(current_node, start_x, end_x, y_level)
3162
+ # Draw the current node, and then recursively divide up
3163
+ # and call again for each of the children
3164
+ if current_node.visited
3165
+ return
3166
+ end
3167
+ current_node.visited = true
3168
+
3169
+ if @gui_theme.use_icons
3170
+ @rendered_nodes[current_node.name] = NodeIconWidget.new(
3171
+ x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
3172
+ y_pixel_to_screen(y_level),
3173
+ current_node,
3174
+ get_node_color(current_node))
3175
+ else
3176
+ @rendered_nodes[current_node.name] = NodeWidget.new(
3177
+ x_pixel_to_screen(start_x + ((end_x - start_x) / 2)),
3178
+ y_pixel_to_screen(y_level),
3179
+ current_node,
3180
+ get_node_color(current_node))
3181
+ end
3182
+
3183
+ number_of_child_nodes = current_node.outputs.size
3184
+ if number_of_child_nodes == 0
3185
+ return
3186
+ end
3187
+ width_for_each_child_tree = (end_x - start_x) / number_of_child_nodes
3188
+ start_child_x = start_x + 5
3189
+
3190
+ current_node.outputs.each do |child|
3191
+ if child.is_a? Edge
3192
+ child = child.destination
3193
+ end
3194
+ set_tree_recursive(child, start_child_x, start_child_x + width_for_each_child_tree - 1, y_level + 40)
3195
+ start_child_x = start_child_x + width_for_each_child_tree
919
3196
  end
920
3197
  end
921
3198
 
922
- def set_display(center_node, max_depth)
3199
+ def set_all_nodes_for_display
3200
+ @visible_data_nodes = @graph.node_map
3201
+ @rendered_nodes = {}
3202
+ populate_rendered_nodes
3203
+ if @size_by_connections
3204
+ scale_node_size
3205
+ end
3206
+ prevent_text_overlap
3207
+ end
3208
+
3209
+ def get_node_color(node)
3210
+ color_tag = node.get_tag(COLOR_TAG)
3211
+ if color_tag.nil?
3212
+ return @color
3213
+ end
3214
+ color_tag
3215
+ end
3216
+
3217
+ def set_center_node(center_node, max_depth = -1)
923
3218
  # Determine the list of nodes to draw
924
- @depth = depth
925
- @visible_data_nodes = @graph.fan_out(center_node, max_depth)
3219
+ @graph.reset_visited
3220
+ @visible_data_nodes = @graph.traverse_and_collect_nodes(center_node, max_depth)
926
3221
 
927
3222
  # Convert the data nodes to rendered nodes
928
3223
  # Start by putting the center node in the center, then draw others around it
929
3224
  @rendered_nodes = {}
930
- @rendered_nodes[center_node.name] = NodeWidget.new(center_node, center_x, center_y, @font,
931
- center_node.get_tag("color"), center_node.get_tag("color"))
3225
+ if @gui_theme.use_icons
3226
+ @rendered_nodes[center_node.name] = NodeIconWidget.new(
3227
+ center_x, center_y, center_node, get_node_color(center_node))
3228
+ else
3229
+ @rendered_nodes[center_node.name] = NodeWidget.new(center_x, center_y,
3230
+ center_node, get_node_color(center_node), get_node_color(center_node))
3231
+ end
3232
+
3233
+ populate_rendered_nodes(center_node)
3234
+
3235
+ if @size_by_connections
3236
+ scale_node_size
3237
+ end
3238
+ prevent_text_overlap
3239
+ end
932
3240
 
3241
+ def populate_rendered_nodes(center_node = nil)
933
3242
  # Spread out the other nodes around the center node
934
- # going in a circle
935
- number_of_visible_nodes = @visible_data_nodes.size
936
- radians_between_nodes = DEG_360 / number_of_visible_nodes.to_f
937
- current_radians = 0.05
3243
+ # going in a circle at each depth level
3244
+ stats = Stats.new("NodesPerDepth")
3245
+ @visible_data_nodes.values.each do |n|
3246
+ stats.increment(n.depth)
3247
+ end
3248
+ current_radians = []
3249
+ radians_increment = []
3250
+ (1..4).each do |n|
3251
+ number_of_nodes_at_depth = stats.count(n)
3252
+ radians_increment[n] = DEG_360 / number_of_nodes_at_depth.to_f
3253
+ current_radians[n] = 0.05
3254
+ end
3255
+
3256
+ padding = 100
3257
+ size_of_x_band = (@width - padding) / 6
3258
+ size_of_y_band = (@height - padding) / 6
3259
+ random_x = size_of_x_band / 8
3260
+ random_y = size_of_y_band / 8
3261
+ half_random_x = random_x / 2
3262
+ half_random_y = random_y / 2
3263
+
3264
+ # Precompute the band center points
3265
+ # then reference by the scale or depth values below
3266
+ band_center_x = padding + (size_of_x_band / 2)
3267
+ band_center_y = padding + (size_of_y_band / 2)
3268
+ # depth 1 [0] - center node, distance should be zero. Should be only one
3269
+ # depth 2 [1] - band one
3270
+ # depth 3 [2] - band two
3271
+ # depth 4 [3] - band three
3272
+ bands_x = [0, band_center_x]
3273
+ bands_x << band_center_x + size_of_x_band
3274
+ bands_x << band_center_x + size_of_x_band + size_of_x_band
3275
+
3276
+ bands_y = [0, band_center_y]
3277
+ bands_y << band_center_y + size_of_y_band
3278
+ bands_y << band_center_y + size_of_y_band + size_of_y_band
938
3279
 
939
3280
  @visible_data_nodes.each do |node_name, data_node|
940
- if node_name == center_node.name
941
- # skip, we already got this one
942
- else
3281
+ process_this_node = true
3282
+ if center_node
3283
+ if node_name == center_node.name
3284
+ process_this_node = false
3285
+ end
3286
+ end
3287
+ if process_this_node
3288
+ scale_to_use = 1
3289
+ if stats.count(1) > 0 and stats.count(2) == 0
3290
+ # if all nodes are depth 1, then size everything
3291
+ # as a small node
3292
+ elsif data_node.depth < 4
3293
+ scale_to_use = 5 - data_node.depth
3294
+ end
3295
+ if @is_explorer
3296
+ # TODO Layer the nodes around the center
3297
+ # We need a better multiplier based on the height and width
3298
+ # max distance x would be (@width / 2) - padding
3299
+ # divide that into three regions, layer 2, 3, and 4
3300
+ # get the center point for each of these regions, and do a random from there
3301
+ # scale to use determines which of the regions
3302
+ band_index = 4 - scale_to_use
3303
+ distance_from_center_x = bands_x[band_index] + rand(random_x) - half_random_x
3304
+ distance_from_center_y = bands_y[band_index] + rand(random_y) - half_random_y
3305
+ else
3306
+ distance_from_center_x = 80 + rand(200)
3307
+ distance_from_center_y = 40 + rand(100)
3308
+ end
943
3309
  # Use radians to spread the other nodes around the center node
944
- # For now, we will randomly place them
945
- node_x = center_x + ((80 + rand(200)) * Math.cos(current_radians))
946
- node_y = center_y - ((40 + rand(100)) * Math.sin(current_radians))
3310
+ radians_to_use = current_radians[data_node.depth]
3311
+ radians_to_use = radians_to_use + (rand(radians_increment[data_node.depth]) / 2)
3312
+ current_radians[data_node.depth] = current_radians[data_node.depth] + radians_increment[data_node.depth]
3313
+ node_x = center_x + (distance_from_center_x * Math.cos(radians_to_use))
3314
+ node_y = center_y - (distance_from_center_y * Math.sin(radians_to_use))
947
3315
  if node_x < @x
948
3316
  node_x = @x + 1
949
3317
  elsif node_x > right_edge - 20
@@ -954,19 +3322,31 @@ module Wads
954
3322
  elsif node_y > bottom_edge - 26
955
3323
  node_y = bottom_edge - 26
956
3324
  end
957
- current_radians = current_radians + radians_between_nodes
958
3325
 
959
3326
  # Note we can link between data nodes and rendered nodes using the node name
960
3327
  # We have a map of each
961
- @rendered_nodes[data_node.name] = NodeWidget.new(
3328
+ if @gui_theme.use_icons
3329
+ @rendered_nodes[data_node.name] = NodeIconWidget.new(
3330
+ node_x,
3331
+ node_y,
962
3332
  data_node,
3333
+ get_node_color(data_node),
3334
+ scale_to_use,
3335
+ @is_explorer)
3336
+ else
3337
+ @rendered_nodes[data_node.name] = NodeWidget.new(
963
3338
  node_x,
964
3339
  node_y,
965
- @font,
966
- data_node.get_tag("color"),
967
- data_node.get_tag("color"))
3340
+ data_node,
3341
+ get_node_color(data_node),
3342
+ scale_to_use,
3343
+ @is_explorer)
3344
+ end
968
3345
  end
969
3346
  end
3347
+ @rendered_nodes.values.each do |rn|
3348
+ rn.base_z = @base_z
3349
+ end
970
3350
  end
971
3351
 
972
3352
  def render
@@ -974,6 +3354,7 @@ module Wads
974
3354
  @rendered_nodes.values.each do |vn|
975
3355
  vn.draw
976
3356
  end
3357
+
977
3358
  # Draw the connections between nodes
978
3359
  @visible_data_nodes.values.each do |data_node|
979
3360
  data_node.outputs.each do |connected_data_node|
@@ -985,9 +3366,16 @@ module Wads
985
3366
  if connected_rendered_node.nil?
986
3367
  # Don't draw if it is not currently visible
987
3368
  else
988
- Gosu::draw_line rendered_node.center_x, rendered_node.center_y, rendered_node.color,
989
- connected_rendered_node.center_x, connected_rendered_node.center_y, connected_rendered_node.color,
990
- Z_ORDER_GRAPHIC_ELEMENTS
3369
+ if @is_explorer and (rendered_node.is_background or connected_rendered_node.is_background)
3370
+ # Use a dull gray color for the line
3371
+ Gosu::draw_line rendered_node.center_x, rendered_node.center_y, COLOR_LIGHT_GRAY,
3372
+ connected_rendered_node.center_x, connected_rendered_node.center_y, COLOR_LIGHT_GRAY,
3373
+ relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
3374
+ else
3375
+ Gosu::draw_line rendered_node.center_x, rendered_node.center_y, rendered_node.graphics_color,
3376
+ connected_rendered_node.center_x, connected_rendered_node.center_y, connected_rendered_node.graphics_color,
3377
+ relative_z_order(Z_ORDER_GRAPHIC_ELEMENTS)
3378
+ end
991
3379
  end
992
3380
  end
993
3381
  end