1--[[ Copyright (c) 2009 Peter "Corsix" Cawley 2 3Permission is hereby granted, free of charge, to any person obtaining a copy of 4this software and associated documentation files (the "Software"), to deal in 5the Software without restriction, including without limitation the rights to 6use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7of the Software, and to permit persons to whom the Software is furnished to do 8so, subject to the following conditions: 9 10The above copyright notice and this permission notice shall be included in all 11copies or substantial portions of the Software. 12 13THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19SOFTWARE. --]] 20 21corsixth.require("window") 22 23--! Top-level container for all other user-interface components. 24class "UI" (Window) 25 26---@type UI 27local UI = _G["UI"] 28 29local TH = require("TH") 30local SDL = require("sdl") 31local WM = SDL.wm 32local lfs = require("lfs") 33 34local function invert(t) 35 local r = {} 36 for k, v in pairs(t) do 37 if type(v) == "table" then 38 for _, val in ipairs(v) do 39 r[val] = k 40 end 41 else 42 r[v] = k 43 end 44 end 45 return r 46end 47 48function UI:initKeyAndButtonCodes() 49 local key_remaps = {} 50 local button_remaps = {} 51 local key_to_button_remaps = {} 52 local key_norms = setmetatable({ 53 [" "] = "space", 54 esc = "escape", 55 }, {__index = function(t, k) 56 k = tostring(k):lower() 57 return rawget(t, k) or k 58 end}) 59 --[===[ 60 do 61 local ourpath = debug.getinfo(1, "S").source:sub(2, -7) 62 local result, err = loadfile_envcall(ourpath .. "key_mapping.txt") 63 if not result then 64 print("Cannot load key mapping:" .. err) 65 else 66 local env = { 67 key_remaps = function(t) 68 for k, v in pairs(t) do 69 key_remaps[key_norms[k]] = key_norms[v] 70 end 71 end, 72 button_remaps = function(t) 73 for k, v in pairs(t) do 74 k = key_norms[k] 75 if k == "left" or k == "middle" or k == "right" then 76 button_remaps[k] = key_norms[v] 77 else 78 key_to_button_remaps[k] = key_norms[v] 79 end 80 end 81 end, 82 } 83 setmetatable(env, {__index = function(_, k) 84 return k 85 end}) 86 result(env) 87 end 88 end 89 ]===] 90 91 local keypad = { 92 ["Keypad 0"] = "insert", 93 ["Keypad 1"] = "end", 94 ["Keypad 2"] = "down", 95 ["Keypad 3"] = "pagedown", 96 ["Keypad 4"] = "left", 97 ["Keypad 6"] = "right", 98 ["Keypad 7"] = "home", 99 ["Keypad 8"] = "up", 100 ["Keypad 9"] = "pageup", 101 ["Keypad ."] = "delete", 102 } 103 104 -- Apply keypad remapping 105 for k, v in pairs(keypad) do 106 key_remaps[key_norms[k]] = key_norms[v] 107 end 108 109 self.key_remaps = key_remaps 110 self.key_to_button_remaps = key_to_button_remaps 111 112 self.button_codes = { 113 left = 1, 114 middle = 2, 115 right = 3, 116 } 117 118 -- Apply button remaps directly to codes, as mouse button codes are reliable 119 -- (keyboard key codes are not). 120 local original_button_codes = {} 121 for input, behave_as in pairs(button_remaps) do 122 local code = original_button_codes[input] or self.button_codes[input] or {} 123 if not original_button_codes[input] then 124 original_button_codes[input] = code 125 self.button_codes[input] = nil 126 end 127 if not original_button_codes[behave_as] then 128 original_button_codes[behave_as] = self.button_codes[behave_as] 129 end 130 self.button_codes[behave_as] = code 131 end 132 133 self.button_codes = invert(self.button_codes) 134end 135 136local LOADED_DIALOGS = false 137 138function UI:UI(app, minimal) 139 self:Window() 140 self:initKeyAndButtonCodes() 141 self.app = app 142 self.screen_offset_x = 0 143 self.screen_offset_y = 0 144 self.cursor = nil 145 self.cursor_entity = nil 146 self.debug_cursor_entity = nil 147 -- through trial and error, this palette seems to give the desired result (white background, black text) 148 -- NB: Need a palette present in both the full game and in the demo data 149 if minimal then 150 self.tooltip_font = app.gfx:loadBuiltinFont() 151 else 152 local palette = app.gfx:loadPalette("QData", "PREF01V.PAL") 153 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 154 self.tooltip_font = app.gfx:loadFont("QData", "Font00V", false, palette) 155 end 156 self.tooltip = nil 157 self.tooltip_counter = 0 158 self.background = false 159 -- tick_scroll_amount will either hold a table containing x and y values, at 160 -- at least one of which being non-zero. If both x and y are zero, then the 161 -- value false should be used instead, so that tests to see if there is any 162 -- scrolling to be done are quick and simple. 163 self.tick_scroll_amount = false 164 self.tick_scroll_amount_mouse = false 165 self.tick_scroll_mult = 1 166 self.modal_windows = { 167 -- [class_name] -> window, 168 } 169 -- Windows can tell UI to pass specific codes forward to them. See addKeyHandler and removeKeyHandler 170 self.key_handlers = {} 171 -- For use in onKeyUp when assigning hotkeys in the "Assign Hotkeys" window. 172 self.temp_button_down = false 173 -- 174 self.key_noted = false 175 self.mouse_released = false 176 177 self.down_count = 0 178 if not minimal then 179 self.default_cursor = app.gfx:loadMainCursor("default") 180 self.down_cursor = app.gfx:loadMainCursor("clicked") 181 self.grab_cursor = app.gfx:loadMainCursor("grab") 182 self.edit_room_cursor = app.gfx:loadMainCursor("edit_room") 183 self.waiting_cursor = app.gfx:loadMainCursor("sleep") 184 end 185 self.editing_allowed = true 186 187 if not LOADED_DIALOGS then 188 app:loadLuaFolder("dialogs", true) 189 app:loadLuaFolder("dialogs/fullscreen", true) 190 app:loadLuaFolder("dialogs/resizables", true) 191 app:loadLuaFolder("dialogs/resizables/menu_list_dialogs", true) 192 app:loadLuaFolder("dialogs/resizables/file_browsers", true) 193 LOADED_DIALOGS = true 194 end 195 196 self:setCursor(self.default_cursor) 197 198 -- to avoid a bug which causes open fullscreen windows to display incorrectly, load 199 -- the sprite sheet associated with all fullscreen windows so they are correctly cached. 200 -- Darrell: Only do this if we have a valid data directory otherwise we won't be able to 201 -- display the directory browser to even find the data directory. 202 -- Edvin: Also, the demo does not contain any of the dialogs. 203 if self.app.good_install_folder and not self.app.using_demo_files then 204 local gfx = self.app.gfx 205 local palette 206 -- load drug casebook sprite table 207 palette = gfx:loadPalette("QData", "DrugN01V.pal") 208 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 209 gfx:loadSpriteTable("QData", "DrugN02V", true, palette) 210 -- load fax sprite table 211 palette = gfx:loadPalette("QData", "Fax01V.pal") 212 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 213 gfx:loadSpriteTable("QData", "Fax02V", true, palette) 214 -- load town map sprite table 215 palette = gfx:loadPalette("QData", "Town01V.pal") 216 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 217 gfx:loadSpriteTable("QData", "Town02V", true, palette) 218 -- load hospital policy sprite table 219 palette = gfx:loadPalette("QData", "Pol01V.pal") 220 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 221 gfx:loadSpriteTable("QData", "Pol02V", true, palette) 222 -- load bank manager sprite table 223 palette = gfx:loadPalette("QData", "Bank01V.pal") 224 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 225 gfx:loadSpriteTable("QData", "Bank02V", true, palette) 226 -- load research screen sprite table 227 palette = gfx:loadPalette("QData", "Res01V.pal") 228 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 229 gfx:loadSpriteTable("QData", "Res02V", true, palette) 230 -- load progress report sprite table 231 palette = gfx:loadPalette("QData", "Rep01V.pal") 232 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 233 gfx:loadSpriteTable("QData", "Rep02V", true, palette) 234 -- load annual report sprite table 235 palette = gfx:loadPalette("QData", "Award02V.pal") 236 palette:setEntry(255, 0xFF, 0x00, 0xFF) -- Make index 255 transparent 237 gfx:loadSpriteTable("QData", "Award03V", true, palette) 238 end 239 240 self:setupGlobalKeyHandlers() 241end 242 243function UI:runDebugScript() 244 print("Executing Debug Script...") 245 local path_sep = package.config:sub(1, 1) 246 local lua_dir = debug.getinfo(1, "S").source:sub(2, -8) 247 _ = TheApp.ui and TheApp.ui.debug_cursor_entity 248 local script = assert(loadfile(lua_dir .. path_sep .. "debug_script.lua")) 249 script() 250 -- Clear _ after the script to prevent save corruption 251 _ = nil 252end 253 254function UI:setupGlobalKeyHandlers() 255 -- Add some global keyhandlers 256 self:addKeyHandler("global_cancel", self, self.closeWindow) 257 self:addKeyHandler("global_cancel_alt", self, self.closeWindow) 258 self:addKeyHandler("global_stop_movie", self, self.stopMovie) 259 self:addKeyHandler("global_stop_movie_alt", self, self.stopMovie) 260 self:addKeyHandler("global_screenshot", self, self.makeScreenshot) 261 self:addKeyHandler("global_fullscreen_toggle", self, self.toggleFullscreen) 262 self:addKeyHandler("global_exitApp", self, self.exitApplication) 263 self:addKeyHandler("global_resetApp", self, self.resetApp) 264 self:addKeyHandler("global_releaseMouse", self, self.releaseMouse) 265 266 self:addOrRemoveDebugModeKeyHandlers() 267end 268 269function UI:connectDebugger() 270 local error_message = TheApp:connectDebugger() 271 if error_message then 272 self:addWindow(UIInformation(self, {error_message})) 273 end 274end 275 276-- Used for everything except music and announcements 277function UI:playSound(name, played_callback, played_callback_delay) 278 if self.app.config.play_sounds then 279 self.app.audio:playSound(name, nil, false, played_callback, played_callback_delay) 280 end 281end 282 283-- Used for announcements only 284function UI:playAnnouncement(name, priority, played_callback, played_callback_delay) 285 if self.app.config.play_announcements then 286 self.app.audio:playSound(name, nil, true, played_callback, played_callback_delay) 287 end 288end 289 290function UI:setDefaultCursor(cursor) 291 if cursor == nil then 292 cursor = "default" 293 end 294 if type(cursor) == "string" then 295 cursor = self.app.gfx:loadMainCursor(cursor) 296 end 297 if self.cursor == self.default_cursor then 298 self:setCursor(cursor) 299 end 300 self.default_cursor = cursor 301end 302 303function UI:setCursor(cursor) 304 if cursor ~= self.cursor then 305 self.cursor = cursor 306 if cursor.use then 307 -- Cursor is a true C cursor, perhaps even a hardware cursor. 308 -- Make the real cursor visible, and use this as it. 309 self.simulated_cursor = nil 310 WM.showCursor(true) 311 cursor:use(self.app.video) 312 else 313 -- Cursor is a Lua simulated cursor. 314 -- Make the real cursor invisible, and simulate it with this. 315 WM.showCursor(self.mouse_released) 316 self.simulated_cursor = cursor 317 end 318 end 319end 320 321function UI:drawTooltip(canvas) 322 if not self.tooltip or not self.tooltip_counter or self.tooltip_counter > 0 then 323 return 324 end 325 326 local x, y = self.tooltip.x, self.tooltip.y 327 if not self.tooltip.x then 328 -- default to cursor position for (lower left corner of) tooltip 329 x, y = self:getCursorPosition() 330 end 331 332 if self.tooltip_font then 333 self.tooltip_font:drawTooltip(canvas, self.tooltip.text, x, y) 334 end 335end 336 337function UI:draw(canvas) 338 local app = self.app 339 if self.background then 340 local bg_w, bg_h = self.background_width, self.background_height 341 local screen_w, screen_h = app.config.width, app.config.height 342 local factor = math.max(screen_w / bg_w, screen_h / bg_h) 343 if canvas:scale(factor, "bitmap") or canvas:scale(factor) then 344 self.background:draw(canvas, math.floor((screen_w - bg_w * factor) / 2), math.floor((screen_h - bg_h * factor) / 2)) 345 canvas:scale(1) 346 else 347 canvas:fillBlack() 348 self.background:draw(canvas, math.floor((screen_w - bg_w) / 2), math.floor((screen_h - bg_h) / 2)) 349 end 350 end 351 Window.draw(self, canvas, 0, 0) 352 self:drawTooltip(canvas) 353 if self.simulated_cursor then 354 self.simulated_cursor.draw(canvas, self.cursor_x, self.cursor_y) 355 end 356end 357 358--! Register a key handler / hotkey for a window. 359--!param keys (string or table) The keyboard key which should trigger the callback (for 360-- example, "left" or "z" or "F9"), or a list with modifier(s) and the key (e.g. {"ctrl", "s"}). 361--!param window (Window) The UI window which should receive the callback. 362--!param callback (function) The method to be called on `window` when `key` is 363-- pressed. 364--!param ... Additional arguments to `callback`. 365function UI:addKeyHandler(keys, window, callback, ...) 366 -- It is necessary to clone the key table into another temporary table, as if we don't the original table that we take it from will lose 367 -- the last key of that table permenently in the next line of code after this one, until the program is restarted. 368 -- I.E. if the "ingame_quitLevel" hotkey from the "hotkeys_values" table in "config_finder.lua" is a table that looks like this: 369 -- {"shift", "q"} 370 -- We would lose the "q" element until we restarted the game and the "hotkey.txt" was read from again, causing the "ingame_quitLevel" 371 -- table to be reset back to {"shift, "q"} 372 local temp_keys = {} 373 374 -- Check to see if "keys" key exist in the hotkeys table. 375 if self.app.hotkeys[keys] ~= nil then 376 if type(self.app.hotkeys[keys]) == "table" then 377 temp_keys = shallow_clone(self.app.hotkeys[keys]) 378 elseif type(self.app.hotkeys[keys]) == "string" then 379 temp_keys = shallow_clone({self.app.hotkeys[keys]}) 380 end 381 else 382 if type(keys) == "string" then 383 print(string.format("\"%s\" does not exist in the hotkeys configuration file.", keys)) 384 else 385 print("Usage of addKeyHandler() requires the first argument to be a string of a key that can be found in the hotkeys configuration file.") 386 end 387 end 388 389 if temp_keys ~= nil then 390 local has_enterOrPlus 391 local temp_keys_copy = {} 392 393 if type(temp_keys) == "table" then 394 temp_keys_copy = shallow_clone(temp_keys) 395 elseif type(temp_keys) == "string" then 396 temp_keys_copy = {temp_keys} 397 end 398 399 for _, v in pairs(temp_keys_copy) do 400 if v == "enter" then 401 has_enterOrPlus = true 402 elseif v == "return" then 403 has_enterOrPlus = true 404 elseif v == "+" then 405 has_enterOrPlus = true 406 elseif v == "=" then 407 has_enterOrPlus = true 408 else 409 has_enterOrPlus = false 410 end 411 end 412 413 local key = table.remove(temp_keys, #temp_keys):lower() 414 local modifiers = list_to_set(temp_keys) -- SET of modifiers 415 if not self.key_handlers[key] then 416 -- No handlers for this key? Create a new table. 417 self.key_handlers[key] = {} 418 end 419 420 table.insert(self.key_handlers[key], { 421 modifiers = modifiers, 422 window = window, 423 callback = callback, 424 ... 425 }) 426 427 -- If the handler added has enter, return, plus, or minus in it... 428 if has_enterOrPlus then 429 for k, _ in pairs(temp_keys_copy) do 430 if temp_keys_copy[k] == "enter" then 431 temp_keys_copy[k] = "return" 432 elseif temp_keys_copy[k] == "return" then 433 temp_keys_copy[k] = "enter" 434 elseif temp_keys_copy[k] == "+" then 435 temp_keys_copy[k] = "=" 436 elseif temp_keys_copy[k] == "=" then 437 temp_keys_copy[k] = "+" 438 end 439 end 440 441 local key_02 = table.remove(temp_keys_copy, #temp_keys_copy):lower() 442 local modifiers_02 = list_to_set(temp_keys_copy) -- SET of modifiers 443 if not self.key_handlers[key_02] then 444 -- No handlers for this key? Create a new table. 445 self.key_handlers[key_02] = {} 446 end 447 448 -- Then make the same handler, but with the complementary button. 449 -- i.e. If it asks for "enter", it will also add "return". 450 table.insert(self.key_handlers[key_02], { 451 modifiers = modifiers_02, 452 window = window, 453 callback = callback, 454 ... 455 }) 456 end 457 else 458 print("addKeyHandler() failed.") 459 end 460end 461 462--! Unregister a key handler previously registered by `addKeyHandler`. 463--!param keys (string or table) The key or list of modifiers+key of a key / window 464-- pair previously passed to `addKeyHandler`. 465--!param window (Window) The window of a key / window pair previously passed 466-- to `addKeyHandler`. 467function UI:removeKeyHandler(keys, window) 468 local temp_keys = nil 469 470 -- Check to see if "keys" key exist in the hotkeys table. 471 if self.app.hotkeys[keys] ~= nil then 472 if type(self.app.hotkeys[keys]) == "table" then 473 temp_keys = shallow_clone(self.app.hotkeys[keys]) 474 elseif type(self.app.hotkeys[keys]) == "string" then 475 temp_keys = shallow_clone({self.app.hotkeys[keys]}) 476 end 477 else 478 if type(keys) == "string" then 479 print(string.format("\"%s\" does not exist in the \"ui.key_handlers\" table.", keys)) 480 else 481 print("Usage of removeKeyHandler() requires the first argument to be a string of a key that can be found in the \"ui.key_handlers\" table.") 482 end 483 end 484 485 if temp_keys ~= nil then 486 local has_enterOrPlus 487 local temp_keys_copy = {} 488 489 if type(temp_keys) == "table" then 490 temp_keys_copy = shallow_clone(temp_keys) 491 elseif type(temp_keys) == "string" then 492 temp_keys_copy = shallow_clone({temp_keys}) 493 end 494 495 for _, v in pairs(temp_keys_copy) do 496 if v == "enter" then 497 has_enterOrPlus = true 498 elseif v == "return" then 499 has_enterOrPlus = true 500 elseif v == "+" then 501 has_enterOrPlus = true 502 elseif v == "=" then 503 has_enterOrPlus = true 504 else 505 has_enterOrPlus = false 506 end 507 end 508 509 local key = table.remove(temp_keys, #temp_keys):lower() 510 local modifiers = list_to_set(temp_keys) -- SET of modifiers 511 if self.key_handlers[key] then 512 for index, info in ipairs(self.key_handlers[key]) do 513 if info.window == window and compare_tables(info.modifiers, modifiers) then 514 table.remove(self.key_handlers[key], index) 515 end 516 end 517 -- If last key handler was removed, delete the (now empty) list. 518 if #self.key_handlers[key] == 0 then 519 self.key_handlers[key] = nil 520 end 521 end 522 523 -- If the handler added has enter, return, plus, or minus in it... 524 if has_enterOrPlus then 525 for k, _ in pairs(temp_keys_copy) do 526 if temp_keys_copy[k] == "enter" then 527 temp_keys_copy[k] = "return" 528 elseif temp_keys_copy[k] == "return" then 529 temp_keys_copy[k] = "enter" 530 elseif temp_keys_copy[k] == "+" then 531 temp_keys_copy[k] = "=" 532 elseif temp_keys_copy[k] == "=" then 533 temp_keys_copy[k] = "+" 534 end 535 end 536 537 local key_02 = table.remove(temp_keys_copy, #temp_keys_copy):lower() 538 local modifiers_02 = list_to_set(temp_keys_copy) -- SET of modifiers 539 if self.key_handlers[key_02] then 540 for index, info in ipairs(self.key_handlers[key_02]) do 541 if info.window == window and compare_tables(info.modifiers, modifiers_02) then 542 table.remove(self.key_handlers[key_02], index) 543 end 544 end 545 -- If last key handler was removed, delete the (now empty) list. 546 if #self.key_handlers[key_02] == 0 then 547 self.key_handlers[key_02] = nil 548 end 549 end 550 end 551 end 552end 553 554--! Set the menu background image 555--! 556--! The menu size closest to, but no larger than the height of the currently 557--! set game window is selected. If no image fits that criteria the smallest 558--! available image is used. 559function UI:setMenuBackground() 560 local screen_h = self.app.config.height 561 local bg_size_idx = 1 562 563 -- Available mainmenu*.bmp sizes 564 local menu_bg_sizes = { 565 {640, 480}, 566 {1280, 720}, 567 {1920, 1080}, 568 } 569 570 for i, bg_size in ipairs(menu_bg_sizes) do 571 if screen_h >= bg_size[2] then 572 bg_size_idx = i 573 else 574 break 575 end 576 end 577 578 local bg_size = menu_bg_sizes[bg_size_idx] 579 self.background_width = bg_size[1] 580 self.background_height = bg_size[2] 581 self.background = self.app.gfx:loadRaw("mainmenu" .. bg_size[2], bg_size[1], bg_size[2], "Bitmap") 582end 583 584function UI:onChangeResolution() 585 -- If we are in the main menu (== no world), reselect the background 586 if not self.app.world then 587 self:setMenuBackground() 588 end 589 -- Inform windows of resolution change 590 if not self.windows then 591 return 592 end 593 for _, window in ipairs(self.windows) do 594 window:onChangeResolution() 595 end 596end 597 598function UI:registerTextBox(box) 599 self.textboxes[#self.textboxes + 1] = box 600end 601 602function UI:unregisterTextBox(box) 603 for num, b in ipairs(self.textboxes) do 604 if b == box then 605 table.remove(self.textboxes, num) 606 break 607 end 608 end 609end 610 611function UI:registerHotkeyBox(box) 612 self.hotkeyboxes[#self.hotkeyboxes + 1] = box 613end 614 615function UI:unregisterHotkeyBox(box) 616 for num, b in ipairs(self.hotkeyboxes) do 617 if b == box then 618 table.remove(self.hotkeyboxes, num) 619 break 620 end 621 end 622end 623 624function UI:changeResolution(width, height) 625 self.app:prepareVideoUpdate() 626 local error_message = self.app.video:update(width, height, unpack(self.app.modes)) 627 self.app:finishVideoUpdate() 628 629 if error_message then 630 print("Warning: Could not change resolution to " .. width .. "x" .. height .. ".") 631 print("The error was: ") 632 print(error_message) 633 return false 634 end 635 636 self.app.config.width = width 637 self.app.config.height = height 638 639 -- Redraw cursor 640 local cursor = self.cursor 641 self.cursor = nil 642 self:setCursor(cursor) 643 -- Save new setting in config 644 self.app:saveConfig() 645 646 self:onChangeResolution() 647 648 return true 649end 650 651function UI:toggleCaptureMouse() 652 self.app.capturemouse = not self.app.capturemouse 653 self.app.video:setCaptureMouse(self.app.capturemouse) 654end 655 656function UI:setMouseReleased(released) 657 if released == self.mouse_released then 658 return 659 end 660 661 self.mouse_released = released 662 663 -- If we are using a software cursor, show the hardware cursor on release 664 -- and hide it again on capture. 665 if self.cursor and not self.cursor.use then 666 WM.showCursor(released) 667 end 668 669 self.app.video:setCaptureMouse(self.app.capturemouse and not self.app.mouse_released) 670end 671 672function UI:releaseMouse() 673 self:setMouseReleased(true) 674end 675 676function UI:toggleFullscreen() 677 local modes = self.app.modes 678 679 local function toggleMode(index) 680 self.app.fullscreen = not self.app.fullscreen 681 if self.app.fullscreen then 682 modes[index] = "fullscreen" 683 else 684 modes[index] = "" 685 end 686 end 687 688 -- Search in modes table if it contains a fullscreen value and keep the index 689 -- If not found, we will add an index at end of table 690 local index = #modes + 1 691 for i=1, #modes do 692 if modes[i] == "fullscreen" then 693 index = i 694 break 695 end 696 end 697 698 -- Toggle Fullscreen mode 699 toggleMode(index) 700 701 local success = true 702 self.app:prepareVideoUpdate() 703 local error_message = self.app.video:update(self.app.config.width, self.app.config.height, unpack(modes)) 704 self.app:finishVideoUpdate() 705 706 if error_message then 707 success = false 708 local mode_string = modes[index] or "windowed" 709 print("Warning: Could not toggle to " .. mode_string .. " mode with resolution of " .. self.app.config.width .. "x" .. self.app.config.height .. ".") 710 -- Revert fullscreen mode modifications 711 toggleMode(index) 712 end 713 714 -- Redraw cursor 715 local cursor = self.cursor 716 self.cursor = nil 717 self:setCursor(cursor) 718 719 if success then 720 -- Save new setting in config 721 self.app.config.fullscreen = self.app.fullscreen 722 self.app:saveConfig() 723 end 724 725 return success 726end 727 728--! Called when the user presses a key on the keyboard 729--!param rawchar (string) The name of the key the user pressed. 730--!param is_repeat (boolean) True if this is a key repeat event 731function UI:onKeyDown(rawchar, modifiers, is_repeat) 732 local handled = false 733 -- Apply key-remapping and normalisation 734 rawchar = string.sub(rawchar,1,6) == "Keypad" and 735 modifiers["numlockactive"] and string.sub(rawchar,8) or rawchar 736 local key = rawchar:lower() 737 do 738 local mapped_button = self.key_to_button_remaps[key] 739 if mapped_button then 740 self:onMouseDown(mapped_button, self.cursor_x, self.cursor_y) 741 return true 742 end 743 key = self.key_remaps[key] or key 744 end 745 746 -- Remove numlock modifier 747 modifiers["numlockactive"] = nil 748 -- If there is one, the current textbox gets the key. 749 -- It will not process any text at this point though. 750 for _, box in ipairs(self.textboxes) do 751 if box.enabled and box.active and not handled then 752 handled = box:keyInput(key, rawchar) 753 end 754 end 755 756 -- If there is a hotkey box 757 for _, hotkeybox in ipairs(self.hotkeyboxes) do 758 if hotkeybox.enabled and hotkeybox.active and not handled then 759 handled = hotkeybox:keyInput(key, rawchar, modifiers) 760 end 761 end 762 763 -- Otherwise, if there is a key handler bound to the given key, then it gets 764 -- the key. 765 if not handled then 766 local keyHandlers = self.key_handlers[key] 767 if keyHandlers then 768 -- Iterate over key handlers and call each one whose modifier(s) are pressed 769 -- NB: Only if the exact correct modifiers are pressed will the shortcut get processed. 770 for _, handler in ipairs(keyHandlers) do 771 if compare_tables(handler.modifiers, modifiers) then 772 handler.callback(handler.window, unpack(handler)) 773 handled = true 774 end 775 end 776 end 777 end 778 779 self.buttons_down[key] = true 780 self.modifiers_down = modifiers 781 self.key_press_handled = handled 782 return handled 783end 784 785--! Called when the user releases a key on the keyboard 786--!param rawchar (string) The name of the key the user pressed. 787function UI:onKeyUp(rawchar) 788 rawchar = SDL.getKeyModifiers().numlockactive and 789 string.sub(rawchar,1,6) == "Keypad" and string.sub(rawchar,8) or 790 rawchar 791 local key = rawchar:lower() 792 793 self.buttons_down[key] = nil 794 795 -- Go through all the hotkeyboxes. 796 for _, hotkeybox in ipairs(self.hotkeyboxes) do 797 -- If one is enabled and active... 798 if hotkeybox.enabled and hotkeybox.active then 799 -- If the key lifted is escape... 800 if(key == "escape") then 801 hotkeybox:abort() 802 hotkeybox.noted_keys = {} 803 else 804 -- Check if the current key lifted has already been noted. 805 self.key_noted = false 806 for _, v in pairs(hotkeybox.noted_keys) do 807 if v == key then 808 self.key_noted = true 809 end 810 end 811 812 -- If the current key hasn't been noted... 813 if self.key_noted == false then 814 hotkeybox.noted_keys[#hotkeybox.noted_keys + 1] = key 815 end 816 817 -- Says if there is still a button being pressed. 818 self.temp_button_down = false 819 820 -- Go through and check if there are still any buttons pressed. If so... 821 for _, _ in pairs(self.buttons_down) do 822 -- Then toggle the corresponding bool. 823 self.temp_button_down = true 824 end 825 826 --If there ISN'T still a button down when a button was released... 827 if self.temp_button_down == false then 828 -- Activate the confirm function on the hotkey box. 829 hotkeybox:confirm() 830 hotkeybox.noted_keys = {} 831 end 832 end 833 end 834 end 835end 836 837function UI:onEditingText(text, start, length) 838 -- Does nothing at the moment. We are handling text input ourselves. 839end 840 841--! Called in-between onKeyDown and onKeyUp. The argument 'text' is a 842--! string containing the input localized according to the keyboard layout 843--! the user uses. 844function UI:onTextInput(text) 845 -- It's time for any active textbox to get input. 846 for _, box in ipairs(self.textboxes) do 847 if box.enabled and box.active then 848 box:textInput(text) 849 end 850 end 851 852 -- Finally it might happen that a hotkey was not recognized because of 853 -- differing local keyboard layout. Give it another shot. 854 if not self.key_press_handled then 855 local keyHandlers = self.key_handlers[text] 856 if keyHandlers then 857 -- Iterate over key handlers and call each one whose modifier(s) are pressed 858 -- NB: Only if the exact correct modifiers are pressed will the shortcut get processed. 859 for _, handler in ipairs(keyHandlers) do 860 if compare_tables(handler.modifiers, self.modifiers_down) then 861 handler.callback(handler.window, unpack(handler)) 862 end 863 end 864 end 865 end 866end 867 868function UI:onMouseDown(code, x, y) 869 self:setMouseReleased(false) 870 local repaint = false 871 local button = self.button_codes[code] or code 872 if self.app.moviePlayer.playing then 873 if button == "left" then 874 self.app.moviePlayer:stop() 875 end 876 return true 877 end 878 if self.cursor_entity == nil and self.down_count == 0 and 879 self.cursor == self.default_cursor then 880 self:setCursor(self.down_cursor) 881 repaint = true 882 end 883 self.down_count = self.down_count + 1 884 if x >= 3 and y >= 3 and x < self.app.config.width - 3 and y < self.app.config.height - 3 then 885 self.buttons_down["mouse_"..button] = true 886 end 887 888 self:updateTooltip() 889 return Window.onMouseDown(self, button, x, y) or repaint 890end 891 892function UI:onMouseUp(code, x, y) 893 local repaint = false 894 local button = self.button_codes[code] or code 895 self.down_count = self.down_count - 1 896 if self.down_count <= 0 then 897 if self.cursor_entity == nil and self.cursor == self.down_cursor then 898 self:setCursor(self.default_cursor) 899 repaint = true 900 end 901 self.down_count = 0 902 end 903 self.buttons_down["mouse_"..button] = nil 904 905 if Window.onMouseUp(self, button, x, y) then 906 repaint = true 907 else 908 if self:ableToClickEntity(self.cursor_entity) then 909 self.cursor_entity:onClick(self, button) 910 repaint = true 911 end 912 end 913 914 self:updateTooltip() 915 return repaint 916end 917 918function UI:onMouseWheel(x, y) 919 Window.onMouseWheel(self, x, y) 920end 921 922--[[ Determines if a cursor entity can be clicked 923@param entity (Entity,nil) cursor entity clicked on if any 924@return true if can be clicked on, false otherwise (boolean) ]] 925function UI:ableToClickEntity(entity) 926 if self.cursor_entity and self.cursor_entity.onClick then 927 local hospital = entity.hospital 928 local epidemic = hospital and hospital.epidemic 929 930 return self.app.world.user_actions_allowed and not epidemic or 931 (epidemic and not epidemic.vaccination_mode_active) 932 else 933 return false 934 end 935end 936 937function UI:getScreenOffset() 938 return self.screen_offset_x, self.screen_offset_y 939end 940 941local tooltip_ticks = 30 -- Amount of ticks until a tooltip is displayed 942 943function UI:updateTooltip() 944 if self.buttons_down.mouse_left then 945 -- Disable tooltips altogether while left button is pressed. 946 self.tooltip = nil 947 self.tooltip_counter = nil 948 return 949 elseif self.tooltip_counter == nil then 950 self.tooltip_counter = tooltip_ticks 951 end 952 local tooltip = self:getTooltipAt(self.cursor_x, self.cursor_y) 953 if tooltip then 954 -- NB: Do not set counter if tooltip changes here. This allows quick tooltip reading of adjacent buttons. 955 self.tooltip = tooltip 956 else 957 -- Not hovering over any button with tooltip -> reset 958 self.tooltip = nil 959 self.tooltip_counter = tooltip_ticks 960 end 961end 962 963local UpdateCursorPosition = TH.cursor.setPosition 964 965--! Called when the mouse enters or leaves the game window. 966function UI:onWindowActive(gain) 967end 968 969--! Window has been resized by the user 970--!param width (integer) New window width 971--!param height (integer) New window height 972function UI:onWindowResize(width, height) 973 if not self.app.config.fullscreen then 974 self:changeResolution(width, height) 975 end 976end 977 978function UI:onMouseMove(x, y, dx, dy) 979 if self.mouse_released then 980 return false 981 end 982 983 local repaint = UpdateCursorPosition(self.app.video, x, y) 984 985 self.cursor_x = x 986 self.cursor_y = y 987 988 if self.drag_mouse_move then 989 self.drag_mouse_move(x, y) 990 return true 991 end 992 993 if Window.onMouseMove(self, x, y, dx, dy) then 994 repaint = true 995 end 996 997 self:updateTooltip() 998 999 return repaint 1000end 1001 1002--! Process SDL_MULTIGESTURE events. 1003--! 1004--!return (boolean) event processed indicator 1005function UI:onMultiGesture() 1006 return false 1007end 1008 1009function UI:onTick() 1010 Window.onTick(self) 1011 local repaint = false 1012 if self.tooltip_counter and self.tooltip_counter > 0 then 1013 self.tooltip_counter = self.tooltip_counter - 1 1014 repaint = (self.tooltip_counter == 0) 1015 end 1016 -- If a tooltip is currently shown, update each tick (may be dynamic) 1017 if self.tooltip then 1018 self:updateTooltip() 1019 end 1020 return repaint 1021end 1022 1023 1024function UI:addWindow(window) 1025 if window.closed then 1026 return 1027 end 1028 if window.modal_class then 1029 -- NB: while instead of if in case of another window being created during the close function 1030 while self.modal_windows[window.modal_class] do 1031 self.modal_windows[window.modal_class]:close() 1032 end 1033 self.modal_windows[window.modal_class] = window 1034 end 1035 if self.app.world and window:mustPause() then 1036 self.app.world:setSpeed("Pause") 1037 self.app.video:setBlueFilterActive(false) -- mustPause windows shouldn't cause tainting 1038 end 1039 if window.modal_class == "main" or window.modal_class == "fullscreen" then 1040 self.editing_allowed = false -- do not allow editing rooms if main windows (build, furnish, hire) are open 1041 end 1042 Window.addWindow(self, window) 1043end 1044 1045function UI:removeWindow(closing_window) 1046 if Window.removeWindow(self, closing_window) then 1047 local class = closing_window.modal_class 1048 if class and self.modal_windows[class] == closing_window then 1049 self.modal_windows[class] = nil 1050 end 1051 if self.app.world and self.app.world:isCurrentSpeed("Pause") then 1052 local pauseGame = self:checkForMustPauseWindows() 1053 if not pauseGame and closing_window:mustPause() then 1054 self.app.world:setSpeed(self.app.world.prev_speed) 1055 end 1056 end 1057 if closing_window.modal_class == "main" or closing_window.modal_class == "fullscreen" then 1058 self.editing_allowed = true -- allow editing rooms again when main window is closed 1059 end 1060 return true 1061 else 1062 return false 1063 end 1064end 1065 1066--! Function to check if we have any must pause windows open 1067--!return (bool) Returns true if a must pause window is found 1068function UI:checkForMustPauseWindows() 1069 for _, window in pairs(self.windows) do 1070 if window:mustPause() then return true end 1071 end 1072 return false 1073end 1074 1075function UI:getCursorPosition(window) 1076 -- Given no argument, returns the cursor position in screen space 1077 -- Otherwise, returns the cursor position in the space of the given window 1078 local x, y = self.cursor_x, self.cursor_y 1079 while window ~= nil and window ~= self do 1080 x = x - window.x 1081 y = y - window.y 1082 window = window.parent 1083 end 1084 return x, y 1085end 1086 1087function UI:addOrRemoveDebugModeKeyHandlers() 1088 self:removeKeyHandler("global_connectDebugger", self) 1089 self:removeKeyHandler("global_showLuaConsole", self) 1090 self:removeKeyHandler("global_runDebugScript", self) 1091 if self.app.config.debug then 1092 self:addKeyHandler("global_connectDebugger", self, self.connectDebugger) 1093 self:addKeyHandler("global_showLuaConsole", self, self.showLuaConsole) 1094 self:addKeyHandler("global_runDebugScript", self, self.runDebugScript) 1095 end 1096end 1097 1098function UI:afterLoad(old, new) 1099 -- Get rid of old key handlers from save file. 1100 self.key_handlers = {} 1101 if old < 5 then 1102 self.editing_allowed = true 1103 end 1104 self:setupGlobalKeyHandlers() 1105 1106 -- Cancel any saved screen movement from edge scrolling 1107 self.tick_scroll_amount_mouse = nil 1108 1109 Window.afterLoad(self, old, new) 1110end 1111 1112-- Stub to allow the function to be called in e.g. the information 1113-- dialog without having to worry about a GameUI being present 1114function UI:tutorialStep(...) 1115end 1116 1117function UI:makeScreenshot() 1118 -- Find an index for screenshot which is not already used 1119 local i = 0 1120 local filename 1121 repeat 1122 filename = TheApp.screenshot_dir .. ("screenshot%i.bmp"):format(i) 1123 i = i + 1 1124 until lfs.attributes(filename, "size") == nil 1125 print("Taking screenshot: " .. filename) 1126 local res, err = self.app.video:takeScreenshot(filename) -- Take screenshot 1127 if not res then 1128 print("Screenshot failed: " .. err) 1129 else 1130 self.app.audio:playSound("SNAPSHOT.WAV") 1131 end 1132end 1133 1134--! Closes one window (the topmost / active window, if possible) 1135--!return true if a window was closed 1136function UI:closeWindow() 1137 if not self.windows then 1138 return false 1139 end 1140 1141 -- Stop the lose message being closed prematurely because we pressed "Escape" on the lose movie 1142 if self.app.moviePlayer.playing then 1143 return false 1144 end 1145 1146 -- Close the topmost window first 1147 local first = self.windows[1] 1148 if first.on_top and first.esc_closes then 1149 first:close() 1150 return true 1151 end 1152 for i = #self.windows, 1, -1 do 1153 local window = self.windows[i] 1154 if window.esc_closes then 1155 window:close() 1156 return true 1157 end 1158 end 1159end 1160 1161--! Shows the Lua console 1162function UI:showLuaConsole() 1163 self:addWindow(UILuaConsole(self)) 1164end 1165 1166--! Triggers reset of the application (reloads .lua files) 1167function UI:resetApp() 1168 debug.getregistry()._RESTART = true 1169 TheApp.running = false 1170end 1171-- Added this function as quit does not exit the application, it only exits the game to the menu screen 1172function UI:exitApplication() 1173 self.app:abandon() 1174end 1175 1176--! Triggers quitting the application 1177function UI:quit() 1178 self.app:exit() 1179end 1180 1181--! Tries to stop a video, if one is currently playing 1182function UI:stopMovie() 1183 if self.app.moviePlayer.playing then 1184 self.app.moviePlayer:stop() 1185 end 1186end 1187 1188-- Stub for compatibility with savegames r1896-1921 1189function UI:stopVideo() end 1190