wads 0.1.1 → 0.2.1

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