Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for stacked list of modes #494

Open
wants to merge 2 commits into
base: r13
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions action_plugins/temporary_mode_switch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ def __init__(self, action):
def process_event(self, event, value):
gremlin.input_devices.ButtonReleaseActions().register_callback(
gremlin.control_action.switch_to_previous_mode,
event
event,
self.mode_name
)
gremlin.control_action.switch_mode(self.mode_name)
gremlin.control_action.switch_mode(self.mode_name, temporary=True)
return True


Expand Down
102 changes: 102 additions & 0 deletions examples/mode-stack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
Introduction
============
This document tries to describe what can be done with mode stack implementation. It is a hack around the current implementation, but hopefully it describes what it provides well enough so a similar functionality can get implemented in r14 and beyond.

JG r13 supports switching modes, temporary mode switching and switching to previous mode. This is great, but in certain situations more flexibility is required. As a demonstration an F-16 profile will be used.

F-16 has 3 major modes: NAV, AA, AG. It also has 2 override modes: AA-DOGFIGHT-OVERRIDE, AA-MISSILE-OVERRIDE.
Let's create modes for each: NAV, AA, AG, AA-DF, AA-MIS. To demonstrate what mode stack is capable let's
assume the following narrative:

*A pilot is flying in NAV mode and switches to AG mode. Performs ground reconnaissance and is bugged
by a threat. The RWR shows the threat is close, so the pilot switches to AA-MIS mode and engages.
After the engagement pilot flips the AA-MIS switch back to center and resumes AG reconnaissance.
When he's finished, he clicks AG again and resumes NAV and flies home.*

Let's assume all buttons available to us are non-latching type, which means
that they return to the original position. FYI, in F-16, AG is non-latching, AA-MIS is latching (AFAIK).

To support the narrative above JG r13 profile would have to look like this:
(there might be other possibilities too, but require programming or extra
configuration steps).
- NAV mode
- a button A to switch to AG mode
- a button O to switch to AA-MIS mode
- AG mode
- a button O to switch to AA-MIS mode
- a button A to switch to NAV mode
- AA-MIS mode
- a button O to switch to AG mode

A sim-pilot would start in NAV mode, then:
1. press A button in NAV mode to get to AG mode
2. press O button in AG mode to get to AA-MIS mode
3. press O button in AA-MIS mode to get back to AG mode
4. press A button in AG mode to get back to NAV mode

That's simple, right? Well, what happens if button O is pressed while mode is NAV? It switches to AA-MIS, good. But when you press it again to return to NAV, it goes to AG mode. Not good.

What about switch to previous mode or temporary switch mode? If you have non-latching buttons you either need to hold them for temporary mode switch to be useful (think of it as a SHIFT button). Switch to previous mode is useful, but only in simple scenarios. Let's setup the same
scenario using switch to previous mode:

- NAV mode
- a button A to switch to AG mode
- a button O to switch to AA-MIS mode
- AG mode
- a button O to switch to AA-MIS mode
- a button A to switch previous mode
- AA-MIS mode
- a button O to switch to previous mode

A sim-pilot would start in NAV mode, then:
1. press A button in NAV mode to get to AG mode
2. press O button in AG mode to get to AA-MIS mode
3. press O button in AA-MIS mode to get back to AG mode, *good*
4. press A button in AG mode to get back to NAV mode, but would end up in AA-MIS mode, *not-good*.

JG mode stack comes to the rescue! The same profile would work with mode stack because it keeps
a track multiple previous modes. It works as a stack - as modes are switched to, they are added
to the stack, as they are switched to previous mode, they're popped from the stack.

- NAV mode
- a button A to switch to AG mode
- a button O to switch to AA-MIS mode (just for demonstrative purposes)
- AG mode
- a button O to switch to AA-MIS mode
- a button A to switch previous mode
- AA-MIS mode
- a button O to switch to previous mode

A sim-pilot would start in NAV mode, then:
1. press A button in NAV mode to get to AG mode, and mode stack contain single mode: NAV
2. press O button in AG mode to get to AA-MIS mode, and mode stack would contain two mode elements: NAV, AG
3. press O button in AA-MIS mode to get back to AG mode, and mode stack
would contain a single element: NAV
4. press A button in AG mode to get back to NAV mode, and mode stack
would be empty - *yaay!!*


Temporary modes
===============
mode stack also supports stacking multiple temporary modes and as the buttons for temporary modes get depressed
they are removed from the stack. If they're depressed in random order the mode will return to the original mode
before the temporary modes were switched to.

Assume the mode stack was: NAV, *TEMP MODE1, *TEMP MODE2
Then TEMP MODE1 button is released, the mode stack is: NAV, *TEMP MODE2
When TEMP MODE2 button is released, the mode stack is NAV

