1-- © 2008 David Given. 2-- WordGrinder is licensed under the MIT open source license. See the COPYING 3-- file in this distribution for the full text. 4 5local int = math.floor 6local Write = wg.write 7local GotoXY = wg.gotoxy 8local GetStringWidth = wg.getstringwidth 9local GetBoundedString = wg.getboundedstring 10local GetBytesOfCharacter = wg.getbytesofcharacter 11local SetNormal = wg.setnormal 12local SetBold = wg.setbold 13local SetBright = wg.setbright 14local SetUnderline = wg.setunderline 15local SetReverse = wg.setreverse 16local string_rep = string.rep 17 18Form = {} 19 20-- Atoms. 21 22Form.Left = {} 23Form.Right = {} 24Form.Centre = {} 25Form.Center = Form.Centre 26 27Form.Large = {} 28 29local function min(a, b) 30 if (a < b) then 31 return a 32 else 33 return b 34 end 35end 36 37local function max(a, b) 38 if (a > b) then 39 return a 40 else 41 return b 42 end 43end 44 45local function makewidgetclass(class) 46 return function(table) 47 setmetatable(table, {__index = class}) 48 table.class = class 49 return table 50 end 51end 52 53Form.Divider = makewidgetclass 54{ 55 draw = function(self) 56 Write(self.realx1, self.realy1, string_rep("─", self.realwidth)) 57 end 58} 59 60Form.WrappedLabel = makewidgetclass 61{ 62 draw = function(self) 63 local words = ParseStringIntoWords(self.value) 64 local paragraph = CreateParagraph(nil, words) 65 local lines = paragraph:wrap(self.realwidth) 66 67 local s = SpellcheckerOff() 68 for i = 1, #lines do 69 paragraph:renderLine(lines[i], self.realx1, self.realy1+i-1) 70 end 71 SpellcheckerRestore(s) 72 end, 73 74 calculate_height = function(self) 75 local words = ParseStringIntoWords(self.value) 76 local paragraph = CreateParagraph(nil, words) 77 local lines = paragraph:wrap(self.realwidth) 78 return #lines 79 end, 80} 81 82Form.Label = makewidgetclass { 83 align = Form.Centre, 84 85 draw = function(self) 86 local xo 87 if (self.align == Form.Centre) then 88 xo = int((self.realwidth - GetStringWidth(self.value)) / 2) 89 elseif (self.align == Form.Left) then 90 xo = 0 91 elseif (self.align == Form.Right) then 92 xo = self.realwidth - GetStringWidth(self.value) 93 end 94 95 Write(self.realx1, self.realy1, string_rep(" ", self.realwidth)) 96 Write(self.realx1 + xo, self.realy1, self.value) 97 end 98} 99 100local checkbox_toggle = function(self, key) 101 self.value = not self.value 102 self:draw() 103end 104 105Form.Checkbox = makewidgetclass { 106 value = false, 107 label = "Checkbox", 108 focusable = true, 109 110 draw = function(self) 111 local s 112 if self.value then 113 s = "> YES" 114 else 115 s = "> NO " 116 end 117 118 Write(self.realx1, self.realy1, GetBoundedString(self.label, self.realwidth - 2)) 119 120 SetBright() 121 Write(self.realx2, self.realy1, s) 122 SetNormal() 123 124 if self.focus then 125 GotoXY(self.realx2, self.realy1) 126 end 127 end, 128 129 [" "] = checkbox_toggle 130} 131 132Form.TextField = makewidgetclass { 133 focusable = true, 134 135 init = function(self) 136 self.cursor = self.cursor or (self.value:len() + 1) 137 self.offset = self.offset or 1 138 end, 139 140 draw = function(self) 141 SetBright() 142 Write(self.realx1, self.realy1 + 1, string_rep("▔", self.realwidth)) 143 Write(self.realx1, self.realy1, string_rep(" ", self.realwidth)) 144 SetNormal() 145 146 -- If the cursor is to the left of the visible area, adjust. 147 148 if (self.cursor < self.offset) then 149 self.offset = self.cursor 150 end 151 152 -- If the cursor is to the right of the visible area, adjust. (This is 153 -- very crude, but I'm not sure there's a more elegant way of doing 154 -- it.) 155 156 while true do 157 local xo = GetStringWidth(self.value:sub(self.offset, self.cursor)) 158 if (xo <= self.realwidth) then 159 break 160 end 161 162 local b = GetBytesOfCharacter(self.value:byte(self.offset)) 163 self.offset = self.offset + b 164 end 165 166 -- Draw the visible bit of the string. 167 168 local s = GetBoundedString(self.value:sub(self.offset), self.realwidth) 169 SetBright() 170 Write(self.realx1, self.realy1, s) 171 SetNormal() 172 173 if self.focus then 174 GotoXY(self.realx1 + GetStringWidth(s:sub(1, self.cursor-self.offset)), self.realy1) 175 end 176 end, 177 178 ["KEY_LEFT"] = function(self, key) 179 if (self.cursor > 1) then 180 while true do 181 self.cursor = self.cursor - 1 182 if (GetBytesOfCharacter(self.value:byte(self.cursor)) ~= 0) then 183 break 184 end 185 end 186 self:draw() 187 end 188 189 return "nop" 190 end, 191 192 ["KEY_RIGHT"] = function(self, key) 193 if (self.cursor <= self.value:len()) then 194 self.cursor = self.cursor + GetBytesOfCharacter(self.value:byte(self.cursor)) 195 self:draw() 196 end 197 198 return "nop" 199 end, 200 201 ["KEY_HOME"] = function(self, key) 202 self.cursor = 1 203 self:draw() 204 205 return "nop" 206 end, 207 208 ["KEY_END"] = function(self, key) 209 self.cursor = self.value:len() + 1 210 self:draw() 211 212 return "nop" 213 end, 214 215 ["KEY_BACKSPACE"] = function(self, key) 216 if (self.cursor > 1) then 217 local w 218 while true do 219 self.cursor = self.cursor - 1 220 w = GetBytesOfCharacter(self.value:byte(self.cursor)) 221 if (w ~= 0) then 222 break 223 end 224 end 225 226 self.value = self.value:sub(1, self.cursor - 1) .. 227 self.value:sub(self.cursor + w) 228 self:draw() 229 end 230 231 return "nop" 232 end, 233 234 ["KEY_DELETE"] = function(self, key) 235 local v = self.value:byte(self.cursor) 236 if v then 237 local w = GetBytesOfCharacter(self.value:byte(self.cursor)) 238 self.value = self.value:sub(1, self.cursor - 1) .. 239 self.value:sub(self.cursor + w) 240 self:draw() 241 end 242 243 return "nop" 244 end, 245 246 ["KEY_^U"] = function(self, key) 247 self.cursor = 1 248 self.offset = 1 249 self.value = "" 250 self:draw() 251 252 return "nop" 253 end, 254 255 key = function(self, key) 256 if not key:match("^KEY_") then 257 self.value = self.value:sub(1, self.cursor-1) .. key .. self.value:sub(self.cursor) 258 self.cursor = self.cursor + GetBytesOfCharacter(key:byte(1)) 259 self:draw() 260 261 return "nop" 262 end 263 end, 264} 265 266Form.Browser = makewidgetclass { 267 focusable = true, 268 269 init = function(self) 270 self.cursor = self.cursor or 1 271 self.offset = self.offset or 0 272 end, 273 274 _adjustOffset = function(self) 275 local h = self.realheight 276 277 if (self.offset == 0) then 278 self.offset = self.cursor - int(h/2) 279 end 280 281 self.offset = min(self.offset, self.cursor) 282 self.offset = max(self.offset, self.cursor - (h-2)) 283 self.offset = min(self.offset, #self.data - (h-2)) 284 self.offset = max(self.offset, 1) 285 end, 286 287 changed = function(self) 288 return "nop" 289 end, 290 291 draw = function(self) 292 local x = self.realx1 293 local y = self.realy1 294 local w = self.realwidth 295 local h = self.realheight 296 297 -- Draw the box. 298 299 do 300 local border = string_rep("─", w - 2) 301 SetBright() 302 Write(x, y, "┌") 303 Write(x+1, y, border) 304 Write(x+w-1, y, "┐") 305 for i = 1, h-1 do 306 Write(x, y+i, "│") 307 Write(x+w-1, y+i, "│") 308 end 309 Write(x, y+h, "└") 310 Write(x+1, y+h, border) 311 Write(x+w-1, y+h, "┘") 312 SetNormal() 313 end 314 315 self:_adjustOffset() 316 317 -- Draw the data. 318 319 local space = string_rep(" ", w - 2) 320 for i = 0, h-2 do 321 local index = self.offset + i 322 local item = self.data[index] 323 if not item then 324 break 325 end 326 327 if (index == self.cursor) then 328 SetReverse() 329 else 330 SetNormal() 331 end 332 333 Write(x+1, y+1+i, space) 334 local s = GetBoundedString(item.label, w-4) 335 Write(x+2, y+1+i, s) 336 337 if (#self.data > (h-2)) then 338 SetNormal() 339 SetBright() 340 s = "│" 341 local yf = (i+1) * #self.data / (h-1) 342 if (yf >= self.offset) and (yf <= (self.offset + h-2)) then 343 s = "║" 344 end 345 Write(x+w-1, y+1+i, s) 346 end 347 SetNormal() 348 end 349 SetNormal() 350 end, 351 352 ["KEY_UP"] = function(self, key) 353 if (self.cursor > 1) then 354 self.cursor = self.cursor - 1 355 self:draw() 356 return self:changed() 357 end 358 359 return "nop" 360 end, 361 362 ["KEY_DOWN"] = function(self, key) 363 if (self.cursor < #self.data) then 364 self.cursor = self.cursor + 1 365 self:draw() 366 return self:changed() 367 end 368 369 return "nop" 370 end, 371 372 ["KEY_PGUP"] = function(self, key) 373 local oldcursor = self.cursor 374 self.cursor = oldcursor - int(self.realheight/2) 375 if (self.cursor < 1) then 376 self.cursor = 1 377 end 378 379 if (self.cursor ~= oldcursor) then 380 self:draw() 381 return self:changed() 382 end 383 return "nop" 384 end, 385 386 ["KEY_PGDN"] = function(self, key) 387 local oldcursor = self.cursor 388 self.cursor = oldcursor + int(self.realheight/2) 389 if (self.cursor > #self.data) then 390 self.cursor = #self.data 391 end 392 393 if (self.cursor ~= oldcursor) then 394 self:draw() 395 return self:changed() 396 end 397 return "nop" 398 end, 399} 400 401local standard_actions = 402{ 403 ["KEY_UP"] = function(dialogue, key) 404 if dialogue.focus then 405 local f = dialogue.focus - 1 406 while (f ~= dialogue.focus) do 407 if (f == 0) then 408 f = #dialogue 409 end 410 411 local widget = dialogue[f] 412 if widget.focusable then 413 dialogue.focus = f 414 return "redraw" 415 end 416 417 f = f - 1 418 end 419 end 420 421 return "nop" 422 end, 423 424 ["KEY_DOWN"] = function(dialogue, key) 425 if dialogue.focus then 426 local f = dialogue.focus + 1 427 while (f ~= dialogue.focus) do 428 if (f > #dialogue) then 429 f = 1 430 end 431 432 local widget = dialogue[f] 433 if widget.focusable then 434 dialogue.focus = f 435 return "redraw" 436 end 437 438 f = f + 1 439 end 440 end 441 442 return "nop" 443 end 444} 445 446local function resolvesize(size, bound) 447 if (size < 0) then 448 return size + bound 449 else 450 return size 451 end 452end 453 454local function findaction(table, object, key) 455 local action = table[key] 456 if action and (type(action) == "function") then 457 action = action(object, key) 458 end 459 if not action and table.key then 460 action = table.key(object, key) 461 end 462 return action 463end 464 465function Form.Run(dialogue, redraw, helptext) 466 -- Ensure the screen is properly sized. 467 468 ResizeScreen() 469 470 -- Find a widget to give the focus to. 471 472 if not dialogue.focus then 473 for i, widget in ipairs(dialogue) do 474 if widget.focusable then 475 dialogue.focus = i 476 break 477 end 478 end 479 end 480 481 -- Initialise any widgets that need it. 482 483 for _, widget in ipairs(dialogue) do 484 if widget.init then 485 widget:init() 486 end 487 end 488 489 -- Redraw the backdrop. 490 491 if redraw then 492 redraw() 493 end 494 495 -- Size the dialogue. 496 497 if (dialogue.width == Form.Large) then 498 dialogue.realwidth = int(ScreenWidth * 6/7) 499 else 500 dialogue.realwidth = dialogue.width 501 end 502 503 if (dialogue.height == Form.Large) then 504 dialogue.realheight = int(ScreenHeight * 5/6) 505 else 506 dialogue.realheight = dialogue.height 507 end 508 509 -- Is this a stretchy dialogue? 510 511 if dialogue.stretchy then 512 -- Automatically scale the height depending on a 'stretchy' widget. 513 514 for _, widget in ipairs(dialogue) do 515 if (widget.y1 > 0) and (widget.y2 < 0) then 516 widget.realx1 = resolvesize(widget.x1, dialogue.realwidth) 517 widget.realx2 = resolvesize(widget.x2, dialogue.realwidth) 518 widget.realwidth = widget.realx2 - widget.realx1 519 520 local h = 1 521 if widget.calculate_height then 522 h = widget:calculate_height() 523 end 524 525 dialogue.realheight = dialogue.height + h 526 break 527 end 528 end 529 end 530 531 -- Place the dialogue. 532 533 dialogue.realx = int(ScreenWidth/2 - dialogue.realwidth/2) 534 dialogue.realy = int(ScreenHeight/2 - dialogue.realheight/2) 535 536 -- Place all widgets in the dialogue. 537 538 for _, widget in ipairs(dialogue) do 539 widget.realx1 = resolvesize(widget.x1, dialogue.realwidth) + dialogue.realx 540 widget.realy1 = resolvesize(widget.y1, dialogue.realheight) + dialogue.realy 541 widget.realx2 = resolvesize(widget.x2, dialogue.realwidth) + dialogue.realx 542 widget.realy2 = resolvesize(widget.y2, dialogue.realheight) + dialogue.realy 543 widget.realwidth = widget.realx2 - widget.realx1 544 widget.realheight = widget.realy2 - widget.realy1 545 end 546 547 -- Draw the dialogue itself. 548 549 do 550 local sizeadjust = 0 551 if helptext then 552 sizeadjust = 1 553 end 554 DrawTitledBox(dialogue.realx - 1, dialogue.realy - 1, 555 dialogue.realwidth, dialogue.realheight + sizeadjust, 556 dialogue.title) 557 558 if helptext then 559 CentreInField(dialogue.realx, dialogue.realy + dialogue.realheight, 560 dialogue.realwidth, "<"..helptext..">") 561 end 562 end 563 564 -- Draw the widgets. 565 566 GotoXY(ScreenWidth-1, ScreenHeight-1) 567 for i, widget in ipairs(dialogue) do 568 widget.focus = (i == dialogue.focus) 569 widget:draw() 570 end 571 572 -- Process keys. 573 574 while true do 575 local key = wg.getchar() 576 577 if (key == "KEY_RESIZE") then 578 ResizeScreen() 579 return Form.Run(dialogue, redraw, helptext) 580 end 581 582 local action = nil 583 if dialogue.focus then 584 local w = dialogue[dialogue.focus] 585 action = findaction(w, w, key) 586 end 587 588 if not action then 589 action = findaction(dialogue, dialogue, key) or 590 findaction(standard_actions, dialogue, key) 591 end 592 593 if (action == "redraw") then 594 return Form.Run(dialogue, redraw, helptext) 595 elseif (action == "cancel") then 596 return false 597 elseif (action == "confirm") then 598 return true 599 end 600 end 601end 602 603-- Test code 604 605function Form.Test() 606 FileBrowser("Title", "Load file:", false) 607end 608