Skip to content

ptz-controls: Add hotkeys for selection relative preset recall - #337

Closed
EdueskaWeiz wants to merge 1 commit into
glikely:mainfrom
EdueskaWeiz:feat/preset-selection-hotkeys
Closed

ptz-controls: Add hotkeys for selection relative preset recall#337
EdueskaWeiz wants to merge 1 commit into
glikely:mainfrom
EdueskaWeiz:feat/preset-selection-hotkeys

Conversation

@EdueskaWeiz

Copy link
Copy Markdown
Contributor

Closes #270.

This is a regression, not a new feature — I had that wrong in the issue at first and corrected it there. Up to v0.17.2 the joystick button dropdown was filled from a fixed list inside the plugin that contained "Previous Preset", "Next Preset" and "Recall Preset". 4b16fd3 ("joystick: Map hotkeys to buttons") reworked the dropdown to list OBS frontend hotkeys and reduced ptz_joy_action to axis actions. "Previous Camera" and "Next Camera" survived because PTZ.SelectPrev and PTZ.SelectNext exist as hotkeys; the three preset actions had no hotkey equivalent, so they dropped out. That commit first shipped in v0.18.0.

Registering them as frontend hotkeys puts them back in the joystick dropdown by itself, and makes them usable from the keyboard as well, which they never were:

PTZ.PresetPrev            Previous Preset
PTZ.PresetNext            Next Preset
PTZ.PresetRecallSelected  Recall Selected Preset

Moving the selection deliberately does not recall anything, so a controller can browse the list and then fire it with a separate button — the workflow described in #270. Recalling with nothing selected is a no-op rather than an invalid preset id.

I named the third one "Recall Selected Preset" rather than the original "Recall Preset" to keep it apart from the numbered "Camera Recall #n" entries in the same list. Happy to restore the old wording.

Testing

Built from source and run against OBS Studio 32.2.1 (Qt 6.11.1) on Windows, plugin version v0.18.3-rc1-65-g737c0fb confirmed in the OBS log. Three presets on one device, hotkeys bound to F9 / F10 / F8.

  • All three appear under Settings → Hotkeys.
  • Next Preset: selection moved 0 → 1 → 2 → 0, wrapping at the end.
  • Previous Preset: from 0 it wrapped back to 2.
  • Neither of them recalled anything — the selection moved and nothing else happened, which is the point.
  • Recall Selected Preset left the selection alone and did not fault, including with a preset that has no stored position.

What I could not verify here, so that it is not silently claimed:

  • No controller. I have no gamepad, so I could not see the three entries appear in the joystick button dropdown. That follows from PTZJoyButtonMapper listing frontend hotkeys, but I have not watched it happen.
  • No PTZ camera. Only a UVC webcam without pan/tilt and a VISCA-over-IP device with no camera behind it, so I could not observe a recall actually moving anything. presetRecallSelected() goes through the same presetRecall() as double-clicking an entry in the list.

The numbered PTZ.Recall<n> hotkeys always address a fixed preset, and
the only way to trigger the preset currently highlighted in the list is
a double click or Enter inside the list itself. PTZJoyButtonMapper only
offers named frontend hotkeys, so browsing the preset list with a
controller and then activating the highlighted entry is not possible.

Register three more frontend hotkeys:

  PTZ.PresetPrev            move the selection up, wrapping around
  PTZ.PresetNext            move the selection down, wrapping around
  PTZ.PresetRecallSelected  recall the highlighted preset

Moving the selection deliberately does not recall anything, so a
controller can browse the list and trigger it with a separate button.
Recalling with nothing selected is a no-op rather than an invalid
preset id. Being frontend hotkeys, they are mappable to joystick
buttons like every other action.

Closes glikely#270
Signed-off-by: Eddy Weiz <eddyweiz@gmail.com>
@EdueskaWeiz

Copy link
Copy Markdown
Contributor Author

Follow-up: the one thing I listed as unverified — that the three hotkeys reappear in the joystick button dropdown — is now checked, so that gap is closed.

I got a joystick without owning one by flipping setVirtualJoystickEnabled() to true in a throwaway build, which makes QJoysticks expose its keyboard driven virtual joystick with 6 axes and 10 buttons. That line is not part of this PR; the same helper is described on #325.

