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 ClearToEOL = wg.cleartoeol 8local GetChar = wg.getchar 9local GotoXY = wg.gotoxy 10local SetBold = wg.setbold 11local SetBright = wg.setbright 12local SetReverse = wg.setreverse 13local SetNormal = wg.setnormal 14local GetStringWidth = wg.getstringwidth 15 16local menu_tab = {} 17local key_tab = {} 18local menu_stack = {} 19 20local function addmenu(n, m, menu) 21 local w = n:len() 22 menu = menu or {} 23 menu.label = n 24 menu.mks = {} 25 26 for i, _ in ipairs(menu) do 27 menu[i] = nil 28 end 29 30 for _, data in ipairs(m) do 31 if (data == "-") then 32 menu[#menu+1] = "-" 33 else 34 local item = { 35 id = data[1], 36 mk = data[2], 37 label = data[3], 38 ak = data[4], 39 fn = data[5] 40 } 41 menu[#menu+1] = item 42 43 if item.mk then 44 if menu.mks[item.mk] then 45 error("Duplicate menu action key "..item.mk) 46 end 47 menu.mks[item.mk] = item 48 end 49 50 if menu_tab[item.id] then 51 error("Dupicate menu ID "..item.id) 52 end 53 menu_tab[item.id] = item 54 55 if item.ak then 56 key_tab[item.ak] = item.id 57 end 58 59 if (item.label:len() > w) then 60 w = item.label:len() 61 end 62 end 63 end 64 65 menu.maxwidth = w 66 return menu 67end 68 69local function submenu(menu) 70 for _, item in ipairs(menu) do 71 menu_tab[item.id] = nil 72 if item.ak then 73 key_tab[item.ak] = nil 74 end 75 end 76end 77 78local DocumentsMenu = addmenu("Documents", {}) 79local ParagraphStylesMenu = addmenu("Paragraph Styles", {}) 80 81local cp = Cmd.Checkpoint 82 83local ImportMenu = addmenu("Import new document", 84{ 85 {"FIodt", "O", "Import ODT file...", nil, Cmd.ImportODTFile}, 86 {"FIhtml", "H", "Import HTML file...", nil, Cmd.ImportHTMLFile}, 87 {"FItxt", "T", "Import text file...", nil, Cmd.ImportTextFile}, 88}) 89 90local ExportMenu = addmenu("Export current document", 91{ 92 {"FEodt", "O", "Export to ODT...", nil, Cmd.ExportODTFile}, 93 {"FEhtml", "H", "Export to HTML...", nil, Cmd.ExportHTMLFile}, 94 {"FEmd", "M", "Export to Markdown...", nil, Cmd.ExportMarkdownFile}, 95 {"FEtxt", "T", "Export to plain text...", nil, Cmd.ExportTextFile}, 96 {"FEtex", "L", "Export to LaTeX...", nil, Cmd.ExportLatexFile}, 97 {"FEtr", "F", "Export to Troff...", nil, Cmd.ExportTroffFile}, 98-- {"FErtf", "R", "Export to Rtf...", nil, Cmd.ExportRTFFile}, 99}) 100 101local DocumentSettingsMenu = addmenu("Document settings", 102{ 103 {"FSautosave", "A", "Autosave...", nil, Cmd.ConfigureAutosave}, 104 {"FSscrapbook", "S", "Scrapbook...", nil, Cmd.ConfigureScrapbook}, 105 {"FSHTMLExport", "H", "HTML export...", nil, Cmd.ConfigureHTMLExport}, 106 {"FSPageCount", "P", "Page count...", nil, Cmd.ConfigurePageCount}, 107 {"FSSmartquotes", "Q", "Smart quotes...", nil, Cmd.ConfigureSmartQuotes}, 108 {"FSSpellchecker", "K", "Spellchecker...", nil, Cmd.ConfigureSpellchecker}, 109}) 110 111local GlobalSettingsMenu = addmenu("Global settings", 112{ 113 {"FSlookandfeel","L", "Change look and feel...", nil, Cmd.ConfigureLookAndFeel}, 114 {"FSDictionary", "D", "Load new system dictionary...", nil, Cmd.ConfigureSystemDictionary}, 115 "-", 116 {"FSDebug", "X", "Debugging options...", nil, Cmd.ConfigureDebug}, 117}) 118 119local FileMenu = addmenu("File", 120{ 121 {"FN", "N", "New document set", nil, Cmd.CreateBlankDocumentSet}, 122 {"FO", "O", "Load document set...", nil, Cmd.LoadDocumentSet}, 123 {"FS", "S", "Save document set", "^S", Cmd.SaveCurrentDocument}, 124 {"FA", "A", "Save document set as...", nil, Cmd.SaveCurrentDocumentAs}, 125 "-", 126 {"FB", "B", "Add new blank document", nil, Cmd.AddBlankDocument}, 127 {"FI", "I", "Import new document ▷", nil, ImportMenu}, 128 {"FE", "E", "Export current document ▷", nil, ExportMenu}, 129 {"Fdocman", "D", "Manage documents...", nil, Cmd.ManageDocumentsUI}, 130 "-", 131 {"Fsettings", "T", "Document settings ▷", nil, DocumentSettingsMenu}, 132 {"Fglobals", "G", "Global settings ▷", nil, GlobalSettingsMenu}, 133 "-", 134 {"Fabout", "Z", "About WordGrinder...", nil, Cmd.AboutWordGrinder}, 135 {"FQ", "X", "Exit", "^Q", Cmd.TerminateProgram} 136}) 137 138local ScrapbookMenu = addmenu("Scrapbook", 139{ 140 {"EScut", "T", "Cut to scrapbook", nil, { cp, Cmd.CutToScrapbook }}, 141 {"EScopy", "C", "Copy to scrapbook", nil, Cmd.CopyToScrapbook}, 142 {"ESpaste", "P", "Paste to scrapbook", nil, { cp, Cmd.PasteToScrapbook }}, 143}) 144 145local SpellcheckMenu = addmenu("Spellchecker", 146{ 147 {"ECfind", "F", "Find next misspelt word", "^L", Cmd.FindNextMisspeltWord }, 148 {"ECadd", "A", "Add current word to dictionary", "^M", { cp, Cmd.AddToUserDictionary }}, 149}) 150 151local EditMenu = addmenu("Edit", 152{ 153 {"ET", "T", "Cut", "^X", { cp, Cmd.Cut }}, 154 {"EC", "C", "Copy", "^C", Cmd.Copy}, 155 {"EP", "P", "Paste", "^V", { cp, Cmd.Paste }}, 156 {"ED", "D", "Delete", nil, { cp, Cmd.Delete }}, 157 "-", 158 {"Eundo", "U", "Undo", "^Z", Cmd.Undo}, 159 {"Eredo", "E", "Redo", "^Y", Cmd.Redo}, 160 "-", 161 {"EF", "F", "Find and replace...", "^F", Cmd.Find}, 162 {"EN", "N", "Find next", "^K", Cmd.FindNext}, 163 {"ER", "R", "Replace then find", "^R", { cp, Cmd.ReplaceThenFind }}, 164 {"Esq", "Q", "Smartquotify selection", nil, Cmd.Smartquotify}, 165 {"Eusq", "W", "Unsmartquotify selection", nil, Cmd.Unsmartquotify}, 166 "-", 167 {"EG", "G", "Go to...", "^G", Cmd.Goto}, 168 {"Escrapbook", "S", "Scrapbook ▷", nil, ScrapbookMenu}, 169 {"Espell", "K", "Spellchecker ▷", nil, SpellcheckMenu}, 170}) 171 172local MarginMenu = addmenu("Margin", 173{ 174 {"SM1", "H", "Hide margin", nil, function() Cmd.SetViewMode(1) end}, 175 {"SM2", "S", "Show paragraph styles", nil, function() Cmd.SetViewMode(2) end}, 176 {"SM3", "N", "Show paragraph numbers", nil, function() Cmd.SetViewMode(3) end}, 177 {"SM4", "W", "Show paragraph word counts", nil, function() Cmd.SetViewMode(4) end}, 178}) 179 180local StyleMenu = addmenu("Style", 181{ 182 {"SI", "I", "Set italic", "^I", { cp, function() Cmd.SetStyle("i") end }}, 183 {"SU", "U", "Set underline", "^U", { cp, function() Cmd.SetStyle("u") end }}, 184 {"SB", "B", "Set bold", "^B", { cp, function() Cmd.SetStyle("b") end }}, 185 {"SO", "O", "Set plain", "^O", { cp, function() Cmd.SetStyle("o") end }}, 186 "-", 187 {"SP", "P", "Change paragraph style ▷", "^P", ParagraphStylesMenu}, 188 {"SM", "M", "Set margin mode ▷", nil, MarginMenu}, 189 {"SS", "S", "Toggle status bar", nil, Cmd.ToggleStatusBar}, 190}) 191 192local NavigationMenu = addmenu("Navigation", 193{ 194 {"ZU", nil, "Cursor up", "UP", { Cmd.MoveWhileSelected, Cmd.GotoPreviousLine }}, 195 {"ZR", nil, "Cursor right", "RIGHT", { Cmd.MoveWhileSelected, Cmd.GotoNextCharW }}, 196 {"ZD", nil, "Cursor down", "DOWN", { Cmd.MoveWhileSelected, Cmd.GotoNextLine }}, 197 {"ZL", nil, "Cursor left", "LEFT", { Cmd.MoveWhileSelected, Cmd.GotoPreviousCharW }}, 198 {"ZSU", nil, "Selection up", "SUP", { Cmd.SetMark, Cmd.GotoPreviousLine }}, 199 {"ZSR", nil, "Selection right", "SRIGHT", { Cmd.SetMark, Cmd.GotoNextCharW }}, 200 {"ZSD", nil, "Selection down", "SDOWN", { Cmd.SetMark, Cmd.GotoNextLine }}, 201 {"ZSL", nil, "Selection left", "SLEFT", { Cmd.SetMark, Cmd.GotoPreviousCharW }}, 202 {"ZSW", nil, "Select word", "^W", Cmd.SelectWord }, 203 {"ZWL", nil, "Goto previous word", "^LEFT", { Cmd.MoveWhileSelected, Cmd.GotoPreviousWordW }}, 204 {"ZWR", nil, "Goto next word", "^RIGHT", { Cmd.MoveWhileSelected, Cmd.GotoNextWordW }}, 205 {"ZNP", nil, "Goto next paragraph", "^DOWN", { Cmd.MoveWhileSelected, Cmd.GotoNextParagraphW }}, 206 {"ZPP", nil, "Goto previous paragraph", "^UP", { Cmd.MoveWhileSelected, Cmd.GotoPreviousParagraphW }}, 207 {"ZSWL", nil, "Select to previous word", "S^LEFT", { Cmd.SetMark, Cmd.GotoPreviousWordW }}, 208 {"ZSWR", nil, "Select to next word", "S^RIGHT", { Cmd.SetMark, Cmd.GotoNextWordW }}, 209 {"ZSNP", nil, "Select to next paragraph", "S^DOWN", { Cmd.SetMark, Cmd.GotoNextParagraphW }}, 210 {"ZSPP", nil, "Select to previous paragraph", "S^UP", { Cmd.SetMark, Cmd.GotoPreviousParagraphW }}, 211 {"ZH", nil, "Goto beginning of line", "HOME", { Cmd.MoveWhileSelected, Cmd.GotoBeginningOfLine }}, 212 {"ZE", nil, "Goto end of line", "END", { Cmd.MoveWhileSelected, Cmd.GotoEndOfLine }}, 213 {"ZSH", nil, "Select to beginning of line", "SHOME", { Cmd.SetMark, Cmd.GotoBeginningOfLine }}, 214 {"ZSE", nil, "Select to end of line", "SEND", { Cmd.SetMark, Cmd.GotoEndOfLine }}, 215 {"ZBD", nil, "Goto beginning of document", "^PGUP", { Cmd.MoveWhileSelected, Cmd.GotoBeginningOfDocument }}, 216 {"ZED", nil, "Goto end of document", "^PGDN", { Cmd.MoveWhileSelected, Cmd.GotoEndOfDocument }}, 217 {"ZSBD", nil, "Select to beginning of document", "S^PGUP", { Cmd.SetMark, Cmd.GotoBeginningOfDocument }}, 218 {"ZSED", nil, "Select to end of document", "S^PGDN", { Cmd.SetMark, Cmd.GotoEndOfDocument }}, 219 {"ZPGUP", nil, "Page up", "PGUP", { Cmd.MoveWhileSelected, Cmd.GotoPreviousPage }}, 220 {"ZPGDN", nil, "Page down", "PGDN", { Cmd.MoveWhileSelected, Cmd.GotoNextPage }}, 221 {"ZSPGUP", nil, "Selection page up", "SPGUP", { Cmd.SetMark, Cmd.GotoPreviousPage }}, 222 {"ZSPGDN", nil, "Selection page down", "SPGDN", { Cmd.SetMark, Cmd.GotoNextPage }}, 223 {"ZDPC", nil, "Delete previous character", "BACKSPACE", { cp, Cmd.DeleteSelectionOrPreviousChar }}, 224 {"ZDNC", nil, "Delete next character", "DELETE", { cp, Cmd.DeleteSelectionOrNextChar }}, 225 {"ZDW", nil, "Delete word", "^E", { cp, Cmd.TypeWhileSelected, Cmd.DeleteWord }}, 226 {"ZM", nil, "Toggle mark", "^@", Cmd.ToggleMark}, 227}) 228 229local MainMenu = addmenu("Main Menu", 230{ 231 {"F", "F", "File ▷", nil, FileMenu}, 232 {"E", "E", "Edit ▷", nil, EditMenu}, 233 {"S", "S", "Style ▷", nil, StyleMenu}, 234 {"D", "D", "Documents ▷", nil, DocumentsMenu}, 235 {"Z", "Z", "Navigation ▷", nil, NavigationMenu} 236}) 237 238function IsMenu(m) 239 return (type(m) == "table") and (type(m[1]) ~= "function") 240end 241 242function RunMenuAction(ff) 243 if (type(ff) == "function") then 244 return ff() 245 elseif IsMenu(ff) then 246 Cmd.ActivateMenu(ff) 247 else 248 for _, f in ipairs(ff) do 249 local result, e = f() 250 if not result then 251 return false, e 252 end 253 end 254 return true 255 end 256end 257 258--- MENU DRIVER CLASS --- 259 260MenuClass = { 261 activate = function(self, menu) 262 menu = menu or MainMenu 263 self:runmenu(0, 0, menu) 264 QueueRedraw() 265 SetNormal() 266 end, 267 268 drawmenu = function(self, x, y, menu, n, top) 269 local akw = 0 270 for _, item in ipairs(menu) do 271 local ak = self.accelerators[item.id] 272 if ak then 273 local l = GetStringWidth(ak) 274 if (akw < l) then 275 akw = l 276 end 277 end 278 end 279 if (akw > 0) then 280 akw = akw + 1 281 end 282 283 local w = menu.maxwidth + 4 + akw 284 local visiblelen = min(#menu, ScreenHeight-y-3) 285 DrawTitledBox(x, y, w, visiblelen, menu.label) 286 287 if (visiblelen < #menu) then 288 local f1 = (top - 1) / #menu 289 local f2 = (top + visiblelen - 1) / #menu 290 local y1 = f1 * visiblelen + y + 1 291 local y2 = f2 * visiblelen + y + 1 292 SetBright() 293 for yy = y1, y2 do 294 Write(x+w+1, yy, "║") 295 end 296 end 297 298 for i = top, top+visiblelen-1 do 299 local item = menu[i] 300 local ak = self.accelerators[item.id] 301 local yy = y+i-top+1 302 303 if (item == "-") then 304 if (i == n) then 305 SetReverse() 306 end 307 SetBright() 308 Write(x+1, yy, string.rep("─", w)) 309 else 310 SetNormal() 311 if (i == n) then 312 SetReverse() 313 Write(x+1, yy, string.rep(" ", w)) 314 end 315 316 Write(x+4, yy, item.label) 317 318 SetBold() 319 SetBright() 320 if ak then 321 local l = GetStringWidth(ak) 322 Write(x+w-l, yy, ak) 323 end 324 325 if item.mk then 326 Write(x+2, yy, item.mk) 327 end 328 end 329 330 SetNormal() 331 end 332 GotoXY(ScreenWidth-1, ScreenHeight-1) 333 334 DrawStatusLine("^V rebinds a menu item; ^X unbinds it; ^R resets all bindings to default.") 335 end, 336 337 drawmenustack = function(self) 338 local osb = DocumentSet.statusbar 339 DocumentSet.statusbar = true 340 RedrawScreen() 341 DocumentSet.statusbar = osb 342 343 local o = 0 344 for _, m in ipairs(menu_stack) do 345 self:drawmenu(o*4, o*2, m.menu, m.n, m.top) 346 o = o + 1 347 end 348 end, 349 350 runmenu = function(self, x, y, menu) 351 local n = 1 352 local top = 1 353 354 while true do 355 local id 356 357 while true do 358 local visiblelen = min(#menu, ScreenHeight-y-3) 359 if (n < top) then 360 top = n 361 end 362 if (n > (top+visiblelen-1)) then 363 top = n - visiblelen + 1 364 end 365 366 self:drawmenu(x, y, menu, n, top) 367 368 local c = GetChar():upper() 369 if (c == "KEY_RESIZE") then 370 ResizeScreen() 371 RedrawScreen() 372 self:drawmenustack() 373 elseif (c == "KEY_UP") and (n > 1) then 374 n = n - 1 375 elseif (c == "KEY_DOWN") and (n < #menu) then 376 n = n + 1 377 elseif (c == "KEY_PGDN") then 378 n = int(min(n + visiblelen/2, #menu)) 379 elseif (c == "KEY_PGUP") then 380 n = int(max(n - visiblelen/2, 1)) 381 elseif (c == "KEY_RETURN") or (c == "KEY_RIGHT") then 382 if (type(menu[n]) ~= "string") then 383 id = menu[n].id 384 break 385 end 386 elseif (c == "KEY_LEFT") then 387 return nil 388 elseif (c == "KEY_ESCAPE") then 389 return false 390 elseif (c == "KEY_^C") then 391 return false 392 elseif (c == "KEY_^X") then 393 local item = menu[n] 394 if (type(item) ~= "string") then 395 local ak = self.accelerators[item.id] 396 if ak then 397 self.accelerators[ak] = nil 398 self.accelerators[item.id] = nil 399 self:drawmenustack() 400 end 401 end 402 elseif (c == "KEY_^V") then 403 local item = menu[n] 404 if (type(item) ~= "string") then 405 DrawStatusLine("Press new accelerator key for menu item.") 406 407 local oak = self.accelerators[item.id] 408 local ak = GetChar():upper() 409 if ak:match("^KEY_") then 410 ak = ak:gsub("^KEY_", "") 411 if self.accelerators[ak] then 412 NonmodalMessage("Sorry, "..ak.." is already bound elsewhere.") 413 elseif (ak == "ESCAPE") or (ak == "RESIZE") then 414 NonmodalMessage("You can't bind that key.") 415 else 416 if oak then 417 self.accelerators[oak] = nil 418 end 419 420 self.accelerators[ak] = item.id 421 self.accelerators[item.id] = ak 422 end 423 self:drawmenustack() 424 end 425 end 426 elseif (c == "KEY_^R") then 427 if PromptForYesNo("Reset menu keybindings?", 428 "Are you sure you want to reset all the menu ".. 429 "keybindings back to their defaults?") then 430 DocumentSet.menu = CreateMenu() 431 DocumentSet:touch() 432 NonmodalMessage("All keybindings have been reset to their default settings.") 433 menu_stack = {} 434 return false 435 end 436 self:drawmenustack() 437 elseif menu.mks[c] then 438 id = menu.mks[c].id 439 break 440 end 441 end 442 443 local item = menu_tab[id] 444 local f = item.fn 445 446 if IsMenu(f) then 447 menu_stack[#menu_stack+1] = { 448 menu = menu, 449 n = n, 450 top = top 451 } 452 453 local r = self:runmenu(x+4, y+2, f) 454 menu_stack[#menu_stack] = nil 455 456 if (r == true) then 457 return true 458 elseif (r == false) then 459 return false 460 end 461 462 self:drawmenustack() 463 else 464 if not f then 465 ModalMessage("Not implemented yet", "Sorry, that feature isn't implemented yet. (This should never happen. Complain.)") 466 else 467 local _, msg = RunMenuAction(f) 468 if msg then 469 NonmodalMessage(msg) 470 end 471 end 472 menu_stack = {} 473 return true 474 end 475 end 476 end, 477 478 lookupAccelerator = function(self, c) 479 c = c:gsub("^KEY_", "") 480 481 -- Check the overrides table and only then the documentset keymap. 482 483 local id = CheckOverrideTable(c) or self.accelerators[c] 484 if not id then 485 return nil 486 end 487 488 -- Found something? Find out what function the menu ID corresponds to. 489 -- (Or maybe it's a raw function.) 490 491 local f 492 if (type(id) == "function") then 493 f = id 494 else 495 local item = menu_tab[id] 496 if not item then 497 f = function() 498 NonmodalMessage("Menu item with ID "..id.." not found.") 499 end 500 else 501 f = item.fn 502 end 503 end 504 505 return f 506 end, 507} 508 509function CreateMenu() 510 local my_key_tab = {} 511 for ak, id in pairs(key_tab) do 512 my_key_tab[ak] = id 513 my_key_tab[id] = ak 514 end 515 516 local m = { 517 accelerators = my_key_tab 518 } 519 setmetatable(m, {__index = MenuClass}) 520 return m 521end 522 523function RebuildParagraphStylesMenu(styles) 524 submenu(ParagraphStylesMenu) 525 526 local m = {} 527 528 for id, style in ipairs(styles) do 529 local shortcut 530 if (id <= 10) then 531 shortcut = tostring(id - 1) 532 else 533 shortcut = string.char(id + 54) 534 end 535 536 m[#m+1] = {"SP"..id, shortcut, style.name..": "..style.desc, nil, 537 function() 538 Cmd.ChangeParagraphStyle(style.name) 539 end} 540 end 541 542 addmenu("Paragraph Styles", m, ParagraphStylesMenu) 543end 544 545function RebuildDocumentsMenu(documents) 546 -- Remember any accelerator keys and unhook the old menu. 547 548 local ak_tab = {} 549 for _, item in ipairs(DocumentsMenu) do 550 local ak = DocumentSet.menu.accelerators[item.id] 551 if ak then 552 ak_tab[item.label] = ak 553 end 554 end 555 submenu(DocumentsMenu) 556 557 -- Construct the new menu. 558 559 local m = {} 560 for id, document in ipairs(documents) do 561 local ak = ak_tab[document.name] 562 local shortcut 563 if (id <= 10) then 564 shortcut = tostring(id - 1) 565 else 566 shortcut = string.char(id + 54) 567 end 568 569 m[#m+1] = {"D"..id, shortcut, document.name, ak, 570 function() 571 Cmd.ChangeDocument(document.name) 572 end} 573 end 574 575 -- Hook it. 576 577 addmenu("Documents", m, DocumentsMenu) 578end 579 580function ListMenuItems() 581 local function list(menu) 582 for _, item in ipairs(menu) do 583 if IsMenu(item) then 584 io.stdout:write( 585 string.format("%15s %s\n", item.id, item.label)) 586 if IsMenu(item.fn) then 587 list(item.fn) 588 end 589 end 590 end 591 end 592 593 io.stdout:write("All supported menu items:\n\n") 594 list(MainMenu) 595end 596 597