# Copyright 2004-2019 Tom Rothamel # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # This file contains code to manage focus on the display. from __future__ import print_function import pygame_sdl2 as pygame import renpy.display class Focus(object): def __init__(self, widget, arg, x, y, w, h, screen): self.widget = widget self.arg = arg self.x = x self.y = y self.w = w self.h = h self.screen = screen def copy(self): return Focus( self.widget, self.arg, self.x, self.y, self.w, self.h, self.screen) def __repr__(self): return "" % ( self.widget, self.arg, self.x, self.y, self.w, self.h, self.screen) # The current focus argument. argument = None # The screen of the currently focused widget. screen_of_focused = None # The widget currently grabbing the input, if any. grab = None # The default focus for the current screen. default_focus = None # The type of input that caused the focus to change last. One of # "keyboard" (for keyboard-like focus devices) or "mouse" (for mouse-like) # focus devices.) focus_type = "mouse" # The same, but for the most recent input that might potentially cause # the focus to change. pending_focus_type = "mouse" # The current tooltip and tooltip screen. tooltip = None # Sets the currently focused widget. def set_focused(widget, arg, screen): global argument argument = arg global screen_of_focused screen_of_focused = screen renpy.game.context().scene_lists.focused = widget renpy.display.tts.displayable(widget) global tooltip # Figure out the tooltip. if widget is None: new_tooltip = None else: new_tooltip = widget._get_tooltip() if tooltip != new_tooltip: tooltip = new_tooltip renpy.exports.restart_interaction() def get_focused(): """ Gets the currently focused displayable. """ return renpy.game.context().scene_lists.focused def get_mouse(): """ Gets the mouse associated with the currently focused displayable. """ focused = get_focused() if focused is None: return None else: return focused.style.mouse def get_tooltip(screen=None): """ Gets the tooltip information. """ if screen is None: return tooltip if screen_of_focused is None: return None if screen_of_focused.screen_name[0] == screen: return tooltip if screen_of_focused.tag == screen: return tooltip return None def set_grab(widget): global grab grab = widget renpy.exports.cancel_gesture() def get_grab(): return grab # The current list of focuses that we know about. focus_list = [ ] # This takes in a focus list from the rendering system. def take_focuses(): global focus_list focus_list = [ ] renpy.display.render.take_focuses(focus_list) global default_focus default_focus = None global grab grab_found = False for f in focus_list: if f.x is None: default_focus = f if f.widget is grab: grab_found = True if not grab_found: grab = None if (default_focus is not None) and (get_focused() is None): change_focus(default_focus, True) def focus_coordinates(): """ :doc: other This attempts to find the coordinates of the currently-focused displayable. If it can, it will return them as a (x, y, w, h) tuple. If not, it will return a (None, None, None, None) tuple. """ current = get_focused() for i in focus_list: if i.widget == current and i.arg == argument: return i.x, i.y, i.w, i.h return None, None, None, None # A map from id(displayable) to the displayable that replaces it. replaced_by = { } def before_interact(roots): """ Called before each interaction to choose the focused and grabbed displayables. """ global new_grab global grab # a list of focusable, name, screen tuples. fwn = [ ] def callback(f, n): fwn.append((f, n, renpy.display.screen._current_screen)) for root in roots: root.find_focusable(callback, None) # Assign a full name to each focusable. namecount = { } fwn2 = [ ] for fwn_tuple in fwn: f, n, screen = fwn_tuple serial = namecount.get(n, 0) namecount[n] = serial + 1 if f is None: continue f.full_focus_name = n, serial replaced_by[id(f)] = f fwn2.append(fwn_tuple) fwn = fwn2 # We assume id(None) is not in replaced_by. replaced_by.pop(None, None) # If there's something with the same full name as the current widget, # it becomes the new current widget. current = get_focused() current = replaced_by.get(id(current), current) if current is not None: current_name = current.full_focus_name for f, n, screen in fwn: if f.full_focus_name == current_name: current = f set_focused(f, argument, screen) break else: current = None # Otherwise, focus the default widget. if current is None: defaults = [ ] for f, n, screen in fwn: if f.default: defaults.append((f.default, f, screen)) if defaults: if len(defaults) > 1: defaults.sort() _, f, screen = defaults[-1] current = f set_focused(f, None, screen) if current is None: set_focused(None, None, None) # Finally, mark the current widget as the focused widget, and # all other widgets as unfocused. for f, n, screen in fwn: if f is not current: renpy.display.screen.push_current_screen(screen) try: f.unfocus(default=True) finally: renpy.display.screen.pop_current_screen() if current: renpy.display.screen.push_current_screen(screen_of_focused) try: current.focus(default=True) finally: renpy.display.screen.pop_current_screen() # Update the grab. grab = replaced_by.get(id(grab), None) # Clear replaced_by. replaced_by.clear() # This changes the focus to be the widget contained inside the new # focus object. def change_focus(newfocus, default=False): rv = None if grab: return if newfocus is None: widget = None else: widget = newfocus.widget current = get_focused() # Nothing to do. if current is widget and (newfocus is None or newfocus.arg == argument): return rv global focus_type focus_type = pending_focus_type if current is not None: try: renpy.display.screen.push_current_screen(screen_of_focused) current.unfocus(default=default) finally: renpy.display.screen.pop_current_screen() current = widget if newfocus is not None: set_focused(current, newfocus.arg, newfocus.screen) else: set_focused(None, None, None) if widget is not None: try: renpy.display.screen.push_current_screen(screen_of_focused) rv = widget.focus(default=default) finally: renpy.display.screen.pop_current_screen() return rv def clear_focus(): """ Clears the focus when the window loses mouse focus. """ change_focus(None) # This handles mouse events, to see if they change the focus. def mouse_handler(ev, x, y, default=False): """ Handle mouse events, to see if they change the focus. `ev` If ev is not None, this function checks to see if it is a mouse event. """ global pending_focus_type if ev is not None: if ev.type not in (pygame.MOUSEMOTION, pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN): return else: pending_focus_type = "mouse" new_focus = renpy.display.render.focus_at_point(x, y) if new_focus is None: new_focus = default_focus return change_focus(new_focus, default=default) # This focuses an extreme widget, which is one of the widgets that's # at an edge. To do this, we multiply the x, y, width, and height by # the supplied multiplers, add them all up, and take the focus with # the largest value. def focus_extreme(xmul, ymul, wmul, hmul): max_focus = None max_score = -(65536**2) for f in focus_list: if f.x is None: continue score = (f.x * xmul + f.y * ymul + f.w * wmul + f.h * hmul) if score > max_score: max_score = score max_focus = f if max_focus: return change_focus(max_focus) # This calculates the distance between two points, applying # the given fudge factors. The distance is left squared. def points_dist(x0, y0, x1, y1, xfudge, yfudge): return (( x0 - x1 ) * xfudge ) ** 2 + \ (( y0 - y1 ) * yfudge ) ** 2 # This computes the distance between two horizontal lines. (So the # distance is either vertical, or has a vertical component to it.) # # The distance is left squared. def horiz_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1): # The lines overlap in x. if bx0 <= ax0 <= ax1 <= bx1 or \ ax0 <= bx0 <= bx1 <= ax1 or \ ax0 <= bx0 <= ax1 <= bx1 or \ bx0 <= ax0 <= bx1 <= ax1: return (ay0 - by0) ** 2 # The right end of a is to the left of the left end of b. if ax0 <= ax1 <= bx0 <= bx1: return points_dist(ax1, ay1, bx0, by0, renpy.config.focus_crossrange_penalty, 1.0) else: return points_dist(ax0, ay0, bx1, by1, renpy.config.focus_crossrange_penalty, 1.0) # This computes the distance between two vertical lines. (So the # distance is either hortizontal, or has a horizontal component to it.) # # The distance is left squared. def verti_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1): # The lines overlap in x. if by0 <= ay0 <= ay1 <= by1 or \ ay0 <= by0 <= by1 <= ay1 or \ ay0 <= by0 <= ay1 <= by1 or \ by0 <= ay0 <= by1 <= ay1: return (ax0 - bx0) ** 2 # The right end of a is to the left of the left end of b. if ay0 <= ay1 <= by0 <= by1: return points_dist(ax1, ay1, bx0, by0, 1.0, renpy.config.focus_crossrange_penalty) else: return points_dist(ax0, ay0, bx1, by1, 1.0, renpy.config.focus_crossrange_penalty) # This focuses the widget that is nearest to the current widget. To # determine nearest, we compute points on the widgets using the # {from,to}_{x,y}off values. We pick the nearest, applying a fudge # multiplier to the distances in each direction, that satisfies # the condition (which is given a Focus object to evaluate). # # If no focus can be found matching the above, we look for one # with an x of None, and make that the focus. Otherwise, we do # nothing. # # If no widget is focused, we pick one and focus it. # # If the current widget has an x of None, we pass things off to # focus_extreme to deal with. def focus_nearest(from_x0, from_y0, from_x1, from_y1, to_x0, to_y0, to_x1, to_y1, line_dist, condition, xmul, ymul, wmul, hmul): global pending_focus_type pending_focus_type = "keyboard" if not focus_list: return # No widget focused. current = get_focused() if not current: change_focus(focus_list[0]) return # Find the current focus. for f in focus_list: if f.widget is current and f.arg == argument: from_focus = f break else: # If we can't pick something. change_focus(focus_list[0]) return # If placeless, focus_extreme. if from_focus.x is None: focus_extreme(xmul, ymul, wmul, hmul) return fx0 = from_focus.x + from_focus.w * from_x0 fy0 = from_focus.y + from_focus.h * from_y0 fx1 = from_focus.x + from_focus.w * from_x1 fy1 = from_focus.y + from_focus.h * from_y1 placeless = None new_focus = None # a really big number. new_focus_dist = (65536.0 * renpy.config.focus_crossrange_penalty) ** 2 for f in focus_list: if f is from_focus: continue if not f.widget.style.keyboard_focus: continue if f.x is None: placeless = f continue if not condition(from_focus, f): continue tx0 = f.x + f.w * to_x0 ty0 = f.y + f.h * to_y0 tx1 = f.x + f.w * to_x1 ty1 = f.y + f.h * to_y1 dist = line_dist(fx0, fy0, fx1, fy1, tx0, ty0, tx1, ty1) if dist < new_focus_dist: new_focus = f new_focus_dist = dist # If we couldn't find anything, try the placeless focus. new_focus = new_focus or placeless # If we have something, switch to it. if new_focus: return change_focus(new_focus) # And, we're done. def focus_ordered(delta): global pending_focus_type pending_focus_type = "keyboard" placeless = None candidates = [ ] index = 0 current = get_focused() current_index = None for f in focus_list: if f.x is None: placeless = f continue if f.arg is not None: continue if not f.widget.style.keyboard_focus: continue if f.widget is current: current_index = index candidates.append(f) index += 1 new_focus = None if current_index is None: if candidates: if delta > 0: new_focus = candidates[delta - 1] else: new_focus = candidates[delta] else: new_index = current_index + delta if 0 <= new_index < len(candidates): new_focus = candidates[new_index] new_focus = new_focus or placeless return change_focus(new_focus) def key_handler(ev): map_event = renpy.display.behavior.map_event if renpy.game.preferences.self_voicing: if map_event(ev, 'focus_right') or map_event(ev, 'focus_down'): return focus_ordered(1) if map_event(ev, 'focus_left') or map_event(ev, 'focus_up'): return focus_ordered(-1) else: if map_event(ev, 'focus_right'): return focus_nearest(0.9, 0.1, 0.9, 0.9, 0.1, 0.1, 0.1, 0.9, verti_line_dist, lambda old, new : old.x + old.w <= new.x, -1, 0, 0, 0) if map_event(ev, 'focus_left'): return focus_nearest(0.1, 0.1, 0.1, 0.9, 0.9, 0.1, 0.9, 0.9, verti_line_dist, lambda old, new : new.x + new.w <= old.x, 1, 0, 1, 0) if map_event(ev, 'focus_up'): return focus_nearest(0.1, 0.1, 0.9, 0.1, 0.1, 0.9, 0.9, 0.9, horiz_line_dist, lambda old, new : new.y + new.h <= old.y, 0, 1, 0, 1) if map_event(ev, 'focus_down'): return focus_nearest(0.1, 0.9, 0.9, 0.9, 0.1, 0.1, 0.9, 0.1, horiz_line_dist, lambda old, new : old.y + old.h <= new.y, 0, -1, 0, 0)