Mode cycles
===========
When mode cycles are used the mode stack keeps a track of modes that were cycled too and keeps rotating them
so the stack does not grow to tall. Assume we have 2 modes in the cycle list: AA-MIS, AA-DF.
Say mode stack is NAV, CYCLED AA-MIS, CYCLED AA-DF. If you press the button to cycle the modes AA-MIS/AA-DF again
then the AA-MIS would be chosen and mode stack becomes: NAV, AA-DF, AA-MIS. Pressing the cycle again chooses
AA-DF mode and mode stack becomes: NAV, AA-MIS, AA-DF.







3 changes: 2 additions & 1 deletion gremlin/code_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import gremlin
from gremlin import event_handler, input_devices, \
joystick_handling, macro, sendinput, user_plugin, util
joystick_handling, macro, sendinput, user_plugin, util, control_action
import vjoy as vjoy_module


Expand Down Expand Up @@ -262,6 +262,7 @@ def start(self, inheritance_tree, settings, start_mode, profile):
input_devices.periodic_registry.start()
macro.MacroManager().start()

control_action.mode_stack_reset(start_mode)
self.event_handler.change_mode(start_mode)
self.event_handler.resume()
self._running = True
Expand Down
165 changes: 160 additions & 5 deletions gremlin/control_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"""Collection of actions that allow controlling JoystickGremlin."""

import gremlin.event_handler
import logging

log = logging.getLogger("mode_stack")

class ModeList:

Expand All @@ -41,18 +43,154 @@ def next(self):
return self._modes[self._current_index]


def switch_mode(mode):

class Mode:
def __init__(self, mode_name, temporary, cycled=False): # TODO: remove =False from cycled when everything else works to cleanup the API
self._mode_name = mode_name
self._temp = temporary
self._cycled = cycled


@property
def mode_name(self):
return self._mode_name


@property
def is_temp(self):
return self._temp


@property
def is_cycled(self):
return self._cycled


def __str__(self):
return f"{self.__class__.__name__}(name: {self._mode_name}, temp: {self._temp}, cycled: {self._cycled})"


def __repr__(self):
return self.__str__()

# a stack of modes - used when using switch to previous and temporary modes
mode_stack = []

# only use temporary mode check if specified
def mode_stack_is_last_mode(mode, temporary=None):

# if temporary is not specified, we remove temp nodes before doing the check
# to be able to return True for cases where mode_stack has the following
# content 'non-temp-mode-A, temp-mode-B' and 'non-temp-mode-A' is being
# passed as mode

if not temporary:
mode_stack_check = list(filter(lambda m: not m.is_temp, mode_stack))
else:
mode_stack_check = mode_stack

return mode_stack_check and mode_stack_check[-1].mode_name == mode and ((temporary is None) or (mode_stack_check[-1].is_temp == temporary))


def mode_stack_get_prev():
return mode_stack[-2] if len(mode_stack) > 1 else mode_stack[0]

def mode_stack_get_last(ignore_temp=False):
# return last mode, if ignore_temp is true, temporary modes are skipped.
log.debug(f"mode_stack_get_last(ignore_temp={ignore_temp}) {mode_stack}")
if not ignore_temp:
log.debug(f"mode_stack_get_last(ignore_temp={ignore_temp}) returning {mode_stack[-1]}")
return mode_stack[-1]

for mode in range(len(mode_stack) - 1, -1, -1):
if mode_stack[mode].is_temp:
continue
log.debug(f"mode_stack_get_last(ignore_temp={ignore_temp}) returning {mode_stack[mode]}")
return mode_stack[mode]

# we should never reach here since at least 1 mode has to be non-temp

def mode_stack_remove(mode, temporary=None):
global mode_stack
result = filter(lambda m: not (m.mode_name == mode and (temporary is None or m.is_temp == temporary)), mode_stack)
mode_stack = list(result)


def mode_stack_reset(mode = None):
global mode_stack
if mode:
mode_stack = [Mode(mode, False)]
else:
mode_stack = []
log.debug("mode_stack reset")


def cycled_mode_already_stacked(mode):
global mode_stack
result = filter(lambda m: m.mode_name == mode and m.is_cycled and not m.is_temp, mode_stack)
if len(list(result)) > 0:
return True
return False


def remove_cycled_mode(mode):
global mode_stack
result = filter(lambda m: not (m.mode_name == mode and m.is_cycled and not m.is_temp), mode_stack)
mode_stack = list(result)


def switch_mode(mode, temporary=False):
"""Switches the currently active mode to the one provided.

:param mode the mode to switch to
"""

log.debug(f"switch_mode({mode}): {mode_stack}")

if not mode_stack_is_last_mode(mode, temporary=temporary):
log.debug(f"Adding mode {mode} to mode_stack. temporary={temporary}")
mode_stack.append(Mode(mode, temporary))