Build v0.18.3-rc1-66-gf9bfe2f — current main + this commit + the test helper. Opening the menu of a joystick button lists, directly after "Camera Save #16":

Previous Preset
Next Preset
Recall Selected Preset

Assigning "Next Preset" to button 0 sticks, exactly like any other frontend hotkey. So the three entries that fell out of the list in v0.18.0 are back where they were, without the joystick code needing to know about presets at all.

Unchanged from the description: no physical controller here, so I have not pressed a real button, and no PTZ camera, so a recall still moves nothing I can watch.

Comment thread src/ptz-controls.cpp
"PTZ.PresetPrev", obs_module_text("PTZ.Action.Preset.Prev"),
[](void *ptz_data, obs_hotkey_id, obs_hotkey_t *, bool pressed) {
if (pressed)
static_cast<PTZControls *>(ptz_data)->presetStep(-1);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the move logic is already implemented in CircularListView. You can drop the presetStep() helper and call static_cast<PTZControls *>(ptz_data)->ui->presetListView->cursorUp() directly.

Comment thread src/ptz-controls.cpp
[](void *ptz_data, obs_hotkey_id, obs_hotkey_t *, bool pressed) {
if (pressed)
static_cast<PTZControls *>(ptz_data)->presetStep(1);
},

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto using cursorDown()

Comment thread src/ptz-controls.cpp
"PTZ.PresetRecallSelected", obs_module_text("PTZ.Action.Preset.RecallSelected"),
[](void *ptz_data, obs_hotkey_id, obs_hotkey_t *, bool pressed) {
if (pressed)
static_cast<PTZControls *>(ptz_data)->presetRecallSelected();

@glikely glikely Aug 8, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't a similar single helper for activating the item, but you could do this:

if (pressed) {
        auto ctrls = static_cast<PTZControls *>(ptz_data);
        ctrls->on_presetListView_activated(ctrls->ui->presetListView->currentIndex());
}

@glikely

glikely commented Aug 9, 2026

Copy link
Copy Markdown
Owner

I've made the changes I suggested and pushed out to main. Thanks

@glikely glikely closed this Aug 9, 2026
@EdueskaWeiz

Copy link
Copy Markdown
Contributor Author

Thanks — and your version is better, dropping the two helpers for cursorUp() / cursorDown() was the right call. It also means the hotkeys and the joystick POV hat now go through exactly the same code, which is one less thing to keep in step.

One thing got lost in the rewrite, though. The commit message still says:

Recalling with nothing selected is a no-op rather than an invalid preset id.

but c20be21 no longer checks the index:

if (ptz_data && pressed) {
	auto ctrls = static_cast<PTZControls *>(ptz_data);
	ctrls->on_presetListView_activated(ctrls->ui->presetListView->currentIndex());
}

With an empty preset list, or before anything has been selected, currentIndex() is invalid and presetIndexToId() returns -1.

Most backends shrug that off: PTZPelco::memory_recall() rejects negative ids, PTZOnvif::memory_recall() finds no token and returns, PTZUSBCam::memory_recall() finds no stored preset. VISCA does not check. visca_u7 keeps the low seven bits of the value, so -1 comes out as 0x7f and the camera is asked to recall preset 127:

81 01 04 3f 02 7f ff

I checked that against int_field::encode() rather than guessing — with mask = 0x7f and val = -1 the loop fills all seven bits.

Two lines put it back:

 			if (ptz_data && pressed) {
 				auto ctrls = static_cast<PTZControls *>(ptz_data);
-				ctrls->on_presetListView_activated(ctrls->ui->presetListView->currentIndex());
+				auto index = ctrls->ui->presetListView->currentIndex();
+				if (index.isValid())
+					ctrls->on_presetListView_activated(index);
 			}

Happy to send it as a PR with a proper commit message and a Fixes: tag if you would rather have it that way — just say which you prefer.

@EdueskaWeiz

Copy link
Copy Markdown
Contributor Author

Small heads-up, since v0.19.0-rc1 went out in the meantime: the missing isValid() check above is in that tag, so a "Recall Selected Preset" press with an empty preset list will ask a VISCA camera for preset 127. Nothing dramatic — worth catching before the final though.

Happy to send the two-liner as a proper pull request with a Fixes: tag, or leave it to you as you did with the review comments. Whatever is less work for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v0.18.1 - Missing "Recall Preset, Next Preset, and Previous Preset"

2 participants