wads 0.1.0 → 0.2.0

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