gremlin.event_handler.EventHandler().change_mode(mode)
log.debug(f"switch_mode({mode}, {temporary}): done - {mode_stack}")


def switch_to_previous_mode():
def switch_to_previous_mode(mode_name=None):
"""Switches to the previously active mode."""
eh = gremlin.event_handler.EventHandler()
eh.change_mode(eh.previous_mode)

log.debug(f"switch_to_previous_mode({mode_name}): {mode_stack}")

# can't go beyond first mode
if len(mode_stack) == 1:
log.debug(f"switch_to_previous_mode({mode_name}): ignoring, can't go beyond first mode.")
return

# if switching to previous mode is a result of temporary mode switch
# button being released and that temporary mode was not on the top of the stack
# keep the current mode and remove the temporary mode from the stack

temporary = None

remove_mode = mode_name
# switch_to_previous_mode w/o mode_name gets called when non-temporary switch to previous mode is called
# therefore we can safely remove the last mode used from the stack
if not mode_name:
remove_mode = mode_stack_get_last(ignore_temp=True).mode_name
log.debug(f"switch_to_previous_mode({mode_name}): mode to be removed from stack is {remove_mode}")

eh = gremlin.event_handler.EventHandler()
if mode_stack_is_last_mode(remove_mode):
prev_mode = mode_stack_get_prev().mode_name;
mode_stack_remove(remove_mode)
eh.change_mode(prev_mode)
log.debug(f"switch_to_previous_mode({mode_name}) performed.")
else:
mode_stack_remove(remove_mode, temporary=True)
eh.change_mode(mode_stack_get_last().mode_name) # update status bar (mode count)
log.debug(f"switch_to_previous_mode({mode_name}) ignored.")


log.debug(f"switch_to_previous_mode({mode_name}): done - {mode_stack}")


def cycle_modes(mode_list):
Expand All @@ -63,7 +201,24 @@ def cycle_modes(mode_list):

:param mode_list list of mode names to cycle through
"""
gremlin.event_handler.EventHandler().change_mode(mode_list.next())
# if the mode being switched to is already in the mode_stack
# remove it first, so the mode stack does not continuously grow.
# If someone is cycling through all the modes it will
# increase the mode stack only by number of modes in the
# mode_list

mode = mode_list.next()

if cycled_mode_already_stacked(mode):
log.debug(f"Removing cycled mode {mode} from stack.")
remove_cycled_mode(mode)

log.debug(f"Adding cycled mode {mode} to mode_stack.")
mode_stack.append(Mode(mode, temporary=False, cycled=True))

gremlin.event_handler.EventHandler().change_mode(mode)

log.debug(f"cycle_modes({mode}): done - {mode_stack}")


def pause():
Expand Down
16 changes: 11 additions & 5 deletions gremlin/input_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def __init__(self):
self._current_mode = eh.active_mode
eh.mode_changed.connect(self._mode_changed_cb)

def register_callback(self, callback, physical_event):
def register_callback(self, callback, physical_event, mode_name = None):
"""Registers a callback with the system.

:param callback the function to run when the corresponding button is
Expand All @@ -596,9 +596,11 @@ def register_callback(self, callback, physical_event):

if release_evt not in self._registry:
self._registry[release_evt] = []
# Do not record the mode since we may want to run the release action
# independent of a mode
self._registry[release_evt].append((callback, None))

# hack: keep a track of mode for temporary switch back, so that
# the temporary mode that might not be at the top of the
# mode stack is removed
self._registry[release_evt].append((callback, None, mode_name))

def register_button_release(self, vjoy_input, physical_event):
"""Registers a physical and vjoy button pair for tracking.
Expand Down Expand Up @@ -647,7 +649,11 @@ def _input_event_cb(self, evt):
"""
if evt in self._registry and not evt.is_pressed:
for entry in self._registry[evt]:
entry[0]()
# hack around switch_to_previous with mode_name, see register_callback() hack note
if len(entry) > 2 and entry[2] is not None:
entry[0](entry[2])
else:
entry[0]()
self._registry[evt] = []

def _mode_changed_cb(self, mode):
Expand Down
8 changes: 7 additions & 1 deletion gremlin/ui/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,15 @@ def __init__(self, parent=None):
os.path.join(gremlin.util.userprofile_path(), "user.log"),
"User"
)
self._create_log_display(
os.path.join(gremlin.util.userprofile_path(), "mode_stack.log"),
"Mode Stack"
)

self.watcher = gremlin.util.FileWatcher([
os.path.join(gremlin.util.userprofile_path(), "system.log"),
os.path.join(gremlin.util.userprofile_path(), "user.log")
os.path.join(gremlin.util.userprofile_path(), "user.log"),
os.path.join(gremlin.util.userprofile_path(), "mode_stack.log")
])
self.watcher.file_changed.connect(self._reload)

Expand Down
Loading