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